@yemi33/minions 0.1.1709 → 0.1.1710
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 +5 -0
- package/dashboard/js/command-center.js +3 -2
- package/dashboard.js +47 -2
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +1 -1
- package/engine/meeting.js +80 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -969,7 +969,7 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
969
969
|
status.style.color = 'var(--green)';
|
|
970
970
|
}
|
|
971
971
|
ccAddMessage('action', status.outerHTML, false, targetTabId);
|
|
972
|
-
if (['dispatch','fix','implement','explore','review','test'].includes(action.type)) wakeEngine();
|
|
972
|
+
if (['dispatch','fix','implement','explore','review','test','create-meeting'].includes(action.type)) wakeEngine();
|
|
973
973
|
refresh();
|
|
974
974
|
return;
|
|
975
975
|
}
|
|
@@ -1175,9 +1175,10 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1175
1175
|
break;
|
|
1176
1176
|
}
|
|
1177
1177
|
case 'create-meeting': {
|
|
1178
|
+
var meetingParticipants = (Array.isArray(action.participants) && action.participants.length > 0) ? action.participants : (action.agents || []);
|
|
1178
1179
|
var res6 = await fetch('/api/meetings', {
|
|
1179
1180
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1180
|
-
body: JSON.stringify({ title: action.title, agenda: action.agenda, participants:
|
|
1181
|
+
body: JSON.stringify({ title: action.title, agenda: action.agenda, participants: meetingParticipants, rounds: action.rounds, project: action.project })
|
|
1181
1182
|
});
|
|
1182
1183
|
if (!res6.ok) { var d6 = await res6.json().catch(function() { return {}; }); throw new Error(d6.error || 'Meeting create failed'); }
|
|
1183
1184
|
var d6r = await res6.json();
|
package/dashboard.js
CHANGED
|
@@ -1539,6 +1539,27 @@ function _parseWatchInterval(val) {
|
|
|
1539
1539
|
return Math.max(60000, Math.round(u === 's' ? n * 1000 : u === 'm' ? n * 60000 : n * 3600000));
|
|
1540
1540
|
}
|
|
1541
1541
|
|
|
1542
|
+
function normalizeMeetingParticipants(participants) {
|
|
1543
|
+
if (!Array.isArray(participants)) return [];
|
|
1544
|
+
const seen = new Set();
|
|
1545
|
+
const normalized = [];
|
|
1546
|
+
for (const participant of participants) {
|
|
1547
|
+
const id = String(participant || '').trim();
|
|
1548
|
+
if (!id || seen.has(id)) continue;
|
|
1549
|
+
seen.add(id);
|
|
1550
|
+
normalized.push(id);
|
|
1551
|
+
}
|
|
1552
|
+
return normalized;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function meetingParticipantsFromAction(action) {
|
|
1556
|
+
return normalizeMeetingParticipants(
|
|
1557
|
+
Array.isArray(action?.participants) && action.participants.length > 0
|
|
1558
|
+
? action.participants
|
|
1559
|
+
: action?.agents
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1542
1563
|
// Required-field validator for CC actions. Returns null when valid, an error string when not.
|
|
1543
1564
|
// Centralises field-required checks so the model can't quietly emit a malformed action and have
|
|
1544
1565
|
// the server silently fall back to placeholder values (e.g. "Untitled"). The handler invokes this
|
|
@@ -1567,6 +1588,12 @@ function _ccValidateAction(action) {
|
|
|
1567
1588
|
case 'plan':
|
|
1568
1589
|
if (!action.title) return 'plan action missing required field: title';
|
|
1569
1590
|
return null;
|
|
1591
|
+
case 'create-meeting': {
|
|
1592
|
+
if (!action.title || typeof action.title !== 'string' || !action.title.trim()) return 'create-meeting action missing required field: title';
|
|
1593
|
+
if (!action.agenda || typeof action.agenda !== 'string' || !action.agenda.trim()) return 'create-meeting action missing required field: agenda';
|
|
1594
|
+
if (meetingParticipantsFromAction(action).length < 2) return 'create-meeting action requires at least 2 participants';
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1570
1597
|
default:
|
|
1571
1598
|
return null; // unknown types fall through to existing handler / generic fallback
|
|
1572
1599
|
}
|
|
@@ -1794,6 +1821,17 @@ async function executeCCActions(actions) {
|
|
|
1794
1821
|
results.push({ type: 'create-watch', id: watch.id, ok: true });
|
|
1795
1822
|
break;
|
|
1796
1823
|
}
|
|
1824
|
+
case 'create-meeting': {
|
|
1825
|
+
const { createMeeting } = require('./engine/meeting');
|
|
1826
|
+
const meeting = createMeeting({
|
|
1827
|
+
title: action.title.trim(),
|
|
1828
|
+
agenda: action.agenda.trim(),
|
|
1829
|
+
participants: meetingParticipantsFromAction(action),
|
|
1830
|
+
});
|
|
1831
|
+
invalidateStatusCache();
|
|
1832
|
+
results.push({ type: 'create-meeting', id: meeting.id, ok: true });
|
|
1833
|
+
break;
|
|
1834
|
+
}
|
|
1797
1835
|
case 'delete-watch': {
|
|
1798
1836
|
const deleted = watchesMod.deleteWatch(action.id);
|
|
1799
1837
|
if (deleted) invalidateStatusCache();
|
|
@@ -6721,9 +6759,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6721
6759
|
{ method: 'POST', path: '/api/meetings', desc: 'Create a team meeting', params: 'title, agenda, participants[]', handler: async (req, res) => {
|
|
6722
6760
|
const body = await readBody(req);
|
|
6723
6761
|
const { title, agenda, participants } = body;
|
|
6724
|
-
if (
|
|
6762
|
+
if (typeof title !== 'string' || !title.trim() || typeof agenda !== 'string' || !agenda.trim()) {
|
|
6763
|
+
return jsonReply(res, 400, { error: 'title and agenda required' });
|
|
6764
|
+
}
|
|
6765
|
+
if (!Array.isArray(participants)) return jsonReply(res, 400, { error: 'participants must be an array' });
|
|
6766
|
+
const meetingParticipants = normalizeMeetingParticipants(participants);
|
|
6767
|
+
if (meetingParticipants.length < 2) return jsonReply(res, 400, { error: 'at least 2 participants required' });
|
|
6725
6768
|
const { createMeeting } = require('./engine/meeting');
|
|
6726
|
-
const meeting = createMeeting({ title, agenda, participants:
|
|
6769
|
+
const meeting = createMeeting({ title: title.trim(), agenda: agenda.trim(), participants: meetingParticipants });
|
|
6727
6770
|
invalidateStatusCache();
|
|
6728
6771
|
return jsonReply(res, 200, { ok: true, meeting });
|
|
6729
6772
|
}},
|
|
@@ -6923,6 +6966,8 @@ module.exports = {
|
|
|
6923
6966
|
_filterCcTabSessions,
|
|
6924
6967
|
_getVersionCheckInterval,
|
|
6925
6968
|
_parseWatchInterval,
|
|
6969
|
+
_normalizeMeetingParticipants: normalizeMeetingParticipants,
|
|
6970
|
+
_meetingParticipantsFromAction: meetingParticipantsFromAction,
|
|
6926
6971
|
parsePinnedEntries,
|
|
6927
6972
|
_parseDocChatResultText,
|
|
6928
6973
|
_messageRequestsOrchestration,
|
package/engine/lifecycle.js
CHANGED
|
@@ -2662,7 +2662,7 @@ 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);
|
|
2665
|
+
collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout, structuredCompletion, meta.round);
|
|
2666
2666
|
} catch (err) { log('warn', `Meeting collect: ${err.message}`); }
|
|
2667
2667
|
}
|
|
2668
2668
|
|
package/engine/meeting.js
CHANGED
|
@@ -20,6 +20,21 @@ const EMPTY_OUTPUT_PATTERNS = ['(no output)', '(no findings)', '(no response)'];
|
|
|
20
20
|
// tests can redirect the meetings directory without patching module internals.
|
|
21
21
|
const MEETINGS_DIR = path.join(shared.MINIONS_DIR, 'meetings');
|
|
22
22
|
const MEETING_NOTE_ARTIFACT_ROOT = path.join(shared.MINIONS_DIR, 'notes', 'inbox');
|
|
23
|
+
const TERMINAL_MEETING_STATUSES = new Set(['completed', 'archived']);
|
|
24
|
+
const ROUND_STATUS_BY_NAME = {
|
|
25
|
+
investigate: 'investigating',
|
|
26
|
+
debate: 'debating',
|
|
27
|
+
conclude: 'concluding',
|
|
28
|
+
};
|
|
29
|
+
const ACTIVE_MEETING_STATUSES = new Set(Object.values(ROUND_STATUS_BY_NAME));
|
|
30
|
+
|
|
31
|
+
function isTerminalMeetingStatus(status) {
|
|
32
|
+
return TERMINAL_MEETING_STATUSES.has(String(status || '').toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function expectedMeetingStatusForRound(roundName) {
|
|
36
|
+
return ROUND_STATUS_BY_NAME[String(roundName || '').toLowerCase()] || null;
|
|
37
|
+
}
|
|
23
38
|
|
|
24
39
|
function isEmptyMeetingContent(text) {
|
|
25
40
|
const value = String(text || '').trim();
|
|
@@ -278,10 +293,11 @@ function discoverMeetingWork(config) {
|
|
|
278
293
|
);
|
|
279
294
|
|
|
280
295
|
for (const meeting of meetings) {
|
|
281
|
-
if (meeting.status
|
|
296
|
+
if (isTerminalMeetingStatus(meeting.status)) continue;
|
|
282
297
|
|
|
283
298
|
const round = meeting.round || 1;
|
|
284
299
|
const roundName = meeting.status; // investigating, debating, concluding
|
|
300
|
+
if (!ACTIVE_MEETING_STATUSES.has(roundName)) continue;
|
|
285
301
|
const agents = config.agents || {};
|
|
286
302
|
|
|
287
303
|
if (roundName === 'concluding') {
|
|
@@ -413,14 +429,28 @@ function discoverMeetingWork(config) {
|
|
|
413
429
|
* Collect findings from a completed meeting agent.
|
|
414
430
|
* Called from runPostCompletionHooks when type === 'meeting'.
|
|
415
431
|
*/
|
|
416
|
-
function collectMeetingFindings(meetingId, agentId, roundName, output, structuredCompletion = null) {
|
|
432
|
+
function collectMeetingFindings(meetingId, agentId, roundName, output, structuredCompletion = null, expectedRound = null) {
|
|
417
433
|
const meeting = getMeeting(meetingId);
|
|
418
434
|
if (!meeting) return;
|
|
419
|
-
if (meeting.status
|
|
435
|
+
if (isTerminalMeetingStatus(meeting.status)) {
|
|
420
436
|
log('info', `Ignoring late findings from ${agentId} for completed meeting ${meetingId}`);
|
|
421
437
|
return;
|
|
422
438
|
}
|
|
423
439
|
|
|
440
|
+
const expectedStatus = expectedMeetingStatusForRound(roundName);
|
|
441
|
+
if (!expectedStatus) {
|
|
442
|
+
log('warn', `Meeting ${meetingId}: ignoring ${agentId} output for unknown round "${roundName || '(empty)'}"`);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (meeting.status !== expectedStatus) {
|
|
446
|
+
log('info', `Ignoring stale ${roundName} output from ${agentId} for meeting ${meetingId} currently ${meeting.status}`);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (expectedRound !== null && expectedRound !== undefined && Number(meeting.round || 1) !== Number(expectedRound)) {
|
|
450
|
+
log('info', `Ignoring stale round ${expectedRound} output from ${agentId} for meeting ${meetingId} currently on round ${meeting.round || 1}`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
424
454
|
const content = resolveMeetingContributionContent(output, structuredCompletion);
|
|
425
455
|
|
|
426
456
|
// Validate output — reject empty or placeholder responses
|
|
@@ -494,21 +524,54 @@ function addMeetingNote(meetingId, note) {
|
|
|
494
524
|
function _killMeetingDispatches(meetingId) {
|
|
495
525
|
try {
|
|
496
526
|
const DISPATCH_PATH = path.join(shared.MINIONS_DIR, 'engine', 'dispatch.json');
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
// Remove from active and move to completed
|
|
527
|
+
const tmpDir = path.join(shared.MINIONS_DIR, 'engine', 'tmp');
|
|
528
|
+
const entriesToStop = [];
|
|
529
|
+
const filesToDelete = [];
|
|
501
530
|
shared.mutateJsonFileLocked(DISPATCH_PATH, (dp) => {
|
|
502
|
-
dp.
|
|
503
|
-
dp.
|
|
504
|
-
|
|
531
|
+
dp.pending = Array.isArray(dp.pending) ? dp.pending : [];
|
|
532
|
+
dp.active = Array.isArray(dp.active) ? dp.active : [];
|
|
533
|
+
dp.completed = Array.isArray(dp.completed) ? dp.completed : [];
|
|
534
|
+
|
|
535
|
+
for (const queue of ['pending', 'active']) {
|
|
536
|
+
const kept = [];
|
|
537
|
+
for (const d of dp[queue]) {
|
|
538
|
+
if (d.meta?.meetingId !== meetingId) {
|
|
539
|
+
kept.push(d);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
entriesToStop.push(d);
|
|
543
|
+
filesToDelete.push(path.join(tmpDir, `pid-${d.id}.pid`));
|
|
544
|
+
filesToDelete.push(path.join(tmpDir, `prompt-${d.id}.md`));
|
|
545
|
+
filesToDelete.push(path.join(tmpDir, `sysprompt-${d.id}.md`));
|
|
546
|
+
filesToDelete.push(path.join(tmpDir, `sysprompt-${d.id}.md.tmp`));
|
|
547
|
+
}
|
|
548
|
+
dp[queue] = kept;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
for (const d of entriesToStop) {
|
|
505
552
|
dp.completed.push({ ...d, result: DISPATCH_RESULT.ERROR, reason: 'Meeting ended/advanced by human', completed_at: ts() });
|
|
506
553
|
}
|
|
507
554
|
if (dp.completed.length > 100) dp.completed = dp.completed.slice(-100);
|
|
508
555
|
return dp;
|
|
509
556
|
}, { defaultValue: { pending: [], active: [], completed: [] } });
|
|
510
|
-
|
|
511
|
-
|
|
557
|
+
|
|
558
|
+
const pidsToKill = [];
|
|
559
|
+
for (const d of entriesToStop) {
|
|
560
|
+
try {
|
|
561
|
+
const pidFile = path.join(tmpDir, `pid-${d.id}.pid`);
|
|
562
|
+
const pid = shared.validatePid(fs.readFileSync(pidFile, 'utf8').trim());
|
|
563
|
+
pidsToKill.push(pid);
|
|
564
|
+
} catch { /* pending entries and already-finished agents may not have PID files */ }
|
|
565
|
+
}
|
|
566
|
+
for (const pid of pidsToKill) {
|
|
567
|
+
try { shared.killGracefully({ pid }); } catch { /* process may already be dead */ }
|
|
568
|
+
}
|
|
569
|
+
for (const fp of filesToDelete) {
|
|
570
|
+
try { fs.unlinkSync(fp); } catch { /* sidecar may not exist */ }
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (entriesToStop.length > 0) log('info', `Killed ${entriesToStop.length} meeting dispatch(es) for ${meetingId}`);
|
|
574
|
+
return entriesToStop.length;
|
|
512
575
|
} catch (e) { log('warn', 'kill meeting dispatches: ' + e.message); return 0; }
|
|
513
576
|
}
|
|
514
577
|
|
|
@@ -572,10 +635,13 @@ function checkMeetingTimeouts(config) {
|
|
|
572
635
|
|| ENGINE_DEFAULTS.meetingRoundTimeout;
|
|
573
636
|
|
|
574
637
|
for (const meeting of meetings) {
|
|
575
|
-
if (meeting.status
|
|
638
|
+
if (isTerminalMeetingStatus(meeting.status)) continue;
|
|
639
|
+
if (!ACTIVE_MEETING_STATUSES.has(meeting.status)) continue;
|
|
576
640
|
if (!meeting.roundStartedAt) continue;
|
|
577
641
|
|
|
578
|
-
const
|
|
642
|
+
const roundStartedMs = new Date(meeting.roundStartedAt).getTime();
|
|
643
|
+
if (!Number.isFinite(roundStartedMs)) continue;
|
|
644
|
+
const elapsed = Date.now() - roundStartedMs;
|
|
579
645
|
if (elapsed < timeout) continue;
|
|
580
646
|
|
|
581
647
|
const respondedCount = meeting.status === 'investigating'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1710",
|
|
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"
|