@yemi33/minions 0.1.155 → 0.1.156

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.156 (2026-04-02)
4
+
5
+ ### Engine
6
+ - engine.js
7
+ - engine/shared.js
8
+
9
+ ### Dashboard
10
+ - dashboard/js/render-plans.js
11
+ - dashboard/js/render-prd.js
12
+
13
+ ### Other
14
+ - test/unit.test.js
15
+
3
16
  ## 0.1.155 (2026-04-02)
4
17
 
5
18
  ### 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/shared.js CHANGED
@@ -501,7 +501,27 @@ function parseSkillFrontmatter(content, filename) {
501
501
  // Never touched by polling loops — only written when a PR is first linked to a PRD item.
502
502
 
503
503
  function getPrLinks() {
504
- try { return JSON.parse(require('fs').readFileSync(PR_LINKS_PATH, 'utf8')); } catch { return {}; }
504
+ // Derive from PR.prdItems (single source of truth) + legacy pr-links.json as fallback
505
+ const links = {};
506
+ try {
507
+ const projects = getProjects();
508
+ for (const project of projects) {
509
+ const prs = safeJson(projectPrPath(project)) || [];
510
+ for (const pr of prs) {
511
+ for (const itemId of (pr.prdItems || [])) {
512
+ if (!links[pr.id]) links[pr.id] = itemId;
513
+ }
514
+ }
515
+ }
516
+ } catch { /* optional */ }
517
+ // Merge legacy pr-links.json for items not yet in PR.prdItems
518
+ try {
519
+ const legacy = JSON.parse(require('fs').readFileSync(PR_LINKS_PATH, 'utf8'));
520
+ for (const [prId, itemId] of Object.entries(legacy)) {
521
+ if (!links[prId]) links[prId] = itemId;
522
+ }
523
+ } catch { /* optional */ }
524
+ return links;
505
525
  }
506
526
 
507
527
  function addPrLink(prId, itemId) {
@@ -512,6 +532,33 @@ function addPrLink(prId, itemId) {
512
532
  safeWrite(PR_LINKS_PATH, links);
513
533
  }
514
534
 
535
+ /**
536
+ * Locked mutation of a project's pull-requests.json.
537
+ * Single source of truth for PR data including prdItems links.
538
+ */
539
+ function mutatePrs(project, mutateFn) {
540
+ const prPath = projectPrPath(project);
541
+ return mutateJsonFileLocked(prPath, (prs) => {
542
+ return mutateFn(Array.isArray(prs) ? prs : []);
543
+ }, { defaultValue: [] });
544
+ }
545
+
546
+ /**
547
+ * Link a PR to a work item via PR.prdItems (single source of truth).
548
+ * Uses file-locked mutation to prevent race conditions.
549
+ */
550
+ function linkPrToItem(project, prId, itemId) {
551
+ if (!prId || !itemId) return;
552
+ mutatePrs(project, (prs) => {
553
+ const pr = prs.find(p => p.id === prId);
554
+ if (pr) {
555
+ pr.prdItems = Array.isArray(pr.prdItems) ? pr.prdItems : [];
556
+ if (!pr.prdItems.includes(itemId)) pr.prdItems.push(itemId);
557
+ }
558
+ return prs;
559
+ });
560
+ }
561
+
515
562
  module.exports = {
516
563
  MINIONS_DIR,
517
564
  PR_LINKS_PATH,
@@ -549,6 +596,8 @@ module.exports = {
549
596
  projectPrPath,
550
597
  getPrLinks,
551
598
  addPrLink,
599
+ mutatePrs,
600
+ linkPrToItem,
552
601
  nextWorkItemId,
553
602
  getAdoOrgBase,
554
603
  sanitizePath,
package/engine.js CHANGED
@@ -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 {
@@ -1190,9 +1195,17 @@ function materializePlansAsWorkItems(config) {
1190
1195
  const root = path.resolve(firstProject.localPath);
1191
1196
  const mainBranch = firstProject.mainBranch || 'main';
1192
1197
  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' });
1198
+ // Create branch from main — verify it actually succeeded
1199
+ try {
1200
+ exec(`git branch "${branch}" "${mainBranch}"`, { cwd: root, stdio: 'pipe', windowsHide: true });
1201
+ } catch (e) {
1202
+ // Branch may already exist — that's fine
1203
+ if (!e.message?.includes('already exists')) throw e;
1204
+ }
1205
+ // Push to remote (best-effort — may not have a remote)
1206
+ try {
1207
+ exec(`git push -u origin "${branch}"`, { cwd: root, stdio: 'pipe', windowsHide: true, timeout: 15000 });
1208
+ } catch { /* no remote or push failed — branch still exists locally */ }
1196
1209
  log('info', `Shared branch pre-created: ${branch} for plan ${file}`);
1197
1210
  } catch (err) {
1198
1211
  log('warn', `Failed to pre-create shared branch for ${file}: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.155",
3
+ "version": "0.1.156",
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"