@yemi33/minions 0.1.828 → 0.1.830

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,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.828 (2026-04-11)
3
+ ## 0.1.830 (2026-04-11)
4
4
 
5
5
  ### Features
6
6
  - stale PRD shows Regenerate PRD + Resume as-is buttons
@@ -11,6 +11,10 @@
11
11
  - hide conflicting action buttons when stale PRD banner is showing
12
12
  - diff-aware plan-to-prd triggered by Resume, not auto-dispatch
13
13
 
14
+ ### Other
15
+ - refactor: add PRD_ITEM_STATUS/PRD_MATERIALIZABLE constants, extract queuePlanToPrd
16
+ - refactor: remove dead criteria variable, DRY plan resume functions
17
+
14
18
  ## 0.1.823 (2026-04-11)
15
19
 
16
20
  ### Features
@@ -766,16 +766,16 @@ async function prdItemRequeue(workItemId, source, prdFile) {
766
766
  }
767
767
  }
768
768
 
769
- async function prdRegenerate(prdFile) {
770
- if (!confirm('Source plan was revised.\n\nRegenerate PRD? An agent will compare the updated plan against existing implementation and update items accordingly.\n\nDone items stay done unless modified. New items are added. Removed items are cancelled.')) return;
769
+ async function _planApproveAction(prdFile, skipRegen, confirmMsg, successMsg) {
770
+ if (!confirm(confirmMsg)) return;
771
771
  try {
772
772
  const res = await fetch('/api/plans/approve', {
773
773
  method: 'POST', headers: { 'Content-Type': 'application/json' },
774
- body: JSON.stringify({ file: prdFile })
774
+ body: JSON.stringify({ file: prdFile, skipRegen: skipRegen || undefined })
775
775
  });
776
776
  const d = await res.json();
777
777
  if (res.ok) {
778
- showToast('cmd-toast', d.diffAwareUpdate ? 'Diff-aware PRD update queued' : 'Plan approved', true);
778
+ showToast('cmd-toast', successMsg || (d.diffAwareUpdate ? 'Diff-aware PRD update queued' : 'Plan approved'), true);
779
779
  refresh();
780
780
  } else {
781
781
  alert('Failed: ' + (d.error || 'unknown'));
@@ -783,21 +783,15 @@ async function prdRegenerate(prdFile) {
783
783
  } catch (e) { alert('Error: ' + e.message); }
784
784
  }
785
785
 
786
- async function prdResumeWithoutRegen(prdFile) {
787
- if (!confirm('Resume this plan without regenerating the PRD?\n\nThe current PRD items will be used as-is. New work items will be materialized from any missing/planned items.')) return;
788
- try {
789
- const res = await fetch('/api/plans/approve', {
790
- method: 'POST', headers: { 'Content-Type': 'application/json' },
791
- body: JSON.stringify({ file: prdFile, skipRegen: true })
792
- });
793
- const d = await res.json();
794
- if (res.ok) {
795
- showToast('cmd-toast', 'Plan resumed (PRD unchanged)', true);
796
- refresh();
797
- } else {
798
- alert('Failed: ' + (d.error || 'unknown'));
799
- }
800
- } catch (e) { alert('Error: ' + e.message); }
786
+ function prdRegenerate(prdFile) {
787
+ _planApproveAction(prdFile, false,
788
+ 'Source plan was revised.\n\nRegenerate PRD? An agent will compare the updated plan against existing implementation and update items accordingly.\n\nDone items stay done unless modified. New items are added. Removed items are cancelled.');
789
+ }
790
+
791
+ function prdResumeWithoutRegen(prdFile) {
792
+ _planApproveAction(prdFile, true,
793
+ 'Resume this plan without regenerating the PRD?\n\nThe current PRD items will be used as-is. New work items will be materialized from any missing/planned items.',
794
+ 'Plan resumed (PRD unchanged)');
801
795
  }
802
796
 
803
797
  function openArchive(i) {
package/dashboard.js CHANGED
@@ -2247,34 +2247,21 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
2247
2247
  const projectName = plan.project || body.file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
2248
2248
  const targetProject = PROJECTS.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) || PROJECTS[0];
2249
2249
  if (targetProject) {
2250
- const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
2251
- mutateJsonFileLocked(centralWiPath, items => {
2252
- if (!Array.isArray(items)) items = [];
2253
- if (items.some(w => w.type === 'plan-to-prd' && w.planFile === plan.source_plan && (w.status === 'pending' || w.status === 'dispatched'))) return items;
2254
- items.push({
2255
- id: 'W-' + shared.uid(),
2256
- title: `Update PRD from revised plan: ${plan.source_plan}`,
2257
- type: 'plan-to-prd',
2258
- priority: 'high',
2259
- description: `Plan file: plans/${plan.source_plan}\nPRD file: prd/${body.file}\n\n` +
2260
- `Source plan was revised. Compare the updated plan against existing PRD and produce an updated version.\n\n` +
2261
- `**Current PRD implementation state:**\n${implContext}\n\n` +
2262
- `**Rules for updating:**\n` +
2263
- `- Items that are done and unchanged in the plan → keep status "done" (preserve their ID)\n` +
2264
- `- Items that are done but modified in the plan (new requirements) → set status "updated" (engine will re-open)\n` +
2265
- `- New items in the plan → set status "missing" (engine will materialize)\n` +
2266
- `- Items removed from the plan → drop from PRD (engine will cancel pending WIs)\n` +
2267
- `- Preserve all existing item IDs for unchanged/modified items — do NOT generate new IDs for them`,
2268
- status: 'pending',
2269
- created: new Date().toISOString(),
2270
- createdBy: 'dashboard:plan-resume',
2271
- project: targetProject.name,
2272
- planFile: plan.source_plan,
2273
- _existingPrdFile: body.file,
2274
- });
2275
- diffAwareQueued = true;
2276
- return items;
2277
- }, { defaultValue: [] });
2250
+ diffAwareQueued = shared.queuePlanToPrd({
2251
+ planFile: plan.source_plan, prdFile: body.file,
2252
+ title: `Update PRD from revised plan: ${plan.source_plan}`,
2253
+ description: `Plan file: plans/${plan.source_plan}\nPRD file: prd/${body.file}\n\n` +
2254
+ `Source plan was revised. Compare the updated plan against existing PRD and produce an updated version.\n\n` +
2255
+ `**Current PRD implementation state:**\n${implContext}\n\n` +
2256
+ `**Rules for updating:**\n` +
2257
+ `- Items that are done and unchanged in the plan → keep status "done" (preserve their ID)\n` +
2258
+ `- Items that are done but modified in the plan (new requirements) → set status "updated" (engine will re-open)\n` +
2259
+ `- New items in the plan set status "missing" (engine will materialize)\n` +
2260
+ `- Items removed from the plan drop from PRD (engine will cancel pending WIs)\n` +
2261
+ `- Preserve all existing item IDs for unchanged/modified items — do NOT generate new IDs for them`,
2262
+ project: targetProject.name, createdBy: 'dashboard:plan-resume',
2263
+ extra: { _existingPrdFile: body.file },
2264
+ });
2278
2265
  }
2279
2266
  }
2280
2267
 
package/engine/shared.js CHANGED
@@ -587,6 +587,8 @@ const PLAN_STATUS = {
587
587
  PAUSED: 'paused', REJECTED: 'rejected', COMPLETED: 'completed',
588
588
  REVISION_REQUESTED: 'revision-requested',
589
589
  };
590
+ const PRD_ITEM_STATUS = { MISSING: 'missing', PLANNED: 'planned', UPDATED: 'updated', DONE: 'done' };
591
+ const PRD_MATERIALIZABLE = new Set([PRD_ITEM_STATUS.MISSING, PRD_ITEM_STATUS.PLANNED, PRD_ITEM_STATUS.UPDATED]);
590
592
  const PR_STATUS = { ACTIVE: 'active', MERGED: 'merged', ABANDONED: 'abandoned', CLOSED: 'closed', LINKED: 'linked' };
591
593
  // PRs eligible for polling (status/build/comment checks) — excludes terminal statuses
592
594
  const PR_POLLABLE_STATUSES = new Set([PR_STATUS.ACTIVE, PR_STATUS.LINKED]);
@@ -605,6 +607,32 @@ function trackReviewMetric(pr, newReviewStatus, config) {
605
607
  });
606
608
  } catch (err) { log('warn', `Metrics update: ${err.message}`); }
607
609
  }
610
+
611
+ /** Queue a plan-to-prd work item with dedup check inside lock. Returns true if queued. */
612
+ function queuePlanToPrd({ planFile, prdFile, title, description, project, createdBy, extra }) {
613
+ const centralWiPath = path.join(__dirname, '..', 'work-items.json');
614
+ let queued = false;
615
+ mutateJsonFileLocked(centralWiPath, items => {
616
+ if (!Array.isArray(items)) items = [];
617
+ if (items.some(w => w.type === 'plan-to-prd' && w.planFile === planFile && (w.status === 'pending' || w.status === 'dispatched'))) return items;
618
+ items.push({
619
+ id: 'W-' + uid(),
620
+ title,
621
+ type: 'plan-to-prd',
622
+ priority: 'high',
623
+ description,
624
+ status: 'pending',
625
+ created: new Date().toISOString(),
626
+ createdBy,
627
+ project,
628
+ planFile,
629
+ ...(extra || {}),
630
+ });
631
+ queued = true;
632
+ return items;
633
+ }, { defaultValue: [] });
634
+ return queued;
635
+ }
608
636
  const DISPATCH_RESULT = { SUCCESS: 'success', ERROR: 'error', TIMEOUT: 'timeout' };
609
637
  const PIPELINE_STATUS = {
610
638
  PENDING: 'pending', RUNNING: 'running', COMPLETED: 'completed',
@@ -1021,7 +1049,7 @@ module.exports = {
1021
1049
  KB_CATEGORIES,
1022
1050
  classifyInboxItem,
1023
1051
  ENGINE_DEFAULTS,
1024
- WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, PR_POLLABLE_STATUSES, DISPATCH_RESULT, trackReviewMetric,
1052
+ WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, DISPATCH_RESULT, trackReviewMetric, queuePlanToPrd,
1025
1053
  PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
1026
1054
  FAILURE_CLASS, ESCALATION_POLICY, COMPLETION_FIELDS,
1027
1055
  DEFAULT_AGENT_METRICS,
package/engine.js CHANGED
@@ -25,7 +25,7 @@ const fs = require('fs');
25
25
  const path = require('path');
26
26
  const shared = require('./engine/shared');
27
27
  const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS: DEFAULTS,
28
- WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT, AGENT_STATUS } = shared;
28
+ WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_MATERIALIZABLE, PR_STATUS, DISPATCH_RESULT, AGENT_STATUS } = shared;
29
29
  const queries = require('./engine/queries');
30
30
 
31
31
  // ─── Paths ──────────────────────────────────────────────────────────────────
@@ -1358,26 +1358,12 @@ function materializePlansAsWorkItems(config) {
1358
1358
  const allProjects = getProjects(config);
1359
1359
  const targetProject = allProjects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) || allProjects[0];
1360
1360
  if (targetProject) {
1361
- const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
1362
- let queued = false;
1363
- mutateJsonFileLocked(centralWiPath, items => {
1364
- if (!Array.isArray(items)) items = [];
1365
- if (items.some(w => w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === plan.source_plan && (w.status === WI_STATUS.PENDING || w.status === WI_STATUS.DISPATCHED))) return items;
1366
- items.push({
1367
- id: 'W-' + shared.uid(),
1368
- title: `Regenerate PRD from revised plan: ${plan.source_plan}`,
1369
- type: 'plan-to-prd',
1370
- priority: 'high',
1371
- description: `Plan file: plans/${plan.source_plan}\nSource plan was revised (PRD was ${prdStatus}) — regenerating.${completedContext}`,
1372
- status: 'pending',
1373
- created: ts(),
1374
- createdBy: 'engine:plan-revision',
1375
- project: targetProject.name,
1376
- planFile: plan.source_plan,
1377
- });
1378
- queued = true;
1379
- return items;
1380
- }, { defaultValue: [] });
1361
+ const queued = shared.queuePlanToPrd({
1362
+ planFile: plan.source_plan, prdFile: file,
1363
+ title: `Regenerate PRD from revised plan: ${plan.source_plan}`,
1364
+ description: `Plan file: plans/${plan.source_plan}\nSource plan was revised (PRD was ${prdStatus}) regenerating.${completedContext}`,
1365
+ project: targetProject.name, createdBy: 'engine:plan-revision',
1366
+ });
1381
1367
  if (queued) log('info', `Queued plan-to-prd regeneration for revised plan ${plan.source_plan} (${completedItems.length} completed items to carry over)`);
1382
1368
  }
1383
1369
  }
@@ -1417,14 +1403,14 @@ function materializePlansAsWorkItems(config) {
1417
1403
  // No project found — use central work-items.json (engine works without projects)
1418
1404
  const useCentral = !defaultProject;
1419
1405
 
1420
- const statusFilter = ['missing', 'planned', 'updated'];
1406
+ const statusFilter = PRD_MATERIALIZABLE;
1421
1407
  // Also materialize in-pr/done items that never got a work item (race with PR status sync)
1422
1408
  const allExistingWiIds = new Set();
1423
1409
  for (const w of queries.getWorkItems()) {
1424
1410
  if (w.id) allExistingWiIds.add(w.id);
1425
1411
  }
1426
1412
  const items = plan.missing_features.filter(f =>
1427
- statusFilter.includes(f.status) ||
1413
+ statusFilter.has(f.status) ||
1428
1414
  (DONE_STATUSES.has(f.status) && f.id && !allExistingWiIds.has(f.id))
1429
1415
  );
1430
1416
 
@@ -1474,11 +1460,10 @@ function materializePlansAsWorkItems(config) {
1474
1460
  for (const item of projItems) {
1475
1461
  // Re-open: PRD item set back to missing/planned/updated but work item is done → reset to pending
1476
1462
  const existingWi = existingItems.find(w => w.id === item.id);
1477
- const shouldReopen = item.status === 'missing' || item.status === 'planned' || item.status === 'updated';
1463
+ const shouldReopen = PRD_MATERIALIZABLE.has(item.status);
1478
1464
  if (existingWi && DONE_STATUSES.has(existingWi.status) && shouldReopen) {
1479
1465
  existingWi.status = WI_STATUS.PENDING;
1480
1466
  existingWi._reopened = true;
1481
- const criteria = (item.acceptance_criteria || []).map(c => `- ${c}`).join('\n');
1482
1467
  existingWi.description = buildWiDescription(item, file);
1483
1468
  existingWi.title = `Implement: ${item.name}`;
1484
1469
  created++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.828",
3
+ "version": "0.1.830",
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"