@yemi33/minions 0.1.155 → 0.1.157

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,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.157 (2026-04-02)
4
+
5
+ ### Engine
6
+ - engine.js
7
+ - engine/ado.js
8
+ - engine/github.js
9
+ - engine/shared.js
10
+
11
+ ### Other
12
+ - pipelines/daily-standup.json
13
+ - test/unit.test.js
14
+
15
+ ## 0.1.156 (2026-04-02)
16
+
17
+ ### Engine
18
+ - engine.js
19
+ - engine/shared.js
20
+
21
+ ### Dashboard
22
+ - dashboard/js/render-plans.js
23
+ - dashboard/js/render-prd.js
24
+
25
+ ### Other
26
+ - test/unit.test.js
27
+
3
28
  ## 0.1.155 (2026-04-02)
4
29
 
5
30
  ### Engine
@@ -66,9 +66,7 @@ function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems) {
66
66
  const implementWi = wi.filter(w => w.type !== 'plan-to-prd' && w.type !== 'verify');
67
67
  const hasPendingPrd = wi.some(w => w.type === 'plan-to-prd' && (w.status === 'pending' || w.status === 'dispatched'));
68
68
  const hasActiveWork = implementWi.some(w => w.status === 'pending' || w.status === 'dispatched');
69
- const allDone = implementWi.length > 0 && implementWi.every(w =>
70
- w.status === 'done'
71
- );
69
+ const allDone = implementWi.length > 0 && implementWi.every(w => w.status === 'done');
72
70
  const hasFailed = implementWi.some(w => w.status === 'failed');
73
71
 
74
72
  // User-set statuses take priority when no work has started
@@ -304,6 +302,48 @@ function openArchivedPlansModal() {
304
302
  document.getElementById('modal').classList.add('open');
305
303
  }
306
304
 
305
+ // Disable all PRD action buttons to prevent double-clicks
306
+ function qaDisablePrdButtons() {
307
+ const container = document.getElementById('qa-generate-prd-btn');
308
+ if (container) container.querySelectorAll('button').forEach(b => { b.disabled = true; b.style.opacity = '0.5'; });
309
+ }
310
+
311
+ // Show plan version action buttons (Run alongside / Replace / Just save)
312
+ function showPlanVersionActions(thread, newFile, originalFile) {
313
+ const esc = newFile.replace(/'/g, "\\'");
314
+ // Look up existing PRD for the original plan's project
315
+ const allPlans = window._lastStatus?.plans || [];
316
+ const origPlan = allPlans.find(p => p.file === originalFile);
317
+ const project = origPlan?.project || '';
318
+ const existingPrd = allPlans.find(p => p.file.endsWith('.json') && p.project === project && p.status !== 'completed');
319
+
320
+ const btn = document.createElement('div');
321
+ btn.id = 'qa-generate-prd-btn';
322
+ btn.style.cssText = 'margin:8px 0;padding:8px 12px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:6px;display:flex;flex-wrap:wrap;align-items:center;gap:8px';
323
+
324
+ if (existingPrd) {
325
+ btn.innerHTML = '<span style="color:var(--green);font-weight:600;font-size:12px;width:100%">New plan version created — existing PRD running</span>' +
326
+ '<button onclick="qaNewPrd(\'' + esc + '\')" style="background:var(--green);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer" title="Execute this plan as a separate PRD alongside the current one">Run alongside</button>' +
327
+ '<button onclick="qaReplacePrd(\'' + esc + '\')" style="background:var(--orange);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer" title="Pause existing PRD, clean pending items, execute this plan instead">Replace old PRD</button>' +
328
+ '<button onclick="qaJustSave(this)" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:4px 12px;font-size:11px;cursor:pointer" title="Keep the new version saved without dispatching any work">Just save</button>' +
329
+ '<span style="color:var(--muted);font-size:10px;width:100%">Run alongside keeps current work going. Replace pauses it and starts fresh.</span>';
330
+ } else {
331
+ btn.innerHTML = '<span style="color:var(--green);font-weight:600;font-size:12px;width:100%">New plan version created</span>' +
332
+ '<button onclick="qaNewPrd(\'' + esc + '\')" style="background:var(--green);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer">Execute plan</button>' +
333
+ '<button onclick="qaJustSave(this)" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:4px 12px;font-size:11px;cursor:pointer">Just save</button>' +
334
+ '<span style="color:var(--muted);font-size:10px">Execute dispatches an agent to create PRD items from this plan</span>';
335
+ }
336
+ // Remove any previous action buttons
337
+ const old = thread.querySelector('#qa-generate-prd-btn');
338
+ if (old) old.remove();
339
+ thread.appendChild(btn);
340
+ }
341
+
342
+ function qaJustSave(el) {
343
+ const container = el.closest('#qa-generate-prd-btn');
344
+ if (container) container.innerHTML = '<span style="color:var(--muted);font-size:11px">Saved. No work dispatched.</span>';
345
+ }
346
+
307
347
  async function planExecute(file, project, btn) {
308
348
  if (btn) { btn.textContent = 'Executing...'; btn.disabled = true; btn.style.color = 'var(--blue)'; }
309
349
  try {
@@ -694,4 +734,4 @@ async function planUnarchive(file, btn) {
694
734
  } catch (e) { resetBtn(); alert('Error: ' + e.message); }
695
735
  }
696
736
 
697
- window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, renderPlans, openArchivedPlansModal, planExecute, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planArchive, planUnarchive, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
737
+ window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, renderPlans, openArchivedPlansModal, qaDisablePrdButtons, showPlanVersionActions, qaJustSave, planExecute, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planArchive, planUnarchive, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
@@ -116,7 +116,7 @@ function renderPrdProgress(prog) {
116
116
  'failed': 'background:rgba(248,81,73,0.15);color:var(--red)',
117
117
  'paused': 'background:rgba(139,148,158,0.15);color:var(--muted)',
118
118
  };
119
- const labels = { 'done': 'DONE', 'in-progress': 'WIP', 'failed': 'FAIL', 'paused': 'PAUSED', 'missing': '' };
119
+ const labels = { 'done': 'DONE', 'in-progress': 'WIP', 'failed': 'FAIL', 'paused': 'PAUSED', 'missing': '\u2014' };
120
120
  const style = styles[s] || 'background:var(--surface);color:var(--muted)';
121
121
  const label = labels[s] || '—';
122
122
  return '<span style="font-size:9px;font-weight:700;padding:2px 6px;border-radius:3px;letter-spacing:0.5px;white-space:nowrap;' + style + '">' + label + '</span>';
package/engine/ado.js CHANGED
@@ -386,19 +386,7 @@ async function reconcilePrs(config) {
386
386
  log('info', `PR reconciliation: added ${prId} (branch: ${branch}${confirmedItemId ? ', linked to ' + confirmedItemId : ''}) to ${project.name}`);
387
387
  }
388
388
 
389
- // Backfill prdItems from pr-links for any PR with empty array
390
- const prLinks = shared.getPrLinks();
391
- let backfilled = 0;
392
- for (const pr of existingPrs) {
393
- const linked = prLinks[pr.id];
394
- if (linked && !(pr.prdItems || []).includes(linked)) {
395
- pr.prdItems = Array.isArray(pr.prdItems) ? pr.prdItems : [];
396
- pr.prdItems.push(linked);
397
- backfilled++;
398
- }
399
- }
400
-
401
- if (projectAdded > 0 || projectUpdated > 0 || backfilled > 0) {
389
+ if (projectAdded > 0 || projectUpdated > 0) {
402
390
  shared.safeWrite(prPath, existingPrs);
403
391
  totalAdded += projectAdded;
404
392
  if (projectUpdated > 0) log('info', `PR reconciliation: linked ${projectUpdated} existing PR(s) to PRD items in ${project.name}`);
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 } = shared;
8
+ const { exec, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeWrite, MINIONS_DIR, addPrLink, log, dateStamp } = shared;
9
9
  const { getPrs } = require('./queries');
10
10
  const path = require('path');
11
11
 
@@ -371,19 +371,7 @@ async function reconcilePrs(config) {
371
371
  log('info', `GitHub PR reconciliation: added ${prId} (branch: ${branch}${confirmedItemId ? ', linked to ' + confirmedItemId : ''}) to ${project.name}`);
372
372
  }
373
373
 
374
- // Backfill prdItems from pr-links for any PR with empty array
375
- const prLinks = getPrLinks();
376
- let backfilled = 0;
377
- for (const pr of existingPrs) {
378
- const linked = prLinks[pr.id];
379
- if (linked && !(pr.prdItems || []).includes(linked)) {
380
- pr.prdItems = Array.isArray(pr.prdItems) ? pr.prdItems : [];
381
- pr.prdItems.push(linked);
382
- backfilled++;
383
- }
384
- }
385
-
386
- if (projectAdded > 0 || backfilled > 0) {
374
+ if (projectAdded > 0) {
387
375
  safeWrite(prPath, existingPrs);
388
376
  totalAdded += projectAdded;
389
377
  }
package/engine/shared.js CHANGED
@@ -404,10 +404,26 @@ function projectStateDir(project) {
404
404
  return dir;
405
405
  }
406
406
 
407
+ const CENTRAL_WI_PATH = path.join(MINIONS_DIR, 'work-items.json');
408
+
407
409
  function projectWorkItemsPath(project) {
408
410
  return path.join(projectStateDir(project), 'work-items.json');
409
411
  }
410
412
 
413
+ /**
414
+ * Resolve work-items.json path from dispatch meta.
415
+ * Central items → CENTRAL_WI_PATH; project items → projects/<name>/work-items.json.
416
+ */
417
+ function resolveWiPath(meta) {
418
+ if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
419
+ return CENTRAL_WI_PATH;
420
+ }
421
+ if (meta.project?.name) {
422
+ return path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json');
423
+ }
424
+ return null;
425
+ }
426
+
411
427
  function projectPrPath(project) {
412
428
  return path.join(projectStateDir(project), 'pull-requests.json');
413
429
  }
@@ -501,15 +517,55 @@ function parseSkillFrontmatter(content, filename) {
501
517
  // Never touched by polling loops — only written when a PR is first linked to a PRD item.
502
518
 
503
519
  function getPrLinks() {
504
- try { return JSON.parse(require('fs').readFileSync(PR_LINKS_PATH, 'utf8')); } catch { return {}; }
520
+ // Derive from PR.prdItems (single source of truth)
521
+ const links = {};
522
+ try {
523
+ const projects = getProjects();
524
+ for (const project of projects) {
525
+ const prs = safeJson(projectPrPath(project)) || [];
526
+ for (const pr of prs) {
527
+ for (const itemId of (pr.prdItems || [])) {
528
+ if (!links[pr.id]) links[pr.id] = itemId;
529
+ }
530
+ }
531
+ }
532
+ } catch { /* optional */ }
533
+ return links;
505
534
  }
506
535
 
507
536
  function addPrLink(prId, itemId) {
508
537
  if (!prId || !itemId) return;
509
- const links = getPrLinks();
510
- if (links[prId] === itemId) return; // already correct, no write needed
511
- links[prId] = itemId;
512
- safeWrite(PR_LINKS_PATH, links);
538
+ try {
539
+ const projects = getProjects();
540
+ for (const project of projects) { linkPrToItem(project, prId, itemId); }
541
+ } catch { /* optional */ }
542
+ }
543
+
544
+ /**
545
+ * Locked mutation of a project's pull-requests.json.
546
+ * Single source of truth for PR data including prdItems links.
547
+ */
548
+ function mutatePrs(project, mutateFn) {
549
+ const prPath = projectPrPath(project);
550
+ return mutateJsonFileLocked(prPath, (prs) => {
551
+ return mutateFn(Array.isArray(prs) ? prs : []);
552
+ }, { defaultValue: [] });
553
+ }
554
+
555
+ /**
556
+ * Link a PR to a work item via PR.prdItems (single source of truth).
557
+ * Uses file-locked mutation to prevent race conditions.
558
+ */
559
+ function linkPrToItem(project, prId, itemId) {
560
+ if (!prId || !itemId) return;
561
+ mutatePrs(project, (prs) => {
562
+ const pr = prs.find(p => p.id === prId);
563
+ if (pr) {
564
+ pr.prdItems = Array.isArray(pr.prdItems) ? pr.prdItems : [];
565
+ if (!pr.prdItems.includes(itemId)) pr.prdItems.push(itemId);
566
+ }
567
+ return prs;
568
+ });
513
569
  }
514
570
 
515
571
  module.exports = {
@@ -545,10 +601,14 @@ module.exports = {
545
601
  getProjects,
546
602
  projectRoot,
547
603
  projectStateDir,
604
+ CENTRAL_WI_PATH,
548
605
  projectWorkItemsPath,
606
+ resolveWiPath,
549
607
  projectPrPath,
550
608
  getPrLinks,
551
609
  addPrLink,
610
+ mutatePrs,
611
+ linkPrToItem,
552
612
  nextWorkItemId,
553
613
  getAdoOrgBase,
554
614
  sanitizePath,
package/engine.js CHANGED
@@ -24,7 +24,7 @@
24
24
  const fs = require('fs');
25
25
  const path = require('path');
26
26
  const shared = require('./engine/shared');
27
- const { exec, execSilent, runFile, ENGINE_DEFAULTS: DEFAULTS } = shared;
27
+ const { exec, execSilent, runFile, ENGINE_DEFAULTS: DEFAULTS, CENTRAL_WI_PATH, resolveWiPath } = shared;
28
28
  const queries = require('./engine/queries');
29
29
 
30
30
  // ─── Paths ──────────────────────────────────────────────────────────────────
@@ -307,7 +307,7 @@ function spawnAgent(dispatchItem, config) {
307
307
 
308
308
  if (isSharedBranch) {
309
309
  log('info', `Creating worktree for shared branch: ${worktreePath} on ${branchName}`);
310
- try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git: ' + e.message); }
310
+ try { exec(`git fetch origin "${branchName}"`, { ..._gitOpts, cwd: rootDir }); } catch (e) { log('warn', 'git fetch: ' + e.message); }
311
311
  try {
312
312
  runWorktreeAdd(rootDir, worktreePath, `"${branchName}"`, _worktreeGitOpts, worktreeCreateRetries);
313
313
  } catch (eShared) {
@@ -317,6 +317,11 @@ function spawnAgent(dispatchItem, config) {
317
317
  log('info', `Shared branch ${branchName} already checked out at ${existingWtPath} — reusing`);
318
318
  worktreePath = existingWtPath;
319
319
  } else { throw eShared; }
320
+ } else if (eShared.message?.includes('invalid reference') || eShared.message?.includes('not a valid branch')) {
321
+ // Branch doesn't exist yet — create it from main
322
+ log('info', `Shared branch ${branchName} not found — creating from ${project.mainBranch || 'main'}`);
323
+ const mainRef = sanitizeBranch(project.mainBranch || 'main');
324
+ runWorktreeAdd(rootDir, worktreePath, `-b "${branchName}" ${mainRef}`, _worktreeGitOpts, worktreeCreateRetries);
320
325
  } else { throw eShared; }
321
326
  }
322
327
  } else {
@@ -874,7 +879,7 @@ const { COOLDOWN_PATH, dispatchCooldowns, loadCooldowns, saveCooldowns,
874
879
  // Auto-clean pending/failed work items for a PRD so they re-materialize with updated plan data
875
880
  function autoCleanPrdWorkItems(prdFile, config) {
876
881
  const allProjects = getProjects(config);
877
- const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
882
+ const wiPaths = [CENTRAL_WI_PATH];
878
883
  for (const proj of allProjects) wiPaths.push(projectWorkItemsPath(proj));
879
884
  const deletedIds = [];
880
885
  for (const wiPath of wiPaths) {
@@ -996,8 +1001,7 @@ function materializePlansAsWorkItems(config) {
996
1001
  const allProjects = getProjects(config);
997
1002
  const targetProject = allProjects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) || allProjects[0];
998
1003
  if (targetProject) {
999
- const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
1000
- const centralItems = safeJson(centralWiPath) || [];
1004
+ const centralItems = safeJson(CENTRAL_WI_PATH) || [];
1001
1005
  const alreadyQueued = centralItems.some(w =>
1002
1006
  w.type === 'plan-to-prd' && w.planFile === plan.source_plan && (w.status === 'pending' || w.status === 'dispatched')
1003
1007
  );
@@ -1065,7 +1069,7 @@ function materializePlansAsWorkItems(config) {
1065
1069
  }
1066
1070
  }
1067
1071
  // Also check central work-items.json
1068
- for (const w of (safeJson(path.join(MINIONS_DIR, 'work-items.json')) || [])) {
1072
+ for (const w of (safeJson(CENTRAL_WI_PATH) || [])) {
1069
1073
  if (w.id) allExistingWiIds.add(w.id);
1070
1074
  }
1071
1075
  const items = plan.missing_features.filter(f =>
@@ -1110,7 +1114,7 @@ function materializePlansAsWorkItems(config) {
1110
1114
 
1111
1115
  let totalCreated = 0;
1112
1116
  for (const [projName, { project, items: projItems }] of itemsByProject) {
1113
- const wiPath = project ? projectWorkItemsPath(project) : path.join(MINIONS_DIR, 'work-items.json');
1117
+ const wiPath = project ? projectWorkItemsPath(project) : CENTRAL_WI_PATH;
1114
1118
  const existingItems = safeJson(wiPath) || [];
1115
1119
  let created = 0;
1116
1120
  const newlyCreatedIds = new Set(); // tracks IDs created in this pass for reconciliation scoping
@@ -1190,9 +1194,17 @@ function materializePlansAsWorkItems(config) {
1190
1194
  const root = path.resolve(firstProject.localPath);
1191
1195
  const mainBranch = firstProject.mainBranch || 'main';
1192
1196
  const branch = sanitizeBranch(plan.feature_branch);
1193
- // Create branch from main (idempotent ignores if exists)
1194
- exec(`git branch "${branch}" "${mainBranch}" 2>/dev/null || true`, { cwd: root, stdio: 'pipe' });
1195
- exec(`git push -u origin "${branch}" 2>/dev/null || true`, { cwd: root, stdio: 'pipe' });
1197
+ // Create branch from main — verify it actually succeeded
1198
+ try {
1199
+ exec(`git branch "${branch}" "${mainBranch}"`, { cwd: root, stdio: 'pipe', windowsHide: true });
1200
+ } catch (e) {
1201
+ // Branch may already exist — that's fine
1202
+ if (!e.message?.includes('already exists')) throw e;
1203
+ }
1204
+ // Push to remote (best-effort — may not have a remote)
1205
+ try {
1206
+ exec(`git push -u origin "${branch}"`, { cwd: root, stdio: 'pipe', windowsHide: true, timeout: 15000 });
1207
+ } catch { /* no remote or push failed — branch still exists locally */ }
1196
1208
  log('info', `Shared branch pre-created: ${branch} for plan ${file}`);
1197
1209
  } catch (err) {
1198
1210
  log('warn', `Failed to pre-create shared branch for ${file}: ${err.message}`);
@@ -1755,7 +1767,7 @@ function extractSpecInfo(filePath, projectRoot_) {
1755
1767
  * Uses the shared work-item.md playbook with multi-project context injected.
1756
1768
  */
1757
1769
  function discoverCentralWorkItems(config) {
1758
- const centralPath = path.join(MINIONS_DIR, 'work-items.json');
1770
+ const centralPath = CENTRAL_WI_PATH;
1759
1771
  const items = safeJson(centralPath) || [];
1760
1772
  const projects = getProjects(config);
1761
1773
  const newWork = [];
@@ -2067,7 +2079,7 @@ function discoverWork(config) {
2067
2079
  const { discoverScheduledWork } = require('./engine/scheduler');
2068
2080
  const scheduledWork = discoverScheduledWork(config);
2069
2081
  if (scheduledWork.length > 0) {
2070
- const centralPath = path.join(MINIONS_DIR, 'work-items.json');
2082
+ const centralPath = CENTRAL_WI_PATH;
2071
2083
  const items = safeJson(centralPath) || [];
2072
2084
  let added = 0;
2073
2085
  for (const item of scheduledWork) {
@@ -2398,9 +2410,7 @@ async function tickInner() {
2398
2410
  // Defensive: ensure the work item is re-queued if completeDispatch didn't fire
2399
2411
  if (item.meta?.item?.id) {
2400
2412
  try {
2401
- const wiPath = item.meta.source === 'central-work-item' || item.meta.source === 'central-work-item-fanout'
2402
- ? path.join(ENGINE_DIR, '..', 'work-items.json')
2403
- : item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
2413
+ const wiPath = resolveWiPath(item.meta);
2404
2414
  if (wiPath) {
2405
2415
  const items = safeJson(wiPath) || [];
2406
2416
  const wi = items.find(i => i.id === item.meta.item.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.155",
3
+ "version": "0.1.157",
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"
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "daily-standup",
3
+ "title": "Daily Squad Stand-up",
4
+ "stages": [
5
+ {
6
+ "id": "standup",
7
+ "type": "meeting",
8
+ "title": "Daily Squad Stand-up � 2026-04-01",
9
+ "agenda": "Daily weekday meeting for all agents to review squad setup improvements and discuss progress on agentic harness engineering.\n\n1. What shipped since yesterday's meeting (Track A/B/C progress)?\n2. Any blockers on current work items?\n3. Review recent agentic harness engineering best practices � are there new Anthropic articles or model improvements that change our assumptions?\n4. Propose 1 concrete improvement each agent wants to make to the squad harness this week.\n5. Agree on today's priorities and any re-sequencing needed.\n\nOutput: a bulleted list of decisions made, action items with owners, and any updated priorities.",
10
+ "participants": [
11
+ "all"
12
+ ]
13
+ }
14
+ ],
15
+ "trigger": {
16
+ "cron": "0 9 1,2,3,4,5"
17
+ },
18
+ "enabled": true
19
+ }