@yemi33/minions 0.1.64 → 0.1.65

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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.65 (2026-03-31)
4
+
5
+ ### Engine
6
+ - engine.js
7
+ - engine/lifecycle.js
8
+ - engine/meeting.js
9
+ - engine/playbook.js
10
+
11
+ ### Dashboard
12
+ - dashboard-build.js
13
+ - dashboard.js
14
+ - dashboard/js/refresh.js
15
+ - dashboard/js/render-meetings.js
16
+ - dashboard/layout.html
17
+ - dashboard/pages/meetings.html
18
+
19
+ ### Playbooks
20
+ - meeting-conclude.md
21
+ - meeting-debate.md
22
+ - meeting-investigate.md
23
+
24
+ ### Other
25
+ - routing.md
26
+
3
27
  ## 0.1.64 (2026-03-31)
4
28
 
5
29
  ### Playbooks
@@ -36,6 +36,7 @@ function _processStatusUpdate(data) {
36
36
  renderSkills(data.skills || []);
37
37
  renderMcpServers(data.mcpServers || []);
38
38
  renderSchedules(data.schedules || []);
39
+ renderMeetings(data.meetings || []);
39
40
  renderPinned(data.pinned || []);
40
41
  // Update sidebar counts
41
42
  const swi = document.getElementById('sidebar-wi');
@@ -0,0 +1,203 @@
1
+ // render-meetings.js — Team meeting rendering
2
+
3
+ function renderMeetings(meetings) {
4
+ const el = document.getElementById('meetings-content');
5
+ const countEl = document.getElementById('meetings-count');
6
+ if (!meetings || meetings.length === 0) {
7
+ countEl.textContent = '0';
8
+ el.innerHTML = '<p class="empty">No meetings yet. Start one to have agents investigate, debate, and conclude on a topic.</p>';
9
+ return;
10
+ }
11
+ countEl.textContent = meetings.length;
12
+
13
+ const statusColors = { investigating: 'var(--blue)', debating: 'var(--purple,#a855f7)', concluding: 'var(--yellow)', completed: 'var(--green)' };
14
+ const statusLabels = { investigating: 'Round 1 — Investigating', debating: 'Round 2 — Debating', concluding: 'Round 3 — Concluding', completed: 'Completed' };
15
+
16
+ el.innerHTML = meetings.map(m => {
17
+ const statusColor = statusColors[m.status] || 'var(--muted)';
18
+ const statusLabel = statusLabels[m.status] || m.status;
19
+ const participantBadges = (m.participants || []).map(p => {
20
+ const hasFindings = m.findings?.[p];
21
+ const hasDebate = m.debate?.[p];
22
+ const icon = m.status === 'completed' ? '✓' : m.status === 'debating' ? (hasDebate ? '✓' : (hasFindings ? '⏳' : '○')) : (hasFindings ? '✓' : '⏳');
23
+ return '<span style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-size:10px">' + icon + ' ' + escHtml(p) + '</span>';
24
+ }).join(' ');
25
+
26
+ return '<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;cursor:pointer" onclick="openMeetingDetail(\'' + escHtml(m.id) + '\')">' +
27
+ '<div style="display:flex;justify-content:space-between;align-items:center">' +
28
+ '<strong style="font-size:13px">' + escHtml(m.title) + '</strong>' +
29
+ '<span style="color:' + statusColor + ';font-size:11px;font-weight:600">' + statusLabel + '</span>' +
30
+ '</div>' +
31
+ '<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap">' + participantBadges + '</div>' +
32
+ '<div style="margin-top:6px;font-size:11px;color:var(--muted)">' + escHtml((m.agenda || '').slice(0, 100)) + (m.agenda?.length > 100 ? '...' : '') + '</div>' +
33
+ '</div>';
34
+ }).join('');
35
+ }
36
+
37
+ function openMeetingDetail(id) {
38
+ fetch('/api/meetings/' + encodeURIComponent(id))
39
+ .then(r => r.json())
40
+ .then(data => {
41
+ if (!data.meeting) { alert('Meeting not found'); return; }
42
+ const m = data.meeting;
43
+ const statusColors = { investigating: 'var(--blue)', debating: 'var(--purple,#a855f7)', concluding: 'var(--yellow)', completed: 'var(--green)' };
44
+ const statusLabels = { investigating: 'Round 1 — Investigating', debating: 'Round 2 — Debating', concluding: 'Round 3 — Concluding', completed: 'Completed' };
45
+
46
+ let html = '<div style="display:flex;flex-direction:column;gap:12px">';
47
+
48
+ // Status bar
49
+ html += '<div style="display:flex;justify-content:space-between;align-items:center">' +
50
+ '<span style="color:' + (statusColors[m.status] || 'var(--muted)') + ';font-weight:600">' + (statusLabels[m.status] || m.status) + '</span>' +
51
+ '<span style="font-size:10px;color:var(--muted)">' + escHtml(m.createdAt?.slice(0, 16).replace('T', ' ') || '') + '</span>' +
52
+ '</div>';
53
+
54
+ // Agenda
55
+ html += '<div style="background:var(--surface2);padding:8px 12px;border-radius:6px;font-size:12px">' +
56
+ '<strong>Agenda:</strong> ' + escHtml(m.agenda) + '</div>';
57
+
58
+ // Per-agent panels
59
+ for (const agent of (m.participants || [])) {
60
+ html += '<div style="border:1px solid var(--border);border-radius:6px;overflow:hidden">';
61
+ html += '<div style="background:var(--surface2);padding:6px 12px;font-weight:600;font-size:12px">' + escHtml(agent) + '</div>';
62
+
63
+ // Findings
64
+ if (m.findings?.[agent]) {
65
+ html += '<div style="padding:8px 12px;font-size:11px;border-bottom:1px solid var(--border)">' +
66
+ '<div style="color:var(--muted);font-size:10px;margin-bottom:4px">Round 1 — Findings</div>' +
67
+ '<div style="white-space:pre-wrap;word-break:break-word">' + escHtml(m.findings[agent].content?.slice(0, 500) || '') + (m.findings[agent].content?.length > 500 ? '...' : '') + '</div></div>';
68
+ }
69
+
70
+ // Debate
71
+ if (m.debate?.[agent]) {
72
+ html += '<div style="padding:8px 12px;font-size:11px;border-bottom:1px solid var(--border)">' +
73
+ '<div style="color:var(--muted);font-size:10px;margin-bottom:4px">Round 2 — Debate</div>' +
74
+ '<div style="white-space:pre-wrap;word-break:break-word">' + escHtml(m.debate[agent].content?.slice(0, 500) || '') + (m.debate[agent].content?.length > 500 ? '...' : '') + '</div></div>';
75
+ }
76
+
77
+ // Status
78
+ if (!m.findings?.[agent] && m.status !== 'completed') {
79
+ html += '<div style="padding:6px 12px;font-size:10px;color:var(--muted)">⏳ Waiting...</div>';
80
+ }
81
+
82
+ html += '</div>';
83
+ }
84
+
85
+ // Conclusion
86
+ if (m.conclusion) {
87
+ html += '<div style="background:rgba(63,185,80,0.08);border:1px solid var(--green);border-radius:6px;padding:10px 14px">' +
88
+ '<div style="color:var(--green);font-weight:600;font-size:12px;margin-bottom:6px">Conclusion (by ' + escHtml(m.conclusion.agent || '?') + ')</div>' +
89
+ '<div style="font-size:12px;white-space:pre-wrap;word-break:break-word">' + escHtml(m.conclusion.content?.slice(0, 1000) || '') + '</div></div>';
90
+ }
91
+
92
+ // Human notes
93
+ if (m.humanNotes?.length > 0) {
94
+ html += '<div style="border-top:1px solid var(--border);padding-top:8px">' +
95
+ '<div style="color:var(--muted);font-size:10px;margin-bottom:4px">Human Notes</div>' +
96
+ m.humanNotes.map(n => '<div style="font-size:11px;margin-bottom:2px">• ' + escHtml(n) + '</div>').join('') +
97
+ '</div>';
98
+ }
99
+
100
+ // Actions
101
+ if (m.status !== 'completed') {
102
+ html += '<div style="display:flex;gap:8px;border-top:1px solid var(--border);padding-top:8px">' +
103
+ '<input id="meeting-note-input" type="text" placeholder="Add context for all agents..." style="flex:1;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:12px" onkeydown="if(event.key===\'Enter\')_submitMeetingNote(\'' + escHtml(m.id) + '\')">' +
104
+ '<button onclick="_submitMeetingNote(\'' + escHtml(m.id) + '\')" style="padding:6px 12px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-size:11px">Add Note</button>' +
105
+ '</div>' +
106
+ '<div style="display:flex;gap:8px;margin-top:4px">' +
107
+ '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--yellow);border-color:var(--yellow)" onclick="_advanceMeeting(\'' + escHtml(m.id) + '\')">Skip to Next Round</button>' +
108
+ '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_endMeeting(\'' + escHtml(m.id) + '\')">End Meeting</button>' +
109
+ '</div>';
110
+ }
111
+
112
+ html += '</div>';
113
+
114
+ document.getElementById('modal-title').textContent = 'Meeting: ' + m.title;
115
+ document.getElementById('modal-body').innerHTML = html;
116
+ document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
117
+ document.getElementById('modal-body').style.whiteSpace = 'normal';
118
+ document.getElementById('modal').classList.add('open');
119
+ })
120
+ .catch(e => alert('Error: ' + e.message));
121
+ }
122
+
123
+ function openCreateMeetingModal() {
124
+ const agentOpts = (typeof cmdAgents !== 'undefined' ? cmdAgents : []).map(a =>
125
+ '<label style="display:flex;align-items:center;gap:6px;cursor:pointer"><input type="checkbox" value="' + escHtml(a.id) + '" checked style="accent-color:var(--blue)"> ' + escHtml(a.name) + ' (' + escHtml(a.role || '') + ')</label>'
126
+ ).join('');
127
+ const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
128
+
129
+ document.getElementById('modal-title').textContent = 'New Team Meeting';
130
+ document.getElementById('modal-body').innerHTML =
131
+ '<div style="display:flex;flex-direction:column;gap:10px">' +
132
+ '<label style="color:var(--text);font-size:var(--text-md)">Title<input id="mtg-title" style="' + inputStyle + '" placeholder="e.g. Should we add SQLite?"></label>' +
133
+ '<label style="color:var(--text);font-size:var(--text-md)">Agenda<textarea id="mtg-agenda" rows="4" style="' + inputStyle + ';resize:vertical" placeholder="What should agents investigate and debate? Be specific about the question to resolve."></textarea></label>' +
134
+ '<div style="color:var(--text);font-size:var(--text-md)">Participants<div style="display:flex;flex-direction:column;gap:4px;margin-top:4px" id="mtg-participants">' + agentOpts + '</div></div>' +
135
+ '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
136
+ '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
137
+ '<button onclick="_submitCreateMeeting()" style="padding:6px 16px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer">Start Meeting</button>' +
138
+ '</div>' +
139
+ '</div>';
140
+ document.getElementById('modal').classList.add('open');
141
+ }
142
+
143
+ async function _submitCreateMeeting() {
144
+ const title = document.getElementById('mtg-title')?.value?.trim();
145
+ const agenda = document.getElementById('mtg-agenda')?.value?.trim();
146
+ if (!title || !agenda) { alert('Title and agenda required'); return; }
147
+ const checks = document.querySelectorAll('#mtg-participants input[type="checkbox"]:checked');
148
+ const participants = [...checks].map(c => c.value);
149
+ if (participants.length < 2) { alert('Select at least 2 participants'); return; }
150
+
151
+ try {
152
+ const res = await fetch('/api/meetings', {
153
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
154
+ body: JSON.stringify({ title, agenda, participants })
155
+ });
156
+ const data = await res.json();
157
+ if (res.ok) {
158
+ try { closeModal(); } catch { /* expected */ }
159
+ wakeEngine();
160
+ refresh();
161
+ try { showToast('cmd-toast', 'Meeting started with ' + participants.length + ' agents', true); } catch { /* expected */ }
162
+ } else { alert('Failed: ' + (data.error || 'unknown')); }
163
+ } catch (e) { alert('Error: ' + e.message); }
164
+ }
165
+
166
+ async function _submitMeetingNote(id) {
167
+ const input = document.getElementById('meeting-note-input');
168
+ if (!input?.value?.trim()) return;
169
+ try {
170
+ await fetch('/api/meetings/note', {
171
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
172
+ body: JSON.stringify({ id, note: input.value.trim() })
173
+ });
174
+ input.value = '';
175
+ openMeetingDetail(id); // refresh the modal
176
+ } catch (e) { alert('Error: ' + e.message); }
177
+ }
178
+
179
+ async function _advanceMeeting(id) {
180
+ if (!confirm('Skip to next round? Agents that haven\'t finished will be skipped.')) return;
181
+ try {
182
+ await fetch('/api/meetings/advance', {
183
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
184
+ body: JSON.stringify({ id })
185
+ });
186
+ wakeEngine();
187
+ openMeetingDetail(id);
188
+ } catch (e) { alert('Error: ' + e.message); }
189
+ }
190
+
191
+ async function _endMeeting(id) {
192
+ if (!confirm('End this meeting? Current round will be stopped.')) return;
193
+ try {
194
+ await fetch('/api/meetings/end', {
195
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
196
+ body: JSON.stringify({ id })
197
+ });
198
+ try { closeModal(); } catch { /* expected */ }
199
+ refresh();
200
+ } catch (e) { alert('Error: ' + e.message); }
201
+ }
202
+
203
+ window.MinionsMeetings = { renderMeetings, openMeetingDetail, openCreateMeetingModal };
@@ -64,6 +64,7 @@
64
64
  <a class="sidebar-link" data-page="inbox" href="/inbox">Notes & KB</a>
65
65
  <a class="sidebar-link" data-page="tools" href="/tools">Skills & MCP</a>
66
66
  <a class="sidebar-link" data-page="schedule" href="/schedule">Schedules</a>
67
+ <a class="sidebar-link" data-page="meetings" href="/meetings">Meetings</a>
67
68
  <a class="sidebar-link" data-page="engine" href="/engine">Engine</a>
68
69
  </nav>
69
70
  <div class="page-content" id="page-content"><!-- __PAGES__ --></div>
@@ -0,0 +1,6 @@
1
+ <section>
2
+ <h2>Team Meetings <span class="count" id="meetings-count">0</span>
3
+ <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:8px" onclick="openCreateMeetingModal()">+ New Meeting</button>
4
+ </h2>
5
+ <div id="meetings-content"><p class="empty">No meetings yet. Start one to have agents investigate, debate, and conclude on a topic.</p></div>
6
+ </section>
@@ -20,7 +20,7 @@ function buildDashboardHtml() {
20
20
  const layout = safeRead(layoutPath);
21
21
  const css = safeRead(path.join(dashDir, 'styles.css'));
22
22
 
23
- const pages = ['home', 'work', 'prs', 'plans', 'inbox', 'tools', 'schedule', 'engine'];
23
+ const pages = ['home', 'work', 'prs', 'plans', 'inbox', 'tools', 'schedule', 'meetings', 'engine'];
24
24
  let pageHtml = '';
25
25
  for (const p of pages) {
26
26
  const content = safeRead(path.join(dashDir, 'pages', p + '.html'));
@@ -32,7 +32,7 @@ function buildDashboardHtml() {
32
32
  'utils', 'state', 'detail-panel', 'live-stream',
33
33
  'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
34
34
  'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
35
- 'render-other', 'render-schedules', 'render-pinned',
35
+ 'render-other', 'render-schedules', 'render-meetings', 'render-pinned',
36
36
  'command-parser', 'command-input', 'command-center', 'command-history',
37
37
  'modal', 'modal-qa', 'settings', 'refresh'
38
38
  ];
package/dashboard.js CHANGED
@@ -63,7 +63,7 @@ function buildDashboardHtml() {
63
63
  const css = safeRead(path.join(dashDir, 'styles.css'));
64
64
 
65
65
  // Assemble page fragments
66
- const pages = ['home', 'work', 'prs', 'plans', 'inbox', 'tools', 'schedule', 'engine'];
66
+ const pages = ['home', 'work', 'prs', 'plans', 'inbox', 'tools', 'schedule', 'meetings', 'engine'];
67
67
  let pageHtml = '';
68
68
  for (const p of pages) {
69
69
  const content = safeRead(path.join(dashDir, 'pages', p + '.html'));
@@ -76,7 +76,7 @@ function buildDashboardHtml() {
76
76
  'utils', 'state', 'detail-panel', 'live-stream',
77
77
  'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
78
78
  'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
79
- 'render-other', 'render-schedules', 'render-pinned',
79
+ 'render-other', 'render-schedules', 'render-meetings', 'render-pinned',
80
80
  'command-parser', 'command-input', 'command-center', 'command-history',
81
81
  'modal', 'modal-qa', 'settings', 'refresh'
82
82
  ];
@@ -227,6 +227,7 @@ function getStatus() {
227
227
  const runs = shared.safeJson(path.join(MINIONS_DIR, 'engine', 'schedule-runs.json')) || {};
228
228
  return scheds.map(s => ({ ...s, _lastRun: runs[s.id] || null }));
229
229
  })(),
230
+ meetings: (() => { try { return require('./engine/meeting').getMeetings(); } catch { return []; } })(),
230
231
  pinned: (() => { try { return parsePinnedEntries(safeRead(path.join(MINIONS_DIR, 'pinned.md'))); } catch { return []; } })(),
231
232
  projects: PROJECTS.map(p => ({ name: p.name, path: p.localPath, description: p.description || '' })),
232
233
  initialized: !!(CONFIG.agents && Object.keys(CONFIG.agents).length > 0),
@@ -3098,6 +3099,59 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3098
3099
  { method: 'POST', path: '/api/schedules/update', desc: 'Update an existing schedule', params: 'id, cron?, title?, type?, project?, agent?, description?, priority?, enabled?', handler: handleSchedulesUpdate },
3099
3100
  { method: 'POST', path: '/api/schedules/delete', desc: 'Delete a schedule', params: 'id', handler: handleSchedulesDelete },
3100
3101
 
3102
+ // Meetings
3103
+ { method: 'POST', path: '/api/meetings', desc: 'Create a team meeting', params: 'title, agenda, participants[]', handler: async (req, res) => {
3104
+ const body = await readBody(req);
3105
+ const { title, agenda, participants } = body;
3106
+ if (!title || !agenda) return jsonReply(res, 400, { error: 'title and agenda required' });
3107
+ const { createMeeting } = require('./engine/meeting');
3108
+ const meeting = createMeeting({ title, agenda, participants: participants || [] });
3109
+ invalidateStatusCache();
3110
+ return jsonReply(res, 200, { ok: true, meeting });
3111
+ }},
3112
+
3113
+ { method: 'GET', path: '/api/meetings', desc: 'List all meetings', handler: async (req, res) => {
3114
+ const { getMeetings } = require('./engine/meeting');
3115
+ return jsonReply(res, 200, { meetings: getMeetings() });
3116
+ }},
3117
+
3118
+ { method: 'GET', path: /^\/api\/meetings\/(MTG-[\w]+)$/, desc: 'Get meeting detail', handler: async (req, res, match) => {
3119
+ const { getMeeting } = require('./engine/meeting');
3120
+ const meeting = getMeeting(match[1]);
3121
+ if (!meeting) return jsonReply(res, 404, { error: 'Meeting not found' });
3122
+ return jsonReply(res, 200, { meeting });
3123
+ }},
3124
+
3125
+ { method: 'POST', path: '/api/meetings/note', desc: 'Add human note to active meeting', params: 'id, note', handler: async (req, res) => {
3126
+ const body = await readBody(req);
3127
+ if (!body.id || !body.note) return jsonReply(res, 400, { error: 'id and note required' });
3128
+ const { addMeetingNote } = require('./engine/meeting');
3129
+ const meeting = addMeetingNote(body.id, body.note);
3130
+ if (!meeting) return jsonReply(res, 404, { error: 'Meeting not found' });
3131
+ invalidateStatusCache();
3132
+ return jsonReply(res, 200, { ok: true, meeting });
3133
+ }},
3134
+
3135
+ { method: 'POST', path: '/api/meetings/advance', desc: 'Force advance meeting to next round', params: 'id', handler: async (req, res) => {
3136
+ const body = await readBody(req);
3137
+ if (!body.id) return jsonReply(res, 400, { error: 'id required' });
3138
+ const { advanceMeetingRound } = require('./engine/meeting');
3139
+ const meeting = advanceMeetingRound(body.id);
3140
+ if (!meeting) return jsonReply(res, 404, { error: 'Meeting not found or already completed' });
3141
+ invalidateStatusCache();
3142
+ return jsonReply(res, 200, { ok: true, meeting });
3143
+ }},
3144
+
3145
+ { method: 'POST', path: '/api/meetings/end', desc: 'End a meeting early', params: 'id', handler: async (req, res) => {
3146
+ const body = await readBody(req);
3147
+ if (!body.id) return jsonReply(res, 400, { error: 'id required' });
3148
+ const { endMeeting } = require('./engine/meeting');
3149
+ const meeting = endMeeting(body.id);
3150
+ if (!meeting) return jsonReply(res, 404, { error: 'Meeting not found' });
3151
+ invalidateStatusCache();
3152
+ return jsonReply(res, 200, { ok: true });
3153
+ }},
3154
+
3101
3155
  // Engine
3102
3156
  { method: 'POST', path: '/api/engine/wakeup', desc: 'Trigger immediate engine tick via control.json signal', handler: async (req, res) => {
3103
3157
  const controlPath = path.join(MINIONS_DIR, 'engine', 'control.json');
@@ -1074,6 +1074,14 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1074
1074
  } catch (err) { e.log('warn', `Decompose cleanup: ${err.message}`); }
1075
1075
  }
1076
1076
  }
1077
+ // Meeting post-completion: collect findings/debate/conclusion
1078
+ if (type === 'meeting' && meta?.meetingId) {
1079
+ try {
1080
+ const { collectMeetingFindings } = require('./meeting');
1081
+ collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout);
1082
+ } catch (err) { engine().log('warn', `Meeting collect: ${err.message}`); }
1083
+ }
1084
+
1077
1085
  // Plan chaining removed — user must explicitly execute plan-to-prd after reviewing the plan
1078
1086
  if (isSuccess && meta?.item?.sourcePlan) checkPlanCompletion(meta, config);
1079
1087
 
@@ -0,0 +1,266 @@
1
+ /**
2
+ * engine/meeting.js — Team meeting orchestration.
3
+ * Manages multi-round meetings: investigate → debate → conclude.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const shared = require('./shared');
9
+ const { safeJson, safeWrite, safeRead, uid } = shared;
10
+ const queries = require('./queries');
11
+ const { getDispatch } = queries;
12
+ const { renderPlaybook } = require('./playbook');
13
+
14
+ let _engine = null;
15
+ function engine() { if (!_engine) _engine = require('../engine'); return _engine; }
16
+
17
+ const MEETINGS_DIR = path.join(__dirname, '..', 'meetings');
18
+
19
+ function getMeetings() {
20
+ if (!fs.existsSync(MEETINGS_DIR)) return [];
21
+ return fs.readdirSync(MEETINGS_DIR)
22
+ .filter(f => f.endsWith('.json'))
23
+ .map(f => safeJson(path.join(MEETINGS_DIR, f)))
24
+ .filter(Boolean);
25
+ }
26
+
27
+ function getMeeting(id) {
28
+ const filePath = path.join(MEETINGS_DIR, id + '.json');
29
+ return safeJson(filePath);
30
+ }
31
+
32
+ function saveMeeting(meeting) {
33
+ if (!fs.existsSync(MEETINGS_DIR)) fs.mkdirSync(MEETINGS_DIR, { recursive: true });
34
+ safeWrite(path.join(MEETINGS_DIR, meeting.id + '.json'), meeting);
35
+ }
36
+
37
+ function createMeeting({ title, agenda, participants }) {
38
+ const id = 'MTG-' + uid().slice(0, 8);
39
+ const meeting = {
40
+ id, title, agenda,
41
+ status: 'investigating',
42
+ round: 1,
43
+ participants: participants || [],
44
+ createdBy: 'human',
45
+ createdAt: new Date().toISOString(),
46
+ findings: {},
47
+ debate: {},
48
+ conclusion: null,
49
+ humanNotes: [],
50
+ transcript: [],
51
+ };
52
+ saveMeeting(meeting);
53
+ return meeting;
54
+ }
55
+
56
+ /**
57
+ * Discover meeting work items for the current round.
58
+ * Called from discoverWork() in engine.js tick cycle.
59
+ */
60
+ function discoverMeetingWork(config) {
61
+ const meetings = getMeetings();
62
+ const work = [];
63
+ const dispatch = getDispatch();
64
+ const activeKeys = new Set(
65
+ [...(dispatch.pending || []), ...(dispatch.active || [])]
66
+ .map(d => d.meta?.dispatchKey)
67
+ .filter(Boolean)
68
+ );
69
+
70
+ for (const meeting of meetings) {
71
+ if (meeting.status === 'completed') continue;
72
+
73
+ const round = meeting.round || 1;
74
+ const roundName = meeting.status; // investigating, debating, concluding
75
+ const agents = config.agents || {};
76
+
77
+ if (roundName === 'concluding') {
78
+ // Only one agent concludes (first participant)
79
+ const concluder = meeting.participants[0];
80
+ if (!concluder) continue;
81
+ const key = `meeting-${meeting.id}-r${round}-${concluder}`;
82
+ if (activeKeys.has(key)) continue;
83
+
84
+ const humanNotes = (meeting.humanNotes || []).map(n => '- ' + n).join('\n');
85
+ const allFindings = Object.entries(meeting.findings || {}).map(([agent, f]) =>
86
+ `### ${agents[agent]?.name || agent}\n\n${f.content || '(no findings)'}`
87
+ ).join('\n\n---\n\n');
88
+ const allDebate = Object.entries(meeting.debate || {}).map(([agent, d]) =>
89
+ `### ${agents[agent]?.name || agent}\n\n${d.content || '(no response)'}`
90
+ ).join('\n\n---\n\n');
91
+
92
+ const vars = {
93
+ agent_name: agents[concluder]?.name || concluder,
94
+ agent_role: agents[concluder]?.role || 'Agent',
95
+ agent_id: concluder,
96
+ meeting_title: meeting.title,
97
+ agenda: meeting.agenda,
98
+ all_findings: allFindings,
99
+ all_debate: allDebate,
100
+ human_notes: humanNotes,
101
+ };
102
+
103
+ const prompt = renderPlaybook('meeting-conclude', vars);
104
+ if (!prompt) continue;
105
+
106
+ work.push({
107
+ type: 'meeting',
108
+ agent: concluder,
109
+ agentName: agents[concluder]?.name || concluder,
110
+ agentRole: agents[concluder]?.role || 'Agent',
111
+ task: `Meeting: ${meeting.title} (Conclude)`,
112
+ prompt,
113
+ meta: {
114
+ dispatchKey: key,
115
+ source: 'meeting',
116
+ meetingId: meeting.id,
117
+ round,
118
+ roundName: 'conclude',
119
+ }
120
+ });
121
+ continue;
122
+ }
123
+
124
+ // For investigate and debate rounds, dispatch all participants
125
+ for (const agentId of meeting.participants) {
126
+ // Skip if already submitted for this round
127
+ if (roundName === 'investigating' && meeting.findings?.[agentId]) continue;
128
+ if (roundName === 'debating' && meeting.debate?.[agentId]) continue;
129
+
130
+ const key = `meeting-${meeting.id}-r${round}-${agentId}`;
131
+ if (activeKeys.has(key)) continue;
132
+
133
+ const humanNotes = (meeting.humanNotes || []).map(n => '- ' + n).join('\n');
134
+ const vars = {
135
+ agent_name: agents[agentId]?.name || agentId,
136
+ agent_role: agents[agentId]?.role || 'Agent',
137
+ agent_id: agentId,
138
+ meeting_title: meeting.title,
139
+ agenda: meeting.agenda,
140
+ human_notes: humanNotes,
141
+ };
142
+
143
+ if (roundName === 'debating') {
144
+ vars.all_findings = Object.entries(meeting.findings || {}).map(([agent, f]) =>
145
+ `### ${agents[agent]?.name || agent}\n\n${f.content || '(no findings)'}`
146
+ ).join('\n\n---\n\n');
147
+ }
148
+
149
+ const playbookName = roundName === 'investigating' ? 'meeting-investigate' : 'meeting-debate';
150
+ const prompt = renderPlaybook(playbookName, vars);
151
+ if (!prompt) continue;
152
+
153
+ work.push({
154
+ type: 'meeting',
155
+ agent: agentId,
156
+ agentName: agents[agentId]?.name || agentId,
157
+ agentRole: agents[agentId]?.role || 'Agent',
158
+ task: `Meeting: ${meeting.title} (Round ${round} — ${roundName})`,
159
+ prompt,
160
+ meta: {
161
+ dispatchKey: key,
162
+ source: 'meeting',
163
+ meetingId: meeting.id,
164
+ round,
165
+ roundName: roundName === 'investigating' ? 'investigate' : 'debate',
166
+ }
167
+ });
168
+ }
169
+ }
170
+ return work;
171
+ }
172
+
173
+ /**
174
+ * Collect findings from a completed meeting agent.
175
+ * Called from runPostCompletionHooks when type === 'meeting'.
176
+ */
177
+ function collectMeetingFindings(meetingId, agentId, roundName, output) {
178
+ const e = engine();
179
+ const meeting = getMeeting(meetingId);
180
+ if (!meeting) return;
181
+
182
+ const { text } = shared.parseStreamJsonOutput(output, { maxTextLength: 50000 });
183
+ const content = text || '(no output)';
184
+
185
+ if (roundName === 'investigate') {
186
+ meeting.findings[agentId] = { content, submittedAt: new Date().toISOString() };
187
+ meeting.transcript.push({ round: meeting.round, agent: agentId, type: 'finding', content, at: new Date().toISOString() });
188
+ } else if (roundName === 'debate') {
189
+ meeting.debate[agentId] = { content, submittedAt: new Date().toISOString() };
190
+ meeting.transcript.push({ round: meeting.round, agent: agentId, type: 'debate', content, at: new Date().toISOString() });
191
+ } else if (roundName === 'conclude') {
192
+ meeting.conclusion = { content, agent: agentId, submittedAt: new Date().toISOString() };
193
+ meeting.transcript.push({ round: meeting.round, agent: agentId, type: 'conclusion', content, at: new Date().toISOString() });
194
+ meeting.status = 'completed';
195
+ meeting.completedAt = new Date().toISOString();
196
+
197
+ // Write transcript to inbox so agents learn from it
198
+ const config = queries.getConfig();
199
+ const agents = config.agents || {};
200
+ const transcript = meeting.transcript.map(t =>
201
+ `### ${agents[t.agent]?.name || t.agent} (${t.type}, Round ${t.round})\n\n${t.content}`
202
+ ).join('\n\n---\n\n');
203
+ const inboxPath = path.join(__dirname, '..', 'notes', 'inbox',
204
+ `meeting-${meetingId}-${new Date().toISOString().slice(0, 10)}.md`);
205
+ safeWrite(inboxPath, `# Meeting Transcript: ${meeting.title}\n\n${transcript}`);
206
+
207
+ e.log('info', `Meeting ${meetingId} completed — transcript written to inbox`);
208
+ saveMeeting(meeting);
209
+ return;
210
+ }
211
+
212
+ // Check if all participants have submitted for this round
213
+ const allSubmitted = meeting.participants.every(p => {
214
+ if (meeting.status === 'investigating') return !!meeting.findings[p];
215
+ if (meeting.status === 'debating') return !!meeting.debate[p];
216
+ return true;
217
+ });
218
+
219
+ if (allSubmitted) {
220
+ // Advance to next round
221
+ if (meeting.status === 'investigating') {
222
+ meeting.status = 'debating';
223
+ meeting.round = 2;
224
+ e.log('info', `Meeting ${meetingId}: all findings in — advancing to debate`);
225
+ } else if (meeting.status === 'debating') {
226
+ meeting.status = 'concluding';
227
+ meeting.round = 3;
228
+ e.log('info', `Meeting ${meetingId}: all debate responses in — advancing to conclusion`);
229
+ }
230
+ }
231
+
232
+ saveMeeting(meeting);
233
+ }
234
+
235
+ function addMeetingNote(meetingId, note) {
236
+ const meeting = getMeeting(meetingId);
237
+ if (!meeting) return null;
238
+ meeting.humanNotes.push(note);
239
+ meeting.transcript.push({ round: meeting.round, agent: 'human', type: 'note', content: note, at: new Date().toISOString() });
240
+ saveMeeting(meeting);
241
+ return meeting;
242
+ }
243
+
244
+ function advanceMeetingRound(meetingId) {
245
+ const meeting = getMeeting(meetingId);
246
+ if (!meeting || meeting.status === 'completed') return null;
247
+ if (meeting.status === 'investigating') { meeting.status = 'debating'; meeting.round = 2; }
248
+ else if (meeting.status === 'debating') { meeting.status = 'concluding'; meeting.round = 3; }
249
+ saveMeeting(meeting);
250
+ return meeting;
251
+ }
252
+
253
+ function endMeeting(meetingId) {
254
+ const meeting = getMeeting(meetingId);
255
+ if (!meeting) return null;
256
+ meeting.status = 'completed';
257
+ meeting.completedAt = new Date().toISOString();
258
+ saveMeeting(meeting);
259
+ return meeting;
260
+ }
261
+
262
+ module.exports = {
263
+ MEETINGS_DIR, getMeetings, getMeeting, saveMeeting, createMeeting,
264
+ discoverMeetingWork, collectMeetingFindings,
265
+ addMeetingNote, advanceMeetingRound, endMeeting,
266
+ };
@@ -440,7 +440,7 @@ function selectPlaybook(workType, item) {
440
440
  if (workType === 'review' && !item?._pr && !item?.pr_id) {
441
441
  return 'work-item';
442
442
  }
443
- const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose'];
443
+ const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose', 'meeting-investigate', 'meeting-debate', 'meeting-conclude'];
444
444
  return typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
445
445
  }
446
446
 
package/engine.js CHANGED
@@ -1960,6 +1960,13 @@ function discoverWork(config) {
1960
1960
  }
1961
1961
  } catch (e) { log('warn', 'discover scheduled work: ' + e.message); }
1962
1962
 
1963
+ // Meeting work (multi-round team discussions)
1964
+ try {
1965
+ const { discoverMeetingWork } = require('./engine/meeting');
1966
+ const meetingWork = discoverMeetingWork(config);
1967
+ allWorkItems.push(...meetingWork);
1968
+ } catch (e) { log('warn', 'discover meeting work: ' + e.message); }
1969
+
1963
1970
  // Gate reviews and fixes: do not dispatch until all implement items are complete
1964
1971
  const hasIncompleteImplements = projects.some(project => {
1965
1972
  const items = safeJson(projectWorkItemsPath(project)) || [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.64",
3
+ "version": "0.1.65",
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"
@@ -0,0 +1,35 @@
1
+ # Meeting: Conclusion
2
+
3
+ You are {{agent_name}} ({{agent_role}}), synthesizing the team meeting results.
4
+
5
+ ## Meeting: {{meeting_title}}
6
+
7
+ ## Agenda
8
+
9
+ {{agenda}}
10
+
11
+ ## Investigation Findings
12
+
13
+ {{all_findings}}
14
+
15
+ ## Debate Responses
16
+
17
+ {{all_debate}}
18
+
19
+ {{#human_notes}}
20
+ ## Human Notes
21
+
22
+ {{human_notes}}
23
+ {{/human_notes}}
24
+
25
+ ## Your Task
26
+
27
+ Write a clear meeting conclusion:
28
+
29
+ 1. **Areas of consensus** — what does the team agree on?
30
+ 2. **Unresolved disagreements** — where do positions still differ?
31
+ 3. **Recommended decision** — what should we do?
32
+ 4. **Action items** — specific next steps with owners
33
+ 5. **Open questions** — what still needs human input?
34
+
35
+ Be decisive. If there's a clear best option, say so.
@@ -0,0 +1,35 @@
1
+ # Meeting: Debate Round
2
+
3
+ You are {{agent_name}} ({{agent_role}}) in Round 2 of a team meeting.
4
+
5
+ ## Meeting: {{meeting_title}}
6
+
7
+ ## Agenda
8
+
9
+ {{agenda}}
10
+
11
+ ## Round 1 Findings from All Participants
12
+
13
+ {{all_findings}}
14
+
15
+ {{#human_notes}}
16
+ ## Human Notes (READ CAREFULLY)
17
+
18
+ {{human_notes}}
19
+ {{/human_notes}}
20
+
21
+ ## Your Task
22
+
23
+ You've read everyone's investigation findings above. Now:
24
+
25
+ 1. **What do you agree with?** Which points are strongest?
26
+ 2. **What do you disagree with?** Challenge weak arguments directly.
27
+ 3. **Play devil's advocate** — what's the strongest counterargument to your OWN position?
28
+ 4. **What's missing?** What did everyone overlook?
29
+ 5. **What should the team decide?**
30
+
31
+ Be direct. Constructive disagreement is encouraged — don't just agree with everyone.
32
+
33
+ ## Output Format
34
+
35
+ Write your debate response as markdown. Reference other agents' findings by name.
@@ -0,0 +1,30 @@
1
+ # Meeting: Investigation Round
2
+
3
+ You are {{agent_name}} ({{agent_role}}) participating in a team meeting.
4
+
5
+ ## Meeting: {{meeting_title}}
6
+
7
+ ## Agenda
8
+
9
+ {{agenda}}
10
+
11
+ {{#human_notes}}
12
+ ## Human Notes (READ CAREFULLY)
13
+
14
+ {{human_notes}}
15
+ {{/human_notes}}
16
+
17
+ ## Your Task
18
+
19
+ Investigate this topic from your unique perspective as {{agent_role}}.
20
+
21
+ 1. **Analyze** the question/issue thoroughly
22
+ 2. **Research** the codebase if relevant (use your tools)
23
+ 3. **Form your position** with evidence
24
+ 4. **Write your findings** clearly — other agents will read and respond
25
+
26
+ Focus on what YOU uniquely bring to this discussion. Be thorough but concise.
27
+
28
+ ## Output Format
29
+
30
+ Write your findings as a clear markdown document. Start with your key conclusion, then supporting evidence.
package/routing.md CHANGED
@@ -18,6 +18,7 @@ How the engine decides who handles what. Parsed by engine.js — keep the table
18
18
  | ask | ripley | rebecca |
19
19
  | verify | dallas | ralph |
20
20
  | decompose | ripley | rebecca |
21
+ | meeting | ripley | rebecca |
21
22
 
22
23
  Notes:
23
24
  - `_author_` means route to the PR author