@yemi33/minions 0.1.329 → 0.1.331

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,14 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.329 (2026-04-03)
3
+ ## 0.1.331 (2026-04-03)
4
4
 
5
5
  ### Fixes
6
+ - standardize PR created field to ISO format
7
+
8
+ ## 0.1.330 (2026-04-03)
9
+
10
+ ### Fixes
11
+ - resolve all 25 lifecycle.js test failures
6
12
  - remove orphan statuses, add validation, replace in-progress with dispatched
7
13
 
8
14
  ## 0.1.328 (2026-04-03)
@@ -27,7 +27,7 @@ function prRow(pr) {
27
27
  '<td>' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escHtml(sq.note || '') + '">' + escHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escHtml(sq.reviewer) + '…</span>' : pr.reviewedBy && pr.reviewedBy.length ? '<span class="pr-agent">' + escHtml(pr.reviewedBy.join(', ')) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
28
28
  '<td><span class="pr-badge ' + buildClass + '">' + escHtml(buildLabel) + '</span></td>' +
29
29
  '<td><span class="pr-badge ' + statusClass + '">' + escHtml(statusLabel) + '</span></td>' +
30
- '<td><span class="pr-date">' + escHtml(pr.created || '—') + '</span></td>' +
30
+ '<td><span class="pr-date">' + escHtml((pr.created || '—').slice(0, 10)) + '</span></td>' +
31
31
  '</tr>';
32
32
  }
33
33
 
package/dashboard.js CHANGED
@@ -3458,7 +3458,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3458
3458
  branch: '',
3459
3459
  reviewStatus: autoObserve ? 'pending' : 'none',
3460
3460
  status: autoObserve ? 'active' : 'linked',
3461
- created: new Date().toISOString().slice(0, 10),
3461
+ created: new Date().toISOString(),
3462
3462
  url,
3463
3463
  prdItems: [],
3464
3464
  _manual: true,
package/engine/ado.js CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  const path = require('path');
7
7
  const shared = require('./shared');
8
- const { exec, getAdoOrgBase, addPrLink, log, dateStamp, PR_STATUS } = shared;
8
+ const { exec, getAdoOrgBase, addPrLink, log, ts, dateStamp, PR_STATUS } = shared;
9
9
  const { getPrs } = require('./queries');
10
10
 
11
11
  // Lazy require to avoid circular dependency — only needed for engine().handlePostMerge
@@ -389,7 +389,7 @@ async function reconcilePrs(config) {
389
389
  branch,
390
390
  reviewStatus: 'pending',
391
391
  status: 'active',
392
- created: (adoPr.creationDate || '').slice(0, 10) || dateStamp(),
392
+ created: adoPr.creationDate || ts(),
393
393
  url: prUrl,
394
394
  prdItems: confirmedItemId ? [confirmedItemId] : [],
395
395
  });
package/engine/github.js CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  const shared = require('./shared');
8
- const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR, addPrLink, getPrLinks, log, dateStamp, PR_STATUS } = shared;
8
+ const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR, addPrLink, getPrLinks, log, ts, dateStamp, PR_STATUS } = shared;
9
9
  const { getPrs } = require('./queries');
10
10
  const path = require('path');
11
11
 
@@ -373,7 +373,7 @@ async function reconcilePrs(config) {
373
373
  branch,
374
374
  reviewStatus: 'pending',
375
375
  status: 'active',
376
- created: (ghPr.created_at || '').slice(0, 10) || dateStamp(),
376
+ created: ghPr.created_at || ts(),
377
377
  url: prUrl,
378
378
  prdItems: confirmedItemId ? [confirmedItemId] : [],
379
379
  });
@@ -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,47 @@ 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: ts(),
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
+ // Normalize legacy YYYY-MM-DD created dates to ISO
685
+ for (const p of prs) {
686
+ if (p.created && p.created.length === 10) p.created = p.created + 'T00:00:00.000Z';
687
+ }
688
+ for (const { prId, fullId, entry } of entries) {
689
+ if (prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
690
+ prs.push(entry);
691
+ if (meta?.item?.id) addPrLink(fullId, meta.item.id);
692
+ added++;
693
+ }
694
+ return prs;
695
+ });
621
696
  log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
622
697
  }
623
698
  return added;
@@ -662,7 +737,7 @@ function updatePrAfterReview(agentId, pr, project) {
662
737
  }
663
738
 
664
739
  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}`);
740
+ log('info', `Updated ${pr.id} → minions review: ${target.reviewStatus} by ${reviewerName}`);
666
741
  createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
667
742
  }
668
743
 
@@ -1137,6 +1212,19 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1137
1212
  let prsCreatedCount = 0;
1138
1213
  if (isSuccess) prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
1139
1214
 
1215
+ // After verify completes, archive the plan
1216
+ if (isSuccess && meta?.item?.itemType === 'verify' && meta?.item?.sourcePlan) {
1217
+ try {
1218
+ const vPlanFile = meta.item.sourcePlan;
1219
+ const vPlanPath = path.join(PRD_DIR, vPlanFile);
1220
+ const vPlan = safeJson(vPlanPath);
1221
+ if (vPlan) {
1222
+ const vProjects = shared.getProjects(config);
1223
+ archivePlan(vPlanFile, vPlan, vProjects, config);
1224
+ }
1225
+ } catch (err) { log('warn', `Verify archive: ${err.message}`); }
1226
+ }
1227
+
1140
1228
  // Clean up worktree for non-shared-branch tasks after completion
1141
1229
  if (meta?.branch && meta?.branchStrategy !== 'shared-branch') {
1142
1230
  try {
@@ -1263,6 +1351,7 @@ function syncPrdFromPrs(config) {
1263
1351
 
1264
1352
  module.exports = {
1265
1353
  checkPlanCompletion,
1354
+ archivePlan,
1266
1355
  updateWorkItemStatus,
1267
1356
  syncPrdItemStatus,
1268
1357
  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.331",
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"