@yemi33/minions 0.1.152 → 0.1.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,7 +58,7 @@ function checkPlanCompletion(meta, config) {
58
58
  const unmaterialized = [...planFeatureIds].filter(id => {
59
59
  if (workItemById[id]) return false;
60
60
  const prdItem = (plan.missing_features || []).find(f => f.id === id);
61
- return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
61
+ return !(prdItem && prdItem.status === 'done');
62
62
  });
63
63
  if (unmaterialized.length > 0) {
64
64
  log('info', `Plan ${planFile}: ${unmaterialized.length}/${planFeatureIds.size} feature(s) not yet materialized as work items: ${unmaterialized.join(', ')}`);
@@ -68,16 +68,16 @@ function checkPlanCompletion(meta, config) {
68
68
  // Check 2: every feature's work item must be done (or PRD item marked done externally)
69
69
  const notDone = [...planFeatureIds].filter(id => {
70
70
  const w = workItemById[id];
71
- if (w && (w.status === 'done' || w.status === 'in-pr')) return false; // in-pr accepted for backward compat
71
+ if (w && w.status === 'done') return false;
72
72
  const prdItem = (plan.missing_features || []).find(f => f.id === id);
73
- return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
73
+ return !(prdItem && prdItem.status === 'done');
74
74
  });
75
75
  if (notDone.length > 0) {
76
76
  log('info', `Plan ${planFile}: waiting for done on ${notDone.length}/${planFeatureIds.size} item(s): ${notDone.join(', ')}`);
77
77
  return;
78
78
  }
79
79
 
80
- const doneItems = planItems.filter(w => w.status === 'done' || w.status === 'in-pr');
80
+ const doneItems = planItems.filter(w => w.status === 'done');
81
81
  const failedItems = planItems.filter(w => w.status === 'failed');
82
82
 
83
83
  // 1. Mark plan as completed
@@ -603,7 +603,14 @@ function syncPrsFromOutput(output, agentId, meta, config) {
603
603
  dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
604
604
  }
605
605
  const entry = dirtyTargets.get(targetName);
606
- if (entry.prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
606
+ const existing = entry.prs.find(p => p.id === fullId || String(p.id) === String(prId));
607
+ if (existing) {
608
+ // Backfill prdItems if the entry was added by the poller before syncPrsFromOutput ran
609
+ if (meta?.item?.id && !existing.prdItems?.includes(meta.item.id)) {
610
+ existing.prdItems = [...(existing.prdItems || []), meta.item.id];
611
+ }
612
+ continue;
613
+ }
607
614
 
608
615
  let title = meta?.item?.title || '';
609
616
  const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
@@ -637,10 +644,26 @@ function syncPrsFromOutput(output, agentId, meta, config) {
637
644
 
638
645
  // ─── Post-Completion Hooks ──────────────────────────────────────────────────
639
646
 
647
+ /**
648
+ * Resolve which project's pull-requests.json contains a given PR ID.
649
+ * Returns the project object, or null if not found in any project file.
650
+ */
651
+ function resolveProjectForPr(prId) {
652
+ const config = getConfig();
653
+ for (const p of shared.getProjects(config)) {
654
+ const prs = safeJson(projectPrPath(p)) || [];
655
+ if (prs.some(pr => pr.id === prId)) return p;
656
+ }
657
+ return null;
658
+ }
659
+
640
660
  function updatePrAfterReview(agentId, pr, project) {
641
661
 
642
662
  if (!pr?.id) return;
643
- const prs = getPrs(project);
663
+ // Resolve actual project if not provided — avoids writing merged array to wrong path
664
+ const resolvedProject = project || resolveProjectForPr(pr.id);
665
+ if (!resolvedProject) { log('warn', `updatePrAfterReview: cannot resolve project for ${pr.id}`); return; }
666
+ const prs = getPrs(resolvedProject);
644
667
  const target = prs.find(p => p.id === pr.id);
645
668
  if (!target) return;
646
669
 
@@ -674,7 +697,7 @@ function updatePrAfterReview(agentId, pr, project) {
674
697
  shared.safeWrite(metricsPath, metrics);
675
698
  }
676
699
 
677
- shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
700
+ shared.safeWrite(shared.projectPrPath(resolvedProject), prs);
678
701
  log('info', `Updated ${pr.id} → minions review: ${target.reviewStatus} by ${reviewerName}`);
679
702
  createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
680
703
  }
@@ -682,7 +705,10 @@ function updatePrAfterReview(agentId, pr, project) {
682
705
  function updatePrAfterFix(pr, project, source) {
683
706
 
684
707
  if (!pr?.id) return;
685
- const prs = getPrs(project);
708
+ // Resolve actual project if not provided — avoids writing merged array to wrong path
709
+ const resolvedProject = project || resolveProjectForPr(pr.id);
710
+ if (!resolvedProject) { log('warn', `updatePrAfterFix: cannot resolve project for ${pr.id}`); return; }
711
+ const prs = getPrs(resolvedProject);
686
712
  const target = prs.find(p => p.id === pr.id);
687
713
  if (!target) return;
688
714
 
@@ -697,7 +723,7 @@ function updatePrAfterFix(pr, project, source) {
697
723
  log('info', `Updated ${pr.id} → reviewStatus: waiting (fix pushed)`);
698
724
  }
699
725
 
700
- shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
726
+ shared.safeWrite(shared.projectPrPath(resolvedProject), prs);
701
727
  }
702
728
 
703
729
  // ─── Post-Merge / Post-Close Hooks ───────────────────────────────────────────
@@ -740,7 +766,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
740
766
  }
741
767
 
742
768
  if (mergedItemId) {
743
- // Mark PRD feature as implemented
769
+ // Mark PRD feature as done
744
770
  const prdDir = path.join(MINIONS_DIR, 'prd');
745
771
  try {
746
772
  const planFiles = fs.readdirSync(prdDir).filter(f => f.endsWith('.json'));
@@ -749,13 +775,13 @@ async function handlePostMerge(pr, project, config, newStatus) {
749
775
  const plan = safeJson(path.join(prdDir, pf));
750
776
  if (!plan?.missing_features) continue;
751
777
  const feature = plan.missing_features.find(f => f.id === mergedItemId);
752
- if (feature && feature.status !== 'implemented') {
753
- feature.status = 'implemented';
778
+ if (feature && feature.status !== 'done') {
779
+ feature.status = 'done';
754
780
  shared.safeWrite(path.join(prdDir, pf), plan);
755
781
  updated++;
756
782
  }
757
783
  }
758
- if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as implemented for ${pr.id}`);
784
+ if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as done for ${pr.id}`);
759
785
  } catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
760
786
 
761
787
  // Mark work item as done
@@ -1011,7 +1037,8 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
1011
1037
  if (taskUsage.numTurns > cp.maxTurns) cp.maxTurns = taskUsage.numTurns;
1012
1038
  // Check if this dispatch hit the turn limit
1013
1039
  const engineConfig = require('./queries').getConfig()?.engine || {};
1014
- const turnLimit = engineConfig.maxTurns || 100;
1040
+ // maxTurns default defined in ENGINE_DEFAULTS (shared.js) avoid hardcoded fallback
1041
+ const turnLimit = engineConfig.maxTurns || shared.ENGINE_DEFAULTS.maxTurns;
1015
1042
  if (taskUsage.numTurns >= turnLimit) cp.turnLimitHits++;
1016
1043
  }
1017
1044
 
@@ -1206,7 +1233,8 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1206
1233
  if (meta?.item?.id) {
1207
1234
  try {
1208
1235
  const engineConfig = (config.engine || {});
1209
- const turnLimit = engineConfig.maxTurns || 100;
1236
+ // maxTurns resolved from config, fallback to ENGINE_DEFAULTS (shared.js)
1237
+ const turnLimit = engineConfig.maxTurns || shared.ENGINE_DEFAULTS.maxTurns;
1210
1238
  const turnCount = taskUsage?.numTurns || 0;
1211
1239
  const hitTurnLimit = turnCount >= turnLimit;
1212
1240
  let outputLogSizeBytes = 0;
@@ -1321,51 +1349,37 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1321
1349
  }
1322
1350
 
1323
1351
  if (!isSuccess && meta?.item?.id) {
1324
- // Auto-retry: read fresh _retryCount from file (not stale dispatch-time snapshot)
1325
- let retries = (meta.item._retryCount || 0);
1326
- try {
1327
- const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
1328
- ? path.join(MINIONS_DIR, 'work-items.json')
1329
- : meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
1330
- if (wiPath) {
1331
- const items = safeJson(wiPath) || [];
1332
- const wi = items.find(i => i.id === meta.item.id);
1333
- if (wi) retries = (wi._retryCount || 0); // Use fresh value from file
1334
- }
1335
- } catch { /* optional */ }
1336
-
1337
- if (retries < 3) {
1338
- log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
1339
- updateWorkItemStatus(meta, 'pending', '');
1352
+ const wiPath = resolveWiPath(meta);
1353
+ if (wiPath) {
1354
+ let finalStatus = null;
1340
1355
  try {
1341
- const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
1342
- ? path.join(MINIONS_DIR, 'work-items.json')
1343
- : meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
1344
- if (wiPath) {
1345
- const items = safeJson(wiPath) || [];
1356
+ mutateJsonFileLocked(wiPath, (items) => {
1357
+ if (!Array.isArray(items)) return items;
1346
1358
  const wi = items.find(i => i.id === meta.item.id);
1347
- if (wi) {
1348
- wi._retryCount = retries + 1; wi.status = 'pending'; delete wi.dispatched_at; delete wi.dispatched_to;
1349
- if (type === 'decompose') delete wi._decomposing; // clear so item can retry decomposition
1350
- shared.safeWrite(wiPath, items);
1359
+ if (!wi) return items;
1360
+
1361
+ const retries = wi._retryCount || 0;
1362
+ if (retries < 3) {
1363
+ log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
1364
+ wi._retryCount = retries + 1;
1365
+ wi.status = 'pending';
1366
+ delete wi.dispatched_at;
1367
+ delete wi.dispatched_to;
1368
+ finalStatus = 'pending';
1369
+ } else {
1370
+ wi.status = 'failed';
1371
+ wi.failReason = 'Agent failed (3 retries exhausted)';
1372
+ wi.failedAt = ts();
1373
+ finalStatus = 'failed';
1351
1374
  }
1352
- }
1375
+ if (type === 'decompose') delete wi._decomposing;
1376
+ return items;
1377
+ });
1353
1378
  } catch (err) { log('warn', `Retry update: ${err.message}`); }
1354
- } else {
1355
- updateWorkItemStatus(meta, 'failed', 'Agent failed (3 retries exhausted)');
1356
- }
1357
- // Clear _decomposing flag on failure so item doesn't get permanently stuck
1358
- if (type === 'decompose') {
1359
- try {
1360
- const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
1361
- ? path.join(MINIONS_DIR, 'work-items.json')
1362
- : meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
1363
- if (wiPath) {
1364
- const items = safeJson(wiPath) || [];
1365
- const wi = items.find(i => i.id === meta.item.id);
1366
- if (wi) { delete wi._decomposing; shared.safeWrite(wiPath, items); }
1367
- }
1368
- } catch (err) { log('warn', `Decompose cleanup: ${err.message}`); }
1379
+ // Sync status to PRD outside the work-items lock
1380
+ if (finalStatus) {
1381
+ syncPrdItemStatus(meta.item.id, finalStatus, meta.item?.sourcePlan);
1382
+ }
1369
1383
  }
1370
1384
  }
1371
1385
  // Meeting post-completion: collect findings/debate/conclusion
@@ -1418,7 +1432,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1418
1432
  }
1419
1433
 
1420
1434
  // Detect implement tasks that completed without creating a PR
1421
- if (isSuccess && (type === 'implement' || type === 'implement:large' || type === 'fix') && prsCreatedCount === 0 && meta?.item?.id) {
1435
+ if (isSuccess && (type === 'implement' || type === 'implement:large' || type === 'fix') && prsCreatedCount === 0 && meta?.item?.id && !meta?.item?.skipPr) {
1422
1436
  // Check if a PR already exists linked to this work item (from a previous attempt)
1423
1437
  const projects = shared.getProjects(config);
1424
1438
  const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
@@ -1511,6 +1525,7 @@ module.exports = {
1511
1525
  updateWorkItemStatus,
1512
1526
  syncPrdItemStatus,
1513
1527
  syncPrsFromOutput,
1528
+ resolveProjectForPr,
1514
1529
  updatePrAfterReview,
1515
1530
  updatePrAfterFix,
1516
1531
  handlePostMerge,
@@ -478,7 +478,7 @@ function selectPlaybook(workType, item) {
478
478
  if (workType === 'review' && !item?._pr && !item?.pr_id) {
479
479
  return 'work-item';
480
480
  }
481
- const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose', 'meeting-investigate', 'meeting-debate', 'meeting-conclude'];
481
+ const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'evaluate', 'decompose', 'meeting-investigate', 'meeting-debate', 'meeting-conclude'];
482
482
  return typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
483
483
  }
484
484
 
package/engine/queries.js CHANGED
@@ -506,9 +506,7 @@ function getWorkItems(config) {
506
506
  pending: 0,
507
507
  queued: 0,
508
508
  dispatched: 1,
509
- 'in-pr': 3, // backward compat — treated as done
510
509
  done: 3,
511
- implemented: 3,
512
510
  failed: 4,
513
511
  paused: 5,
514
512
  };
@@ -627,7 +625,7 @@ function getPrdInfo(config) {
627
625
 
628
626
  const byStatus = {};
629
627
  items.forEach(item => { const s = item.status || 'missing'; byStatus[s] = byStatus[s] || []; byStatus[s].push(item); });
630
- const complete = (byStatus['done'] || []).length + (byStatus['in-pr'] || []).length; // in-pr counted as done for backward compat
628
+ const complete = (byStatus['done'] || []).length;
631
629
  const inProgress = (byStatus['in-progress'] || []).length;
632
630
  const paused = (byStatus['paused'] || []).length;
633
631
  const missing = (byStatus['missing'] || []).length;
@@ -641,7 +639,7 @@ function getPrdInfo(config) {
641
639
  const t = planTimings[wi.sourcePlan];
642
640
  if (wi.dispatched_at) { const d = new Date(wi.dispatched_at).getTime(); if (!t.firstDispatched || d < t.firstDispatched) t.firstDispatched = d; }
643
641
  if (wi.completedAt) { const c = new Date(wi.completedAt).getTime(); if (!t.lastCompleted || c > t.lastCompleted) t.lastCompleted = c; }
644
- if (wi.status !== 'done' && wi.status !== 'in-pr') t.allDone = false; // in-pr treated as done for backward compat
642
+ if (wi.status !== 'done') t.allDone = false;
645
643
  }
646
644
 
647
645
  const progress = {
package/engine.js CHANGED
@@ -743,7 +743,7 @@ function areDependenciesMet(item, config) {
743
743
  } catch (e) { log('warn', 'read project work items for deps: ' + e.message); }
744
744
  }
745
745
  // PRD item statuses that count as "done" for dep resolution
746
- const PRD_MET_STATUSES = new Set(['done', 'in-pr', 'implemented', 'complete']);
746
+ const PRD_MET_STATUSES = new Set(['done']);
747
747
 
748
748
  for (const depId of deps) {
749
749
  const depItem = allWorkItems.find(w => w.id === depId);
@@ -977,7 +977,7 @@ function materializePlansAsWorkItems(config) {
977
977
  log('info', `PRD ${file} invalidated (was awaiting-approval) — queuing regeneration from revised plan`);
978
978
 
979
979
  // Collect completed items to carry over to new PRD
980
- const completedStatuses = new Set(['done', 'in-pr', 'implemented']); // in-pr kept for backward compat
980
+ const completedStatuses = new Set(['done']);
981
981
  const completedItems = (plan.missing_features || [])
982
982
  .filter(f => completedStatuses.has(f.status))
983
983
  .map(f => ({ id: f.id, name: f.name, status: f.status }));
@@ -1057,7 +1057,7 @@ function materializePlansAsWorkItems(config) {
1057
1057
  const useCentral = !defaultProject;
1058
1058
 
1059
1059
  const statusFilter = ['missing', 'planned'];
1060
- // Also materialize in-pr/done items that never got a work item (race with PR status sync)
1060
+ // Also materialize done items that never got a work item (race with PR status sync)
1061
1061
  const allExistingWiIds = new Set();
1062
1062
  for (const p of allProjects) {
1063
1063
  for (const w of (safeJson(projectWorkItemsPath(p)) || [])) {
@@ -1070,7 +1070,7 @@ function materializePlansAsWorkItems(config) {
1070
1070
  }
1071
1071
  const items = plan.missing_features.filter(f =>
1072
1072
  statusFilter.includes(f.status) ||
1073
- ((f.status === 'in-pr' || f.status === 'done') && f.id && !allExistingWiIds.has(f.id))
1073
+ (f.status === 'done' && f.id && !allExistingWiIds.has(f.id))
1074
1074
  );
1075
1075
 
1076
1076
  // Group items by target project (per-item project field overrides plan-level project)
@@ -2203,7 +2203,7 @@ async function tickInner() {
2203
2203
  try { await ghPollPrStatus(config); } catch (err) { log('warn', `GitHub PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
2204
2204
  // Sync PR status back to PRD items (missing → done when active PR exists)
2205
2205
  try { syncPrdFromPrs(config); } catch (err) { log('warn', `PRD sync error: ${err?.message || err}`); }
2206
- // Check if any plans can be marked completed (all features done/in-pr)
2206
+ // Check if any plans can be marked completed (all features done)
2207
2207
  try {
2208
2208
  const prdFiles = safeReadDir(PRD_DIR).filter(f => f.endsWith('.json'));
2209
2209
  for (const file of prdFiles) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.152",
3
+ "version": "0.1.154",
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"