@yemi33/minions 0.1.1053 → 0.1.1055

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,11 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1053 (2026-04-17)
3
+ ## 0.1.1055 (2026-04-17)
4
4
 
5
5
  ### Features
6
6
  - seed realActivityMap at spawn time, stamp pid in live-output (#1200)
7
7
 
8
8
  ### Fixes
9
+ - guard undefined agent in pending dispatch loop (closes #1206) (#1210)
10
+ - improve fallback meeting conclusion
9
11
  - remove command center chevron
10
12
  - stamp live-output.log stub before spawn (#1198)
11
13
  - harden settings save and migrate pr poll config
package/engine/meeting.js CHANGED
@@ -32,6 +32,100 @@ function formatMeetingContributions(entries, agents, emptyText, label, maxBytes)
32
32
  return truncateMeetingContext(combined, maxBytes, label);
33
33
  }
34
34
 
35
+ function cleanMeetingSummaryText(text) {
36
+ return String(text || '')
37
+ .replace(/\r/g, '')
38
+ .replace(/```[\s\S]*?```/g, ' ')
39
+ .replace(/`([^`]+)`/g, '$1')
40
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
41
+ .replace(/^[#>*-]+\s*/gm, '')
42
+ .replace(/\s+/g, ' ')
43
+ .trim();
44
+ }
45
+
46
+ function splitMeetingSummaryFragments(text) {
47
+ return cleanMeetingSummaryText(text)
48
+ .split(/\n+|(?:[.!?])\s+|;\s+/)
49
+ .map(s => s.trim())
50
+ .filter(Boolean);
51
+ }
52
+
53
+ function truncateMeetingSummary(text, maxLen) {
54
+ if (text.length <= maxLen) return text;
55
+ return text.slice(0, Math.max(0, maxLen - 1)).trimEnd() + '…';
56
+ }
57
+
58
+ function formatMeetingSummaryBullets(entries, agents, emptyText, maxLen) {
59
+ const pairs = Object.entries(typeof entries === 'object' && entries ? entries : {});
60
+ if (pairs.length === 0) return [`- ${emptyText}`];
61
+ return pairs.map(([agent, value]) => {
62
+ const fragments = splitMeetingSummaryFragments(value?.content || '');
63
+ const summary = truncateMeetingSummary(fragments[0] || cleanMeetingSummaryText(value?.content || '') || emptyText, maxLen);
64
+ return `- **${agents[agent]?.name || agent}**: ${summary}`;
65
+ });
66
+ }
67
+
68
+ function scoreMeetingTakeaway(fragment) {
69
+ const lower = fragment.toLowerCase();
70
+ let score = 0;
71
+ if (/(should|must|need to|needs to|recommend|recommended|action|next step|follow up|fix|mitigat|investigat|verify|test|block)/.test(lower)) score += 4;
72
+ if (/(agree|aligned|consensus|support|prefer)/.test(lower)) score += 3;
73
+ if (/(disagree|however|but|risk|risky|concern|trade-off|question|uncertain|worry)/.test(lower)) score += 3;
74
+ if (fragment.length >= 40 && fragment.length <= 180) score += 2;
75
+ if (fragment.length > 220) score -= 1;
76
+ return score;
77
+ }
78
+
79
+ function collectMeetingTakeaways(entries, agents, maxItems) {
80
+ const seen = new Set();
81
+ const candidates = [];
82
+ for (const [agent, value] of Object.entries(typeof entries === 'object' && entries ? entries : {})) {
83
+ for (const fragment of splitMeetingSummaryFragments(value?.content || '')) {
84
+ if (fragment.length < 20) continue;
85
+ const normalized = fragment.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
86
+ if (!normalized || seen.has(normalized)) continue;
87
+ seen.add(normalized);
88
+ candidates.push({
89
+ text: `- **${agents[agent]?.name || agent}**: ${truncateMeetingSummary(fragment, 180)}`,
90
+ score: scoreMeetingTakeaway(fragment),
91
+ });
92
+ }
93
+ }
94
+ return candidates
95
+ .sort((a, b) => b.score - a.score || a.text.length - b.text.length)
96
+ .slice(0, maxItems)
97
+ .map(item => item.text);
98
+ }
99
+
100
+ function collectMeetingNextSteps(meeting) {
101
+ const actionPattern = /\b(should|must|need to|needs to|recommend|recommended|follow up|fix|mitigate|investigate|verify|test|document|ship|patch|review)\b/i;
102
+ const seen = new Set();
103
+ const steps = [];
104
+ for (const entries of [meeting.debate, meeting.findings]) {
105
+ for (const value of Object.values(typeof entries === 'object' && entries ? entries : {})) {
106
+ for (const fragment of splitMeetingSummaryFragments(value?.content || '')) {
107
+ if (!actionPattern.test(fragment)) continue;
108
+ const normalized = fragment.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
109
+ if (!normalized || seen.has(normalized)) continue;
110
+ seen.add(normalized);
111
+ steps.push(`- ${truncateMeetingSummary(fragment, 180)}`);
112
+ if (steps.length >= 3) return steps;
113
+ }
114
+ }
115
+ }
116
+ return ['- Review the findings and debate, then add a human-written conclusion if more nuance is needed.'];
117
+ }
118
+
119
+ function buildTimedOutMeetingConclusion(meeting, agents) {
120
+ const findingsCount = Object.keys(meeting.findings || {}).length;
121
+ const debateCount = Object.keys(meeting.debate || {}).length;
122
+ const findingsHighlights = formatMeetingSummaryBullets(meeting.findings, agents, '(none)', 180);
123
+ const debateTakeaways = collectMeetingTakeaways(meeting.debate, agents, 4);
124
+ const fallbackDebate = formatMeetingSummaryBullets(meeting.debate, agents, '(none)', 180);
125
+ const nextSteps = collectMeetingNextSteps(meeting);
126
+ 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')}`;
127
+ }
128
+
35
129
  function getMeetings() {
36
130
  if (!fs.existsSync(MEETINGS_DIR)) return [];
37
131
  return fs.readdirSync(MEETINGS_DIR)
@@ -417,14 +511,7 @@ function checkMeetingTimeouts(config) {
417
511
  saveMeeting(meeting);
418
512
  } else if (meeting.status === 'concluding') {
419
513
  log('warn', `Meeting ${meeting.id}: conclusion round timed out after ${Math.round(elapsed / 60000)}min — auto-summarizing`);
420
- // Synthesize a conclusion from available findings and debate rather than leaving it empty
421
- const findingsSummary = Object.entries(meeting.findings || {}).map(([agent, f]) =>
422
- `**${(config.agents || {})[agent]?.name || agent}**: ${(f.content || '').slice(0, 200)}`
423
- ).join('\n');
424
- const debateSummary = Object.entries(meeting.debate || {}).map(([agent, d]) =>
425
- `**${(config.agents || {})[agent]?.name || agent}**: ${(d.content || '').slice(0, 200)}`
426
- ).join('\n');
427
- const autoConclusion = `*Auto-generated — conclusion round timed out.*\n\n## Key Findings\n${findingsSummary || '(none)'}\n\n## Debate Summary\n${debateSummary || '(none)'}`;
514
+ const autoConclusion = buildTimedOutMeetingConclusion(meeting, config.agents || {});
428
515
  meeting.conclusion = { content: autoConclusion, agent: 'system', submittedAt: ts() };
429
516
  meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'conclusion', content: autoConclusion, at: ts() });
430
517
  meeting.status = 'completed';
package/engine.js CHANGED
@@ -3483,6 +3483,35 @@ async function tickInner() {
3483
3483
  log('warn', `Duplicate dispatch ID ${item.id} in pending queue — skipping`);
3484
3484
  continue;
3485
3485
  }
3486
+ // #1206: Guard against undefined/non-string item.agent. A corrupted dispatch
3487
+ // entry (manual edit, serialization round-trip, cleared field) would otherwise
3488
+ // be handed to spawnAgent, which crashes with `TypeError: "path" argument must
3489
+ // be of type string. Received undefined` and re-queues — every tick. Try to
3490
+ // resolve a fallback via routing; if none is available, skip this tick.
3491
+ if (!item.agent || typeof item.agent !== 'string') {
3492
+ const fallback = resolveAgent(item.type || WORK_TYPE.FIX, config);
3493
+ if (!fallback) {
3494
+ log('warn', `Pending dispatch ${item.id} has no agent and routing returned no fallback — skipping`);
3495
+ continue;
3496
+ }
3497
+ log('info', `Pending dispatch ${item.id} missing agent; routed → ${fallback} (#1206 guard)`);
3498
+ item.agent = fallback;
3499
+ item.agentName = config.agents[fallback]?.name || tempAgents.get(fallback)?.name || fallback;
3500
+ item.agentRole = config.agents[fallback]?.role || tempAgents.get(fallback)?.role || 'Agent';
3501
+ // Persist so the fix survives across ticks even if this dispatch is skipped
3502
+ // later in the loop (branch lock, concurrency cap, agent busy, etc.).
3503
+ try {
3504
+ mutateDispatch((dp) => {
3505
+ const p = (dp.pending || []).find(d => d.id === item.id);
3506
+ if (p) {
3507
+ p.agent = item.agent;
3508
+ p.agentName = item.agentName;
3509
+ p.agentRole = item.agentRole;
3510
+ }
3511
+ return dp;
3512
+ });
3513
+ } catch (e) { log('warn', `Persist agent resolution for ${item.id} failed: ${e.message}`); }
3514
+ }
3486
3515
  if (busyAgents.has(item.agent)) {
3487
3516
  // Agent busy reassignment: if item has been waiting on a busy agent past the threshold,
3488
3517
  // try to find an alternative agent via routing. Skip explicitly assigned items.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1053",
3
+ "version": "0.1.1055",
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"