@yemi33/minions 0.1.728 → 0.1.729

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,10 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.728 (2026-04-09)
3
+ ## 0.1.729 (2026-04-09)
4
4
 
5
5
  ### Features
6
+ - playbook improvements — soften CAPS, add context-awareness, restructure decompose output
7
+ - pipeline auto-detect — skip wait/plan stages when artifacts exist
6
8
  - replace raw status strings with WI_STATUS constants in dashboard.js
7
9
  - Soften playbook CAPS emphasis and add context-awareness guidance
8
10
  - Add structured completion protocol to PR-producing playbooks
@@ -13,6 +15,7 @@
13
15
  - make post-verify plan archiving opt-in via autoArchive config
14
16
 
15
17
  ### Fixes
18
+ - resolve 3 failing unit tests on PR-661
16
19
  - repair 3 pre-existing test failures in source-pattern tests
17
20
  - merge master, sync dashboard-build jsFiles, bump HTML size limit
18
21
  - resolve 4 pre-existing test failures breaking CI
@@ -401,9 +401,10 @@ function openPipelineDetail(id) {
401
401
  document.getElementById('modal-body').innerHTML = html;
402
402
  document.getElementById('modal').classList.add('open');
403
403
 
404
- // Live-poll while modal is open and pipeline has an active run
405
- if (activeRun) {
406
- _pipelinePollId = id;
404
+ // Live-poll while modal is open always poll (not just active runs)
405
+ // so the modal updates when stages advance, pipelines get triggered, etc.
406
+ _pipelinePollId = id;
407
+ if (!_pipelinePollInterval) {
407
408
  _pipelinePollInterval = setInterval(function() {
408
409
  if (!document.getElementById('modal')?.classList?.contains('open') || _pipelinePollId !== id) {
409
410
  _stopPipelinePoll(); return;
@@ -426,12 +427,29 @@ function openPipelineDetail(id) {
426
427
  }
427
428
  var _pipelinePollHash = '';
428
429
 
430
+ /**
431
+ * Fetch fresh pipeline data and re-render the detail modal immediately.
432
+ * Used after actions (continue, trigger, abort) to avoid waiting for the 4s poll.
433
+ */
434
+ async function _refreshPipelineDetail(id) {
435
+ try {
436
+ var res = await fetch('/api/pipelines');
437
+ var d = await res.json();
438
+ var fresh = (d.pipelines || []).find(function(x) { return x.id === id; });
439
+ if (fresh) {
440
+ _pipelinesData = _pipelinesData.map(function(x) { return x.id === id ? fresh : x; });
441
+ _pipelinePollHash = JSON.stringify(fresh.runs || []);
442
+ openPipelineDetail(id);
443
+ }
444
+ } catch (e) { /* silent — next poll will catch up */ }
445
+ }
446
+
429
447
  async function _triggerPipeline(id, btn) {
430
448
  if (btn) { btn.textContent = 'Starting...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
431
449
  try {
432
450
  var res = await fetch('/api/pipelines/trigger', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id }) });
433
451
  var d = await res.json();
434
- if (res.ok) { showToast('cmd-toast', 'Pipeline triggered: ' + (d.runId || ''), true); try { closeModal(); } catch {} refresh(); }
452
+ if (res.ok) { showToast('cmd-toast', 'Pipeline triggered: ' + (d.runId || ''), true); refresh(); await _refreshPipelineDetail(id); }
435
453
  else { if (btn) { btn.textContent = 'Run Now'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } alert('Failed: ' + (d.error || 'unknown')); }
436
454
  } catch (e) { if (btn) { btn.textContent = 'Run Now'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } alert('Error: ' + e.message); }
437
455
  }
@@ -444,20 +462,8 @@ async function _abortPipeline(id, btn) {
444
462
  if (res.ok) {
445
463
  var d = await res.json().catch(function() { return {}; });
446
464
  showToast('cmd-toast', 'Pipeline aborted — ' + (d.cancelledWorkItems || 0) + ' work items cancelled', true);
447
- // Replace abort button with Run Now
448
- if (btn) {
449
- btn.textContent = 'Run Now';
450
- btn.style.color = 'var(--green)';
451
- btn.style.borderColor = 'var(--green)';
452
- btn.style.pointerEvents = '';
453
- btn.style.opacity = '';
454
- btn.onclick = function() { _triggerPipeline(id, btn); };
455
- // Remove the retrigger button next to it (no longer needed)
456
- var next = btn.nextElementSibling;
457
- if (next && next.textContent.trim() === 'Retrigger') next.remove();
458
- }
459
465
  refresh();
460
- openPipelineDetail(id);
466
+ await _refreshPipelineDetail(id);
461
467
  } else { var d = await res.json().catch(function() { return {}; }); alert('Abort failed: ' + (d.error || 'unknown')); if (btn) { btn.textContent = 'Abort'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } }
462
468
  } catch (e) { alert('Error: ' + e.message); if (btn) { btn.textContent = 'Abort'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } }
463
469
  }
@@ -470,8 +476,8 @@ async function _retriggerPipeline(id, btn) {
470
476
  var d = await res.json();
471
477
  if (res.ok) {
472
478
  showToast('cmd-toast', 'Pipeline retriggered: ' + (d.runId || ''), true);
473
- if (btn) { btn.textContent = '\u2713 Retriggered'; btn.style.color = 'var(--green)'; }
474
- setTimeout(function() { openPipelineDetail(id); }, 1500);
479
+ refresh();
480
+ await _refreshPipelineDetail(id);
475
481
  } else { alert('Retrigger failed: ' + (d.error || 'unknown')); if (btn) { btn.textContent = 'Retrigger'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } }
476
482
  } catch (e) { alert('Error: ' + e.message); if (btn) { btn.textContent = 'Retrigger'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } }
477
483
  }
@@ -493,7 +499,8 @@ async function _continuePipeline(id, stageId, btn) {
493
499
  if (res.ok) {
494
500
  showToast('cmd-toast', 'Stage continued — dispatching next tick', true);
495
501
  if (btn) { btn.textContent = '\u2713 Continued'; btn.style.color = 'var(--green)'; btn.style.borderColor = 'var(--green)'; btn.style.opacity = '1'; }
496
- setTimeout(function() { openPipelineDetail(id); }, 2000);
502
+ // Immediate refresh no waiting for 4s poll
503
+ await _refreshPipelineDetail(id);
497
504
  } else {
498
505
  var d = await res.json().catch(function() { return {}; }); alert('Failed: ' + (d.error || 'unknown'));
499
506
  if (btn) { btn.textContent = 'Continue'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
@@ -320,6 +320,17 @@ function _findExistingPlanForMeeting(meetingIds, plansDir) {
320
320
  return slugMatch;
321
321
  }
322
322
 
323
+ // Check if a PRD already exists for a given plan file (plan already converted)
324
+ function _findExistingPrdForPlan(planFile, prdDir) {
325
+ if (!fs.existsSync(prdDir)) return null;
326
+ const prdFiles = safeReadDir(prdDir).filter(f => f.endsWith('.json'));
327
+ for (const pf of prdFiles) {
328
+ const prd = safeJson(path.join(prdDir, pf));
329
+ if (prd?.source_plan === planFile) return pf;
330
+ }
331
+ return null;
332
+ }
333
+
323
334
  async function executePlanStage(stage, stageState, run, config) {
324
335
  const plansDir = path.join(__dirname, '..', 'plans');
325
336
  if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
@@ -334,6 +345,18 @@ async function executePlanStage(stage, stageState, run, config) {
334
345
  const existingPlanFile = _findExistingPlanForMeeting(meetingIds, plansDir);
335
346
  if (existingPlanFile) {
336
347
  log('info', `Pipeline ${run.pipelineId}: reconciling plan stage — adopting existing plan "${existingPlanFile}"`);
348
+
349
+ // Check if a PRD already exists for this plan (skip plan-to-prd entirely)
350
+ const prdDir = path.join(__dirname, '..', 'prd');
351
+ const existingPrdFile = _findExistingPrdForPlan(existingPlanFile, prdDir);
352
+ if (existingPrdFile) {
353
+ log('info', `Pipeline ${run.pipelineId}: PRD "${existingPrdFile}" already exists for plan "${existingPlanFile}" — skipping plan-to-prd`);
354
+ return {
355
+ status: PIPELINE_STATUS.RUNNING,
356
+ artifacts: { plans: [existingPlanFile], workItems: [], prds: [existingPrdFile], prs: [] },
357
+ };
358
+ }
359
+
337
360
  // Adopt or create plan-to-prd WI atomically under lock
338
361
  let adoptedWiId = wiId;
339
362
  mutateWorkItems(wiPath, workItems => {
@@ -563,12 +586,17 @@ function isStageComplete(stage, stageState, run, config) {
563
586
 
564
587
  switch (stage.type) {
565
588
  case STAGE_TYPE.TASK: {
589
+ // Check root + all project work-items.json (WIs may be moved to project paths)
566
590
  const wiPath = path.join(__dirname, '..', 'work-items.json');
567
591
  const workItems = safeJson(wiPath) || [];
592
+ const allProjectWi = shared.getProjects(config).reduce((acc, p) => {
593
+ return acc.concat(safeJson(shared.projectWorkItemsPath(p)) || []);
594
+ }, []);
595
+ const all = [...workItems, ...allProjectWi];
568
596
  const ids = artifacts.workItems || [];
569
597
  if (ids.length === 0) return false;
570
598
  return ids.every(id => {
571
- const wi = workItems.find(w => w.id === id);
599
+ const wi = all.find(w => w.id === id);
572
600
  return !wi || wi.status === WI_STATUS.DONE || wi.status === WI_STATUS.FAILED; // missing = treat as done
573
601
  });
574
602
  }
@@ -725,8 +753,10 @@ async function discoverPipelineWork(config) {
725
753
  if (stage.type === STAGE_TYPE.TASK) {
726
754
  const wiPath = path.join(__dirname, '..', 'work-items.json');
727
755
  const workItems = safeJson(wiPath) || [];
756
+ const projWi = shared.getProjects(config).reduce((acc, p) => acc.concat(safeJson(shared.projectWorkItemsPath(p)) || []), []);
757
+ const allWi = [...workItems, ...projWi];
728
758
  output = (stageState.artifacts?.workItems || []).map(id => {
729
- const wi = workItems.find(w => w.id === id);
759
+ const wi = allWi.find(w => w.id === id);
730
760
  return wi?.resultSummary || wi?.title || id;
731
761
  }).join('\n');
732
762
  } else if (stage.type === STAGE_TYPE.MEETING) {
@@ -771,7 +801,34 @@ async function discoverPipelineWork(config) {
771
801
  }
772
802
  }
773
803
 
774
- if (stageState.status === PIPELINE_STATUS.WAITING_HUMAN) { allComplete = false; continue; }
804
+ if (stageState.status === PIPELINE_STATUS.WAITING_HUMAN) {
805
+ // Auto-complete wait stages when the preceding meeting already produced a plan
806
+ // Common pattern: meeting → wait → plan — if plan exists, nothing to wait for
807
+ if (stage.type === STAGE_TYPE.WAIT) {
808
+ const nextPlanStage = stages.find(s =>
809
+ s.type === STAGE_TYPE.PLAN && (s.dependsOn || []).includes(stage.id)
810
+ );
811
+ if (nextPlanStage) {
812
+ const meetingIds = _findMeetingsInRun(activeRun);
813
+ if (meetingIds.length > 0) {
814
+ const plansDir = path.join(__dirname, '..', 'plans');
815
+ if (fs.existsSync(plansDir)) {
816
+ const existingPlan = _findExistingPlanForMeeting(meetingIds, plansDir);
817
+ if (existingPlan) {
818
+ updateRunStage(pipeline.id, activeRun.runId, stage.id, {
819
+ status: PIPELINE_STATUS.COMPLETED, completedAt: ts(),
820
+ output: `Auto-completed: plan "${existingPlan}" already exists for meeting`,
821
+ });
822
+ stageState.status = PIPELINE_STATUS.COMPLETED;
823
+ log('info', `Pipeline ${pipeline.id}: wait stage ${stage.id} auto-completed — plan "${existingPlan}" already exists`);
824
+ continue; // re-evaluate as completed on next iteration
825
+ }
826
+ }
827
+ }
828
+ }
829
+ }
830
+ allComplete = false; continue;
831
+ }
775
832
 
776
833
  // Check if pending stage is ready to start
777
834
  if (stageState.status === PIPELINE_STATUS.PENDING) {
@@ -852,4 +909,5 @@ module.exports = {
852
909
  getPipelineRuns, getActiveRun, startRun, updateRunStage, completeRun,
853
910
  discoverPipelineWork,
854
911
  evaluateCondition, // exported for testing
912
+ _findMeetingsInRun, _findExistingPlanForMeeting, _findExistingPrdForPlan, // exported for testing
855
913
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.728",
3
+ "version": "0.1.729",
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"
@@ -48,10 +48,13 @@ Write the decomposition result as a JSON code block in your response:
48
48
  {
49
49
  "id": "{{item_id}}-a",
50
50
  "name": "Short descriptive name",
51
+ "objective": "One-sentence goal: what this sub-task achieves.",
51
52
  "description": "What to build, where, and how. Be specific enough that an engineer can implement without further exploration.",
52
53
  "estimated_complexity": "small|medium",
53
54
  "depends_on": [],
54
- "acceptance_criteria": ["Criterion 1", "Criterion 2"]
55
+ "acceptance_criteria": ["Criterion 1", "Criterion 2"],
56
+ "expected_output": "What done looks like — the artifact produced (e.g., a PR adding X, a new file at Y, a config change).",
57
+ "scope_boundaries": ["Out-of-scope item 1", "Out-of-scope item 2"]
55
58
  }
56
59
  ]
57
60
  }
@@ -4,6 +4,8 @@ Your context window may be compacted or summarized mid-task by Claude's automati
4
4
 
5
5
  ## Engine Rules (apply to all tasks)
6
6
 
7
+ **Context compaction:** Your context window may be compacted mid-task by Claude's infrastructure. If you notice your earlier conversation history appears truncated or summarized, this is normal and expected. Do not interpret compaction as a signal to stop early or wrap up. Continue working toward your task objective — all relevant instructions and state remain available.
8
+
7
9
  - Do NOT write to `agents/*/status.json` — the engine manages your status automatically.
8
10
  - Do NOT remove worktrees — the engine handles cleanup automatically.
9
11
  - Do NOT checkout branches in the main working tree — use worktrees or `git diff`/`git show`.