@yemi33/minions 0.1.1710 → 0.1.1711

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1711 (2026-05-04)
4
+
5
+ ### Other
6
+ - Ensure meetings advance after terminal outcomes
7
+
3
8
  ## 0.1.1710 (2026-05-04)
4
9
 
5
10
  ### Other
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-04T17:10:04.905Z"
4
+ "cachedAt": "2026-05-04T17:19:35.871Z"
5
5
  }
@@ -2662,7 +2662,14 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
2662
2662
  if (type === WORK_TYPE.MEETING && meta?.meetingId) {
2663
2663
  try {
2664
2664
  const { collectMeetingFindings } = require('./meeting');
2665
- collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout, structuredCompletion, meta.round);
2665
+ collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout, structuredCompletion, meta.round, {
2666
+ success: effectiveSuccess,
2667
+ result,
2668
+ code,
2669
+ completionStatus,
2670
+ agentReportedFailure,
2671
+ summary: resultSummary,
2672
+ });
2666
2673
  } catch (err) { log('warn', `Meeting collect: ${err.message}`); }
2667
2674
  }
2668
2675
 
package/engine/meeting.js CHANGED
@@ -26,6 +26,7 @@ const ROUND_STATUS_BY_NAME = {
26
26
  debate: 'debating',
27
27
  conclude: 'concluding',
28
28
  };
29
+ const ROUND_NUMBER_BY_NAME = { investigate: 1, debate: 2, conclude: 3 };
29
30
  const ACTIVE_MEETING_STATUSES = new Set(Object.values(ROUND_STATUS_BY_NAME));
30
31
 
31
32
  function isTerminalMeetingStatus(status) {
@@ -36,6 +37,90 @@ function expectedMeetingStatusForRound(roundName) {
36
37
  return ROUND_STATUS_BY_NAME[String(roundName || '').toLowerCase()] || null;
37
38
  }
38
39
 
40
+ function roundKeyFor(roundName, round) {
41
+ const numeric = Number(round);
42
+ if (Number.isFinite(numeric) && numeric > 0) return String(numeric);
43
+ return String(ROUND_NUMBER_BY_NAME[String(roundName || '').toLowerCase()] || 1);
44
+ }
45
+
46
+ function getRoundFailures(meeting, roundName, round, create = false) {
47
+ if (!meeting.roundFailures || typeof meeting.roundFailures !== 'object') {
48
+ if (!create) return {};
49
+ meeting.roundFailures = {};
50
+ }
51
+ const key = roundKeyFor(roundName, round);
52
+ if (!meeting.roundFailures[key] || typeof meeting.roundFailures[key] !== 'object') {
53
+ if (!create) return {};
54
+ meeting.roundFailures[key] = {};
55
+ }
56
+ return meeting.roundFailures[key];
57
+ }
58
+
59
+ function hasRoundFailure(meeting, roundName, agentId, round = meeting.round) {
60
+ return Boolean(getRoundFailures(meeting, roundName, round, false)[agentId]);
61
+ }
62
+
63
+ function hasRoundSuccess(meeting, roundName, agentId) {
64
+ if (roundName === 'investigate') return Boolean(meeting.findings?.[agentId]);
65
+ if (roundName === 'debate') return Boolean(meeting.debate?.[agentId]);
66
+ return Boolean(meeting.conclusion && meeting.conclusion.agent === agentId);
67
+ }
68
+
69
+ function hasRoundTerminalOutcome(meeting, roundName, agentId, round = meeting.round) {
70
+ return hasRoundSuccess(meeting, roundName, agentId) || hasRoundFailure(meeting, roundName, agentId, round);
71
+ }
72
+
73
+ function allParticipantsFinishedRound(meeting, roundName, round = meeting.round) {
74
+ const participants = Array.isArray(meeting.participants) ? meeting.participants : [];
75
+ return participants.length > 0 && participants.every(agentId =>
76
+ hasRoundTerminalOutcome(meeting, roundName, agentId, round)
77
+ );
78
+ }
79
+
80
+ function formatRoundFailuresForConclusion(meeting, roundName, round = meeting.round) {
81
+ const failures = getRoundFailures(meeting, roundName, round, false);
82
+ return Object.entries(failures)
83
+ .map(([agent, failure]) => `- **${agent}**: ${failure.reason || 'Agent failed before producing a meeting contribution.'}`)
84
+ .join('\n') || '- No structured failure details were captured.';
85
+ }
86
+
87
+ function buildFailedMeetingConclusion(meeting, agents, reason) {
88
+ const base = buildTimedOutMeetingConclusion(meeting, agents)
89
+ .replace('*Auto-generated — conclusion round timed out.*', '*Auto-generated — conclusion round failed.*');
90
+ return `${base}\n\n## Conclusion Failure\n${reason || formatRoundFailuresForConclusion(meeting, 'conclude', meeting.round)}`;
91
+ }
92
+
93
+ function advanceMeetingIfRoundComplete(meeting, roundName, meetingId, config = null) {
94
+ if (roundName === 'investigate') {
95
+ if (!allParticipantsFinishedRound(meeting, roundName, meeting.round)) return false;
96
+ meeting.status = 'debating';
97
+ meeting.round = 2;
98
+ meeting.roundStartedAt = ts();
99
+ log('info', `Meeting ${meetingId}: all findings finished — advancing to debate`);
100
+ return true;
101
+ }
102
+ if (roundName === 'debate') {
103
+ if (!allParticipantsFinishedRound(meeting, roundName, meeting.round)) return false;
104
+ meeting.status = 'concluding';
105
+ meeting.round = 3;
106
+ meeting.roundStartedAt = ts();
107
+ log('info', `Meeting ${meetingId}: all debate responses finished — advancing to conclusion`);
108
+ return true;
109
+ }
110
+ if (roundName === 'conclude' && !meeting.conclusion) {
111
+ const agents = (config || queries.getConfig()).agents || {};
112
+ const autoConclusion = buildFailedMeetingConclusion(meeting, agents);
113
+ meeting.conclusion = { content: autoConclusion, agent: 'system', submittedAt: ts() };
114
+ meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'conclusion', content: autoConclusion, at: ts() });
115
+ meeting.status = 'completed';
116
+ meeting.completedAt = ts();
117
+ writeMeetingTranscriptToInbox(meeting, meetingId, agents);
118
+ log('warn', `Meeting ${meetingId}: conclusion failed — auto-generated fallback conclusion`);
119
+ return true;
120
+ }
121
+ return false;
122
+ }
123
+
39
124
  function isEmptyMeetingContent(text) {
40
125
  const value = String(text || '').trim();
41
126
  return !value || EMPTY_OUTPUT_PATTERNS.includes(value);
@@ -233,6 +318,15 @@ function buildTimedOutMeetingConclusion(meeting, agents) {
233
318
  return `*Auto-generated — conclusion round timed out.*\n\nThis summary is based on ${findingsCount} finding${findingsCount === 1 ? '' : 's'} and ${debateCount} debate response${debateCount === 1 ? '' : 's'}.\n\n## Findings Highlights\n${findingsHighlights.join('\n')}\n\n## Debate Takeaways\n${(debateTakeaways.length ? debateTakeaways : fallbackDebate).join('\n')}\n\n## Recommended Next Steps\n${nextSteps.join('\n')}`;
234
319
  }
235
320
 
321
+ function writeMeetingTranscriptToInbox(meeting, meetingId, agents) {
322
+ try {
323
+ const transcript = meeting.transcript.map(t =>
324
+ `### ${agents[t.agent]?.name || t.agent} (${t.type}, Round ${t.round})\n\n${t.content}`
325
+ ).join('\n\n---\n\n');
326
+ shared.writeToInbox('meeting', meetingId, `# Meeting Transcript: ${meeting.title}\n\n${transcript}`);
327
+ } catch (e) { log('warn', `Meeting ${meetingId} inbox write: ${e.message}`); }
328
+ }
329
+
236
330
  function getMeetings() {
237
331
  if (!fs.existsSync(MEETINGS_DIR)) return [];
238
332
  return fs.readdirSync(MEETINGS_DIR)
@@ -249,6 +343,8 @@ function getMeeting(id) {
249
343
  if (!m.debate) m.debate = {};
250
344
  if (!m.humanNotes) m.humanNotes = [];
251
345
  if (!m.participants) m.participants = [];
346
+ if (!m.transcript) m.transcript = [];
347
+ if (!m.roundFailures || typeof m.roundFailures !== 'object') m.roundFailures = {};
252
348
  }
253
349
  return m;
254
350
  }
@@ -272,6 +368,7 @@ function createMeeting({ title, agenda, participants }) {
272
368
  debate: {},
273
369
  conclusion: null,
274
370
  humanNotes: [],
371
+ roundFailures: {},
275
372
  transcript: [],
276
373
  };
277
374
  saveMeeting(meeting);
@@ -373,6 +470,8 @@ function discoverMeetingWork(config) {
373
470
  // Skip if already submitted for this round
374
471
  if (roundName === 'investigating' && meeting.findings?.[agentId]) continue;
375
472
  if (roundName === 'debating' && meeting.debate?.[agentId]) continue;
473
+ const dispatchRoundName = roundName === 'investigating' ? 'investigate' : 'debate';
474
+ if (hasRoundFailure(meeting, dispatchRoundName, agentId, round)) continue;
376
475
 
377
476
  const key = `meeting-${meeting.id}-r${round}-${agentId}`;
378
477
  if (activeKeys.has(key)) continue;
@@ -417,7 +516,7 @@ function discoverMeetingWork(config) {
417
516
  source: 'meeting',
418
517
  meetingId: meeting.id,
419
518
  round,
420
- roundName: roundName === 'investigating' ? 'investigate' : 'debate',
519
+ roundName: dispatchRoundName,
421
520
  }
422
521
  });
423
522
  }
@@ -429,7 +528,7 @@ function discoverMeetingWork(config) {
429
528
  * Collect findings from a completed meeting agent.
430
529
  * Called from runPostCompletionHooks when type === 'meeting'.
431
530
  */
432
- function collectMeetingFindings(meetingId, agentId, roundName, output, structuredCompletion = null, expectedRound = null) {
531
+ function collectMeetingFindings(meetingId, agentId, roundName, output, structuredCompletion = null, expectedRound = null, completionInfo = {}) {
433
532
  const meeting = getMeeting(meetingId);
434
533
  if (!meeting) return;
435
534
  if (isTerminalMeetingStatus(meeting.status)) {
@@ -450,13 +549,33 @@ function collectMeetingFindings(meetingId, agentId, roundName, output, structure
450
549
  log('info', `Ignoring stale round ${expectedRound} output from ${agentId} for meeting ${meetingId} currently on round ${meeting.round || 1}`);
451
550
  return;
452
551
  }
552
+ if (hasRoundTerminalOutcome(meeting, roundName, agentId, meeting.round)) {
553
+ log('info', `Ignoring duplicate ${roundName} output from ${agentId} for meeting ${meetingId}`);
554
+ return;
555
+ }
453
556
 
454
557
  const content = resolveMeetingContributionContent(output, structuredCompletion);
455
-
456
- // Validate output — reject empty or placeholder responses
457
- if (isEmptyMeetingContent(content)) {
458
- log('warn', `Meeting ${meetingId}: agent ${agentId} returned empty output for ${roundName} rejecting`);
459
- // Don't record it — agent will be re-dispatched on next tick
558
+ const completionSucceeded = completionInfo?.success !== false;
559
+
560
+ if (!completionSucceeded || isEmptyMeetingContent(content)) {
561
+ const failures = getRoundFailures(meeting, roundName, meeting.round, true);
562
+ const reason = !completionSucceeded
563
+ ? (completionInfo?.reason || completionInfo?.completionStatus || 'Agent failed before completing the meeting round')
564
+ : 'Agent produced empty meeting output';
565
+ failures[agentId] = {
566
+ reason,
567
+ content: content || completionInfo?.summary || '',
568
+ submittedAt: ts(),
569
+ };
570
+ meeting.transcript.push({
571
+ round: meeting.round,
572
+ agent: agentId,
573
+ type: 'failure',
574
+ content: reason,
575
+ at: ts(),
576
+ });
577
+ log('warn', `Meeting ${meetingId}: agent ${agentId} failed ${roundName} — ${reason}`);
578
+ advanceMeetingIfRoundComplete(meeting, roundName, meetingId);
460
579
  saveMeeting(meeting);
461
580
  return;
462
581
  }
@@ -476,11 +595,7 @@ function collectMeetingFindings(meetingId, agentId, roundName, output, structure
476
595
  // Write transcript to inbox so agents learn from it (slug-based dedup)
477
596
  try {
478
597
  const config = queries.getConfig();
479
- const agents = config.agents || {};
480
- const transcript = meeting.transcript.map(t =>
481
- `### ${agents[t.agent]?.name || t.agent} (${t.type}, Round ${t.round})\n\n${t.content}`
482
- ).join('\n\n---\n\n');
483
- shared.writeToInbox('meeting', meetingId, `# Meeting Transcript: ${meeting.title}\n\n${transcript}`);
598
+ writeMeetingTranscriptToInbox(meeting, meetingId, config.agents || {});
484
599
  } catch (e) { log('warn', `Meeting ${meetingId} inbox write: ${e.message}`); }
485
600
 
486
601
  log('info', `Meeting ${meetingId} completed — transcript written to inbox`);
@@ -488,26 +603,7 @@ function collectMeetingFindings(meetingId, agentId, roundName, output, structure
488
603
  return;
489
604
  }
490
605
 
491
- // Check if all participants have submitted for this round
492
- const participantCount = meeting.participants.length;
493
- const allSubmitted =
494
- (meeting.status === 'investigating' && Object.keys(meeting.findings || {}).length >= participantCount) ||
495
- (meeting.status === 'debating' && Object.keys(meeting.debate || {}).length >= participantCount);
496
-
497
- if (allSubmitted) {
498
- // Advance to next round
499
- if (meeting.status === 'investigating') {
500
- meeting.status = 'debating';
501
- meeting.round = 2;
502
- meeting.roundStartedAt = ts();
503
- log('info', `Meeting ${meetingId}: all findings in — advancing to debate`);
504
- } else if (meeting.status === 'debating') {
505
- meeting.status = 'concluding';
506
- meeting.round = 3;
507
- meeting.roundStartedAt = ts();
508
- log('info', `Meeting ${meetingId}: all debate responses in — advancing to conclusion`);
509
- }
510
- }
606
+ advanceMeetingIfRoundComplete(meeting, roundName, meetingId);
511
607
 
512
608
  saveMeeting(meeting);
513
609
  }
@@ -626,7 +722,8 @@ function deleteMeeting(id) {
626
722
 
627
723
  /**
628
724
  * Check for meeting rounds that have exceeded the timeout.
629
- * Auto-advances to the next round with whatever responses were received.
725
+ * Timeout is observational: rounds advance only after every participant has
726
+ * succeeded or failed, and conclusion waits for the conclusion agent outcome.
630
727
  * Called from engine.js tick cycle.
631
728
  */
632
729
  function checkMeetingTimeouts(config) {
@@ -651,38 +748,23 @@ function checkMeetingTimeouts(config) {
651
748
  : 0;
652
749
  const totalCount = meeting.participants.length;
653
750
 
654
- if (meeting.status === 'investigating') {
655
- log('warn', `Meeting ${meeting.id}: round 1 timed out after ${Math.round(elapsed / 60000)}min — ${respondedCount}/${totalCount} responded, advancing to debate`);
656
- meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round 1 timed out — ${respondedCount}/${totalCount} findings received`, at: ts() });
657
- meeting.status = 'debating';
658
- meeting.round = 2;
659
- meeting.roundStartedAt = ts();
660
- saveMeeting(meeting);
661
- } else if (meeting.status === 'debating') {
662
- log('warn', `Meeting ${meeting.id}: round 2 timed out after ${Math.round(elapsed / 60000)}min ${respondedCount}/${totalCount} responded, advancing to conclusion`);
663
- meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round 2 timed out ${respondedCount}/${totalCount} debate responses received`, at: ts() });
664
- meeting.status = 'concluding';
665
- meeting.round = 3;
666
- meeting.roundStartedAt = ts();
667
- saveMeeting(meeting);
751
+ const roundName = meeting.status === 'investigating'
752
+ ? 'investigate'
753
+ : meeting.status === 'debating'
754
+ ? 'debate'
755
+ : 'conclude';
756
+
757
+ if (roundName !== 'conclude') {
758
+ if (allParticipantsFinishedRound(meeting, roundName, meeting.round)) {
759
+ log('warn', `Meeting ${meeting.id}: round ${meeting.round} timed out after ${Math.round(elapsed / 60000)}min but all participants are terminal — advancing`);
760
+ meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round ${meeting.round} timed out after all participants finished`, at: ts() });
761
+ advanceMeetingIfRoundComplete(meeting, roundName, meeting.id, config);
762
+ saveMeeting(meeting);
763
+ } else {
764
+ log('warn', `Meeting ${meeting.id}: round ${meeting.round} timed out after ${Math.round(elapsed / 60000)}min — waiting for all participants to finish (${respondedCount}/${totalCount} succeeded)`);
765
+ }
668
766
  } else if (meeting.status === 'concluding') {
669
- log('warn', `Meeting ${meeting.id}: conclusion round timed out after ${Math.round(elapsed / 60000)}min — auto-summarizing`);
670
- const autoConclusion = buildTimedOutMeetingConclusion(meeting, config.agents || {});
671
- meeting.conclusion = { content: autoConclusion, agent: 'system', submittedAt: ts() };
672
- meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'conclusion', content: autoConclusion, at: ts() });
673
- meeting.status = 'completed';
674
- meeting.completedAt = ts();
675
-
676
- // Write transcript to inbox (same as normal conclusion path)
677
- try {
678
- const agents = config.agents || {};
679
- const transcript = meeting.transcript.map(t =>
680
- `### ${agents[t.agent]?.name || t.agent} (${t.type}, Round ${t.round})\n\n${t.content}`
681
- ).join('\n\n---\n\n');
682
- shared.writeToInbox('meeting', meeting.id, `# Meeting Transcript: ${meeting.title}\n\n${transcript}`);
683
- } catch (e) { log('warn', `Meeting ${meeting.id} inbox write: ${e.message}`); }
684
-
685
- saveMeeting(meeting);
767
+ log('warn', `Meeting ${meeting.id}: conclusion round timed out after ${Math.round(elapsed / 60000)}min — waiting for the conclusion agent to finish`);
686
768
  }
687
769
  }
688
770
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1710",
3
+ "version": "0.1.1711",
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"