@yemi33/minions 0.1.1778 → 0.1.1780

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1780 (2026-05-07)
4
+
5
+ ### Features
6
+ - fix command center action parity (#2174)
7
+
3
8
  ## 0.1.1778 (2026-05-07)
4
9
 
5
10
  ### Features
@@ -103,15 +103,15 @@ function _ccActiveTab() {
103
103
 
104
104
  // Build a plain-text transcript from a tab's stored messages — sent on every
105
105
  // initial request so the server can carry it over if the session has to reset
106
- // (runtime switch, system-prompt change). Last 20 user/assistant turns only;
107
- // system/action rows are skipped because they're UI artifacts, not dialog.
106
+ // (runtime switch, system-prompt change) or if the previous turn has local
107
+ // action results the runtime session never saw.
108
108
  var CC_TRANSCRIPT_MAX_TURNS = 20;
109
109
  function _ccBuildTranscript(tab) {
110
110
  if (!tab || !Array.isArray(tab.messages) || tab.messages.length === 0) return [];
111
111
  var out = [];
112
112
  for (var i = 0; i < tab.messages.length; i++) {
113
113
  var m = tab.messages[i];
114
- if (!m || (m.role !== 'user' && m.role !== 'assistant')) continue;
114
+ if (!m || (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'action' && m.role !== 'system')) continue;
115
115
  var html = typeof m.html === 'string' ? m.html : '';
116
116
  var tmp = document.createElement('div');
117
117
  tmp.innerHTML = html;
@@ -141,6 +141,23 @@ function _ccMergeStreamText(prev, incoming) {
141
141
  return current + '\n\n' + next;
142
142
  }
143
143
 
144
+ var CC_DISPATCH_ACTION_ALIASES = ['fix', 'implement', 'explore', 'review', 'test'];
145
+ function _ccNormalizeDispatchAction(action) {
146
+ if (!action || typeof action !== 'object' || typeof action.type !== 'string') return action;
147
+ var type = action.type.trim().toLowerCase();
148
+ if (type === 'dispatch') {
149
+ if (action.type === 'dispatch') return action;
150
+ var dispatchAction = Object.assign({}, action);
151
+ dispatchAction.type = 'dispatch';
152
+ return dispatchAction;
153
+ }
154
+ if (CC_DISPATCH_ACTION_ALIASES.indexOf(type) < 0) return action;
155
+ var normalized = Object.assign({}, action);
156
+ normalized.type = 'dispatch';
157
+ if (!normalized.workType) normalized.workType = type;
158
+ return normalized;
159
+ }
160
+
144
161
  async function _ccDashboardHealth() {
145
162
  var controller = new AbortController();
146
163
  var timer = setTimeout(function() { controller.abort(); }, 3000);
@@ -745,7 +762,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
745
762
  if (!isReconnect && res.status === 429 && (!activeTab._429retries || activeTab._429retries < 3)) {
746
763
  activeTab._429retries = (activeTab._429retries || 0) + 1;
747
764
  await new Promise(function(r) { setTimeout(r, 1500); });
748
- return await _ccConsumeStream({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null }, false);
765
+ return await _ccConsumeStream({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null, transcript: _ccBuildTranscript(activeTab) }, false);
749
766
  }
750
767
  activeTab._429retries = 0;
751
768
  var errText = await res.text();
@@ -963,8 +980,25 @@ function ccRetryLast(tabId, retryId) {
963
980
  });
964
981
  }
965
982
 
966
- async function _ccFetch(url, body) {
967
- var res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
983
+ async function _ccFetch(url, body, method) {
984
+ method = (method || 'POST').toUpperCase();
985
+ var fetchUrl = url;
986
+ var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
987
+ if (method === 'GET') {
988
+ var qs = new URLSearchParams();
989
+ Object.entries(body || {}).forEach(function(entry) {
990
+ var key = entry[0], value = entry[1];
991
+ if (value === undefined || value === null) return;
992
+ if (Array.isArray(value)) value.forEach(function(v) { qs.append(key, String(v)); });
993
+ else if (typeof value === 'object') qs.append(key, JSON.stringify(value));
994
+ else qs.append(key, String(value));
995
+ });
996
+ var text = qs.toString();
997
+ if (text) fetchUrl += (fetchUrl.includes('?') ? '&' : '?') + text;
998
+ } else {
999
+ opts.body = JSON.stringify(body || {});
1000
+ }
1001
+ var res = await fetch(fetchUrl, opts);
968
1002
  if (!res.ok) {
969
1003
  var d = await res.json().catch(function() { return {}; });
970
1004
  var err = new Error(d.error || 'Request failed (' + res.status + ')');
@@ -994,6 +1028,7 @@ function _tagServerExecuted(actions, actionResults) {
994
1028
  }
995
1029
 
996
1030
  async function ccExecuteAction(action, targetTabId) {
1031
+ action = _ccNormalizeDispatchAction(action);
997
1032
  var status = document.createElement('div');
998
1033
  status.style.cssText = 'padding:4px 10px;border-radius:4px;font-size:10px;align-self:flex-start;border:1px dashed var(--border);color:var(--muted)';
999
1034
 
@@ -1010,7 +1045,7 @@ async function ccExecuteAction(action, targetTabId) {
1010
1045
  status.style.color = action._serverDuplicate ? 'var(--orange)' : 'var(--green)';
1011
1046
  }
1012
1047
  ccAddMessage('action', status.outerHTML, false, targetTabId);
1013
- if (['dispatch','fix','implement','explore','review','test','create-meeting'].includes(action.type)) wakeEngine();
1048
+ if (['dispatch','create-meeting'].includes(action.type)) wakeEngine();
1014
1049
  refresh();
1015
1050
  return;
1016
1051
  }
@@ -1023,7 +1058,7 @@ async function ccExecuteAction(action, targetTabId) {
1023
1058
  case 'explore':
1024
1059
  case 'review':
1025
1060
  case 'test': {
1026
- var workType = action.workType || (action.type !== 'dispatch' ? action.type : 'implement');
1061
+ var workType = action.workType || 'implement';
1027
1062
  // Forward both singular (`agent`) and plural (`agents`) hint shapes —
1028
1063
  // the LLM emits either depending on phrasing ("assign to lambert" vs
1029
1064
  // "dispatch to dallas, ralph"). The server-side handler promotes a
@@ -1050,22 +1085,25 @@ async function ccExecuteAction(action, targetTabId) {
1050
1085
  if (notePageLink && !notePageLink.querySelector('.notif-badge')) { var noteCurPage = document.querySelector('.sidebar-link.active')?.getAttribute('data-page'); if (noteCurPage !== 'inbox') showNotifBadge(notePageLink); }
1051
1086
  break;
1052
1087
  }
1053
- case 'pin': {
1088
+ case 'pin':
1089
+ case 'pin-to-pinned': {
1054
1090
  await _ccFetch('/api/pinned', { title: action.title, content: action.content || action.description, level: action.level || '' });
1055
1091
  status.innerHTML = '&#x1F4CC; Pinned: <strong>' + escHtml(action.title) + '</strong> — visible to all agents';
1056
1092
  status.style.color = 'var(--green)';
1057
1093
  break;
1058
1094
  }
1059
1095
  case 'plan': {
1060
- await _ccFetch('/api/plan', { title: action.title, description: action.description, project: action.project, branchStrategy: action.branchStrategy || 'parallel' });
1096
+ var branchStrategy = action.branch_strategy || action.branchStrategy || 'parallel';
1097
+ await _ccFetch('/api/plan', { title: action.title, description: action.description, project: action.project, branch_strategy: branchStrategy, branchStrategy: branchStrategy });
1061
1098
  status.innerHTML = '&#10003; Plan queued: <strong>' + escHtml(action.title) + '</strong>';
1062
1099
  status.style.color = 'var(--green)';
1063
1100
  wakeEngine();
1064
1101
  break;
1065
1102
  }
1066
1103
  case 'cancel': {
1067
- await _ccFetch('/api/agents/cancel', { agentId: action.agent, reason: action.reason || 'Cancelled via command center' });
1068
- status.innerHTML = '&#10003; Cancelled agent: <strong>' + escHtml(action.agent) + '</strong>';
1104
+ var cancelAgent = action.agent || action.agentId || '';
1105
+ await _ccFetch('/api/agents/cancel', { agent: cancelAgent, agentId: cancelAgent, task: action.task || action.cancelTask || '', reason: action.reason || 'Cancelled via command center' });
1106
+ status.innerHTML = '&#10003; Cancelled agent: <strong>' + escHtml(cancelAgent || action.task || action.cancelTask || '') + '</strong>';
1069
1107
  status.style.color = 'var(--orange)';
1070
1108
  break;
1071
1109
  }
@@ -1457,8 +1495,9 @@ async function ccExecuteAction(action, targetTabId) {
1457
1495
  break;
1458
1496
  }
1459
1497
  case 'add-project': {
1460
- await _ccFetch('/api/projects/add', { localPath: action.localPath, name: action.name || '', repoHost: action.repoHost || 'github' });
1461
- status.innerHTML = '&#10003; Project added: <strong>' + escHtml(action.name || action.localPath) + '</strong>';
1498
+ var projectPath = action.path || action.localPath;
1499
+ await _ccFetch('/api/projects/add', { path: projectPath, localPath: projectPath, name: action.name || '', repoHost: action.repoHost || 'github', allowNonRepo: action.allowNonRepo, confirmToken: action.confirmToken });
1500
+ status.innerHTML = '&#10003; Project added: <strong>' + escHtml(action.name || projectPath) + '</strong>';
1462
1501
  status.style.color = 'var(--green)';
1463
1502
  break;
1464
1503
  }
@@ -1489,7 +1528,7 @@ async function ccExecuteAction(action, targetTabId) {
1489
1528
  default: {
1490
1529
  // Generic fallback: if action has an `endpoint` field, call it directly (local API only)
1491
1530
  if (action.endpoint && action.endpoint.startsWith('/api/') && !action.endpoint.includes('..') && !/\%2e/i.test(action.endpoint)) {
1492
- var genRes = await _ccFetch(action.endpoint, action.params || {});
1531
+ var genRes = await _ccFetch(action.endpoint, action.params || {}, action.method || 'POST');
1493
1532
  var genData = await genRes.json().catch(function() { return {}; });
1494
1533
  status.innerHTML = '&#10003; ' + escHtml(action.type) + ': ' + escHtml(genData.message || genData.id || 'done');
1495
1534
  status.style.color = 'var(--green)';
@@ -28,6 +28,7 @@ let _qaAbortController = null;
28
28
  let _qaQueue = []; // queued messages while processing
29
29
  const QA_QUEUE_CAP = 10; // max queued messages
30
30
  const QA_STREAM_STALL_MS = 6 * 60 * 1000; // allow the full doc-chat timeout before treating heartbeat-only streams as stalled
31
+ const QA_TRANSCRIPT_MAX_TURNS = 20;
31
32
  let _qaSessionKey = ''; // key for current conversation (title or filePath)
32
33
 
33
34
  const QA_STICKY_BOTTOM_PX = 80;
@@ -97,6 +98,40 @@ function _qaCloneQueue(queue) {
97
98
  return Array.isArray(queue) ? queue.map(item => ({ ...item })) : [];
98
99
  }
99
100
 
101
+ function _qaBuildTranscript(history, currentMessage) {
102
+ if (!Array.isArray(history) || history.length === 0) return [];
103
+ const current = typeof currentMessage === 'string' ? currentMessage.trim() : '';
104
+ const out = [];
105
+ for (let i = 0; i < history.length; i++) {
106
+ const m = history[i];
107
+ if (!m || (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'action' && m.role !== 'system')) continue;
108
+ const text = typeof m.text === 'string' ? m.text.trim() : '';
109
+ if (!text) continue;
110
+ if (current && m.role === 'user' && text === current && i === history.length - 1) continue;
111
+ out.push({ role: m.role, text });
112
+ }
113
+ return out.slice(-QA_TRANSCRIPT_MAX_TURNS);
114
+ }
115
+
116
+ function _qaSummarizeActionContext(actions, actionResults) {
117
+ if (!Array.isArray(actions) || actions.length === 0) return '';
118
+ const lines = [];
119
+ for (let i = 0; i < actions.length; i++) {
120
+ const action = actions[i] || {};
121
+ const result = Array.isArray(actionResults) ? actionResults[i] : null;
122
+ const type = action.type || 'action';
123
+ const subject = result?.id || action.id || action.title || action.file || action.target || action.endpoint || '';
124
+ if (result?.error) {
125
+ lines.push(`${type}${subject ? ' ' + subject : ''} failed: ${result.error}`);
126
+ } else if (result?.ok || action._serverExecuted) {
127
+ lines.push(`${type}${subject ? ' ' + subject : ''} completed${result?.duplicate ? ' (duplicate)' : ''}${result?.warning ? ': ' + result.warning : ''}`);
128
+ } else {
129
+ lines.push(`${type}${subject ? ' ' + subject : ''} emitted`);
130
+ }
131
+ }
132
+ return lines.join('\n');
133
+ }
134
+
100
135
  function _qaGetRuntime(key) {
101
136
  if (!key) return null;
102
137
  let runtime = _qaRuntime.get(key);
@@ -609,6 +644,7 @@ async function _processQaMessage(message, selection, opts) {
609
644
  filePath: capturedFilePath || null,
610
645
  model: window._lastStatus?.autoMode?.ccModel || undefined,
611
646
  contentHash: capturedDocContext.content ? (function(s) { const m = Math.floor(s.length / 2); return s.length + ':' + s.charCodeAt(0) + ':' + s.charCodeAt(m) + ':' + s.charCodeAt(s.length - 1); })(capturedDocContext.content) : undefined,
647
+ transcript: _qaBuildTranscript(runtime.history, message),
612
648
  }),
613
649
  });
614
650
  let sessionDocContext = { ...capturedDocContext };
@@ -692,20 +728,21 @@ async function _processQaMessage(message, selection, opts) {
692
728
  if (rawErrorHtml) _qaInsertBeforeQueued(tmp, rawErrorHtml);
693
729
  });
694
730
 
695
- runtime.history.push({ role: 'user', text: message });
696
- runtime.history.push({ role: 'assistant', text: finalText || '' });
697
- if (_qaIsActiveSession(sessionKey)) _qaHistory = runtime.history.slice();
698
-
699
731
  _qaNotifySidebar(capturedFilePath);
732
+ runtime.history.push({ role: 'user', text: message });
733
+ runtime.history.push({ role: 'assistant', text: bodyText || finalText || '' });
700
734
  if (evt.actions && evt.actions.length > 0) {
701
735
  if (evt.actionResults && typeof _tagServerExecuted === 'function') _tagServerExecuted(evt.actions, evt.actionResults);
702
736
  for (const action of evt.actions) await ccExecuteAction(action);
737
+ const actionContext = _qaSummarizeActionContext(evt.actions, evt.actionResults);
738
+ if (actionContext) runtime.history.push({ role: 'action', text: actionContext });
703
739
  } else if (evt.actionParseError) {
704
740
  const warning = '<div class="modal-qa-a" style="color:var(--red)">Actions block emitted but JSON could not be parsed — no actions were executed. Resend or rephrase. (' + escHtml(String(evt.actionParseError).slice(0, 200)) + ')</div>';
705
741
  updatedThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
706
742
  _qaInsertBeforeQueued(tmp, warning);
707
743
  });
708
744
  }
745
+ if (_qaIsActiveSession(sessionKey)) _qaHistory = runtime.history.slice();
709
746
 
710
747
  if (evt.edited && evt.content) {
711
748
  const display = evt.content.replace(/^---[\s\S]*?---\n*/m, '');
package/dashboard.js CHANGED
@@ -1366,10 +1366,36 @@ function _readCcTabSessions({ prune = true } = {}) {
1366
1366
 
1367
1367
  const CC_CARRYOVER_MAX_TURNS = 20;
1368
1368
  const CC_CARRYOVER_PER_MSG_CHARS = 2000;
1369
+ const CC_TRANSCRIPT_DIALOG_ROLES = new Set(['user', 'assistant']);
1370
+ const CC_TRANSCRIPT_CONTEXT_ROLES = new Set(['user', 'assistant', 'action', 'system']);
1369
1371
 
1370
- function _buildTranscriptCarryover(transcript, { previousRuntime, currentMessage } = {}) {
1372
+ function _normalizeTranscriptRole(role) {
1373
+ const value = String(role || '').toLowerCase();
1374
+ return CC_TRANSCRIPT_CONTEXT_ROLES.has(value) ? value : null;
1375
+ }
1376
+
1377
+ function _transcriptHasCarryoverContext(transcript, { outOfBandOnly = false, currentMessage } = {}) {
1378
+ if (!Array.isArray(transcript)) return false;
1379
+ const current = typeof currentMessage === 'string' ? currentMessage.trim() : '';
1380
+ return transcript.some(m => {
1381
+ const role = _normalizeTranscriptRole(m?.role);
1382
+ if (!role || typeof m.text !== 'string' || !m.text.trim()) return false;
1383
+ if (outOfBandOnly && CC_TRANSCRIPT_DIALOG_ROLES.has(role)) return false;
1384
+ return !(current && role === 'user' && m.text.trim() === current);
1385
+ });
1386
+ }
1387
+
1388
+ function _buildTranscriptCarryover(transcript, { previousRuntime, currentMessage, outOfBandOnly = false } = {}) {
1371
1389
  if (!Array.isArray(transcript) || transcript.length === 0) return '';
1372
- let filtered = transcript.filter(m => m && (m.role === 'user' || m.role === 'assistant') && typeof m.text === 'string' && m.text.trim());
1390
+ let filtered = transcript
1391
+ .map(m => {
1392
+ const role = _normalizeTranscriptRole(m?.role);
1393
+ return role && typeof m?.text === 'string' && m.text.trim()
1394
+ ? { role, text: m.text }
1395
+ : null;
1396
+ })
1397
+ .filter(Boolean);
1398
+ if (outOfBandOnly) filtered = filtered.filter(m => !CC_TRANSCRIPT_DIALOG_ROLES.has(m.role));
1373
1399
  const current = typeof currentMessage === 'string' ? currentMessage.trim() : '';
1374
1400
  if (current && filtered.length > 0) {
1375
1401
  const last = filtered[filtered.length - 1];
@@ -1378,11 +1404,19 @@ function _buildTranscriptCarryover(transcript, { previousRuntime, currentMessage
1378
1404
  if (filtered.length === 0) return '';
1379
1405
  const recent = filtered.slice(-CC_CARRYOVER_MAX_TURNS);
1380
1406
  const truncated = filtered.length > recent.length;
1381
- const header = previousRuntime
1382
- ? `--- Previous conversation (carried over from ${previousRuntime} session) ---`
1383
- : `--- Previous conversation (carried over) ---`;
1407
+ const header = outOfBandOnly
1408
+ ? `--- Previous out-of-band UI/server events (carried over) ---`
1409
+ : previousRuntime
1410
+ ? `--- Previous conversation (carried over from ${previousRuntime} session) ---`
1411
+ : `--- Previous conversation (carried over) ---`;
1384
1412
  const lines = recent.map(m => {
1385
- const who = m.role === 'user' ? 'User' : 'Assistant';
1413
+ const who = m.role === 'user'
1414
+ ? 'User'
1415
+ : m.role === 'assistant'
1416
+ ? 'Assistant'
1417
+ : m.role === 'action'
1418
+ ? 'Action result'
1419
+ : 'System note';
1386
1420
  let text = m.text.trim();
1387
1421
  if (text.length > CC_CARRYOVER_PER_MSG_CHARS) text = text.slice(0, CC_CARRYOVER_PER_MSG_CHARS) + '… [truncated]';
1388
1422
  return `${who}: ${text}`;
@@ -1415,6 +1449,13 @@ try {
1415
1449
  let _preambleCache = null;
1416
1450
  let _preambleCacheTs = 0;
1417
1451
  const PREAMBLE_TTL = 30000; // 30s — longer TTL since preamble is lightweight orientation, not real-time data
1452
+ const CC_API_FALLBACK_TIMEOUT_MS = 15000;
1453
+ const CC_API_FALLBACK_METHODS = new Set(['GET', 'POST', 'DELETE']);
1454
+ const CC_API_FALLBACK_BLOCKED_PREFIXES = [
1455
+ '/api/command-center',
1456
+ '/api/doc-chat',
1457
+ '/api/bot',
1458
+ ];
1418
1459
 
1419
1460
  // SoT for CC's runtime API index. Captured lazily on the first HTTP request
1420
1461
  // because ROUTES is closed over inside the request handler. Subsequent
@@ -1535,13 +1576,20 @@ function _routesAsMeta(routes) {
1535
1576
 
1536
1577
  function _captureApiRoutesMeta(routes) {
1537
1578
  if (_ccApiRoutesMeta || !Array.isArray(routes)) return;
1538
- _ccApiRoutesMeta = _routesAsMeta(routes);
1579
+ _ccApiRoutesMeta = routes.map(r => ({
1580
+ ..._routesAsMeta([r])[0],
1581
+ _pathRegex: r.path instanceof RegExp ? r.path : null,
1582
+ }));
1583
+ }
1584
+
1585
+ function _resetCcApiRoutesMetaForTest() {
1586
+ _ccApiRoutesMeta = null;
1539
1587
  }
1540
1588
 
1541
1589
  function _formatCcApiRoutesIndex() {
1542
1590
  if (!Array.isArray(_ccApiRoutesMeta) || _ccApiRoutesMeta.length === 0) return '';
1543
1591
  return _ccApiRoutesMeta
1544
- .filter(r => r.path.startsWith('/api/'))
1592
+ .filter(r => r.path.startsWith('/api/') || r.path.startsWith('/^\\/api'))
1545
1593
  .map(r => {
1546
1594
  const params = r.params ? ` — params: ${r.params}` : '';
1547
1595
  const flags = [
@@ -1601,8 +1649,8 @@ ${apiIndex || '(routes not yet captured — first request still pending)'}
1601
1649
  ### CLI Index (auto-generated from engine/cli.js CLI_COMMAND_DOCS — single source of truth)
1602
1650
  ${cliIndex || '(unavailable)'}
1603
1651
 
1604
- For \`POST /api/...\` endpoints marked \`generic-fallback\` and not covered by a named CC action, use the generic fallback:
1605
- \`{"type":"<descriptive>","endpoint":"/api/...","params":{...}}\`.` : '';
1652
+ For any safe local \`/api/...\` endpoint not covered by a named CC action, use the generic fallback:
1653
+ \`{"type":"<descriptive>","endpoint":"/api/...","method":"GET|POST|DELETE","params":{...}}\`.` : '';
1606
1654
 
1607
1655
  const result = `### Agents
1608
1656
  ${agents}
@@ -1751,6 +1799,19 @@ function _extractActionsJson(segment) {
1751
1799
  return null;
1752
1800
  }
1753
1801
 
1802
+ const CC_DISPATCH_ACTION_ALIASES = new Set(['fix', 'implement', 'explore', 'review', 'test']);
1803
+
1804
+ function normalizeCCAction(action) {
1805
+ if (!action || typeof action !== 'object') return action;
1806
+ if (typeof action.type !== 'string') return action;
1807
+ const type = action.type.trim().toLowerCase();
1808
+ if (type === 'dispatch') {
1809
+ return action.type === 'dispatch' ? action : { ...action, type: 'dispatch' };
1810
+ }
1811
+ if (!CC_DISPATCH_ACTION_ALIASES.has(type)) return action;
1812
+ return { ...action, type: 'dispatch', workType: action.workType || type };
1813
+ }
1814
+
1754
1815
  function parseCCActions(text) {
1755
1816
  let actions = [];
1756
1817
  let displayText = stripCCActionsForDisplay(text);
@@ -1791,6 +1852,7 @@ function parseCCActions(text) {
1791
1852
  parseError = null; // legacy fallback recovered actions
1792
1853
  }
1793
1854
  }
1855
+ actions = actions.map(normalizeCCAction);
1794
1856
  const result = { text: displayText, actions };
1795
1857
  if (parseError && actions.length === 0) {
1796
1858
  result._actionParseError = parseError;
@@ -2184,9 +2246,296 @@ function _ccValidateAction(action) {
2184
2246
  }
2185
2247
  }
2186
2248
 
2249
+ let _ccLocalApiInvokerForTest = null;
2250
+
2251
+ function _setCcLocalApiInvokerForTest(fn) {
2252
+ _ccLocalApiInvokerForTest = typeof fn === 'function' ? fn : null;
2253
+ }
2254
+
2255
+ function _ccRouteMethodsForPath(pathname) {
2256
+ if (!Array.isArray(_ccApiRoutesMeta) || _ccApiRoutesMeta.length === 0) return null;
2257
+ const methods = new Set();
2258
+ for (const route of _ccApiRoutesMeta) {
2259
+ if (route._pathRegex instanceof RegExp) {
2260
+ route._pathRegex.lastIndex = 0;
2261
+ if (route._pathRegex.test(pathname)) methods.add(String(route.method || '').toUpperCase());
2262
+ } else if (route.path === pathname) {
2263
+ methods.add(String(route.method || '').toUpperCase());
2264
+ }
2265
+ }
2266
+ return methods;
2267
+ }
2268
+
2269
+ function _ccValidateLocalApiFallback(endpoint, method) {
2270
+ if (typeof endpoint !== 'string' || !endpoint.trim()) return 'generic API fallback requires endpoint';
2271
+ const raw = endpoint.trim();
2272
+ if (!(raw === '/api' || raw.startsWith('/api/'))) return 'generic API fallback endpoint must be a local /api/ path';
2273
+ if (/[\0\r\n\\]/.test(raw) || raw.includes('..') || /%2e/i.test(raw) || /%5c/i.test(raw)) {
2274
+ return 'generic API fallback endpoint is unsafe';
2275
+ }
2276
+ let parsed;
2277
+ try {
2278
+ parsed = new URL(raw, 'http://127.0.0.1');
2279
+ } catch {
2280
+ return 'generic API fallback endpoint is invalid';
2281
+ }
2282
+ if (parsed.origin !== 'http://127.0.0.1' || !(parsed.pathname === '/api' || parsed.pathname.startsWith('/api/'))) {
2283
+ return 'generic API fallback endpoint must be a local /api/ path';
2284
+ }
2285
+ if (CC_API_FALLBACK_BLOCKED_PREFIXES.some(prefix => parsed.pathname === prefix || parsed.pathname.startsWith(prefix + '/'))) {
2286
+ return 'generic API fallback cannot call Command Center, doc-chat, or bot endpoints';
2287
+ }
2288
+ if (/stream/i.test(parsed.pathname) || parsed.pathname === '/api/hot-reload') {
2289
+ return 'generic API fallback cannot call streaming endpoints';
2290
+ }
2291
+ const normalizedMethod = String(method || 'POST').toUpperCase();
2292
+ if (!CC_API_FALLBACK_METHODS.has(normalizedMethod)) {
2293
+ return `generic API fallback method ${normalizedMethod} is not allowed`;
2294
+ }
2295
+ const routeMethods = _ccRouteMethodsForPath(parsed.pathname);
2296
+ if (routeMethods && routeMethods.size > 0 && !routeMethods.has(normalizedMethod)) {
2297
+ return `API endpoint ${parsed.pathname} does not allow ${normalizedMethod}; allowed methods: ${[...routeMethods].join(', ')}`;
2298
+ }
2299
+ if (routeMethods && routeMethods.size === 0) {
2300
+ return `API endpoint ${parsed.pathname} is not in the local API index`;
2301
+ }
2302
+ return null;
2303
+ }
2304
+
2305
+ function _ccBuildQueryString(params) {
2306
+ if (!params || typeof params !== 'object' || Array.isArray(params)) return '';
2307
+ const search = new URLSearchParams();
2308
+ for (const [key, value] of Object.entries(params)) {
2309
+ if (value === undefined || value === null) continue;
2310
+ if (Array.isArray(value)) {
2311
+ for (const item of value) search.append(key, String(item));
2312
+ } else if (typeof value === 'object') {
2313
+ search.append(key, JSON.stringify(value));
2314
+ } else {
2315
+ search.append(key, String(value));
2316
+ }
2317
+ }
2318
+ const text = search.toString();
2319
+ return text ? '?' + text : '';
2320
+ }
2321
+
2322
+ function _ccRequestPath(endpoint, method, params) {
2323
+ const parsed = new URL(endpoint, 'http://127.0.0.1');
2324
+ if (method === 'GET') {
2325
+ const extra = _ccBuildQueryString(params);
2326
+ if (extra) {
2327
+ const glue = parsed.search ? '&' : '?';
2328
+ return parsed.pathname + parsed.search + glue + extra.slice(1);
2329
+ }
2330
+ }
2331
+ return parsed.pathname + parsed.search;
2332
+ }
2333
+
2334
+ async function _ccInvokeLocalApi({ method, endpoint, params }) {
2335
+ if (_ccLocalApiInvokerForTest) return _ccLocalApiInvokerForTest({ method, endpoint, params });
2336
+ const requestPath = _ccRequestPath(endpoint, method, params);
2337
+ return new Promise((resolve, reject) => {
2338
+ const body = method === 'GET' ? null : JSON.stringify(params || {});
2339
+ const req = http.request({
2340
+ hostname: '127.0.0.1',
2341
+ port: PORT,
2342
+ method,
2343
+ path: requestPath,
2344
+ timeout: CC_API_FALLBACK_TIMEOUT_MS,
2345
+ headers: body ? {
2346
+ 'Content-Type': 'application/json',
2347
+ 'Content-Length': Buffer.byteLength(body),
2348
+ } : {},
2349
+ }, res => {
2350
+ let text = '';
2351
+ res.setEncoding('utf8');
2352
+ res.on('data', chunk => { text += chunk; });
2353
+ res.on('end', () => {
2354
+ let data = text;
2355
+ try { data = text ? JSON.parse(text) : {}; } catch { /* non-JSON API response */ }
2356
+ resolve({ status: res.statusCode || 0, data });
2357
+ });
2358
+ });
2359
+ req.on('timeout', () => {
2360
+ req.destroy(new Error(`local API fallback timed out after ${CC_API_FALLBACK_TIMEOUT_MS}ms`));
2361
+ });
2362
+ req.on('error', reject);
2363
+ if (body) req.write(body);
2364
+ req.end();
2365
+ });
2366
+ }
2367
+
2368
+ function _ccApiRequest(endpoint, params = {}, method = 'POST') {
2369
+ return { endpoint, params, method };
2370
+ }
2371
+
2372
+ function _ccMappedApiRequests(action) {
2373
+ switch (action.type) {
2374
+ case 'pin':
2375
+ case 'pin-to-pinned':
2376
+ return _ccApiRequest('/api/pinned', { title: action.title, content: action.content || action.description, level: action.level || '' });
2377
+ case 'plan': {
2378
+ const branchStrategy = action.branch_strategy || action.branchStrategy || 'parallel';
2379
+ return _ccApiRequest('/api/plan', {
2380
+ title: action.title, description: action.description || '', priority: action.priority,
2381
+ project: action.project, agent: action.agent, branch_strategy: branchStrategy,
2382
+ });
2383
+ }
2384
+ case 'cancel':
2385
+ return _ccApiRequest('/api/agents/cancel', {
2386
+ agent: action.agent || action.agentId,
2387
+ task: action.task || action.cancelTask,
2388
+ reason: action.reason || 'Cancelled via command center',
2389
+ });
2390
+ case 'retry':
2391
+ return (action.ids || []).map(id => _ccApiRequest('/api/work-items/retry', { id, source: action.source || '' }));
2392
+ case 'pause-plan':
2393
+ return _ccApiRequest('/api/plans/pause', { file: action.file });
2394
+ case 'approve-plan':
2395
+ return _ccApiRequest('/api/plans/approve', { file: action.file });
2396
+ case 'reject-plan':
2397
+ return _ccApiRequest('/api/plans/reject', { file: action.file, reason: action.reason || '' });
2398
+ case 'archive-plan':
2399
+ return _ccApiRequest('/api/plans/archive', { file: action.file });
2400
+ case 'unarchive-plan':
2401
+ return _ccApiRequest('/api/plans/unarchive', { file: action.file });
2402
+ case 'execute-plan':
2403
+ return _ccApiRequest('/api/plans/execute', { file: action.file, project: action.project || '' });
2404
+ case 'trigger-verify':
2405
+ return _ccApiRequest('/api/plans/trigger-verify', { file: action.file });
2406
+ case 'regenerate-plan':
2407
+ return _ccApiRequest('/api/plans/approve', { file: action.file, forceRegen: true });
2408
+ case 'revise-plan':
2409
+ return _ccApiRequest('/api/plans/revise', { file: action.file, feedback: action.feedback || action.description, requestedBy: 'command-center' });
2410
+ case 'edit-prd-item':
2411
+ return _ccApiRequest('/api/prd-items/update', {
2412
+ source: action.source, itemId: action.itemId, name: action.name, description: action.description,
2413
+ priority: action.priority, estimated_complexity: action.estimated_complexity || action.complexity,
2414
+ });
2415
+ case 'remove-prd-item':
2416
+ return _ccApiRequest('/api/prd-items/remove', { source: action.source, itemId: action.itemId });
2417
+ case 'reopen-prd-item':
2418
+ return _ccApiRequest('/api/prd-items/update', { source: action.file, itemId: action.id, status: 'updated' });
2419
+ case 'delete-work-item':
2420
+ return _ccApiRequest('/api/work-items/delete', { id: action.id, source: action.source || '' });
2421
+ case 'cancel-work-item':
2422
+ return _ccApiRequest('/api/work-items/cancel', { id: action.id, source: action.source || '', reason: action.reason || 'cc' });
2423
+ case 'archive-work-item':
2424
+ return _ccApiRequest('/api/work-items/archive', { id: action.id });
2425
+ case 'work-item-feedback':
2426
+ return _ccApiRequest('/api/work-items/feedback', { id: action.id, rating: action.rating || 'up', comment: action.comment || '' });
2427
+ case 'schedule':
2428
+ return _ccApiRequest(action._update ? '/api/schedules/update' : '/api/schedules', {
2429
+ id: action.id, title: action.title, cron: action.cron, type: action.workType || 'implement',
2430
+ project: action.project, agent: action.agent, description: action.description,
2431
+ priority: action.priority, enabled: action.enabled !== false,
2432
+ });
2433
+ case 'delete-schedule':
2434
+ return _ccApiRequest('/api/schedules/delete', { id: action.id });
2435
+ case 'edit-pipeline':
2436
+ return _ccApiRequest('/api/pipelines/update', {
2437
+ id: action.id, title: action.title, stages: action.stages,
2438
+ trigger: action.trigger, enabled: action.enabled, stopWhen: action.stopWhen,
2439
+ monitoredResources: action.monitoredResources,
2440
+ });
2441
+ case 'delete-pipeline':
2442
+ return _ccApiRequest('/api/pipelines/delete', { id: action.id });
2443
+ case 'trigger-pipeline':
2444
+ return _ccApiRequest('/api/pipelines/trigger', { id: action.id });
2445
+ case 'continue-pipeline':
2446
+ return _ccApiRequest('/api/pipelines/continue', { id: action.id, stageId: action.stageId });
2447
+ case 'abort-pipeline':
2448
+ return _ccApiRequest('/api/pipelines/abort', { id: action.id });
2449
+ case 'retrigger-pipeline':
2450
+ return _ccApiRequest('/api/pipelines/retrigger', { id: action.id });
2451
+ case 'add-meeting-note':
2452
+ return _ccApiRequest('/api/meetings/note', { id: action.id, note: action.note || action.content });
2453
+ case 'advance-meeting':
2454
+ return _ccApiRequest('/api/meetings/advance', { id: action.id });
2455
+ case 'end-meeting':
2456
+ return _ccApiRequest('/api/meetings/end', { id: action.id });
2457
+ case 'archive-meeting':
2458
+ return _ccApiRequest('/api/meetings/archive', { id: action.id });
2459
+ case 'unarchive-meeting':
2460
+ return _ccApiRequest('/api/meetings/unarchive', { id: action.id });
2461
+ case 'delete-meeting':
2462
+ return _ccApiRequest('/api/meetings/delete', { id: action.id });
2463
+ case 'set-config':
2464
+ return _ccApiRequest('/api/settings', { engine: { [action.setting]: action.value } });
2465
+ case 'update-routing':
2466
+ return _ccApiRequest('/api/settings/routing', { content: action.content });
2467
+ case 'steer-agent':
2468
+ return _ccApiRequest('/api/agents/steer', { agent: action.agent, message: action.message || action.content });
2469
+ case 'link-pr':
2470
+ return _ccApiRequest('/api/pull-requests/link', { url: action.url, title: action.title || '', project: action.project || '', autoObserve: action.autoObserve !== false });
2471
+ case 'delete-pr':
2472
+ return _ccApiRequest('/api/pull-requests/delete', { id: action.id, project: action.project || '' });
2473
+ case 'file-bug':
2474
+ return _ccApiRequest('/api/issues/create', { title: action.title, description: action.description, labels: action.labels });
2475
+ case 'promote-to-kb':
2476
+ return _ccApiRequest('/api/inbox/promote-kb', { name: action.file, category: action.category || 'project-notes' });
2477
+ case 'kb-sweep':
2478
+ return _ccApiRequest('/api/knowledge/sweep', {});
2479
+ case 'toggle-kb-pin':
2480
+ return _ccApiRequest('/api/kb-pins/toggle', { key: action.key });
2481
+ case 'unpin':
2482
+ return _ccApiRequest('/api/pinned' + '/remove', { title: action.title });
2483
+ case 'add-project':
2484
+ return _ccApiRequest('/api/projects/add', {
2485
+ path: action.path || action.localPath, name: action.name || '',
2486
+ repoHost: action.repoHost || 'github', allowNonRepo: action.allowNonRepo,
2487
+ confirmToken: action.confirmToken,
2488
+ });
2489
+ case 'restart-engine':
2490
+ return _ccApiRequest('/api/engine/restart', {});
2491
+ case 'reset-settings':
2492
+ return _ccApiRequest('/api/settings/reset', {});
2493
+ default:
2494
+ if (action.endpoint) return _ccApiRequest(action.endpoint, action.params || {}, action.method || 'POST');
2495
+ return null;
2496
+ }
2497
+ }
2498
+
2499
+ async function _ccExecuteLocalApiAction(action) {
2500
+ const mapped = _ccMappedApiRequests(action);
2501
+ if (!mapped) return null;
2502
+ const requests = Array.isArray(mapped) ? mapped : [mapped];
2503
+ if (requests.length === 0) throw new Error(`${action.type} action has no API requests to execute`);
2504
+ const apiResults = [];
2505
+ for (const request of requests) {
2506
+ const method = String(request.method || 'POST').toUpperCase();
2507
+ const endpoint = String(request.endpoint || '').trim();
2508
+ const params = request.params || {};
2509
+ const validationError = _ccValidateLocalApiFallback(endpoint, method);
2510
+ if (validationError) throw new Error(validationError);
2511
+ const response = await _ccInvokeLocalApi({ method, endpoint, params });
2512
+ const status = Number(response?.status) || 0;
2513
+ const data = response?.data === undefined ? {} : response.data;
2514
+ if (status < 200 || status >= 300) {
2515
+ const detail = data && typeof data === 'object' && data.error ? data.error : `HTTP ${status}`;
2516
+ throw new Error(`${method} ${endpoint} failed: ${detail}`);
2517
+ }
2518
+ if (data && typeof data === 'object' && data.error) throw new Error(`${method} ${endpoint} failed: ${data.error}`);
2519
+ apiResults.push({ status, data, endpoint, method });
2520
+ }
2521
+ const firstData = apiResults[0]?.data && typeof apiResults[0].data === 'object' ? apiResults[0].data : {};
2522
+ return {
2523
+ type: action.type,
2524
+ ok: true,
2525
+ endpoint: apiResults[0]?.endpoint,
2526
+ method: apiResults[0]?.method,
2527
+ status: apiResults[0]?.status,
2528
+ ...(firstData.id ? { id: firstData.id } : {}),
2529
+ ...(firstData.file ? { file: firstData.file } : {}),
2530
+ ...(firstData.message ? { message: firstData.message } : {}),
2531
+ ...(apiResults.length > 1 ? { count: apiResults.length, results: apiResults.map(r => r.data) } : { data: firstData }),
2532
+ };
2533
+ }
2534
+
2187
2535
  async function executeCCActions(actions) {
2188
2536
  const results = [];
2189
- for (const action of actions) {
2537
+ for (const rawAction of actions) {
2538
+ const action = normalizeCCAction(rawAction);
2190
2539
  const validationError = _ccValidateAction(action);
2191
2540
  if (validationError) {
2192
2541
  results.push({ type: action?.type || 'unknown', error: validationError });
@@ -2195,7 +2544,7 @@ async function executeCCActions(actions) {
2195
2544
  try {
2196
2545
  switch (action.type) {
2197
2546
  case 'dispatch': case 'fix': case 'implement': case 'explore': case 'review': case 'test': {
2198
- const workType = routing.normalizeWorkType(action.workType || (action.type !== 'dispatch' ? action.type : WORK_TYPE.IMPLEMENT), WORK_TYPE.IMPLEMENT);
2547
+ const workType = routing.normalizeWorkType(action.workType || WORK_TYPE.IMPLEMENT, WORK_TYPE.IMPLEMENT);
2199
2548
  const id = 'W-' + shared.uid();
2200
2549
  const project = action.project || '';
2201
2550
  const prRef = getWorkItemPrRef(action);
@@ -2447,10 +2796,16 @@ async function executeCCActions(actions) {
2447
2796
  results.push({ type: 'resume-watch', id: action.id, ok: !!resumed });
2448
2797
  break;
2449
2798
  }
2450
- default:
2451
- // Server didn't handle — frontend must execute
2452
- results.push({ type: action.type });
2799
+ default: {
2800
+ const apiResult = await _ccExecuteLocalApiAction(action);
2801
+ if (apiResult) {
2802
+ results.push(apiResult);
2803
+ } else {
2804
+ // Server didn't handle — frontend must execute.
2805
+ results.push({ type: action.type });
2806
+ }
2453
2807
  break;
2808
+ }
2454
2809
  }
2455
2810
  } catch (e) {
2456
2811
  results.push({ type: action.type, error: e.message });
@@ -2661,12 +3016,14 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
2661
3016
  const existing = resolveSession(store, sessionKey);
2662
3017
  let sessionId = existing ? existing.sessionId : null;
2663
3018
  const resumeNeedsCarryover = !!sessionId && _ccRuntimeNeedsResumeCarryover(shared.resolveCcCli(CONFIG.engine));
3019
+ const resumeHasOutOfBandCarryover = !!sessionId && _transcriptHasCarryoverContext(transcript, { outOfBandOnly: true, currentMessage: message });
3020
+ const freshNeedsCarryover = _transcriptHasCarryoverContext(transcript, { currentMessage: message });
2664
3021
 
2665
- function buildPrompt({ includePreamble = true, includeCarryover = false } = {}) {
3022
+ function buildPrompt({ includePreamble = true, includeCarryover = false, outOfBandOnly = false } = {}) {
2666
3023
  const parts = (!skipStatePreamble && includePreamble) ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
2667
3024
  if (extraContext) parts.push(extraContext);
2668
3025
  if (includeCarryover) {
2669
- const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message });
3026
+ const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message, outOfBandOnly });
2670
3027
  if (carryover) parts.push(carryover);
2671
3028
  }
2672
3029
  parts.push(message);
@@ -2677,7 +3034,11 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
2677
3034
 
2678
3035
  // Attempt 1: resume existing session — skip preamble (session already has context)
2679
3036
  if (sessionId && maxTurns > 1) {
2680
- const p1 = llm.callLLM(buildPrompt({ includePreamble: false, includeCarryover: resumeNeedsCarryover }), '', {
3037
+ const p1 = llm.callLLM(buildPrompt({
3038
+ includePreamble: false,
3039
+ includeCarryover: resumeNeedsCarryover || resumeHasOutOfBandCarryover,
3040
+ outOfBandOnly: !resumeNeedsCarryover,
3041
+ }), '', {
2681
3042
  timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
2682
3043
  engineConfig: CONFIG.engine,
2683
3044
  });
@@ -2714,7 +3075,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
2714
3075
  }
2715
3076
 
2716
3077
  // Attempt 2: fresh session (include preamble for full context)
2717
- const freshPrompt = buildPrompt({ includeCarryover: resumeNeedsCarryover });
3078
+ const freshPrompt = buildPrompt({ includeCarryover: freshNeedsCarryover });
2718
3079
  const p2 = llm.callLLM(freshPrompt, systemPrompt, {
2719
3080
  timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
2720
3081
  engineConfig: CONFIG.engine,
@@ -2762,12 +3123,14 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
2762
3123
  const existing = resolveSession(store, sessionKey);
2763
3124
  let sessionId = existing ? existing.sessionId : null;
2764
3125
  const resumeNeedsCarryover = !!sessionId && _ccRuntimeNeedsResumeCarryover(shared.resolveCcCli(CONFIG.engine));
3126
+ const resumeHasOutOfBandCarryover = !!sessionId && _transcriptHasCarryoverContext(transcript, { outOfBandOnly: true, currentMessage: message });
3127
+ const freshNeedsCarryover = _transcriptHasCarryoverContext(transcript, { currentMessage: message });
2765
3128
 
2766
- function buildPrompt({ includePreamble = true, includeCarryover = false } = {}) {
3129
+ function buildPrompt({ includePreamble = true, includeCarryover = false, outOfBandOnly = false } = {}) {
2767
3130
  const parts = (!skipStatePreamble && includePreamble) ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
2768
3131
  if (extraContext) parts.push(extraContext);
2769
3132
  if (includeCarryover) {
2770
- const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message });
3133
+ const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message, outOfBandOnly });
2771
3134
  if (carryover) parts.push(carryover);
2772
3135
  }
2773
3136
  parts.push(message);
@@ -2777,7 +3140,11 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
2777
3140
  let result;
2778
3141
 
2779
3142
  if (sessionId && maxTurns > 1) {
2780
- const p1 = llm.callLLMStreaming(buildPrompt({ includePreamble: false, includeCarryover: resumeNeedsCarryover }), '', {
3143
+ const p1 = llm.callLLMStreaming(buildPrompt({
3144
+ includePreamble: false,
3145
+ includeCarryover: resumeNeedsCarryover || resumeHasOutOfBandCarryover,
3146
+ outOfBandOnly: !resumeNeedsCarryover,
3147
+ }), '', {
2781
3148
  timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
2782
3149
  engineConfig: CONFIG.engine,
2783
3150
  onChunk,
@@ -2814,7 +3181,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
2814
3181
  }
2815
3182
 
2816
3183
  if (onRetry) onRetry(2);
2817
- const freshPrompt = buildPrompt({ includeCarryover: resumeNeedsCarryover });
3184
+ const freshPrompt = buildPrompt({ includeCarryover: freshNeedsCarryover });
2818
3185
  const p2 = llm.callLLMStreaming(freshPrompt, systemPrompt, {
2819
3186
  timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
2820
3187
  engineConfig: CONFIG.engine,
@@ -2998,7 +3365,7 @@ async function _retryDocChatAfterResumeFailure({ result, initialPass, freshSessi
2998
3365
  function _buildDocChatErrorEnvelope(result) {
2999
3366
  return {
3000
3367
  code: result.code ?? null,
3001
- stderr: (result.stderr || '').slice(-2048),
3368
+ stderr: String(result.stderr || '').slice(-2048),
3002
3369
  errorClass: result.errorClass || null,
3003
3370
  errorMessage: result.errorMessage || null,
3004
3371
  runtime: result.runtime || null,
@@ -3186,7 +3553,7 @@ function _makeDocChatStreamStripper(onChunk) {
3186
3553
  }
3187
3554
 
3188
3555
  // Doc-specific wrapper — adds document context, parses ---DOCUMENT---
3189
- async function ccDocCall({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, onAbortReady }) {
3556
+ async function ccDocCall({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, transcript, onAbortReady }) {
3190
3557
  const sessionKey = filePath || title;
3191
3558
  const docSlice = String(document || '');
3192
3559
 
@@ -3213,6 +3580,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
3213
3580
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
3214
3581
  skipStatePreamble: true,
3215
3582
  systemPrompt: DOC_CHAT_SYSTEM_PROMPT,
3583
+ transcript,
3216
3584
  ...(model ? { model } : {}),
3217
3585
  onAbortReady,
3218
3586
  });
@@ -3250,7 +3618,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
3250
3618
  return _parseDocChatResultText(result.text);
3251
3619
  }
3252
3620
 
3253
- async function ccDocCallStreaming({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, onAbortReady, onChunk, onToolUse, onRetry }) {
3621
+ async function ccDocCallStreaming({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, transcript, onAbortReady, onChunk, onToolUse, onRetry }) {
3254
3622
  const sessionKey = filePath || title;
3255
3623
  const docSlice = String(document || '');
3256
3624
  const streamStripper = _makeDocChatStreamStripper(onChunk);
@@ -3273,6 +3641,7 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
3273
3641
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
3274
3642
  skipStatePreamble: true,
3275
3643
  systemPrompt: DOC_CHAT_SYSTEM_PROMPT,
3644
+ transcript,
3276
3645
  ...(model ? { model } : {}),
3277
3646
  onAbortReady,
3278
3647
  onChunk: streamStripper,
@@ -4110,7 +4479,7 @@ const server = http.createServer(async (req, res) => {
4110
4479
  id, title: body.title, type: 'plan',
4111
4480
  priority: body.priority || 'high', description: body.description || '',
4112
4481
  status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
4113
- branchStrategy: body.branch_strategy || 'parallel',
4482
+ branchStrategy: body.branch_strategy || body.branchStrategy || 'parallel',
4114
4483
  };
4115
4484
  if (body.project) item.project = body.project;
4116
4485
  if (body.agent) item.agent = body.agent;
@@ -4313,14 +4682,17 @@ const server = http.createServer(async (req, res) => {
4313
4682
  async function handleAgentsCancel(req, res) {
4314
4683
  try {
4315
4684
  const body = await readBody(req);
4685
+ const requestedAgent = body.agent || body.agentId;
4686
+ const requestedTask = body.task || body.cancelTask;
4687
+ if (!requestedAgent && !requestedTask) return jsonReply(res, 400, { error: 'agent or task required' });
4316
4688
  const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
4317
4689
  const dispatch = safeJsonObj(dispatchPath);
4318
4690
  const active = dispatch.active || [];
4319
4691
  const cancelled = [];
4320
4692
 
4321
4693
  for (const d of active) {
4322
- const matchAgent = body.agent && d.agent === body.agent;
4323
- const matchTask = body.task && (d.task || '').toLowerCase().includes((body.task || '').toLowerCase());
4694
+ const matchAgent = requestedAgent && d.agent === requestedAgent;
4695
+ const matchTask = requestedTask && (d.task || '').toLowerCase().includes(String(requestedTask).toLowerCase());
4324
4696
  if (!matchAgent && !matchTask) continue;
4325
4697
 
4326
4698
  // Kill agent process
@@ -5433,6 +5805,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5433
5805
  filePath: body.filePath, selection: body.selection, canEdit, isJson,
5434
5806
  model: body.model || undefined,
5435
5807
  freshSession: !!body.freshSession,
5808
+ transcript: body.transcript,
5436
5809
  onAbortReady: (abort) => { _docAbort = abort; },
5437
5810
  });
5438
5811
  const actionResults = await executeDocChatActions(actions);
@@ -5523,6 +5896,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5523
5896
  filePath: body.filePath, selection: body.selection, canEdit, isJson,
5524
5897
  model: body.model || undefined,
5525
5898
  freshSession: !!body.freshSession,
5899
+ transcript: body.transcript,
5526
5900
  onAbortReady: (abort) => { _docAbort = abort; },
5527
5901
  onChunk: (text) => { writeDocEvent({ type: 'chunk', text }); },
5528
5902
  onToolUse: (name, input) => { writeDocEvent({ type: 'tool', name, input: _lightToolInput(input) }); },
@@ -6279,11 +6653,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6279
6653
  const wasResume = !!tabSessionId;
6280
6654
  const sessionId = tabSessionId || null;
6281
6655
  const resumeNeedsCarryover = wasResume && _ccRuntimeNeedsResumeCarryover(currentRuntime);
6656
+ const resumeHasOutOfBandCarryover = wasResume && _transcriptHasCarryoverContext(body.transcript, { outOfBandOnly: true, currentMessage: body.message });
6282
6657
  const preamble = wasResume ? '' : buildCCStatePreamble();
6283
- const carryover = (sessionReset || resumeNeedsCarryover)
6658
+ const includeFullCarryover = sessionReset || resumeNeedsCarryover;
6659
+ const carryover = (includeFullCarryover || resumeHasOutOfBandCarryover)
6284
6660
  ? _buildTranscriptCarryover(body.transcript, {
6285
6661
  previousRuntime: sessionReset ? previousRuntime : null,
6286
6662
  currentMessage: body.message,
6663
+ outOfBandOnly: !includeFullCarryover,
6287
6664
  })
6288
6665
  : '';
6289
6666
  const prompt = _joinCcPromptParts(preamble, carryover, body.message);
@@ -7361,7 +7738,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7361
7738
  { method: 'POST', path: '/api/notes-save', desc: 'Save edited notes.md content', params: 'content, file?', handler: handleNotesSave },
7362
7739
 
7363
7740
  // Plans
7364
- { method: 'POST', path: '/api/plan', desc: 'Create a plan work item that chains to PRD on completion', params: 'title, description?, priority?, project?, agent?, branch_strategy?', handler: handlePlanCreate },
7741
+ { method: 'POST', path: '/api/plan', desc: 'Create a plan work item that chains to PRD on completion', params: 'title, description?, priority?, project?, agent?, branch_strategy? or branchStrategy?', handler: handlePlanCreate },
7365
7742
  { method: 'GET', path: '/api/plans', desc: 'List plan files (.md drafts + .json PRDs)', handler: handlePlansList },
7366
7743
  { method: 'POST', path: '/api/plans/trigger-verify', desc: 'Manually trigger verification for a completed plan', params: 'file', handler: handlePlansTriggerVerify },
7367
7744
  { method: 'POST', path: '/api/plans/approve', desc: 'Approve a plan for execution', params: 'file, approvedBy?', handler: handlePlansApprove },
@@ -7546,7 +7923,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7546
7923
  inboxCount: steering.listUnreadSteeringMessages(agentId).length,
7547
7924
  });
7548
7925
  }},
7549
- { method: 'POST', path: '/api/agents/cancel', desc: 'Cancel an active agent by ID or task substring', params: 'agent?, task?', handler: handleAgentsCancel },
7926
+ { method: 'POST', path: '/api/agents/cancel', desc: 'Cancel an active agent by ID or task substring', params: 'agent? or agentId?, task?', handler: handleAgentsCancel },
7550
7927
  { method: 'POST', path: /^\/api\/agent\/([\w-]+)\/kill$/, template: '/api/agent/:id/kill', desc: 'Kill a running agent: stop process, clear dispatch, reset work items to pending', handler: handleAgentKill },
7551
7928
  { method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/, template: '/api/agent/:id/live-stream', desc: 'SSE real-time live output streaming', handler: handleAgentLiveStream },
7552
7929
  { method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live(?:\?.*)?$/, template: '/api/agent/:id/live', desc: 'Tail live output for a working agent', params: 'tail? (bytes, default 8192)', handler: handleAgentLive },
@@ -7592,8 +7969,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7592
7969
  { method: 'GET', path: /^\/api\/knowledge\/([^/]+)\/([^?]+)/, template: '/api/knowledge/:category/:file', desc: 'Read a specific knowledge base entry', handler: handleKnowledgeRead },
7593
7970
 
7594
7971
  // Doc chat
7595
- { method: 'POST', path: '/api/doc-chat', desc: 'Minions-aware doc Q&A + editing via CC session', params: 'message, document, title?, filePath?, selection?, contentHash?', handler: handleDocChat },
7596
- { method: 'POST', path: '/api/doc-chat/stream', desc: 'Streaming doc chat — SSE with text chunks and tool progress', params: 'message, document, title?, filePath?, selection?, contentHash?', handler: handleDocChatStream },
7972
+ { method: 'POST', path: '/api/doc-chat', desc: 'Minions-aware doc Q&A + editing via CC session', params: 'message, document, title?, filePath?, selection?, contentHash?, transcript?', handler: handleDocChat },
7973
+ { method: 'POST', path: '/api/doc-chat/stream', desc: 'Streaming doc chat — SSE with text chunks and tool progress', params: 'message, document, title?, filePath?, selection?, contentHash?, transcript?', handler: handleDocChatStream },
7597
7974
 
7598
7975
  // Inbox
7599
7976
  { method: 'POST', path: '/api/inbox/persist', desc: 'Promote an inbox item to team notes', params: 'name', handler: handleInboxPersist },
@@ -7617,8 +7994,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7617
7994
  // Command Center
7618
7995
  { method: 'POST', path: '/api/command-center/new-session', desc: 'Clear active CC session', handler: handleCommandCenterNewSession },
7619
7996
  { method: 'POST', path: '/api/command-center/abort', desc: 'Abort an in-flight CC request for a tab', params: 'tabId?', handler: handleCommandCenterAbort },
7620
- { method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, sessionId?', handler: handleCommandCenter },
7621
- { method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message, tabId?', handler: handleCommandCenterStream },
7997
+ { method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, tabId?, sessionId?, transcript?', handler: handleCommandCenter },
7998
+ { method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message, tabId?, sessionId?, transcript?', handler: handleCommandCenterStream },
7622
7999
  { method: 'GET', path: '/api/cc-sessions', desc: 'List CC session metadata for all tabs', handler: handleCCSessionsList },
7623
8000
  { method: 'DELETE', path: /^\/api\/cc-sessions\/([\w-]+)$/, template: '/api/cc-sessions/:id', desc: 'Delete a CC session by tab ID', handler: handleCCSessionDelete },
7624
8001
 
@@ -8018,10 +8395,15 @@ module.exports = {
8018
8395
  _resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
8019
8396
  _collectArchivedWorkItems: collectArchivedWorkItems,
8020
8397
  _createPipelineFromAction: createPipelineFromAction,
8398
+ _setCcLocalApiInvokerForTest,
8399
+ _resetCcApiRoutesMetaForTest,
8400
+ _ccValidateLocalApiFallback,
8021
8401
  executeCCActions,
8402
+ executeDocChatActions,
8022
8403
  buildCCStatePreamble,
8023
8404
  _routesAsMeta,
8024
8405
  _buildTranscriptCarryover,
8406
+ _transcriptHasCarryoverContext,
8025
8407
  _ccRuntimeNeedsResumeCarryover,
8026
8408
  _joinCcPromptParts,
8027
8409
  _captureApiRoutesMeta,
@@ -93,6 +93,8 @@ When you ask CC to *do* something, it includes structured action blocks in its r
93
93
  | `remove-prd-item` | Remove a PRD item | "Remove P011 from the plan" |
94
94
  | `delete-work-item` | Delete a work item | "Delete work item W025" |
95
95
 
96
+ For endpoints without a named action, CC may emit a local API fallback action with `endpoint`, `method`, and `params`. The server only invokes safe local `/api/...` paths, validates the requested method against the route index when available, sends GET params as query strings, and rejects streaming/recursive CC/doc-chat endpoints.
97
+
96
98
  ## Error Handling
97
99
 
98
100
  - **Frontend timeout**: 10-minute `AbortSignal` on the fetch — prevents infinite "thinking" spinner
@@ -155,4 +157,3 @@ Frontend
155
157
  ## Command Bar
156
158
 
157
159
  The command bar at the top of the dashboard routes all input to the CC panel. Typing in the command bar opens the CC drawer and sends the message as a CC turn.
158
-
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-07T23:22:07.269Z"
4
+ "cachedAt": "2026-05-07T23:26:31.525Z"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1778",
3
+ "version": "0.1.1780",
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"
@@ -81,17 +81,17 @@ I'll dispatch dallas to fix that bug.
81
81
  ===ACTIONS===
82
82
  [{"type": "dispatch", "title": "Fix login bug", "workType": "fix", "agents": ["dallas"], "project": "MyApp", "description": "..."}]
83
83
 
84
- **Generic fallback:** For any action not listed below, include `"endpoint": "/api/..."` and `"params": {...}` to call the API directly. Example: `{"type": "custom-op", "endpoint": "/api/some/endpoint", "params": {"key": "value"}}`.
84
+ **Generic fallback:** For any action not listed below, include `"endpoint": "/api/..."`, `"method": "GET|POST|DELETE"`, and `"params": {...}` to call the API directly. Omit `method` only for POST endpoints. Example: `{"type": "custom-op", "endpoint": "/api/some/endpoint", "method": "POST", "params": {"key": "value"}}`.
85
85
 
86
86
  **Required fields per action type — server rejects with an error if missing:**
87
87
 
88
- - `dispatch` (and aliases: `fix`, `implement`, `explore`, `review`, `test`): `title` is REQUIRED. `description` recommended. `project` REQUIRED when multiple projects are configured (server returns the list of known names if you guess wrong). For agent hints emit either `agents: ["dallas"]` (array, preferred) or `agent: "dallas"` (string — auto-promoted server-side). Unknown agent names error.
88
+ - `dispatch`: `title` is REQUIRED. `description` recommended. `project` REQUIRED when multiple projects are configured (server returns the list of known names if you guess wrong). For agent hints emit either `agents: ["dallas"]` (array, preferred) or `agent: "dallas"` (string — auto-promoted server-side). Unknown agent names error. Always emit `"type":"dispatch"` for dispatch-like work and preserve the semantic intent in `workType` (`fix`, `implement`, `explore`, `review`, or `test`) instead of using those words as action types.
89
89
  - `build-and-test`: `pr` REQUIRED (number, ID, or URL).
90
90
  - `note`: `title` and `content` (or `description`) REQUIRED.
91
91
  - `knowledge`: `title`, `content`, and `category` REQUIRED. Valid categories: architecture, conventions, project-notes, build-reports, reviews.
92
92
 
93
93
  Core action types:
94
- - **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)
94
+ - **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 `workType:"implement"`.
95
95
  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` when fixing review comments/build failures on an existing PR), `review` (code review, NO PR), `test` (tests, PR if new), `verify` (merge/build/maintenance, NO PR)
96
96
  If the user wants a design/architecture artifact committed through a PR, dispatch `implement` or `docs` rather than `explore`.
97
97
  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"`.
@@ -143,7 +143,7 @@ Additional actions (all take `id` or `file` as primary key):
143
143
  - KB/Inbox: promote-to-kb (file, category), kb-sweep, toggle-kb-pin (key)
144
144
  - Plan lifecycle: revise-plan (file, feedback — dispatches agent to revise)
145
145
  - Pipeline: continue-pipeline (id — resume past wait stage)
146
- - Projects: add-project (localPath, name, repoHost)
146
+ - Projects: add-project (path or localPath, name, repoHost)
147
147
  - Engine: restart-engine, reset-settings
148
148
  - Other: unpin (title), link-pr (url, title, project, autoObserve), delete-pr (id, project), update-routing (content), file-bug (title, description, labels)
149
149
 
@@ -159,8 +159,8 @@ Terms like schedules, pipelines, agents, inbox, work items, plans, PRD, PRs, dis
159
159
  ## API & CLI Index (auto-injected)
160
160
  Your state preamble (delivered alongside this prompt at session start) carries an auto-generated **API Index** rendered from `dashboard.js` `ROUTES` and a **CLI Index** rendered from `engine/cli.js` `CLI_COMMAND_DOCS`. Both are single-source-of-truth — adding a new HTTP endpoint or CLI command auto-surfaces it in your preamble; do not memorize the named action shorthand list above as exhaustive.
161
161
 
162
- For a `POST /api/...` endpoint marked `generic-fallback` in the API Index that doesn't have a matching named action above, emit the generic fallback shape:
163
- `{"type":"<short-descriptor>","endpoint":"/api/...","params":{...}}`
164
- The action runner POSTs `params` as JSON; do not use the fallback for read-only GET routes, DELETE routes, or endpoints not marked generic-fallback.
162
+ For any safe local `/api/...` endpoint that doesn't have a matching named action above, emit the generic fallback shape:
163
+ `{"type":"<short-descriptor>","endpoint":"/api/...","method":"GET|POST|DELETE","params":{...}}`
164
+ The action runner enforces the endpoint method from the API index when available, sends GET params as query strings, sends POST/DELETE params as JSON, and rejects Command Center, doc-chat, bot, or streaming endpoints.
165
165
 
166
166
  For CLI commands (`minions <cmd>`), use Bash to invoke them when delegating would be heavier than just running the command.