@yemi33/minions 0.1.306 → 0.1.308

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.308 (2026-04-03)
4
+
5
+ ### Features
6
+ - Fix PR write race conditions in lifecycle.js
7
+
3
8
  ## 0.1.306 (2026-04-03)
4
9
 
5
10
  ### Features
@@ -142,7 +142,8 @@ function checkPlanCompletion(meta, config) {
142
142
  if (wrote) log('info', `PRD completion summary written to notes/inbox/`);
143
143
 
144
144
  // Persist status and _completionNotified atomically BEFORE creating work items
145
- plan._completionNotified = true;
145
+ // NOTE: Do NOT set plan._completionNotified in-memory before persist —
146
+ // if persist fails, the in-memory flag would prevent retry on next tick.
146
147
  mutateJsonFileLocked(planPath, (data) => {
147
148
  data.status = 'completed';
148
149
  data.completedAt = plan.completedAt;
@@ -154,7 +155,11 @@ function checkPlanCompletion(meta, config) {
154
155
  // Resolve the primary project for writing new work items (PR, verify)
155
156
  const projectName = plan.project;
156
157
  const primaryProject = projectName
157
- ? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) : projects[0];
158
+ ? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) : (projects[0] || null);
159
+ if (!primaryProject) {
160
+ log('warn', `checkPlanCompletion: no project available (projects array ${projects.length === 0 ? 'empty' : 'no match for ' + projectName}) — skipping PR/verify creation for ${planFile}`);
161
+ return;
162
+ }
158
163
  const wiPath = primaryProject ? shared.projectWorkItemsPath(primaryProject) : null;
159
164
  const workItems = wiPath ? (safeJson(wiPath) || []) : [];
160
165
 
@@ -424,6 +429,10 @@ function chainPlanToPrd(dispatchItem, meta, config) {
424
429
 
425
430
  const projectName = meta?.item?.project || meta?.project?.name;
426
431
  const projects = shared.getProjects(config);
432
+ if (projects.length === 0) {
433
+ log('error', 'Plan chaining: no projects configured — cannot chain plan to PRD');
434
+ return;
435
+ }
427
436
  const targetProject = projectName
428
437
  ? projects.find(p => p.name === projectName) || projects[0]
429
438
  : projects[0];
@@ -584,7 +593,11 @@ function syncPrsFromOutput(output, agentId, meta, config) {
584
593
  if (prMatches.size === 0) return 0;
585
594
 
586
595
  const projects = shared.getProjects(config);
587
- const defaultProject = (meta?.project?.name && projects.find(p => p.name === meta.project.name)) || projects[0];
596
+ if (projects.length === 0 && !meta?.project?.name) {
597
+ log('warn', `syncPrsFromOutput: no projects configured and no project in meta — cannot sync PRs`);
598
+ return 0;
599
+ }
600
+ const defaultProject = (meta?.project?.name && projects.find(p => p.name === meta.project.name)) || (projects[0] || null);
588
601
  const useCentral = !defaultProject;
589
602
 
590
603
  // Match each PR to its correct project by finding which repo URL appears near the PR number in output
@@ -612,47 +625,51 @@ function syncPrsFromOutput(output, agentId, meta, config) {
612
625
  const agentName = config.agents?.[agentId]?.name || agentId;
613
626
  let added = 0;
614
627
  const centralPrPath = path.join(MINIONS_DIR, 'pull-requests.json');
615
- // Track which PR files need writing — keyed by target name
616
- const dirtyTargets = new Map(); // name -> { prs, prPath }
617
628
 
629
+ // Group PR matches by target file so we take one lock per target
630
+ const targetPrIds = new Map(); // targetName -> { prPath, prIds: [{ prId, fullId }] }
618
631
  for (const prId of prMatches) {
619
632
  const fullId = `PR-${prId}`;
620
633
  const targetProject = useCentral ? null : resolveProjectForPr(prId);
621
634
  const targetName = targetProject ? targetProject.name : '_central';
622
635
  const prPath = targetProject ? shared.projectPrPath(targetProject) : centralPrPath;
623
-
624
- // Load PRs for this target (cache per target)
625
- if (!dirtyTargets.has(targetName)) {
626
- dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
627
- }
628
- const entry = dirtyTargets.get(targetName);
629
- if (entry.prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
630
-
631
- let title = meta?.item?.title || '';
632
- const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
633
- if (titleMatch) title = titleMatch[1].trim();
634
- if (title.includes('session_id') || title.includes('is_error') || title.includes('uuid') || title.length > 120) {
635
- title = meta?.item?.title || '';
636
+ if (!targetPrIds.has(targetName)) {
637
+ targetPrIds.set(targetName, { prPath, prIds: [] });
636
638
  }
637
- entry.prs.push({
638
- id: fullId,
639
- title: (title || `PR created by ${agentName}`).slice(0, 120),
640
- agent: agentName,
641
- branch: meta?.branch || '',
642
- reviewStatus: 'pending',
643
- status: 'active',
644
- created: dateStamp(),
645
- url: extractPrUrl(prId),
646
- prdItems: meta?.item?.id ? [meta.item.id] : [],
647
- sourcePlan: meta?.item?.sourcePlan || '',
648
- itemType: meta?.item?.itemType || ''
649
- });
650
- if (meta?.item?.id) addPrLink(fullId, meta.item.id);
651
- added++;
639
+ targetPrIds.get(targetName).prIds.push({ prId, fullId });
652
640
  }
653
641
 
654
- for (const [name, entry] of dirtyTargets) {
655
- shared.safeWrite(entry.prPath, entry.prs);
642
+ // For each target, read+modify+write inside a single locked callback
643
+ for (const [name, { prPath, prIds }] of targetPrIds) {
644
+ mutateJsonFileLocked(prPath, (prs) => {
645
+ if (!Array.isArray(prs)) prs = [];
646
+ for (const { prId, fullId } of prIds) {
647
+ if (prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
648
+
649
+ let title = meta?.item?.title || '';
650
+ const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
651
+ if (titleMatch) title = titleMatch[1].trim();
652
+ if (title.includes('session_id') || title.includes('is_error') || title.includes('uuid') || title.length > 120) {
653
+ title = meta?.item?.title || '';
654
+ }
655
+ prs.push({
656
+ id: fullId,
657
+ title: (title || `PR created by ${agentName}`).slice(0, 120),
658
+ agent: agentName,
659
+ branch: meta?.branch || '',
660
+ reviewStatus: 'pending',
661
+ status: 'active',
662
+ created: dateStamp(),
663
+ url: extractPrUrl(prId),
664
+ prdItems: meta?.item?.id ? [meta.item.id] : [],
665
+ sourcePlan: meta?.item?.sourcePlan || '',
666
+ itemType: meta?.item?.itemType || ''
667
+ });
668
+ if (meta?.item?.id) addPrLink(fullId, meta.item.id);
669
+ added++;
670
+ }
671
+ return prs;
672
+ }, { defaultValue: [] });
656
673
  log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
657
674
  }
658
675
  return added;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.306",
3
+ "version": "0.1.308",
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"