@yemi33/minions 0.1.1820 → 0.1.1822
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 +6 -2
- package/dashboard/js/command-center.js +120 -28
- package/dashboard.js +117 -32
- package/engine/copilot-models.json +1 -1
- package/package.json +1 -1
- package/playbooks/fix.md +2 -0
- package/prompts/cc-system.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1822 (2026-05-09)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
-
-
|
|
6
|
+
- keep CC fixes on referenced PR (#2260)
|
|
7
|
+
- harden CC action parsing (#2261)
|
|
8
|
+
- correlate CC action failures (#2264)
|
|
9
|
+
|
|
10
|
+
## 0.1.1819 (2026-05-09)
|
|
7
11
|
|
|
8
12
|
### Fixes
|
|
9
13
|
- stop visibility=hidden beacons from racing pagehide and resurrecting closed-tab entries in dashboard-browser.json (#2275)
|
|
@@ -10,6 +10,7 @@ var _ccTabs = []; // [{id, title, sessionId, messages: [{role, html}]}]
|
|
|
10
10
|
var _ccActiveTabId = null;
|
|
11
11
|
var _ccOpen = false;
|
|
12
12
|
var _ccRetrySeq = 0;
|
|
13
|
+
var _ccMessageSeq = 0;
|
|
13
14
|
// Per-tab sending state stored on tab objects: tab._sending, tab._queue, tab._abortController
|
|
14
15
|
// Legacy globals for backward compat (badge, drawer close check)
|
|
15
16
|
var _ccSending = false; // true if active tab is sending (UI indicator only)
|
|
@@ -19,11 +20,11 @@ try { localStorage.removeItem('cc-sending'); } catch {}
|
|
|
19
20
|
function _ccStripActionBlockFromText(value) {
|
|
20
21
|
var text = value || '';
|
|
21
22
|
if (!text) return text;
|
|
22
|
-
// Tier 1 — strict:
|
|
23
|
-
var full = /(?:^|\r?\n)===ACTIONS
|
|
23
|
+
// Tier 1 — strict: exact canonical delimiter on its own line.
|
|
24
|
+
var full = /(?:^|\r?\n)===ACTIONS===[ \t]*(?=\r?\n|$)/m.exec(text);
|
|
24
25
|
if (full) return text.slice(0, full.index + full[0].indexOf('===ACTIONS')).trim();
|
|
25
|
-
// Tier 2 — loose:
|
|
26
|
-
var block = /(?:^|\r?\n)===ACTIONS\
|
|
26
|
+
// Tier 2 — loose: sentinel-looking malformed delimiters; not prose.
|
|
27
|
+
var block = /(?:^|\r?\n)===ACTIONS(?:[ \t]*(?:[-=]>?|={1,}|$)|[^A-Za-z0-9_\s\r\n][^\r\n]*)(?=\r?\n|$)/m.exec(text);
|
|
27
28
|
if (block) return text.slice(0, block.index + block[0].indexOf('===ACTIONS')).trim();
|
|
28
29
|
// Tier 3 — very loose: 2+ leading equals + ACTIONS keyword + 0+ trailing
|
|
29
30
|
// equals, case-insensitive. Catches ====ACTIONS===, ===actions===, etc.
|
|
@@ -341,7 +342,7 @@ function ccSwitchTab(id) {
|
|
|
341
342
|
el.innerHTML = '';
|
|
342
343
|
// Re-render messages from the tab's data
|
|
343
344
|
for (var i = 0; i < tab.messages.length; i++) {
|
|
344
|
-
ccAddMessage(tab.messages[i].role, tab.messages[i].html, true);
|
|
345
|
+
ccAddMessage(tab.messages[i].role, tab.messages[i].html, true, null, tab.messages[i]);
|
|
345
346
|
}
|
|
346
347
|
// If this tab is still processing, restore the full streaming UX (tools, partial text, thinking)
|
|
347
348
|
if (tab._sending) {
|
|
@@ -454,7 +455,7 @@ function ccRestoreMessages() {
|
|
|
454
455
|
if (!tab) return;
|
|
455
456
|
if (el.children.length > 0 || tab.messages.length === 0) return; // Already rendered or nothing to restore
|
|
456
457
|
for (var i = 0; i < tab.messages.length; i++) {
|
|
457
|
-
ccAddMessage(tab.messages[i].role, tab.messages[i].html, true);
|
|
458
|
+
ccAddMessage(tab.messages[i].role, tab.messages[i].html, true, null, tab.messages[i]);
|
|
458
459
|
}
|
|
459
460
|
// Restore "thinking" indicator if CC was mid-request when page refreshed
|
|
460
461
|
try {
|
|
@@ -502,7 +503,7 @@ function ccSaveState() {
|
|
|
502
503
|
if (!m || typeof m.html !== 'string') return m;
|
|
503
504
|
var stripped = _ccStripActionBlockFromText(m.html);
|
|
504
505
|
if (stripped === m.html) return m;
|
|
505
|
-
return {
|
|
506
|
+
return Object.assign({}, m, { html: stripped });
|
|
506
507
|
});
|
|
507
508
|
return { id: t.id, title: t.title, sessionId: t.sessionId, messages: msgs };
|
|
508
509
|
});
|
|
@@ -526,11 +527,21 @@ function ccUpdateSessionIndicator() {
|
|
|
526
527
|
}
|
|
527
528
|
}
|
|
528
529
|
|
|
530
|
+
function _ccNewMessageId(prefix) {
|
|
531
|
+
_ccMessageSeq++;
|
|
532
|
+
return (prefix || 'cc-msg') + '-' + Date.now().toString(36) + '-' + _ccMessageSeq;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function _ccMessageDomId(messageId) {
|
|
536
|
+
return 'cc-msg-' + String(messageId || '').replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
537
|
+
}
|
|
538
|
+
|
|
529
539
|
function ccAddMessage(role, html, skipSave, targetTabId, meta) {
|
|
530
540
|
var isUser = role === 'user';
|
|
531
541
|
var isSystem = role === 'system';
|
|
532
542
|
var isAction = role === 'action';
|
|
533
543
|
var isAssistant = !isUser && !isSystem && !isAction;
|
|
544
|
+
var messageId = meta && (meta.messageId || meta._messageId) ? String(meta.messageId || meta._messageId) : '';
|
|
534
545
|
var targetTab = targetTabId ? _ccTabs.find(function(t) { return t.id === targetTabId; }) : _ccActiveTab();
|
|
535
546
|
// Only render to DOM if this message is for the currently visible tab
|
|
536
547
|
var isVisible = !targetTabId || targetTabId === _ccActiveTabId;
|
|
@@ -538,6 +549,10 @@ function ccAddMessage(role, html, skipSave, targetTabId, meta) {
|
|
|
538
549
|
var el = document.getElementById('cc-messages');
|
|
539
550
|
var div = document.createElement('div');
|
|
540
551
|
div.className = isAssistant ? 'cc-msg-assistant' : '';
|
|
552
|
+
if (messageId) {
|
|
553
|
+
div.id = _ccMessageDomId(messageId);
|
|
554
|
+
div.setAttribute('data-cc-message-id', messageId);
|
|
555
|
+
}
|
|
541
556
|
if (meta && meta.retryId) div.setAttribute('data-cc-retry-id', meta.retryId);
|
|
542
557
|
div.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:12px;line-height:1.6;max-width:95%;' +
|
|
543
558
|
(isUser ? 'background:var(--blue);color:#fff;align-self:flex-end' : isSystem ? 'align-self:center;max-width:100%' : isAction ? 'align-self:flex-start;padding:2px 0' : 'background:var(--surface2);color:var(--text);align-self:flex-start;border:1px solid var(--border);position:relative');
|
|
@@ -550,6 +565,7 @@ function ccAddMessage(role, html, skipSave, targetTabId, meta) {
|
|
|
550
565
|
var tab = targetTab;
|
|
551
566
|
if (tab) {
|
|
552
567
|
var msg = { role: role, html: html };
|
|
568
|
+
if (messageId) msg._messageId = messageId;
|
|
553
569
|
if (meta && meta.retryId) msg._retryId = meta.retryId;
|
|
554
570
|
tab.messages.push(msg);
|
|
555
571
|
// Auto-title from first user message
|
|
@@ -565,6 +581,7 @@ function ccAddMessage(role, html, skipSave, targetTabId, meta) {
|
|
|
565
581
|
}
|
|
566
582
|
ccSaveState();
|
|
567
583
|
}
|
|
584
|
+
return messageId;
|
|
568
585
|
}
|
|
569
586
|
|
|
570
587
|
async function ccSend(options) {
|
|
@@ -809,34 +826,26 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
|
809
826
|
addMsg('system', '<div style="text-align:center;padding:6px 12px;font-size:11px;color:var(--muted);background:var(--surface2);border-radius:6px;margin:4px 0">' + resetText + '</div>', false, activeTabId);
|
|
810
827
|
}
|
|
811
828
|
var finalText = _ccMergeStreamText(streamedText, evt.text || '');
|
|
829
|
+
if (evt.actions && evt.actions.length > 0) _tagServerExecuted(evt.actions, evt.actionResults);
|
|
812
830
|
var rendered = renderMd(finalText || streamedText || '');
|
|
813
|
-
|
|
831
|
+
var assistantMessageId = _ccNewMessageId('cc-turn');
|
|
832
|
+
var actionFeedbackHtml = _ccBuildActionResultFeedbackHtml(evt.actions || [], evt.actionResults || [], {
|
|
833
|
+
delayed: !!isReconnect,
|
|
834
|
+
emittedAt: evt.actionResultsAt || evt.emittedAt || ''
|
|
835
|
+
});
|
|
836
|
+
addMsg('assistant', rendered + _ccElapsedFooter('{seconds}s') + actionFeedbackHtml, false, { messageId: assistantMessageId });
|
|
814
837
|
if (evt.sessionId !== undefined) {
|
|
815
838
|
var originTab = _ccTabs.find(function(t) { return t.id === activeTabId; });
|
|
816
839
|
if (originTab) { originTab.sessionId = evt.sessionId || null; }
|
|
817
840
|
ccSaveState(); ccUpdateSessionIndicator();
|
|
818
841
|
}
|
|
819
842
|
if (evt.actions && evt.actions.length > 0) {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
// appeared as a small "executed" pill which gave no detail.
|
|
825
|
-
if (evt.actionResults && Array.isArray(evt.actionResults)) {
|
|
826
|
-
var failures = evt.actionResults.filter(function(r) { return r && r.error; });
|
|
827
|
-
var warnings = evt.actionResults.filter(function(r) { return r && r.warning; });
|
|
828
|
-
if (failures.length > 0) {
|
|
829
|
-
var failHtml = failures.map(function(r) {
|
|
830
|
-
return '<li>' + escHtml(r.type || (r.actionContext && r.actionContext.type) || 'action') + ': ' + escHtml(r.error) + _ccActionContextSuffix(r.actionContext) + '</li>';
|
|
831
|
-
}).join('');
|
|
832
|
-
addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--red);background:var(--surface2);border-radius:6px;margin:4px 0">⚠️ ' + failures.length + ' action' + (failures.length > 1 ? 's' : '') + ' failed:<ul style="margin:4px 0 0 16px;padding:0">' + failHtml + '</ul></div>', false, activeTabId);
|
|
833
|
-
}
|
|
834
|
-
if (warnings.length > 0) {
|
|
835
|
-
var warnHtml = warnings.map(function(r) {
|
|
836
|
-
return '<li>' + escHtml(r.type || (r.actionContext && r.actionContext.type) || 'action') + ': ' + escHtml(r.warning) + _ccActionContextSuffix(r.actionContext) + '</li>';
|
|
837
|
-
}).join('');
|
|
838
|
-
addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--orange);background:var(--surface2);border-radius:6px;margin:4px 0">ℹ️ ' + warnings.length + ' action' + (warnings.length > 1 ? '' : '') + ' completed with warnings:<ul style="margin:4px 0 0 16px;padding:0">' + warnHtml + '</ul></div>', false, activeTabId);
|
|
843
|
+
for (var ai = 0; ai < evt.actions.length; ai++) {
|
|
844
|
+
if (evt.actions[ai] && evt.actions[ai]._serverExecuted) {
|
|
845
|
+
_ccRunServerActionSideEffects(evt.actions[ai]);
|
|
846
|
+
continue;
|
|
839
847
|
}
|
|
848
|
+
await ccExecuteAction(evt.actions[ai], activeTabId, { originMessageId: assistantMessageId });
|
|
840
849
|
}
|
|
841
850
|
} else if (evt.actionParseError) {
|
|
842
851
|
// Issue #1834: server saw ===ACTIONS=== but couldn't parse the JSON.
|
|
@@ -1016,6 +1025,79 @@ async function _ccFetch(url, body, method) {
|
|
|
1016
1025
|
return res;
|
|
1017
1026
|
}
|
|
1018
1027
|
|
|
1028
|
+
function _ccActionResultSubject(action, result) {
|
|
1029
|
+
var value = (result && (result.title || result.id || result.duplicateOf || result.file || result.target || result.endpoint)) ||
|
|
1030
|
+
(action && (action.title || action.id || action.file || action.target || action.endpoint)) || '';
|
|
1031
|
+
return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function _ccActionResultType(action, result) {
|
|
1035
|
+
return String((result && result.type) || (action && action.type) || 'action').trim() || 'action';
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function _ccActionResultLine(action, result) {
|
|
1039
|
+
var type = _ccActionResultType(action, result);
|
|
1040
|
+
var subject = _ccActionResultSubject(action, result);
|
|
1041
|
+
var label = escHtml(type) + (subject ? ' <strong>' + escHtml(subject) + '</strong>' : '');
|
|
1042
|
+
var contextSuffix = result && result.actionContext ? _ccActionContextSuffix(result.actionContext) : '';
|
|
1043
|
+
if (result && result.error) {
|
|
1044
|
+
return '<li class="cc-action-feedback-row cc-action-feedback-error">✗ ' + label + ': ' + escHtml(result.error) + contextSuffix + '</li>';
|
|
1045
|
+
}
|
|
1046
|
+
if (result && result.warning) {
|
|
1047
|
+
return '<li class="cc-action-feedback-row cc-action-feedback-warning">⚠ ' + label + ': ' + escHtml(result.warning) + contextSuffix + '</li>';
|
|
1048
|
+
}
|
|
1049
|
+
if (result && result.ok) {
|
|
1050
|
+
var duplicate = result.duplicate ? ' <span style="color:var(--orange)">already exists</span>' : '';
|
|
1051
|
+
return '<li class="cc-action-feedback-row cc-action-feedback-ok">✓ ' + label + duplicate + contextSuffix + '</li>';
|
|
1052
|
+
}
|
|
1053
|
+
return '';
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function _ccBuildActionResultFeedbackHtml(actions, actionResults, opts) {
|
|
1057
|
+
if (!Array.isArray(actions) || !Array.isArray(actionResults)) return '';
|
|
1058
|
+
var rows = [];
|
|
1059
|
+
for (var i = 0; i < actions.length && i < actionResults.length; i++) {
|
|
1060
|
+
var r = actionResults[i];
|
|
1061
|
+
if (!r || (!r.ok && !r.error && !r.warning)) continue;
|
|
1062
|
+
var row = _ccActionResultLine(actions[i] || {}, r);
|
|
1063
|
+
if (row) rows.push(row);
|
|
1064
|
+
}
|
|
1065
|
+
if (rows.length === 0) return '';
|
|
1066
|
+
var failures = actionResults.filter(function(r) { return r && r.error; }).length;
|
|
1067
|
+
var warnings = actionResults.filter(function(r) { return r && r.warning; }).length;
|
|
1068
|
+
var delayed = !!(opts && opts.delayed);
|
|
1069
|
+
var emittedAt = opts && opts.emittedAt ? String(opts.emittedAt) : '';
|
|
1070
|
+
var label = delayed ? 'Action results from previous turn' : 'Action results';
|
|
1071
|
+
var timing = emittedAt ? ' <span style="color:var(--muted);font-weight:400">(' + escHtml(emittedAt) + ')</span>' : '';
|
|
1072
|
+
var color = failures > 0 ? 'var(--red)' : warnings > 0 ? 'var(--orange)' : 'var(--green)';
|
|
1073
|
+
return '<div class="cc-action-feedback" data-cc-action-feedback="true" style="margin-top:8px;padding:6px 10px;border:1px dashed var(--border);border-radius:6px;background:var(--surface);font-size:11px;color:' + color + '">' +
|
|
1074
|
+
'<div style="font-weight:700">' + label + timing + '</div>' +
|
|
1075
|
+
'<ul style="margin:4px 0 0 16px;padding:0">' + rows.join('') + '</ul>' +
|
|
1076
|
+
'</div>';
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function _ccAppendHtmlToMessage(tabOrId, messageId, html) {
|
|
1080
|
+
if (!messageId || !html) return false;
|
|
1081
|
+
var tab = typeof tabOrId === 'object' && tabOrId ? tabOrId : _ccTabs.find(function(t) { return t.id === tabOrId; });
|
|
1082
|
+
if (!tab || !Array.isArray(tab.messages)) return false;
|
|
1083
|
+
var msg = tab.messages.find(function(m) { return m && m._messageId === messageId; });
|
|
1084
|
+
if (!msg) return false;
|
|
1085
|
+
msg.html = (msg.html || '') + html;
|
|
1086
|
+
if (typeof _ccActiveTabId !== 'undefined' && tab.id === _ccActiveTabId && typeof document !== 'undefined') {
|
|
1087
|
+
var el = document.getElementById(_ccMessageDomId(messageId));
|
|
1088
|
+
if (el && typeof el.insertAdjacentHTML === 'function') el.insertAdjacentHTML('beforeend', html);
|
|
1089
|
+
}
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function _ccRunServerActionSideEffects(action) {
|
|
1094
|
+
var type = String(action && action.type || '');
|
|
1095
|
+
if (['dispatch','fix','explore','review','test','create-meeting','plan','execute-plan','approve-plan','resume-plan','trigger-pipeline','advance-meeting','trigger-verify','regenerate-plan','continue-pipeline'].indexOf(type) >= 0) {
|
|
1096
|
+
wakeEngine();
|
|
1097
|
+
}
|
|
1098
|
+
refresh();
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1019
1101
|
var CC_ACTION_CONTEXT_STALE_MS = 2 * 60 * 1000;
|
|
1020
1102
|
function _ccActionContextIsStale(ctx, nowMs) {
|
|
1021
1103
|
if (!ctx || !ctx.requestedAt) return false;
|
|
@@ -1059,7 +1141,7 @@ function _tagServerExecuted(actions, actionResults) {
|
|
|
1059
1141
|
}
|
|
1060
1142
|
}
|
|
1061
1143
|
|
|
1062
|
-
async function ccExecuteAction(action, targetTabId) {
|
|
1144
|
+
async function ccExecuteAction(action, targetTabId, opts) {
|
|
1063
1145
|
action = _ccNormalizeDispatchAction(action);
|
|
1064
1146
|
var status = document.createElement('div');
|
|
1065
1147
|
status.style.cssText = 'padding:4px 10px;border-radius:4px;font-size:10px;align-self:flex-start;border:1px dashed var(--border);color:var(--muted)';
|
|
@@ -1081,6 +1163,11 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1081
1163
|
_ccActionContextSuffix(action._serverContext);
|
|
1082
1164
|
status.style.color = 'var(--green)';
|
|
1083
1165
|
}
|
|
1166
|
+
if (opts && opts.originMessageId && _ccAppendHtmlToMessage(targetTabId || _ccActiveTabId, opts.originMessageId, status.outerHTML)) {
|
|
1167
|
+
ccSaveState();
|
|
1168
|
+
refresh();
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1084
1171
|
ccAddMessage('action', status.outerHTML, false, targetTabId);
|
|
1085
1172
|
if (['dispatch','fix','explore','review','test','create-meeting'].includes(action.type)) wakeEngine();
|
|
1086
1173
|
refresh();
|
|
@@ -1584,6 +1671,11 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1584
1671
|
status.style.color = 'var(--red)';
|
|
1585
1672
|
}
|
|
1586
1673
|
|
|
1674
|
+
if (opts && opts.originMessageId && _ccAppendHtmlToMessage(targetTabId || _ccActiveTabId, opts.originMessageId, status.outerHTML)) {
|
|
1675
|
+
ccSaveState();
|
|
1676
|
+
refresh();
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1587
1679
|
ccAddMessage('action', status.outerHTML, false, targetTabId);
|
|
1588
1680
|
refresh();
|
|
1589
1681
|
}
|
package/dashboard.js
CHANGED
|
@@ -233,9 +233,30 @@ function normalizePrMetadata(metadata) {
|
|
|
233
233
|
};
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
function
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
function normalizePrTitleMatchText(value) {
|
|
237
|
+
return String(value || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function findPrRecordReferencedByText(prs, text, project = null) {
|
|
241
|
+
if (!Array.isArray(prs) || !String(text || '').trim()) return null;
|
|
242
|
+
const explicitRef = extractPrRefFromText(text);
|
|
243
|
+
if (explicitRef) return shared.findPrRecord(prs, explicitRef, project);
|
|
244
|
+
const normalizedText = normalizePrTitleMatchText(text);
|
|
245
|
+
if (!normalizedText) return null;
|
|
246
|
+
const matches = prs.filter(pr => {
|
|
247
|
+
const title = normalizePrTitleMatchText(pr?.title);
|
|
248
|
+
return title.length >= 8 && normalizedText.includes(title);
|
|
249
|
+
});
|
|
250
|
+
return matches.length === 1 ? matches[0] : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function inferActionPrRecord(action, prs, project = null) {
|
|
254
|
+
const text = [
|
|
255
|
+
action?.title,
|
|
256
|
+
action?.description,
|
|
257
|
+
action?.reason,
|
|
258
|
+
].filter(value => typeof value === 'string' && value.trim()).join('\n');
|
|
259
|
+
return findPrRecordReferencedByText(prs, text, project);
|
|
239
260
|
}
|
|
240
261
|
|
|
241
262
|
function copyWorkItemPrFields(item, input, pr = null) {
|
|
@@ -1901,11 +1922,11 @@ function findCCActionsDelimiter(text) {
|
|
|
1901
1922
|
// but are not parsed (they shouldn't reach the user as actions).
|
|
1902
1923
|
function findCCActionsHeader(text) {
|
|
1903
1924
|
if (!text) return null;
|
|
1904
|
-
// Tier 1 — strict, parseable:
|
|
1905
|
-
const strict = /(?:^|\r?\n)===ACTIONS
|
|
1925
|
+
// Tier 1 — strict, parseable: the exact canonical delimiter on its own line.
|
|
1926
|
+
const strict = /(?:^|\r?\n)===ACTIONS===[ \t]*(?=\r?\n|$)/m.exec(text);
|
|
1906
1927
|
if (strict) {
|
|
1907
1928
|
const headerStart = strict.index + strict[0].indexOf('===ACTIONS');
|
|
1908
|
-
const headerMatch = text.slice(headerStart).match(/^===ACTIONS
|
|
1929
|
+
const headerMatch = text.slice(headerStart).match(/^===ACTIONS===[ \t]*/);
|
|
1909
1930
|
return {
|
|
1910
1931
|
index: headerStart,
|
|
1911
1932
|
headerLength: headerMatch ? headerMatch[0].length : '===ACTIONS==='.length,
|
|
@@ -2020,6 +2041,32 @@ function _extractActionsJson(segment) {
|
|
|
2020
2041
|
const CC_DISPATCH_ACTION_ALIASES = new Set(['fix', 'explore', 'review', 'test', 'implement', 'implement:large', 'ask', 'verify']);
|
|
2021
2042
|
const CC_ACTION_INTENT_WORK_TYPES = new Set(['fix', 'implement', 'implement:large', 'explore', 'review', 'test', 'ask', 'verify']);
|
|
2022
2043
|
|
|
2044
|
+
function getWorkItemPrRef(input) {
|
|
2045
|
+
if (!input || typeof input !== 'object') return null;
|
|
2046
|
+
return input.targetPr || input.pr || input.prId || input.prNumber || input.pullRequest || input.sourcePr || input.prUrl || null;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function isPrTargetedWorkType(workType) {
|
|
2050
|
+
return ['fix', 'review', 'test'].includes(String(workType || '').toLowerCase());
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
function trimTrailingPrRefPunctuation(value) {
|
|
2054
|
+
return String(value || '').replace(/[),.;:]+$/g, '');
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
function extractPrRefFromText(value) {
|
|
2058
|
+
const text = String(value || '');
|
|
2059
|
+
if (!text.trim()) return null;
|
|
2060
|
+
const urlMatch = text.match(/https?:\/\/[^\s<>()]+(?:\/pull\/\d+|\/pullrequest\/\d+)[^\s<>()]*/i);
|
|
2061
|
+
if (urlMatch) return trimTrailingPrRefPunctuation(urlMatch[0]);
|
|
2062
|
+
const canonicalMatch = text.match(/\b(?:github|ado):[^\s#]+#\d+\b/i);
|
|
2063
|
+
if (canonicalMatch) return canonicalMatch[0];
|
|
2064
|
+
const legacyMatch = text.match(/\bPR-(\d+)\b/i);
|
|
2065
|
+
if (legacyMatch) return legacyMatch[1];
|
|
2066
|
+
const numberMatch = text.match(/\b(?:pr|pull\s+request|pullrequest)\s*#?\s*(\d+)\b/i);
|
|
2067
|
+
return numberMatch ? numberMatch[1] : null;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2023
2070
|
function normalizeCCAction(action) {
|
|
2024
2071
|
if (!action || typeof action !== 'object') return action;
|
|
2025
2072
|
if (typeof action.type !== 'string') return action;
|
|
@@ -2051,7 +2098,7 @@ function _ccCleanIntentString(value, max = 500) {
|
|
|
2051
2098
|
function _ccNormalizeIntentMetadata(meta) {
|
|
2052
2099
|
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return {};
|
|
2053
2100
|
const out = {};
|
|
2054
|
-
for (const key of ['intent', 'type', 'title', 'description', 'priority', 'project', 'branchStrategy', 'branch_strategy']) {
|
|
2101
|
+
for (const key of ['intent', 'type', 'title', 'description', 'priority', 'project', 'branchStrategy', 'branch_strategy', 'pr', 'targetPr', 'prId', 'prNumber', 'pullRequest', 'sourcePr', 'prUrl']) {
|
|
2055
2102
|
const value = _ccCleanIntentString(meta[key], key === 'description' ? 2000 : 300);
|
|
2056
2103
|
if (value) out[key] = value;
|
|
2057
2104
|
}
|
|
@@ -2165,7 +2212,7 @@ function _actionsWithIntentFallback(actions, opts = {}) {
|
|
|
2165
2212
|
const { message = '', intentMetadata = null, source = 'command-center', filePath = null, title: docTitle = null, answerText = '', toolUses = [] } = opts;
|
|
2166
2213
|
const existing = (Array.isArray(actions) ? actions.map(normalizeCCAction) : [])
|
|
2167
2214
|
.filter(action => !_ccShouldSuppressAnsweredAskDispatch(action, { message, source, answerText, toolUses }));
|
|
2168
|
-
if (_messageRequestsDirectHandling(message)) return existing.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
|
|
2215
|
+
if (_messageRequestsDirectHandling(message, { answerText, toolUses })) return existing.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
|
|
2169
2216
|
if (existing.some(a => normalizeCCAction(a)?.type === 'dispatch')) return existing;
|
|
2170
2217
|
if (existing.length > 0) return existing;
|
|
2171
2218
|
const meta = _ccNormalizeIntentMetadata(intentMetadata);
|
|
@@ -2199,6 +2246,8 @@ function _actionsWithIntentFallback(actions, opts = {}) {
|
|
|
2199
2246
|
workType: intent.workType,
|
|
2200
2247
|
...common,
|
|
2201
2248
|
};
|
|
2249
|
+
const prRef = getWorkItemPrRef(meta) || extractPrRefFromText([message, meta.title, meta.description].filter(Boolean).join('\n'));
|
|
2250
|
+
if (prRef && isPrTargetedWorkType(intent.workType)) action.pr = prRef;
|
|
2202
2251
|
if (!hasTarget) action._intentFallbackError = _ccFallbackMissingTargetError(intent);
|
|
2203
2252
|
if (_ccShouldSuppressAnsweredAskDispatch(action, { message, source, answerText, toolUses })) return existing;
|
|
2204
2253
|
return [action];
|
|
@@ -2246,7 +2295,11 @@ function parseCCActions(text) {
|
|
|
2246
2295
|
if (jsonStr) {
|
|
2247
2296
|
try {
|
|
2248
2297
|
const parsed = JSON.parse(jsonStr);
|
|
2249
|
-
|
|
2298
|
+
if (Array.isArray(parsed)) {
|
|
2299
|
+
actions = parsed;
|
|
2300
|
+
} else {
|
|
2301
|
+
parseError = 'actions JSON must be an array after ===ACTIONS=== delimiter';
|
|
2302
|
+
}
|
|
2250
2303
|
} catch (e) {
|
|
2251
2304
|
parseError = e.message || 'invalid JSON';
|
|
2252
2305
|
}
|
|
@@ -2261,17 +2314,6 @@ function parseCCActions(text) {
|
|
|
2261
2314
|
parseError = 'Malformed ===ACTIONS=== delimiter (extra equals, lowercase, or trailing punctuation). Actions silently discarded — fix the model output.';
|
|
2262
2315
|
}
|
|
2263
2316
|
}
|
|
2264
|
-
if (actions.length === 0) {
|
|
2265
|
-
const actionRegex = /`{3,}\s*action\s*\r?\n([\s\S]*?)`{3,}/g;
|
|
2266
|
-
let match;
|
|
2267
|
-
while ((match = actionRegex.exec(displayText)) !== null) {
|
|
2268
|
-
try { actions.push(JSON.parse(match[1].trim())); } catch {}
|
|
2269
|
-
}
|
|
2270
|
-
if (actions.length > 0) {
|
|
2271
|
-
displayText = displayText.replace(/`{3,}\s*action\s*\r?\n[\s\S]*?`{3,}\n?/g, '').trim();
|
|
2272
|
-
parseError = null; // legacy fallback recovered actions
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
2317
|
actions = actions.map(normalizeCCAction);
|
|
2276
2318
|
const result = { text: displayText, actions };
|
|
2277
2319
|
if (parseError && actions.length === 0) {
|
|
@@ -2455,15 +2497,33 @@ function _messageExplicitlyRequestsDelegation(message) {
|
|
|
2455
2497
|
return false;
|
|
2456
2498
|
}
|
|
2457
2499
|
|
|
2458
|
-
function _messageRequestsDirectHandling(message) {
|
|
2500
|
+
function _messageRequestsDirectHandling(message, opts = {}) {
|
|
2459
2501
|
const normalized = _normalizeIntentText(message);
|
|
2460
2502
|
if (!normalized.trim()) return false;
|
|
2503
|
+
if (_messageRequestsInformationOnly(message) && !_unansweredInvestigationSignal(opts)) return true;
|
|
2461
2504
|
if (_intentHasAnyPhrase(normalized, DIRECT_HANDLING_PHRASES)) return true;
|
|
2462
2505
|
if (_intentHasAnyToken(normalized, DIRECT_QUICK_TERMS) && _intentHasAnyToken(normalized, DIRECT_QUICK_OBJECTS)) return true;
|
|
2463
2506
|
if (_intentHasVerbObject(normalized, DIRECT_REPLY_TERMS, DIRECT_REPLY_TARGETS)) return true;
|
|
2464
2507
|
return _intentHasVerbObject(normalized, DIRECT_SELF_TERMS, DIRECT_SELF_TARGETS);
|
|
2465
2508
|
}
|
|
2466
2509
|
|
|
2510
|
+
function _unansweredInvestigationSignal({ answerText = '', toolUses = [] } = {}) {
|
|
2511
|
+
const answerLen = String(answerText || '').trim().length;
|
|
2512
|
+
const toolCount = Array.isArray(toolUses) ? toolUses.length : 0;
|
|
2513
|
+
return toolCount >= 4 && answerLen < ANSWERED_ASK_MIN_CHARS;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
function _messageRequestsInformationOnly(message) {
|
|
2517
|
+
const normalized = _normalizeIntentText(message);
|
|
2518
|
+
const tokens = _intentTokens(normalized);
|
|
2519
|
+
if (!tokens.length) return false;
|
|
2520
|
+
if (['what', 'which', 'who', 'when', 'where', 'why', 'how'].includes(tokens[0])) return true;
|
|
2521
|
+
if (['show', 'list'].includes(tokens[0])) return true;
|
|
2522
|
+
if (tokens[0] === 'tell' && tokens[1] === 'me') return true;
|
|
2523
|
+
if (String(message || '').trim().endsWith('?') && !_intentHasAnyToken(normalized, DELEGATION_ACTION_TERMS)) return true;
|
|
2524
|
+
return false;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2467
2527
|
function _messageIsSmallDocOnlyRequest(normalized) {
|
|
2468
2528
|
if (_intentHasAnyToken(normalized, DOC_CHAT_DIRECT_DOC_TERMS)) return true;
|
|
2469
2529
|
return _intentHasVerbObject(normalized, DOC_CHAT_DOC_ACTION_TERMS, DOC_CHAT_DOC_OBJECTS);
|
|
@@ -2575,7 +2635,7 @@ function _priorityFromDelegationMessage(message) {
|
|
|
2575
2635
|
}
|
|
2576
2636
|
|
|
2577
2637
|
function _inferDelegationActionFromMessage(message, { source = 'command-center', filePath = null, title = null, answerText = '', toolUses = [] } = {}) {
|
|
2578
|
-
if (_messageRequestsDirectHandling(message)) return null;
|
|
2638
|
+
if (_messageRequestsDirectHandling(message, { answerText, toolUses })) return null;
|
|
2579
2639
|
const explicit = _messageHasDelegationIntent(message);
|
|
2580
2640
|
const mediumLarge = _messageHasMediumLargeWorkIntent(message, { source });
|
|
2581
2641
|
const toolCount = Array.isArray(toolUses) ? toolUses.length : 0;
|
|
@@ -2606,7 +2666,7 @@ function _inferDelegationActionFromMessage(message, { source = 'command-center',
|
|
|
2606
2666
|
function _ensureDelegationForIntent(actions, opts = {}) {
|
|
2607
2667
|
const list = (Array.isArray(actions) ? actions.map(normalizeCCAction) : [])
|
|
2608
2668
|
.filter(action => !_ccShouldSuppressAnsweredAskDispatch(action, opts));
|
|
2609
|
-
if (_messageRequestsDirectHandling(opts.message)) return list.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
|
|
2669
|
+
if (_messageRequestsDirectHandling(opts.message, opts)) return list.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
|
|
2610
2670
|
if (list.some(a => normalizeCCAction(a)?.type === 'dispatch')) return list;
|
|
2611
2671
|
if (list.length > 0) return list;
|
|
2612
2672
|
const inferred = _inferDelegationActionFromMessage(opts.message, opts);
|
|
@@ -3325,6 +3385,16 @@ async function _ccExecuteLocalApiAction(action) {
|
|
|
3325
3385
|
};
|
|
3326
3386
|
}
|
|
3327
3387
|
|
|
3388
|
+
function _ccActionFailureLabel(action) {
|
|
3389
|
+
const type = String(action?.type || 'action').trim() || 'action';
|
|
3390
|
+
const title = String(action?.title || action?.id || action?.file || action?.target || '').replace(/\s+/g, ' ').trim();
|
|
3391
|
+
return title ? `${type} failed for '${title.slice(0, 120)}'` : `${type} failed`;
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
function _ccMultiProjectRequiredError(action) {
|
|
3395
|
+
return `${_ccActionFailureLabel(action)}: project field is required when ${PROJECTS.length} projects are configured: ${PROJECTS.map(p => p.name).join(', ')}`;
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3328
3398
|
async function executeCCActions(actions, { source = 'command-center', inferredProject = null } = {}) {
|
|
3329
3399
|
const results = [];
|
|
3330
3400
|
const dispatchIdsCreatedInThisCall = new Map();
|
|
@@ -3351,8 +3421,15 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3351
3421
|
const workType = routing.normalizeWorkType(action.workType || (action.type !== 'dispatch' ? action.type : WORK_TYPE.IMPLEMENT), WORK_TYPE.IMPLEMENT);
|
|
3352
3422
|
const id = 'W-' + shared.uid();
|
|
3353
3423
|
const project = action.project || '';
|
|
3354
|
-
const
|
|
3424
|
+
const prTargetedWorkType = isPrTargetedWorkType(workType);
|
|
3425
|
+
let prRef = getWorkItemPrRef(action);
|
|
3355
3426
|
let linkedPr = null;
|
|
3427
|
+
let allPrsForAction = null;
|
|
3428
|
+
if (!prRef && prTargetedWorkType) {
|
|
3429
|
+
allPrsForAction = getPullRequests().filter(p => !p._ghost);
|
|
3430
|
+
linkedPr = inferActionPrRecord(action, allPrsForAction, null);
|
|
3431
|
+
if (linkedPr) prRef = linkedPr.id || linkedPr.url || linkedPr.prNumber;
|
|
3432
|
+
}
|
|
3356
3433
|
|
|
3357
3434
|
// Strict project resolution. Silent fallback to PROJECTS[0] when the model named an unknown
|
|
3358
3435
|
// project caused work items to land in the wrong repo. Now: unknown name → error; ambiguous
|
|
@@ -3367,8 +3444,8 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3367
3444
|
break;
|
|
3368
3445
|
}
|
|
3369
3446
|
} else if (prRef) {
|
|
3370
|
-
const allPrs = getPullRequests().filter(p => !p._ghost);
|
|
3371
|
-
linkedPr = shared.findPrRecord(allPrs, prRef) || null;
|
|
3447
|
+
const allPrs = allPrsForAction || getPullRequests().filter(p => !p._ghost);
|
|
3448
|
+
if (!linkedPr) linkedPr = shared.findPrRecord(allPrs, prRef) || null;
|
|
3372
3449
|
if (linkedPr?._project && linkedPr._project !== 'central') {
|
|
3373
3450
|
targetProject = resolveProjectSourceTarget(linkedPr._project, PROJECTS, { allowCentral: false }).project || null;
|
|
3374
3451
|
}
|
|
@@ -3379,19 +3456,24 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3379
3456
|
}
|
|
3380
3457
|
if (!targetProject) {
|
|
3381
3458
|
if (PROJECTS.length > 1) {
|
|
3382
|
-
results.push({ type: action.type, error:
|
|
3459
|
+
results.push({ type: action.type, error: _ccMultiProjectRequiredError(action) });
|
|
3383
3460
|
break;
|
|
3384
3461
|
}
|
|
3385
3462
|
if (PROJECTS.length === 1) targetProject = PROJECTS[0];
|
|
3386
3463
|
}
|
|
3387
3464
|
// PROJECTS.length === 0 → targetProject stays null, falls back to root work-items.json (existing behavior).
|
|
3388
3465
|
|
|
3389
|
-
if (
|
|
3466
|
+
if (targetProject && (!linkedPr || !prRef)) {
|
|
3390
3467
|
const projectPrs = shared.safeJson(shared.projectPrPath(targetProject)) || [];
|
|
3391
3468
|
shared.normalizePrRecords(projectPrs, targetProject);
|
|
3392
|
-
|
|
3469
|
+
if (!prRef && prTargetedWorkType) {
|
|
3470
|
+
linkedPr = inferActionPrRecord(action, projectPrs, targetProject);
|
|
3471
|
+
if (linkedPr) prRef = linkedPr.id || linkedPr.url || linkedPr.prNumber;
|
|
3472
|
+
} else if (prRef && !linkedPr) {
|
|
3473
|
+
linkedPr = shared.findPrRecord(projectPrs, prRef, targetProject) || null;
|
|
3474
|
+
}
|
|
3393
3475
|
}
|
|
3394
|
-
if (prRef &&
|
|
3476
|
+
if (prRef && prTargetedWorkType && !linkedPr) {
|
|
3395
3477
|
results.push({ type: action.type, error: `PR not found: ${prRef}` });
|
|
3396
3478
|
break;
|
|
3397
3479
|
}
|
|
@@ -3541,7 +3623,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3541
3623
|
}
|
|
3542
3624
|
if (!targetProject) {
|
|
3543
3625
|
if (PROJECTS.length > 1) {
|
|
3544
|
-
results.push({ type: 'reopen-work-item', id: action.id, error:
|
|
3626
|
+
results.push({ type: 'reopen-work-item', id: action.id, error: _ccMultiProjectRequiredError({ ...action, type: 'reopen-work-item' }) });
|
|
3545
3627
|
break;
|
|
3546
3628
|
}
|
|
3547
3629
|
if (PROJECTS.length === 1) targetProject = PROJECTS[0];
|
|
@@ -7263,6 +7345,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7263
7345
|
await executeCCActions(parsed.actions),
|
|
7264
7346
|
{ message: body.message, requestedAt: actionRequestedAt }
|
|
7265
7347
|
);
|
|
7348
|
+
parsed.actionResultsAt = new Date().toISOString();
|
|
7266
7349
|
}
|
|
7267
7350
|
// Mirror only user-facing text to Teams; never send the internal action block.
|
|
7268
7351
|
if (!tabId.startsWith('teams-')) {
|
|
@@ -7594,14 +7677,16 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7594
7677
|
{ message: body.message, intentMetadata: body.intentMetadata, source: 'command-center', answerText: displayText, toolUses }
|
|
7595
7678
|
);
|
|
7596
7679
|
let actionResults;
|
|
7680
|
+
let actionResultsAt;
|
|
7597
7681
|
if (actions.length > 0) {
|
|
7598
7682
|
actionResults = _annotateCCActionResults(
|
|
7599
7683
|
actions,
|
|
7600
7684
|
await executeCCActions(actions),
|
|
7601
7685
|
{ message: body.message, requestedAt: actionRequestedAt }
|
|
7602
7686
|
);
|
|
7687
|
+
actionResultsAt = new Date().toISOString();
|
|
7603
7688
|
}
|
|
7604
|
-
const donePayload = { type: 'done', text: displayText, actions, actionResults, sessionId: responseSessionId, newSession: !wasResume };
|
|
7689
|
+
const donePayload = { type: 'done', text: displayText, actions, actionResults, actionResultsAt, sessionId: responseSessionId, newSession: !wasResume };
|
|
7605
7690
|
// Issue #1834: surface action JSON parse failures so the UI can warn
|
|
7606
7691
|
// instead of silently dropping. Client renders this as a small notice.
|
|
7607
7692
|
if (_actionParseError) donePayload.actionParseError = _actionParseError;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1822",
|
|
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"
|
package/playbooks/fix.md
CHANGED
|
@@ -11,6 +11,8 @@ Repo: {{repo_name}} | Org: {{ado_org}} | Project: {{ado_project}}
|
|
|
11
11
|
Fix issues found by {{reviewer}} on **{{pr_id}}**: {{pr_title}}
|
|
12
12
|
Branch: `{{pr_branch}}`
|
|
13
13
|
|
|
14
|
+
When `{{pr_id}}`/`{{pr_branch}}` identify an existing pull request, this is a fix-on-existing-PR task. Work in the provided PR branch, push the fix commit to that branch, and do not create a replacement branch or a new PR.
|
|
15
|
+
|
|
14
16
|
{{checkpoint_context}}
|
|
15
17
|
|
|
16
18
|
## Review Findings to Address
|
package/prompts/cc-system.md
CHANGED
|
@@ -97,7 +97,8 @@ I'll dispatch dallas to fix that bug.
|
|
|
97
97
|
|
|
98
98
|
Core action types:
|
|
99
99
|
- **dispatch**: title (REQUIRED), workType, priority (low/medium/high), agents[] or agent (optional — both shapes accepted), project (REQUIRED when multi-project unless `pr` resolves to a tracked PR), description, pr (optional PR number/id/url for work that targets an existing PR), scope (`"fan-out"` only when the user explicitly asks to fan out to all agents). Do not emit `type:"fix"` or `type:"implement"`; emit `type:"dispatch"` with `workType:"fix"` or emit `type:"dispatch"` with `workType:"implement"`.
|
|
100
|
-
workTypes: `explore` (research/report only, NO PR), `ask` (answer/report, NO PR), `implement` (new code, PR REQUIRED), `fix` (standalone bug fix creates a PR; include `pr`
|
|
100
|
+
workTypes: `explore` (research/report only, NO PR), `ask` (answer/report, NO PR), `implement` (new code, PR REQUIRED), `fix` (standalone bug fix creates a PR; include `pr` whenever the user references an existing PR by number, URL, or title, including "fix/bypass this policy on this PR", review comments, or build failures), `review` (code review, NO PR), `test` (tests, PR if new), `verify` (merge/build/maintenance, NO PR)
|
|
101
|
+
For fix-style requests that name or contextually refer to an existing PR, preserving `pr` is mandatory: the agent must check out that PR's branch and push a fix commit there, not create a new branch or new PR.
|
|
101
102
|
Do not dispatch an `ask` work item for a question you already answered inline in the same turn, especially after using tools. If the answer needs deeper investigation, give a brief handoff note and dispatch; do not both provide a complete answer and queue an agent to repeat it.
|
|
102
103
|
If the user wants a design/architecture artifact committed through a PR, dispatch `implement` or `docs` rather than `explore`.
|
|
103
104
|
When the user names a specific agent ("assign this to lambert"), put exactly that one name in `agents` (e.g. `"agents": ["lambert"]`). A single-agent assignment is hard-pinned by the server — it will queue for that agent only and skip the routing table. If the user explicitly asks for fan-out/all agents, set `scope: "fan-out"`.
|