@yemi33/minions 0.1.64 → 0.1.66
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 +30 -0
- package/dashboard/js/refresh.js +4 -8
- package/dashboard/js/render-meetings.js +203 -0
- package/dashboard/layout.html +1 -0
- package/dashboard/pages/meetings.html +6 -0
- package/dashboard-build.js +2 -2
- package/dashboard.js +61 -3
- package/engine/lifecycle.js +8 -0
- package/engine/meeting.js +266 -0
- package/engine/playbook.js +1 -1
- package/engine.js +7 -0
- package/package.json +1 -1
- package/playbooks/meeting-conclude.md +35 -0
- package/playbooks/meeting-debate.md +35 -0
- package/playbooks/meeting-investigate.md +30 -0
- package/routing.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.66 (2026-03-31)
|
|
4
|
+
|
|
5
|
+
### Dashboard
|
|
6
|
+
- dashboard.js
|
|
7
|
+
- dashboard/js/refresh.js
|
|
8
|
+
|
|
9
|
+
## 0.1.65 (2026-03-31)
|
|
10
|
+
|
|
11
|
+
### Engine
|
|
12
|
+
- engine.js
|
|
13
|
+
- engine/lifecycle.js
|
|
14
|
+
- engine/meeting.js
|
|
15
|
+
- engine/playbook.js
|
|
16
|
+
|
|
17
|
+
### Dashboard
|
|
18
|
+
- dashboard-build.js
|
|
19
|
+
- dashboard.js
|
|
20
|
+
- dashboard/js/refresh.js
|
|
21
|
+
- dashboard/js/render-meetings.js
|
|
22
|
+
- dashboard/layout.html
|
|
23
|
+
- dashboard/pages/meetings.html
|
|
24
|
+
|
|
25
|
+
### Playbooks
|
|
26
|
+
- meeting-conclude.md
|
|
27
|
+
- meeting-debate.md
|
|
28
|
+
- meeting-investigate.md
|
|
29
|
+
|
|
30
|
+
### Other
|
|
31
|
+
- routing.md
|
|
32
|
+
|
|
3
33
|
## 0.1.64 (2026-03-31)
|
|
4
34
|
|
|
5
35
|
### Playbooks
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -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');
|
|
@@ -56,15 +57,16 @@ async function refresh() {
|
|
|
56
57
|
|
|
57
58
|
refresh();
|
|
58
59
|
|
|
59
|
-
// SSE status stream — real-time push
|
|
60
|
+
// SSE status stream — real-time push + hot-reload on one connection
|
|
61
|
+
// (avoids exhausting HTTP/1.1's 6-connection-per-origin limit)
|
|
60
62
|
let _statusStream = null;
|
|
61
63
|
try {
|
|
62
64
|
_statusStream = new EventSource('/api/status-stream');
|
|
63
65
|
_statusStream.onmessage = (e) => {
|
|
64
66
|
try { _processStatusUpdate(JSON.parse(e.data)); } catch (e2) { console.error('status-stream:', e2.message); }
|
|
65
67
|
};
|
|
68
|
+
_statusStream.addEventListener('reload', () => { location.reload(); });
|
|
66
69
|
_statusStream.onerror = () => {
|
|
67
|
-
// Fall back to polling
|
|
68
70
|
if (_statusStream) { _statusStream.close(); _statusStream = null; }
|
|
69
71
|
setInterval(refresh, 4000);
|
|
70
72
|
};
|
|
@@ -78,10 +80,4 @@ document.querySelectorAll('.sidebar-link').forEach(link => {
|
|
|
78
80
|
});
|
|
79
81
|
switchPage(currentPage);
|
|
80
82
|
|
|
81
|
-
// Hot-reload: auto-refresh browser when dashboard files change
|
|
82
|
-
try {
|
|
83
|
-
const _hotReload = new EventSource('/api/hot-reload');
|
|
84
|
-
_hotReload.onmessage = (e) => { if (e.data === 'reload') location.reload(); };
|
|
85
|
-
} catch { /* expected */ }
|
|
86
|
-
|
|
87
83
|
window.MinionsRefresh = { refresh };
|
|
@@ -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 };
|
package/dashboard/layout.html
CHANGED
|
@@ -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>
|
package/dashboard-build.js
CHANGED
|
@@ -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
|
];
|
|
@@ -109,7 +109,11 @@ function rebuildDashboardHtml() {
|
|
|
109
109
|
HTML_GZ = zlib.gzipSync(HTML);
|
|
110
110
|
HTML_ETAG = '"' + require('crypto').createHash('md5').update(HTML).digest('hex') + '"';
|
|
111
111
|
console.log(' Dashboard hot-reloaded');
|
|
112
|
-
// Push reload to all connected browsers
|
|
112
|
+
// Push reload to all connected browsers via status-stream (saves a connection)
|
|
113
|
+
for (const res of _statusStreamClients) {
|
|
114
|
+
try { res.write('event: reload\ndata: reload\n\n'); } catch { _statusStreamClients.delete(res); }
|
|
115
|
+
}
|
|
116
|
+
// Legacy hot-reload clients
|
|
113
117
|
for (const res of _hotReloadClients) {
|
|
114
118
|
try { res.write('data: reload\n\n'); } catch { _hotReloadClients.delete(res); }
|
|
115
119
|
}
|
|
@@ -227,6 +231,7 @@ function getStatus() {
|
|
|
227
231
|
const runs = shared.safeJson(path.join(MINIONS_DIR, 'engine', 'schedule-runs.json')) || {};
|
|
228
232
|
return scheds.map(s => ({ ...s, _lastRun: runs[s.id] || null }));
|
|
229
233
|
})(),
|
|
234
|
+
meetings: (() => { try { return require('./engine/meeting').getMeetings(); } catch { return []; } })(),
|
|
230
235
|
pinned: (() => { try { return parsePinnedEntries(safeRead(path.join(MINIONS_DIR, 'pinned.md'))); } catch { return []; } })(),
|
|
231
236
|
projects: PROJECTS.map(p => ({ name: p.name, path: p.localPath, description: p.description || '' })),
|
|
232
237
|
initialized: !!(CONFIG.agents && Object.keys(CONFIG.agents).length > 0),
|
|
@@ -3098,6 +3103,59 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3098
3103
|
{ method: 'POST', path: '/api/schedules/update', desc: 'Update an existing schedule', params: 'id, cron?, title?, type?, project?, agent?, description?, priority?, enabled?', handler: handleSchedulesUpdate },
|
|
3099
3104
|
{ method: 'POST', path: '/api/schedules/delete', desc: 'Delete a schedule', params: 'id', handler: handleSchedulesDelete },
|
|
3100
3105
|
|
|
3106
|
+
// Meetings
|
|
3107
|
+
{ method: 'POST', path: '/api/meetings', desc: 'Create a team meeting', params: 'title, agenda, participants[]', handler: async (req, res) => {
|
|
3108
|
+
const body = await readBody(req);
|
|
3109
|
+
const { title, agenda, participants } = body;
|
|
3110
|
+
if (!title || !agenda) return jsonReply(res, 400, { error: 'title and agenda required' });
|
|
3111
|
+
const { createMeeting } = require('./engine/meeting');
|
|
3112
|
+
const meeting = createMeeting({ title, agenda, participants: participants || [] });
|
|
3113
|
+
invalidateStatusCache();
|
|
3114
|
+
return jsonReply(res, 200, { ok: true, meeting });
|
|
3115
|
+
}},
|
|
3116
|
+
|
|
3117
|
+
{ method: 'GET', path: '/api/meetings', desc: 'List all meetings', handler: async (req, res) => {
|
|
3118
|
+
const { getMeetings } = require('./engine/meeting');
|
|
3119
|
+
return jsonReply(res, 200, { meetings: getMeetings() });
|
|
3120
|
+
}},
|
|
3121
|
+
|
|
3122
|
+
{ method: 'GET', path: /^\/api\/meetings\/(MTG-[\w]+)$/, desc: 'Get meeting detail', handler: async (req, res, match) => {
|
|
3123
|
+
const { getMeeting } = require('./engine/meeting');
|
|
3124
|
+
const meeting = getMeeting(match[1]);
|
|
3125
|
+
if (!meeting) return jsonReply(res, 404, { error: 'Meeting not found' });
|
|
3126
|
+
return jsonReply(res, 200, { meeting });
|
|
3127
|
+
}},
|
|
3128
|
+
|
|
3129
|
+
{ method: 'POST', path: '/api/meetings/note', desc: 'Add human note to active meeting', params: 'id, note', handler: async (req, res) => {
|
|
3130
|
+
const body = await readBody(req);
|
|
3131
|
+
if (!body.id || !body.note) return jsonReply(res, 400, { error: 'id and note required' });
|
|
3132
|
+
const { addMeetingNote } = require('./engine/meeting');
|
|
3133
|
+
const meeting = addMeetingNote(body.id, body.note);
|
|
3134
|
+
if (!meeting) return jsonReply(res, 404, { error: 'Meeting not found' });
|
|
3135
|
+
invalidateStatusCache();
|
|
3136
|
+
return jsonReply(res, 200, { ok: true, meeting });
|
|
3137
|
+
}},
|
|
3138
|
+
|
|
3139
|
+
{ method: 'POST', path: '/api/meetings/advance', desc: 'Force advance meeting to next round', params: 'id', handler: async (req, res) => {
|
|
3140
|
+
const body = await readBody(req);
|
|
3141
|
+
if (!body.id) return jsonReply(res, 400, { error: 'id required' });
|
|
3142
|
+
const { advanceMeetingRound } = require('./engine/meeting');
|
|
3143
|
+
const meeting = advanceMeetingRound(body.id);
|
|
3144
|
+
if (!meeting) return jsonReply(res, 404, { error: 'Meeting not found or already completed' });
|
|
3145
|
+
invalidateStatusCache();
|
|
3146
|
+
return jsonReply(res, 200, { ok: true, meeting });
|
|
3147
|
+
}},
|
|
3148
|
+
|
|
3149
|
+
{ method: 'POST', path: '/api/meetings/end', desc: 'End a meeting early', params: 'id', handler: async (req, res) => {
|
|
3150
|
+
const body = await readBody(req);
|
|
3151
|
+
if (!body.id) return jsonReply(res, 400, { error: 'id required' });
|
|
3152
|
+
const { endMeeting } = require('./engine/meeting');
|
|
3153
|
+
const meeting = endMeeting(body.id);
|
|
3154
|
+
if (!meeting) return jsonReply(res, 404, { error: 'Meeting not found' });
|
|
3155
|
+
invalidateStatusCache();
|
|
3156
|
+
return jsonReply(res, 200, { ok: true });
|
|
3157
|
+
}},
|
|
3158
|
+
|
|
3101
3159
|
// Engine
|
|
3102
3160
|
{ method: 'POST', path: '/api/engine/wakeup', desc: 'Trigger immediate engine tick via control.json signal', handler: async (req, res) => {
|
|
3103
3161
|
const controlPath = path.join(MINIONS_DIR, 'engine', 'control.json');
|
package/engine/lifecycle.js
CHANGED
|
@@ -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
|
+
};
|
package/engine/playbook.js
CHANGED
|
@@ -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
|
@@ -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
|