@yemi33/minions 0.1.329 → 0.1.330

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,8 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.329 (2026-04-03)
3
+ ## 0.1.330 (2026-04-03)
4
4
 
5
5
  ### Fixes
6
+ - resolve all 25 lifecycle.js test failures
6
7
  - remove orphan statuses, add validation, replace in-progress with dispatched
7
8
 
8
9
  ## 0.1.328 (2026-04-03)
@@ -137,16 +137,28 @@ function checkPlanCompletion(meta, config) {
137
137
  ].filter(Boolean).join('\n');
138
138
 
139
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}`);
140
+ const summarySlug = `prd-completion-${planFile.replace('.json', '')}`;
141
+ shared.writeToInbox('engine', summarySlug, summary);
142
+ log('info', `PRD completion summary written to notes/inbox/${summarySlug}`);
143
+
144
+ // Persist completed status + _completionNotified via file lock
145
+ mutateJsonFileLocked(planPath, (data) => {
146
+ data.status = PLAN_STATUS.COMPLETED;
147
+ data.completedAt = plan.completedAt;
148
+ data._completionNotified = true;
149
+ return data;
150
+ });
143
151
 
144
152
  // Resolve the primary project for writing new work items (PR, verify)
145
153
  const projectName = plan.project;
146
154
  const primaryProject = projectName
147
155
  ? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) : projects[0];
148
- const wiPath = primaryProject ? shared.projectWorkItemsPath(primaryProject) : null;
149
- const workItems = wiPath ? (safeJson(wiPath) || []) : [];
156
+ if (!primaryProject) {
157
+ log('warn', `Plan ${planFile}: no primary project found skipping PR/verify creation`);
158
+ return;
159
+ }
160
+ const wiPath = shared.projectWorkItemsPath(primaryProject);
161
+ const workItems = safeJson(wiPath) || [];
150
162
 
151
163
  // 3. For shared-branch plans, create PR work item
152
164
  if (plan.branch_strategy === 'shared-branch' && plan.feature_branch && wiPath) {
@@ -262,47 +274,94 @@ function checkPlanCompletion(meta, config) {
262
274
  log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
263
275
  }
264
276
 
265
- // 5. Archive: move PRD .json to prd/archive/ and source .md plan to plans/archive/
277
+ // Archive deferred until verify completes
278
+
279
+ log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
280
+ }
281
+
282
+ // ─── Archive Plan ───────────────────────────────────────────────────────────
283
+ function archivePlan(planFile, plan, projects, config) {
284
+ const planPath = path.join(PRD_DIR, planFile);
285
+
286
+ // Archive PRD .json to prd/archive/
266
287
  const prdArchiveDir = path.join(PRD_DIR, 'archive');
267
288
  if (!fs.existsSync(prdArchiveDir)) fs.mkdirSync(prdArchiveDir, { recursive: true });
268
- shared.safeWrite(planPath, plan); // save completed status first
269
289
  try {
270
- fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
271
- log('info', `Archived completed PRD: prd/archive/${planFile}`);
290
+ if (fs.existsSync(planPath)) {
291
+ fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
292
+ log('info', `Archived completed PRD: prd/archive/${planFile}`);
293
+ }
272
294
  } catch (err) {
273
295
  log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
274
- shared.safeWrite(planPath, plan);
275
296
  }
276
297
 
277
- // Also archive the source .md plan if it exists
298
+ // Archive the source .md plan if it exists
299
+ const projectName = plan.project;
278
300
  const planArchiveDir = path.join(PLANS_DIR, 'archive');
279
301
  if (!fs.existsSync(planArchiveDir)) fs.mkdirSync(planArchiveDir, { recursive: true });
280
302
  try {
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;
303
+ // Direct match by source_plan field or planFile-derived name
304
+ const sourcePlanName = plan.source_plan || planFile.replace(/\.json$/, '.md');
305
+ if (sourcePlanName && fs.existsSync(path.join(PLANS_DIR, sourcePlanName))) {
306
+ try {
307
+ fs.renameSync(path.join(PLANS_DIR, sourcePlanName), path.join(planArchiveDir, sourcePlanName));
308
+ log('info', `Archived source plan: plans/archive/${sourcePlanName}`);
309
+ } catch (err) { log('warn', `Failed to archive plan ${sourcePlanName}: ${err.message}`); }
310
+ } else {
311
+ // Fallback: match by content
312
+ const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
313
+ for (const md of mdFiles) {
314
+ const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
315
+ if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
316
+ try {
317
+ fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
318
+ log('info', `Archived source plan: plans/archive/${md}`);
319
+ } catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
320
+ break;
321
+ }
291
322
  }
292
323
  }
293
324
  } catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
294
325
 
295
- // 6. Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
326
+ // Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
296
327
  try {
297
- // Collect all branch slugs: shared-branch + per-item branches + item IDs
298
328
  const branchSlugs = new Set();
299
329
  if (plan.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
330
+
331
+ // Collect work items for this plan
332
+ let allWorkItems = [];
333
+ for (const p of projects) {
334
+ try {
335
+ const wi = safeJson(shared.projectWorkItemsPath(p)) || [];
336
+ allWorkItems = allWorkItems.concat(wi);
337
+ } catch { /* optional */ }
338
+ }
339
+ try {
340
+ const central = safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [];
341
+ for (const w of central) {
342
+ if (!allWorkItems.some(existing => existing.id === w.id)) allWorkItems.push(w);
343
+ }
344
+ } catch { /* optional */ }
345
+ const planItems = allWorkItems.filter(w => w.sourcePlan === planFile);
346
+ const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
347
+
300
348
  for (const w of doneItems) {
301
349
  if (w.branch) branchSlugs.add(shared.sanitizeBranch(w.branch).toLowerCase());
302
350
  if (w.id) branchSlugs.add(w.id.toLowerCase());
303
351
  }
304
- for (const pr of uniquePrs) {
305
- if (pr.branch) branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
352
+
353
+ // Collect PR branches
354
+ for (const p of projects) {
355
+ try {
356
+ const prs = safeJson(shared.projectPrPath(p)) || [];
357
+ const prLinks = getPrLinks();
358
+ for (const pr of prs) {
359
+ const linkedId = prLinks[pr.id];
360
+ if (linkedId && doneItems.find(w => w.id === linkedId) && pr.branch) {
361
+ branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
362
+ }
363
+ }
364
+ } catch { /* optional */ }
306
365
  }
307
366
 
308
367
  let cleanedWt = 0;
@@ -323,10 +382,8 @@ function checkPlanCompletion(meta, config) {
323
382
  }
324
383
  }
325
384
  }
326
- if (cleanedWt > 0) log('info', `Plan completion: cleaned ${cleanedWt} worktree(s)`);
385
+ if (cleanedWt > 0) log('info', `Archive: cleaned ${cleanedWt} worktree(s)`);
327
386
  } catch (err) { log('warn', `Worktree cleanup: ${err.message}`); }
328
-
329
- log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
330
387
  }
331
388
 
332
389
  // ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
@@ -385,6 +442,10 @@ function chainPlanToPrd(dispatchItem, meta, config) {
385
442
 
386
443
  const projectName = meta?.item?.project || meta?.project?.name;
387
444
  const projects = shared.getProjects(config);
445
+ if (projects.length === 0) {
446
+ log('error', 'Plan chaining: no projects configured');
447
+ return;
448
+ }
388
449
  const targetProject = projectName
389
450
  ? projects.find(p => p.name === projectName) || projects[0]
390
451
  : projects[0];
@@ -397,7 +458,10 @@ function chainPlanToPrd(dispatchItem, meta, config) {
397
458
  log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
398
459
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
399
460
  let items = [];
400
- try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch {}
461
+ try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch (err) {
462
+ log('warn', `Failed to parse ${wiPath}: ${err.message}`);
463
+ try { fs.copyFileSync(wiPath, wiPath + '.bak'); } catch {}
464
+ }
401
465
  items.push({
402
466
  id: 'W-' + shared.uid(),
403
467
  title: `Convert plan to PRD: ${meta?.item?.title || planFile.name}`,
@@ -549,6 +613,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
549
613
  if (prMatches.size === 0) return 0;
550
614
 
551
615
  const projects = shared.getProjects(config);
616
+ if (projects.length === 0 && !meta?.project?.name) return 0;
552
617
  const defaultProject = (meta?.project?.name && projects.find(p => p.name === meta.project.name)) || projects[0];
553
618
  const useCentral = !defaultProject;
554
619
 
@@ -577,8 +642,9 @@ function syncPrsFromOutput(output, agentId, meta, config) {
577
642
  const agentName = config.agents?.[agentId]?.name || agentId;
578
643
  let added = 0;
579
644
  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 }
645
+
646
+ // Group new PRs by target file path
647
+ const newPrsByPath = new Map(); // prPath -> [{ prId, newEntry }]
582
648
 
583
649
  for (const prId of prMatches) {
584
650
  const fullId = `PR-${prId}`;
@@ -586,38 +652,43 @@ function syncPrsFromOutput(output, agentId, meta, config) {
586
652
  const targetName = targetProject ? targetProject.name : '_central';
587
653
  const prPath = targetProject ? shared.projectPrPath(targetProject) : centralPrPath;
588
654
 
589
- // Load PRs for this target (cache per target)
590
- if (!dirtyTargets.has(targetName)) {
591
- dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
592
- }
593
- const entry = dirtyTargets.get(targetName);
594
- if (entry.prs.some(p => p.id === fullId || String(p.id).includes(prId))) continue;
595
-
596
655
  let title = meta?.item?.title || '';
597
656
  const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
598
657
  if (titleMatch) title = titleMatch[1].trim();
599
658
  if (title.includes('session_id') || title.includes('is_error') || title.includes('uuid') || title.length > 120) {
600
659
  title = meta?.item?.title || '';
601
660
  }
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 || ''
661
+
662
+ if (!newPrsByPath.has(prPath)) newPrsByPath.set(prPath, { name: targetName, entries: [] });
663
+ newPrsByPath.get(prPath).entries.push({
664
+ prId, fullId,
665
+ entry: {
666
+ id: fullId,
667
+ title: (title || `PR created by ${agentName}`).slice(0, 120),
668
+ agent: agentName,
669
+ branch: meta?.branch || '',
670
+ reviewStatus: 'pending',
671
+ status: PR_STATUS.ACTIVE,
672
+ created: dateStamp(),
673
+ url: extractPrUrl(prId),
674
+ prdItems: meta?.item?.id ? [meta.item.id] : [],
675
+ sourcePlan: meta?.item?.sourcePlan || '',
676
+ itemType: meta?.item?.itemType || ''
677
+ }
614
678
  });
615
- if (meta?.item?.id) addPrLink(fullId, meta.item.id);
616
- added++;
617
679
  }
618
680
 
619
- for (const [name, entry] of dirtyTargets) {
620
- shared.safeWrite(entry.prPath, entry.prs);
681
+ for (const [prPath, { name, entries }] of newPrsByPath) {
682
+ mutateJsonFileLocked(prPath, (data) => {
683
+ const prs = Array.isArray(data) ? data : [];
684
+ for (const { prId, fullId, entry } of entries) {
685
+ if (prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
686
+ prs.push(entry);
687
+ if (meta?.item?.id) addPrLink(fullId, meta.item.id);
688
+ added++;
689
+ }
690
+ return prs;
691
+ });
621
692
  log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
622
693
  }
623
694
  return added;
@@ -662,7 +733,7 @@ function updatePrAfterReview(agentId, pr, project) {
662
733
  }
663
734
 
664
735
  shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
665
- log('info', `Updated ${pr.id} → minions review: ${minionsVerdict} by ${reviewerName}`);
736
+ log('info', `Updated ${pr.id} → minions review: ${target.reviewStatus} by ${reviewerName}`);
666
737
  createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
667
738
  }
668
739
 
@@ -1137,6 +1208,19 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1137
1208
  let prsCreatedCount = 0;
1138
1209
  if (isSuccess) prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
1139
1210
 
1211
+ // After verify completes, archive the plan
1212
+ if (isSuccess && meta?.item?.itemType === 'verify' && meta?.item?.sourcePlan) {
1213
+ try {
1214
+ const vPlanFile = meta.item.sourcePlan;
1215
+ const vPlanPath = path.join(PRD_DIR, vPlanFile);
1216
+ const vPlan = safeJson(vPlanPath);
1217
+ if (vPlan) {
1218
+ const vProjects = shared.getProjects(config);
1219
+ archivePlan(vPlanFile, vPlan, vProjects, config);
1220
+ }
1221
+ } catch (err) { log('warn', `Verify archive: ${err.message}`); }
1222
+ }
1223
+
1140
1224
  // Clean up worktree for non-shared-branch tasks after completion
1141
1225
  if (meta?.branch && meta?.branchStrategy !== 'shared-branch') {
1142
1226
  try {
@@ -1263,6 +1347,7 @@ function syncPrdFromPrs(config) {
1263
1347
 
1264
1348
  module.exports = {
1265
1349
  checkPlanCompletion,
1350
+ archivePlan,
1266
1351
  updateWorkItemStatus,
1267
1352
  syncPrdItemStatus,
1268
1353
  syncPrsFromOutput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.329",
3
+ "version": "0.1.330",
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"