@yemi33/minions 0.1.307 → 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,9 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.307 (2026-04-03)
3
+ ## 0.1.308 (2026-04-03)
4
+
5
+ ### Features
6
+ - Fix PR write race conditions in lifecycle.js
7
+
8
+ ## 0.1.306 (2026-04-03)
4
9
 
5
10
  ### Features
6
- - guard projects[0] fallbacks in lifecycle.js
7
11
  - Add null guards to dashboard.js request handlers
8
12
 
9
13
  ### Fixes
@@ -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;
@@ -624,47 +625,51 @@ function syncPrsFromOutput(output, agentId, meta, config) {
624
625
  const agentName = config.agents?.[agentId]?.name || agentId;
625
626
  let added = 0;
626
627
  const centralPrPath = path.join(MINIONS_DIR, 'pull-requests.json');
627
- // Track which PR files need writing — keyed by target name
628
- const dirtyTargets = new Map(); // name -> { prs, prPath }
629
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 }] }
630
631
  for (const prId of prMatches) {
631
632
  const fullId = `PR-${prId}`;
632
633
  const targetProject = useCentral ? null : resolveProjectForPr(prId);
633
634
  const targetName = targetProject ? targetProject.name : '_central';
634
635
  const prPath = targetProject ? shared.projectPrPath(targetProject) : centralPrPath;
635
-
636
- // Load PRs for this target (cache per target)
637
- if (!dirtyTargets.has(targetName)) {
638
- dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
639
- }
640
- const entry = dirtyTargets.get(targetName);
641
- if (entry.prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
642
-
643
- let title = meta?.item?.title || '';
644
- const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
645
- if (titleMatch) title = titleMatch[1].trim();
646
- if (title.includes('session_id') || title.includes('is_error') || title.includes('uuid') || title.length > 120) {
647
- title = meta?.item?.title || '';
636
+ if (!targetPrIds.has(targetName)) {
637
+ targetPrIds.set(targetName, { prPath, prIds: [] });
648
638
  }
649
- entry.prs.push({
650
- id: fullId,
651
- title: (title || `PR created by ${agentName}`).slice(0, 120),
652
- agent: agentName,
653
- branch: meta?.branch || '',
654
- reviewStatus: 'pending',
655
- status: 'active',
656
- created: dateStamp(),
657
- url: extractPrUrl(prId),
658
- prdItems: meta?.item?.id ? [meta.item.id] : [],
659
- sourcePlan: meta?.item?.sourcePlan || '',
660
- itemType: meta?.item?.itemType || ''
661
- });
662
- if (meta?.item?.id) addPrLink(fullId, meta.item.id);
663
- added++;
639
+ targetPrIds.get(targetName).prIds.push({ prId, fullId });
664
640
  }
665
641
 
666
- for (const [name, entry] of dirtyTargets) {
667
- 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: [] });
668
673
  log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
669
674
  }
670
675
  return added;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.307",
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"