@yemi33/minions 0.1.802 → 0.1.804

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,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.802 (2026-04-10)
3
+ ## 0.1.804 (2026-04-10)
4
4
 
5
5
  ### Features
6
6
  - configurable ignored comment authors — auto-filtered, never trigger fixes
@@ -9,6 +9,8 @@
9
9
  - cap review→fix cycles at evalMaxIterations (default 3)
10
10
 
11
11
  ### Fixes
12
+ - allow PRD item retry when work item is missing (closes #781) (#807)
13
+ - handle force-pushed dep branches on retry (closes #738) (#806)
12
14
  - project-scoped skill work items instruct wrong file path format (closes #790) (#804)
13
15
  - move ignoredAuthors construction outside inner comment loop (ADO)
14
16
  - approved is permanent — no path can ever downgrade it
@@ -171,9 +171,10 @@ function renderPrdProgress(prog) {
171
171
  const agentLabel = wiAgent ? '<span style="font-size:9px;color:var(--muted)" title="' + escHtml(wiAgent.name || wi.dispatched_to) + '">' +
172
172
  (wiAgent.emoji || '') + ' ' + escHtml(wiAgent.name || wi.dispatched_to) + '</span>' : '';
173
173
 
174
- // Requeue button for failed items
175
- const canRequeue = wi && (wi.status === 'failed' || i.status === 'failed');
176
- const requeueState = wi ? getPrdRequeueState(wi.id) : null;
174
+ // Requeue button for failed items or PRD items with no work item (orphaned/deleted)
175
+ const canRequeue = (wi && (wi.status === 'failed' || i.status === 'failed')) ||
176
+ (!wi && i.status && i.status !== 'missing' && i.status !== 'done' && i.status !== 'planned');
177
+ const requeueState = getPrdRequeueState(wi ? wi.id : i.id);
177
178
  let requeueBtn = '';
178
179
  if (requeueState && requeueState.status === 'pending') {
179
180
  requeueBtn = '<span style="color:var(--yellow);cursor:wait;font-size:9px;padding:1px 5px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.35);border-radius:3px" title="Retry request in progress">requeuing…</span>';
@@ -182,7 +183,7 @@ function renderPrdProgress(prog) {
182
183
  } else if (requeueState && requeueState.status === 'error') {
183
184
  requeueBtn = '<span style="color:var(--red);cursor:default;font-size:9px;padding:1px 5px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.35);border-radius:3px" title="' + escHtml(requeueState.message || 'Retry failed') + '">retry failed</span>';
184
185
  } else if (canRequeue) {
185
- requeueBtn = '<span onclick="event.stopPropagation();prdItemRequeue(\'' + escHtml(wi.id) + '\',\'' + escHtml(wi._source || '') + '\')" style="color:var(--green);cursor:pointer;font-size:9px;padding:1px 5px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px" title="Requeue this work item">retry</span>';
186
+ requeueBtn = '<span onclick="event.stopPropagation();prdItemRequeue(\'' + escHtml(wi ? wi.id : i.id) + '\',\'' + escHtml(wi ? (wi._source || '') : '') + '\',\'' + escHtml(i.source || '') + '\')" style="color:var(--green);cursor:pointer;font-size:9px;padding:1px 5px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px" title="Requeue this work item">retry</span>';
186
187
  }
187
188
 
188
189
  return '<div class="prd-item-row st-' + (i.status || 'missing') + '" style="flex-wrap:wrap;cursor:pointer" onclick="prdItemEdit(\'' + src + '\',\'' + iid + '\')">' +
@@ -381,8 +382,8 @@ function renderPrdProgress(prog) {
381
382
  (agentDisplay ? '<span style="font-size:8px;color:var(--muted)">' + agentDisplay + '</span>' : '') +
382
383
  (function() {
383
384
  const w = wi[i.id];
384
- if (!w) return '';
385
- const rq = getPrdRequeueState(w.id);
385
+ const rqId = w ? w.id : i.id;
386
+ const rq = getPrdRequeueState(rqId);
386
387
  if (rq && rq.status === 'pending') {
387
388
  return '<span style="font-size:8px;color:var(--yellow);cursor:wait;padding:1px 4px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.35);border-radius:3px">requeuing…</span>';
388
389
  }
@@ -392,8 +393,11 @@ function renderPrdProgress(prog) {
392
393
  if (rq && rq.status === 'error') {
393
394
  return '<span style="font-size:8px;color:var(--red);cursor:default;padding:1px 4px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.35);border-radius:3px" title="' + escHtml(rq.message || 'Retry failed') + '">failed</span>';
394
395
  }
395
- if (i.status !== 'failed') return '';
396
- return '<span onclick="event.stopPropagation();prdItemRequeue(\'' + escHtml(w.id) + '\',\'' + escHtml(w._source || '') + '\')" style="font-size:8px;color:var(--green);cursor:pointer;padding:1px 4px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">retry</span>';
396
+ // Show retry for failed items, or PRD items with no work item (orphaned/deleted)
397
+ const canRetry = (w && i.status === 'failed') ||
398
+ (!w && i.status && i.status !== 'missing' && i.status !== 'done' && i.status !== 'planned');
399
+ if (!canRetry) return '';
400
+ return '<span onclick="event.stopPropagation();prdItemRequeue(\'' + escHtml(rqId) + '\',\'' + escHtml(w ? (w._source || '') : '') + '\',\'' + escHtml(i.source || '') + '\')" style="font-size:8px;color:var(--green);cursor:pointer;padding:1px 4px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">retry</span>';
397
401
  })() +
398
402
  (deps ? '<span style="font-size:8px;color:var(--muted)" title="Depends on: ' + escHtml(deps) + '">deps: ' + escHtml(deps) + '</span>' : '') +
399
403
  '</div>' +
@@ -713,13 +717,15 @@ async function prdItemRemove(source, itemId) {
713
717
  } catch (e) { alert('Error: ' + e.message); refresh(); }
714
718
  }
715
719
 
716
- async function prdItemRequeue(workItemId, source) {
720
+ async function prdItemRequeue(workItemId, source, prdFile) {
717
721
  setPrdRequeueState(workItemId, { status: 'pending', message: '' });
718
722
  rerenderPrdFromCache();
719
723
  try {
724
+ const payload = { id: workItemId, source };
725
+ if (prdFile) payload.prdFile = prdFile;
720
726
  const res = await fetch('/api/work-items/retry', {
721
727
  method: 'POST', headers: { 'Content-Type': 'application/json' },
722
- body: JSON.stringify({ id: workItemId, source })
728
+ body: JSON.stringify(payload)
723
729
  });
724
730
  if (res.ok) {
725
731
  setPrdRequeueState(workItemId, { status: 'queued', until: Date.now() + 10000 });
package/dashboard.js CHANGED
@@ -1040,7 +1040,74 @@ const server = http.createServer(async (req, res) => {
1040
1040
  }
1041
1041
  }
1042
1042
  }
1043
- if (!wiPath) return jsonReply(res, 404, { error: 'work item not found in any source' });
1043
+ // If no work item found, attempt to re-materialize from PRD item definition
1044
+ if (!wiPath) {
1045
+ const prdFile = body.prdFile;
1046
+ if (!prdFile) return jsonReply(res, 404, { error: 'work item not found in any source' });
1047
+
1048
+ // Look up PRD item to create a new work item on-demand
1049
+ const prdPath = path.join(PRD_DIR, prdFile);
1050
+ const plan = shared.safeJson(prdPath);
1051
+ if (!plan?.missing_features) return jsonReply(res, 404, { error: 'PRD file not found or invalid' });
1052
+ const prdItem = plan.missing_features.find(f => f.id === id);
1053
+ if (!prdItem) return jsonReply(res, 404, { error: 'PRD item not found in ' + prdFile });
1054
+
1055
+ // Determine target work-items file (project from PRD item or plan, fallback to central)
1056
+ const projName = prdItem.project || plan.project || prdFile.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
1057
+ const proj = PROJECTS.find(p => p.name?.toLowerCase() === projName.toLowerCase());
1058
+ const targetWiPath = proj ? shared.projectWorkItemsPath(proj) : path.join(MINIONS_DIR, 'work-items.json');
1059
+
1060
+ // Create new work item from PRD item definition (same logic as materializePlansAsWorkItems)
1061
+ const complexity = prdItem.estimated_complexity || 'medium';
1062
+ const criteria = (prdItem.acceptance_criteria || []).map(c => `- ${c}`).join('\n');
1063
+ const newItem = {
1064
+ id,
1065
+ title: `Implement: ${prdItem.name}`,
1066
+ type: complexity === 'large' ? 'implement:large' : 'implement',
1067
+ priority: prdItem.priority || 'medium',
1068
+ description: `${prdItem.description || ''}\n\n**Plan:** ${prdFile}\n**Plan Item:** ${prdItem.id}\n**Complexity:** ${complexity}${criteria ? '\n\n**Acceptance Criteria:**\n' + criteria : ''}`,
1069
+ status: WI_STATUS.PENDING,
1070
+ created: new Date().toISOString(),
1071
+ createdBy: 'dashboard:prd-retry',
1072
+ sourcePlan: prdFile,
1073
+ depends_on: prdItem.depends_on || [],
1074
+ branchStrategy: plan.branch_strategy || 'parallel',
1075
+ featureBranch: plan.feature_branch || null,
1076
+ project: prdItem.project || plan.project || null,
1077
+ _source: proj?.name || 'central',
1078
+ _retryCount: 0,
1079
+ };
1080
+ mutateWorkItems(targetWiPath, items => { items.push(newItem); });
1081
+
1082
+ // Reset PRD item status to pending
1083
+ try {
1084
+ const lifecycle = require('./engine/lifecycle');
1085
+ lifecycle.syncPrdItemStatus(id, WI_STATUS.PENDING, prdFile);
1086
+ } catch (e) { console.error('PRD status sync:', e.message); }
1087
+
1088
+ // Clear dispatch history and cooldowns for this item
1089
+ const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
1090
+ const sourcePrefix = proj ? `work-${proj.name}-` : 'central-work-';
1091
+ const dispatchKey = sourcePrefix + id;
1092
+ try {
1093
+ mutateJsonFileLocked(dispatchPath, (dispatch) => {
1094
+ dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
1095
+ dispatch.completed = dispatch.completed.filter(d => d.meta?.dispatchKey !== dispatchKey);
1096
+ dispatch.completed = dispatch.completed.filter(d => !d.meta?.parentKey || d.meta.parentKey !== dispatchKey);
1097
+ return dispatch;
1098
+ }, { defaultValue: { pending: [], active: [], completed: [] } });
1099
+ } catch (e) { console.error('dispatch cleanup:', e.message); }
1100
+ try {
1101
+ const cooldownPath = path.join(MINIONS_DIR, 'engine', 'cooldowns.json');
1102
+ const cooldowns = safeJsonObj(cooldownPath);
1103
+ if (cooldowns[dispatchKey]) {
1104
+ delete cooldowns[dispatchKey];
1105
+ safeWrite(cooldownPath, cooldowns);
1106
+ }
1107
+ } catch (e) { console.error('cooldown cleanup:', e.message); }
1108
+
1109
+ return jsonReply(res, 200, { ok: true, id, rematerialized: true });
1110
+ }
1044
1111
 
1045
1112
  let found = false;
1046
1113
  mutateJsonFileLocked(wiPath, (items) => {
package/engine.js CHANGED
@@ -493,8 +493,25 @@ async function spawnAgent(dispatchItem, config) {
493
493
  await execAsync(`git merge "origin/${depBranch}" --no-edit`, { ..._gitOpts, cwd: worktreePath });
494
494
  log('info', `Merged dependency branch ${depBranch} (${prId}) into worktree ${branchName}`);
495
495
  } catch (mergeErr) {
496
- log('warn', `Failed to merge dependency ${depBranch} into ${branchName}: ${mergeErr.message}`);
497
- depMergeFailed = true;
496
+ // Merge failed — possibly due to diverged history from a force-pushed (rebased) dep branch.
497
+ // Abort partial merge, reset worktree to clean main base, and re-merge all deps from scratch.
498
+ log('warn', `Merge of ${depBranch} into ${branchName} failed: ${mergeErr.message} — attempting reset and re-merge of all deps`);
499
+ try { await execAsync(`git merge --abort`, { ..._gitOpts, cwd: worktreePath }); } catch (_) { /* no merge in progress */ }
500
+ const mainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
501
+ try {
502
+ await execAsync(`git reset --hard "origin/${mainRef}"`, { ..._gitOpts, cwd: worktreePath });
503
+ log('info', `Reset worktree ${branchName} to origin/${mainRef} for clean dep re-merge`);
504
+ // Re-merge ALL fetched dep branches from scratch on clean base
505
+ for (const { branch: reBranch, prId: rePrId } of fetched) {
506
+ await execAsync(`git merge "origin/${reBranch}" --no-edit`, { ..._gitOpts, cwd: worktreePath });
507
+ log('info', `Re-merged dependency branch ${reBranch} (${rePrId}) into worktree ${branchName}`);
508
+ }
509
+ log('info', `Successfully re-merged all ${fetched.length} dep branches after reset for ${branchName}`);
510
+ } catch (resetErr) {
511
+ log('warn', `Failed to reset and re-merge deps for ${branchName}: ${resetErr.message}`);
512
+ try { await execAsync(`git merge --abort`, { ..._gitOpts, cwd: worktreePath }); } catch (_) { /* no merge in progress */ }
513
+ depMergeFailed = true;
514
+ }
498
515
  break;
499
516
  }
500
517
  }
@@ -1185,6 +1202,10 @@ function autoCleanPrdWorkItems(prdFile, config) {
1185
1202
  }
1186
1203
  return dispatch;
1187
1204
  });
1205
+ // Reset PRD item status to 'missing' so engine re-materializes on next tick
1206
+ for (const id of deletedIds) {
1207
+ try { syncPrdItemStatus(id, 'missing', prdFile); } catch (e) { log('warn', `PRD status reset for ${id}: ${e.message}`); }
1208
+ }
1188
1209
  log('info', `Plan sync: cleared ${deletedIds.length} pending/failed work items for ${prdFile}`);
1189
1210
  }
1190
1211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.802",
3
+ "version": "0.1.804",
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"