@yemi33/minions 0.1.148 → 0.1.150

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.150 (2026-04-01)
4
+
5
+ ### Dashboard
6
+ - dashboard/js/render-plans.js
7
+
8
+ ## 0.1.149 (2026-04-01)
9
+
10
+ ### Engine
11
+ - engine.js
12
+ - engine/meeting.js
13
+ - engine/shared.js
14
+
15
+ ### Other
16
+ - test/unit.test.js
17
+
3
18
  ## 0.1.148 (2026-04-01)
4
19
 
5
20
  ### Engine
@@ -172,19 +172,15 @@ function renderPlans(plans) {
172
172
  const isArchived = p.archived;
173
173
 
174
174
  // For .md plans with a linked PRD, use the PRD's status as the authoritative intent
175
- // (p.status for .md is 'draft'/'converted'/'active', not the PRD lifecycle status)
176
175
  let prdJsonStatus = p.status || 'active';
177
176
  if (prdFile && p.format !== 'prd') {
178
177
  const linkedPrd = plans.find(pp => pp.file === prdFile && pp.format === 'prd');
179
178
  if (linkedPrd) prdJsonStatus = linkedPrd.status || prdJsonStatus;
180
- // If the linked PRD was archived, treat the .md plan as completed
181
179
  else if (!linkedPrd) {
182
180
  const archivedPrd = archivedPlans.find(pp => pp.file === prdFile && pp.format === 'prd');
183
181
  if (archivedPrd) prdJsonStatus = 'completed';
184
182
  }
185
183
  }
186
- // 'converted' means plan-to-PRD succeeded — treat as 'approved' for status derivation
187
- if (prdJsonStatus === 'converted') prdJsonStatus = prdFile ? 'approved' : 'completed';
188
184
 
189
185
  // Single source of truth: derive status from work items
190
186
  const effectiveStatus = isArchived ? 'completed' : derivePlanStatus(prdFile, p.file, prdJsonStatus, allWi);
@@ -192,14 +188,13 @@ function renderPlans(plans) {
192
188
  const statusLabelsMap = {
193
189
  'completed': 'Completed', 'in-progress': 'In Progress', 'paused': 'Paused',
194
190
  'awaiting-approval': 'Awaiting Approval', 'approved': 'Approved', 'rejected': 'Rejected',
195
- 'revision-requested': 'Revision Requested', 'has-failures': 'Has Failures', 'active': 'Active',
196
- 'converted': 'Converted to PRD', 'draft': 'Draft'
191
+ 'revision-requested': 'Revision Requested', 'has-failures': 'Has Failures', 'active': 'Active'
197
192
  };
198
193
  const label = statusLabelsMap[effectiveStatus] || effectiveStatus;
199
194
  const needsAction = (effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused') && !isArchived;
200
195
  const isRevision = effectiveStatus === 'revision-requested';
201
196
  const isCompleted = effectiveStatus === 'completed';
202
- const isDraft = (p.format === 'draft' || rawStatus === 'draft') && !isCompleted;
197
+ const isDraft = p.format === 'draft' && !isCompleted;
203
198
  // For .md drafts: show Execute only if no PRD exists yet (not already executed)
204
199
 
205
200
  let actions = '';
@@ -571,7 +566,10 @@ async function planArchive(file, btn) {
571
566
  const d = await res.json().catch(() => ({}));
572
567
  if (res.ok && d.ok) {
573
568
  try { closeModal(); } catch { /* may not be open */ }
574
- showToast('cmd-toast', 'Archived' + (file.endsWith('.json') ? ' PRD and source plan' : ''), true);
569
+ var msg = 'Archived';
570
+ if (d.archivedSource) msg += ' PRD + source plan (' + d.archivedSource + ')';
571
+ if (d.cancelledItems) msg += ', cancelled ' + d.cancelledItems + ' pending item(s)';
572
+ showToast('cmd-toast', msg, true);
575
573
  refreshPlans();
576
574
  refresh();
577
575
  } else {
package/engine/meeting.js CHANGED
@@ -88,11 +88,11 @@ function discoverMeetingWork(config) {
88
88
  const key = `meeting-${meeting.id}-r${round}-${concluder}`;
89
89
  if (activeKeys.has(key)) continue;
90
90
 
91
- const humanNotes = (meeting.humanNotes || []).map(n => '- ' + n).join('\n');
92
- const allFindings = Object.entries(meeting.findings || {}).map(([agent, f]) =>
91
+ const humanNotes = (Array.isArray(meeting.humanNotes) ? meeting.humanNotes : []).map(n => '- ' + n).join('\n');
92
+ const allFindings = Object.entries(typeof meeting.findings === 'object' && meeting.findings ? meeting.findings : {}).map(([agent, f]) =>
93
93
  `### ${agents[agent]?.name || agent}\n\n${f.content || '(no findings)'}`
94
94
  ).join('\n\n---\n\n');
95
- const allDebate = Object.entries(meeting.debate || {}).map(([agent, d]) =>
95
+ const allDebate = Object.entries(typeof meeting.debate === 'object' && meeting.debate ? meeting.debate : {}).map(([agent, d]) =>
96
96
  `### ${agents[agent]?.name || agent}\n\n${d.content || '(no response)'}`
97
97
  ).join('\n\n---\n\n');
98
98
 
@@ -184,6 +184,10 @@ function discoverMeetingWork(config) {
184
184
  function collectMeetingFindings(meetingId, agentId, roundName, output) {
185
185
  const meeting = getMeeting(meetingId);
186
186
  if (!meeting) return;
187
+ if (meeting.status === 'completed' || meeting.status === 'archived') {
188
+ log('info', `Ignoring late findings from ${agentId} for completed meeting ${meetingId}`);
189
+ return;
190
+ }
187
191
 
188
192
  const { text } = shared.parseStreamJsonOutput(output, { maxTextLength: 50000 });
189
193
  const rawContent = (text || '').trim();
@@ -256,11 +260,35 @@ function addMeetingNote(meetingId, note) {
256
260
  return meeting;
257
261
  }
258
262
 
263
+ function _killMeetingDispatches(meetingId) {
264
+ try {
265
+ const DISPATCH_PATH = path.join(__dirname, '..', 'engine', 'dispatch.json');
266
+ const dispatch = safeJson(DISPATCH_PATH) || {};
267
+ const toKill = (dispatch.active || []).filter(d => d.meta?.meetingId === meetingId);
268
+ if (toKill.length === 0) return 0;
269
+ // Remove from active and move to completed
270
+ shared.mutateJsonFileLocked(DISPATCH_PATH, (dp) => {
271
+ dp.active = (dp.active || []).filter(d => d.meta?.meetingId !== meetingId);
272
+ dp.completed = dp.completed || [];
273
+ for (const d of toKill) {
274
+ dp.completed.push({ ...d, result: 'error', reason: 'Meeting ended/advanced by human', completed_at: new Date().toISOString() });
275
+ }
276
+ if (dp.completed.length > 100) dp.completed = dp.completed.slice(-100);
277
+ return dp;
278
+ }, { defaultValue: { pending: [], active: [], completed: [] } });
279
+ log('info', `Killed ${toKill.length} active meeting dispatch(es) for ${meetingId}`);
280
+ return toKill.length;
281
+ } catch (e) { log('warn', 'kill meeting dispatches: ' + e.message); return 0; }
282
+ }
283
+
259
284
  function advanceMeetingRound(meetingId) {
260
285
  const meeting = getMeeting(meetingId);
261
- if (!meeting || meeting.status === 'completed') return null;
286
+ if (!meeting || meeting.status === 'completed' || meeting.status === 'archived') return null;
287
+ _killMeetingDispatches(meetingId);
262
288
  if (meeting.status === 'investigating') { meeting.status = 'debating'; meeting.round = 2; }
263
289
  else if (meeting.status === 'debating') { meeting.status = 'concluding'; meeting.round = 3; }
290
+ else if (meeting.status === 'concluding') { meeting.status = 'completed'; meeting.completedAt = new Date().toISOString(); }
291
+ else return meeting; // no change
264
292
  meeting.roundStartedAt = new Date().toISOString();
265
293
  saveMeeting(meeting);
266
294
  return meeting;
@@ -269,6 +297,7 @@ function advanceMeetingRound(meetingId) {
269
297
  function endMeeting(meetingId) {
270
298
  const meeting = getMeeting(meetingId);
271
299
  if (!meeting) return null;
300
+ _killMeetingDispatches(meetingId);
272
301
  meeting.status = 'completed';
273
302
  meeting.completedAt = new Date().toISOString();
274
303
  saveMeeting(meeting);
@@ -294,6 +323,7 @@ function unarchiveMeeting(id) {
294
323
  }
295
324
 
296
325
  function deleteMeeting(id) {
326
+ _killMeetingDispatches(id);
297
327
  const filePath = path.join(MEETINGS_DIR, id + '.json');
298
328
  if (!fs.existsSync(filePath)) return false;
299
329
  fs.unlinkSync(filePath);
package/engine/shared.js CHANGED
@@ -258,6 +258,7 @@ function gitEnv() {
258
258
  * Single source of truth — used by llm.js, consolidation.js, and lifecycle.js.
259
259
  */
260
260
  function parseStreamJsonOutput(raw, { maxTextLength = 0 } = {}) {
261
+ if (typeof raw !== 'string') raw = '';
261
262
  let text = '';
262
263
  let usage = null;
263
264
  let sessionId = null;
package/engine.js CHANGED
@@ -1496,9 +1496,9 @@ function discoverFromWorkItems(config, project) {
1496
1496
  '',
1497
1497
  'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
1498
1498
  '',
1499
- cpData.completed && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
1500
- cpData.remaining && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
1501
- cpData.blockers && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
1499
+ Array.isArray(cpData.completed) && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
1500
+ Array.isArray(cpData.remaining) && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
1501
+ Array.isArray(cpData.blockers) && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
1502
1502
  cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
1503
1503
  ].filter(Boolean).join('\n');
1504
1504
  vars.checkpoint_context = cpSummary;
@@ -1914,9 +1914,9 @@ function discoverCentralWorkItems(config) {
1914
1914
  '',
1915
1915
  'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
1916
1916
  '',
1917
- cpData.completed && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
1918
- cpData.remaining && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
1919
- cpData.blockers && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
1917
+ Array.isArray(cpData.completed) && cpData.completed.length > 0 ? `### Completed\n${cpData.completed.map(s => '- ' + s).join('\n')}` : '',
1918
+ Array.isArray(cpData.remaining) && cpData.remaining.length > 0 ? `### Remaining\n${cpData.remaining.map(s => '- ' + s).join('\n')}` : '',
1919
+ Array.isArray(cpData.blockers) && cpData.blockers.length > 0 ? `### Blockers\n${cpData.blockers.map(s => '- ' + s).join('\n')}` : '',
1920
1920
  cpData.branch_state ? `### Branch State\n${cpData.branch_state}` : '',
1921
1921
  ].filter(Boolean).join('\n');
1922
1922
  vars.checkpoint_context = cpSummary;
@@ -2125,7 +2125,7 @@ function discoverWork(config) {
2125
2125
  for (const item of allWork) {
2126
2126
  addToDispatch(item);
2127
2127
  if (item.meta?.source === 'pr-human-feedback') {
2128
- clearPendingHumanFeedbackFlag(item.meta.project, item.meta.pr?.id);
2128
+ clearPendingHumanFeedbackFlag(item.meta?.project, item.meta?.pr?.id);
2129
2129
  }
2130
2130
  }
2131
2131
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.148",
3
+ "version": "0.1.150",
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"