@yemi33/minions 0.1.1779 → 0.1.1781
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 +11 -3
- package/dashboard/js/command-center.js +30 -9
- package/dashboard.js +326 -13
- package/docs/command-center.md +2 -1
- package/engine/cleanup.js +14 -5
- package/engine/copilot-models.json +1 -1
- package/engine/meeting.js +2 -8
- package/engine/preflight.js +3 -4
- package/engine/shared.js +25 -14
- package/engine.js +2 -0
- package/package.json +1 -1
- package/prompts/cc-system.md +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1781 (2026-05-07)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- plug Property-1 holes + consolidate isPathInside helper
|
|
7
|
+
|
|
8
|
+
## 0.1.1780 (2026-05-07)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- fix command center action parity (#2174)
|
|
12
|
+
|
|
13
|
+
## 0.1.1778 (2026-05-07)
|
|
4
14
|
|
|
5
15
|
### Features
|
|
6
|
-
- fix cc doc chat resume continuity (#2184)
|
|
7
|
-
- consolidate CC dispatch action type (#2183)
|
|
8
16
|
- harden dashboard state mutations (#2175)
|
|
9
17
|
|
|
10
18
|
## 0.1.1777 (2026-05-07)
|
|
@@ -980,8 +980,25 @@ function ccRetryLast(tabId, retryId) {
|
|
|
980
980
|
});
|
|
981
981
|
}
|
|
982
982
|
|
|
983
|
-
async function _ccFetch(url, body) {
|
|
984
|
-
|
|
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);
|
|
985
1002
|
if (!res.ok) {
|
|
986
1003
|
var d = await res.json().catch(function() { return {}; });
|
|
987
1004
|
var err = new Error(d.error || 'Request failed (' + res.status + ')');
|
|
@@ -1068,22 +1085,25 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1068
1085
|
if (notePageLink && !notePageLink.querySelector('.notif-badge')) { var noteCurPage = document.querySelector('.sidebar-link.active')?.getAttribute('data-page'); if (noteCurPage !== 'inbox') showNotifBadge(notePageLink); }
|
|
1069
1086
|
break;
|
|
1070
1087
|
}
|
|
1071
|
-
case 'pin':
|
|
1088
|
+
case 'pin':
|
|
1089
|
+
case 'pin-to-pinned': {
|
|
1072
1090
|
await _ccFetch('/api/pinned', { title: action.title, content: action.content || action.description, level: action.level || '' });
|
|
1073
1091
|
status.innerHTML = '📌 Pinned: <strong>' + escHtml(action.title) + '</strong> — visible to all agents';
|
|
1074
1092
|
status.style.color = 'var(--green)';
|
|
1075
1093
|
break;
|
|
1076
1094
|
}
|
|
1077
1095
|
case 'plan': {
|
|
1078
|
-
|
|
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 });
|
|
1079
1098
|
status.innerHTML = '✓ Plan queued: <strong>' + escHtml(action.title) + '</strong>';
|
|
1080
1099
|
status.style.color = 'var(--green)';
|
|
1081
1100
|
wakeEngine();
|
|
1082
1101
|
break;
|
|
1083
1102
|
}
|
|
1084
1103
|
case 'cancel': {
|
|
1085
|
-
|
|
1086
|
-
|
|
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 = '✓ Cancelled agent: <strong>' + escHtml(cancelAgent || action.task || action.cancelTask || '') + '</strong>';
|
|
1087
1107
|
status.style.color = 'var(--orange)';
|
|
1088
1108
|
break;
|
|
1089
1109
|
}
|
|
@@ -1475,8 +1495,9 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1475
1495
|
break;
|
|
1476
1496
|
}
|
|
1477
1497
|
case 'add-project': {
|
|
1478
|
-
|
|
1479
|
-
|
|
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 = '✓ Project added: <strong>' + escHtml(action.name || projectPath) + '</strong>';
|
|
1480
1501
|
status.style.color = 'var(--green)';
|
|
1481
1502
|
break;
|
|
1482
1503
|
}
|
|
@@ -1507,7 +1528,7 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1507
1528
|
default: {
|
|
1508
1529
|
// Generic fallback: if action has an `endpoint` field, call it directly (local API only)
|
|
1509
1530
|
if (action.endpoint && action.endpoint.startsWith('/api/') && !action.endpoint.includes('..') && !/\%2e/i.test(action.endpoint)) {
|
|
1510
|
-
var genRes = await _ccFetch(action.endpoint, action.params || {});
|
|
1531
|
+
var genRes = await _ccFetch(action.endpoint, action.params || {}, action.method || 'POST');
|
|
1511
1532
|
var genData = await genRes.json().catch(function() { return {}; });
|
|
1512
1533
|
status.innerHTML = '✓ ' + escHtml(action.type) + ': ' + escHtml(genData.message || genData.id || 'done');
|
|
1513
1534
|
status.style.color = 'var(--green)';
|
package/dashboard.js
CHANGED
|
@@ -1449,6 +1449,13 @@ try {
|
|
|
1449
1449
|
let _preambleCache = null;
|
|
1450
1450
|
let _preambleCacheTs = 0;
|
|
1451
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
|
+
];
|
|
1452
1459
|
|
|
1453
1460
|
// SoT for CC's runtime API index. Captured lazily on the first HTTP request
|
|
1454
1461
|
// because ROUTES is closed over inside the request handler. Subsequent
|
|
@@ -1569,13 +1576,20 @@ function _routesAsMeta(routes) {
|
|
|
1569
1576
|
|
|
1570
1577
|
function _captureApiRoutesMeta(routes) {
|
|
1571
1578
|
if (_ccApiRoutesMeta || !Array.isArray(routes)) return;
|
|
1572
|
-
_ccApiRoutesMeta =
|
|
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;
|
|
1573
1587
|
}
|
|
1574
1588
|
|
|
1575
1589
|
function _formatCcApiRoutesIndex() {
|
|
1576
1590
|
if (!Array.isArray(_ccApiRoutesMeta) || _ccApiRoutesMeta.length === 0) return '';
|
|
1577
1591
|
return _ccApiRoutesMeta
|
|
1578
|
-
.filter(r => r.path.startsWith('/api/'))
|
|
1592
|
+
.filter(r => r.path.startsWith('/api/') || r.path.startsWith('/^\\/api'))
|
|
1579
1593
|
.map(r => {
|
|
1580
1594
|
const params = r.params ? ` — params: ${r.params}` : '';
|
|
1581
1595
|
const flags = [
|
|
@@ -1635,8 +1649,8 @@ ${apiIndex || '(routes not yet captured — first request still pending)'}
|
|
|
1635
1649
|
### CLI Index (auto-generated from engine/cli.js CLI_COMMAND_DOCS — single source of truth)
|
|
1636
1650
|
${cliIndex || '(unavailable)'}
|
|
1637
1651
|
|
|
1638
|
-
For
|
|
1639
|
-
\`{"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":{...}}\`.` : '';
|
|
1640
1654
|
|
|
1641
1655
|
const result = `### Agents
|
|
1642
1656
|
${agents}
|
|
@@ -2232,6 +2246,292 @@ function _ccValidateAction(action) {
|
|
|
2232
2246
|
}
|
|
2233
2247
|
}
|
|
2234
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
|
+
|
|
2235
2535
|
async function executeCCActions(actions) {
|
|
2236
2536
|
const results = [];
|
|
2237
2537
|
for (const rawAction of actions) {
|
|
@@ -2496,10 +2796,16 @@ async function executeCCActions(actions) {
|
|
|
2496
2796
|
results.push({ type: 'resume-watch', id: action.id, ok: !!resumed });
|
|
2497
2797
|
break;
|
|
2498
2798
|
}
|
|
2499
|
-
default:
|
|
2500
|
-
|
|
2501
|
-
|
|
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
|
+
}
|
|
2502
2807
|
break;
|
|
2808
|
+
}
|
|
2503
2809
|
}
|
|
2504
2810
|
} catch (e) {
|
|
2505
2811
|
results.push({ type: action.type, error: e.message });
|
|
@@ -3059,7 +3365,7 @@ async function _retryDocChatAfterResumeFailure({ result, initialPass, freshSessi
|
|
|
3059
3365
|
function _buildDocChatErrorEnvelope(result) {
|
|
3060
3366
|
return {
|
|
3061
3367
|
code: result.code ?? null,
|
|
3062
|
-
stderr: (result.stderr || '').slice(-2048),
|
|
3368
|
+
stderr: String(result.stderr || '').slice(-2048),
|
|
3063
3369
|
errorClass: result.errorClass || null,
|
|
3064
3370
|
errorMessage: result.errorMessage || null,
|
|
3065
3371
|
runtime: result.runtime || null,
|
|
@@ -4173,7 +4479,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4173
4479
|
id, title: body.title, type: 'plan',
|
|
4174
4480
|
priority: body.priority || 'high', description: body.description || '',
|
|
4175
4481
|
status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
|
|
4176
|
-
branchStrategy: body.branch_strategy || 'parallel',
|
|
4482
|
+
branchStrategy: body.branch_strategy || body.branchStrategy || 'parallel',
|
|
4177
4483
|
};
|
|
4178
4484
|
if (body.project) item.project = body.project;
|
|
4179
4485
|
if (body.agent) item.agent = body.agent;
|
|
@@ -4376,14 +4682,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
4376
4682
|
async function handleAgentsCancel(req, res) {
|
|
4377
4683
|
try {
|
|
4378
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' });
|
|
4379
4688
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
4380
4689
|
const dispatch = safeJsonObj(dispatchPath);
|
|
4381
4690
|
const active = dispatch.active || [];
|
|
4382
4691
|
const cancelled = [];
|
|
4383
4692
|
|
|
4384
4693
|
for (const d of active) {
|
|
4385
|
-
const matchAgent =
|
|
4386
|
-
const matchTask =
|
|
4694
|
+
const matchAgent = requestedAgent && d.agent === requestedAgent;
|
|
4695
|
+
const matchTask = requestedTask && (d.task || '').toLowerCase().includes(String(requestedTask).toLowerCase());
|
|
4387
4696
|
if (!matchAgent && !matchTask) continue;
|
|
4388
4697
|
|
|
4389
4698
|
// Kill agent process
|
|
@@ -7429,7 +7738,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7429
7738
|
{ method: 'POST', path: '/api/notes-save', desc: 'Save edited notes.md content', params: 'content, file?', handler: handleNotesSave },
|
|
7430
7739
|
|
|
7431
7740
|
// Plans
|
|
7432
|
-
{ 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 },
|
|
7433
7742
|
{ method: 'GET', path: '/api/plans', desc: 'List plan files (.md drafts + .json PRDs)', handler: handlePlansList },
|
|
7434
7743
|
{ method: 'POST', path: '/api/plans/trigger-verify', desc: 'Manually trigger verification for a completed plan', params: 'file', handler: handlePlansTriggerVerify },
|
|
7435
7744
|
{ method: 'POST', path: '/api/plans/approve', desc: 'Approve a plan for execution', params: 'file, approvedBy?', handler: handlePlansApprove },
|
|
@@ -7614,7 +7923,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7614
7923
|
inboxCount: steering.listUnreadSteeringMessages(agentId).length,
|
|
7615
7924
|
});
|
|
7616
7925
|
}},
|
|
7617
|
-
{ 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 },
|
|
7618
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 },
|
|
7619
7928
|
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/, template: '/api/agent/:id/live-stream', desc: 'SSE real-time live output streaming', handler: handleAgentLiveStream },
|
|
7620
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 },
|
|
@@ -8086,7 +8395,11 @@ module.exports = {
|
|
|
8086
8395
|
_resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
|
|
8087
8396
|
_collectArchivedWorkItems: collectArchivedWorkItems,
|
|
8088
8397
|
_createPipelineFromAction: createPipelineFromAction,
|
|
8398
|
+
_setCcLocalApiInvokerForTest,
|
|
8399
|
+
_resetCcApiRoutesMetaForTest,
|
|
8400
|
+
_ccValidateLocalApiFallback,
|
|
8089
8401
|
executeCCActions,
|
|
8402
|
+
executeDocChatActions,
|
|
8090
8403
|
buildCCStatePreamble,
|
|
8091
8404
|
_routesAsMeta,
|
|
8092
8405
|
_buildTranscriptCarryover,
|
package/docs/command-center.md
CHANGED
|
@@ -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
|
-
|
package/engine/cleanup.js
CHANGED
|
@@ -369,13 +369,23 @@ async function runCleanup(config, verbose = false) {
|
|
|
369
369
|
// 2b. Detect git worktrees registered inside any linked project's working tree.
|
|
370
370
|
// Nested worktrees cause glob/grep tools running with cwd=projectRoot to match
|
|
371
371
|
// BOTH copies of every file; a single Edit/MultiEdit then writes the same
|
|
372
|
-
// change to both locations, producing
|
|
372
|
+
// change to both locations, producing mirror-write leaks.
|
|
373
373
|
// We only WARN here — removing someone else's worktree without consent could
|
|
374
374
|
// destroy in-flight work. The operator runs `git worktree remove <path>`.
|
|
375
375
|
cleaned.nestedWorktrees = 0;
|
|
376
|
+
const _scannedRoots = new Set(); // dedup projects sharing localPath
|
|
376
377
|
for (const project of projects) {
|
|
377
|
-
|
|
378
|
-
|
|
378
|
+
if (!project.localPath) continue;
|
|
379
|
+
const root = path.resolve(project.localPath);
|
|
380
|
+
if (_scannedRoots.has(root)) continue;
|
|
381
|
+
_scannedRoots.add(root);
|
|
382
|
+
if (!fs.existsSync(root)) {
|
|
383
|
+
// Configured project whose checkout has been moved or deleted — surface
|
|
384
|
+
// it so the operator knows their config is out of sync. A missing root
|
|
385
|
+
// means we cannot scan it, leaving nested worktrees there undetectable.
|
|
386
|
+
log('warn', `Project "${project.name || root}" has localPath "${root}" which does not exist on disk — skipping worktree scan`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
379
389
|
let raw;
|
|
380
390
|
try {
|
|
381
391
|
raw = String(shared.execSilent('git worktree list --porcelain', { cwd: root, timeout: 10000, windowsHide: true }) || '');
|
|
@@ -384,8 +394,7 @@ async function runCleanup(config, verbose = false) {
|
|
|
384
394
|
if (!line.startsWith('worktree ')) continue;
|
|
385
395
|
const wt = line.slice('worktree '.length).trim();
|
|
386
396
|
if (!wt) continue;
|
|
387
|
-
if (
|
|
388
|
-
if (!shared.isPathInsideOrEqual(wt, root)) continue;
|
|
397
|
+
if (!shared.isPathInside(wt, root)) continue; // strict — main worktree (equal path) is expected, descendants are the leak
|
|
389
398
|
cleaned.nestedWorktrees++;
|
|
390
399
|
log('warn', `Nested worktree in project "${project.name || root}": "${wt}" is inside "${root}". This causes glob tools to match both copies and produces mirror writes. Run: git worktree remove "${wt}"`);
|
|
391
400
|
}
|
package/engine/meeting.js
CHANGED
|
@@ -155,17 +155,12 @@ function getStructuredNoteArtifacts(structuredCompletion) {
|
|
|
155
155
|
);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function isPathInside(parent, child) {
|
|
159
|
-
const rel = path.relative(parent, child);
|
|
160
|
-
return Boolean(rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
158
|
function resolveMeetingNoteArtifactPath(artifactPath) {
|
|
164
159
|
const raw = String(artifactPath || '').trim();
|
|
165
160
|
if (!raw || raw.includes('\0')) return null;
|
|
166
161
|
const resolved = path.resolve(path.isAbsolute(raw) ? raw : path.join(shared.MINIONS_DIR, raw));
|
|
167
162
|
const root = path.resolve(MEETING_NOTE_ARTIFACT_ROOT);
|
|
168
|
-
if (!isPathInside(
|
|
163
|
+
if (!shared.isPathInside(resolved, root)) return null;
|
|
169
164
|
if (path.extname(resolved).toLowerCase() !== '.md') return null;
|
|
170
165
|
return resolved;
|
|
171
166
|
}
|
|
@@ -179,7 +174,7 @@ function readMeetingNoteArtifact(artifactPath) {
|
|
|
179
174
|
try {
|
|
180
175
|
const realRoot = fs.realpathSync(MEETING_NOTE_ARTIFACT_ROOT);
|
|
181
176
|
const realPath = fs.realpathSync(resolved);
|
|
182
|
-
if (!isPathInside(
|
|
177
|
+
if (!shared.isPathInside(realPath, realRoot)) {
|
|
183
178
|
log('warn', `Ignoring meeting note artifact outside notes/inbox: ${artifactPath}`);
|
|
184
179
|
return '';
|
|
185
180
|
}
|
|
@@ -914,7 +909,6 @@ module.exports = {
|
|
|
914
909
|
// exported for testing — engine code MUST go through
|
|
915
910
|
// getMeetings/discoverMeetingWork/collectMeetingFindings/checkMeetingTimeouts,
|
|
916
911
|
// never these helpers directly.
|
|
917
|
-
isPathInside,
|
|
918
912
|
resolveMeetingNoteArtifactPath,
|
|
919
913
|
cleanMeetingSummaryText,
|
|
920
914
|
splitMeetingSummaryFragments,
|
package/engine/preflight.js
CHANGED
|
@@ -275,10 +275,9 @@ function runPreflight(opts = {}) {
|
|
|
275
275
|
// 5. worktreeRoot config check — for every linked project, verify that the
|
|
276
276
|
// configured engine.worktreeRoot resolves OUTSIDE the project's
|
|
277
277
|
// localPath. A nested worktreeRoot causes glob/grep to match both
|
|
278
|
-
// copies of every file, producing silent mirror writes
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
// is the second line of defense.
|
|
278
|
+
// copies of every file, producing silent mirror writes. Hard-fail at
|
|
279
|
+
// preflight so the operator sees it before any agent dispatch — the
|
|
280
|
+
// runtime guard in spawnAgent is the second line of defense.
|
|
282
281
|
try {
|
|
283
282
|
const path = require('path');
|
|
284
283
|
const projects = shared.getProjects(opts.config) || [];
|
package/engine/shared.js
CHANGED
|
@@ -2143,30 +2143,40 @@ function buildWorktreeDirName({
|
|
|
2143
2143
|
}
|
|
2144
2144
|
|
|
2145
2145
|
/**
|
|
2146
|
-
* True when `childPath` is
|
|
2147
|
-
*
|
|
2148
|
-
*
|
|
2149
|
-
*
|
|
2150
|
-
* escapes via `..`.
|
|
2151
|
-
*
|
|
2152
|
-
* Why this helper exists: a git worktree placed inside the parent repo's
|
|
2153
|
-
* working tree causes glob/grep tools running with `cwd = projectRoot` to
|
|
2154
|
-
* match BOTH copies of every file. A single Edit/MultiEdit then writes the
|
|
2155
|
-
* same change to both locations, producing the "mirror dirty file" pattern.
|
|
2156
|
-
* Worktrees must always be siblings/cousins of the project root, never
|
|
2157
|
-
* descendants.
|
|
2146
|
+
* True when `childPath` is strictly nested within `parentPath` (descendant,
|
|
2147
|
+
* NOT the same path). Cross-platform via `path.relative`; resilient to mixed
|
|
2148
|
+
* separators. Returns false for equal paths, different roots/drives, or
|
|
2149
|
+
* `childPath` escapes via `..`.
|
|
2158
2150
|
*/
|
|
2159
|
-
function
|
|
2151
|
+
function isPathInside(childPath, parentPath) {
|
|
2160
2152
|
if (!childPath || !parentPath) return false;
|
|
2161
2153
|
const childAbs = path.resolve(String(childPath));
|
|
2162
2154
|
const parentAbs = path.resolve(String(parentPath));
|
|
2163
2155
|
const rel = path.relative(parentAbs, childAbs);
|
|
2164
|
-
if (rel === '') return
|
|
2156
|
+
if (rel === '') return false;
|
|
2165
2157
|
if (rel.startsWith('..')) return false;
|
|
2166
2158
|
if (path.isAbsolute(rel)) return false;
|
|
2167
2159
|
return true;
|
|
2168
2160
|
}
|
|
2169
2161
|
|
|
2162
|
+
/**
|
|
2163
|
+
* Same as `isPathInside` but ALSO returns true when paths are equal.
|
|
2164
|
+
*
|
|
2165
|
+
* Why this helper exists: a git worktree placed at — or inside — the parent
|
|
2166
|
+
* repo's working tree causes glob/grep tools running with `cwd = projectRoot`
|
|
2167
|
+
* to match BOTH copies of every file. A single Edit/MultiEdit then writes
|
|
2168
|
+
* the same change to both locations, producing the "mirror dirty file"
|
|
2169
|
+
* pattern. Worktrees must always be siblings/cousins of the project root,
|
|
2170
|
+
* never the root itself nor a descendant.
|
|
2171
|
+
*/
|
|
2172
|
+
function isPathInsideOrEqual(childPath, parentPath) {
|
|
2173
|
+
if (!childPath || !parentPath) return false;
|
|
2174
|
+
const childAbs = path.resolve(String(childPath));
|
|
2175
|
+
const parentAbs = path.resolve(String(parentPath));
|
|
2176
|
+
if (childAbs === parentAbs) return true;
|
|
2177
|
+
return isPathInside(childAbs, parentAbs);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2170
2180
|
/**
|
|
2171
2181
|
* Throws when `worktreePath` would land inside (or equal) `projectRoot`.
|
|
2172
2182
|
* Called by the engine spawn path before `git worktree add`, and by the
|
|
@@ -3257,6 +3267,7 @@ module.exports = {
|
|
|
3257
3267
|
sanitizePath,
|
|
3258
3268
|
sanitizeBranch,
|
|
3259
3269
|
buildWorktreeDirName, // exported for testing
|
|
3270
|
+
isPathInside,
|
|
3260
3271
|
isPathInsideOrEqual,
|
|
3261
3272
|
assertWorktreeOutsideProject,
|
|
3262
3273
|
isLiveCommandCenterPath,
|
package/engine.js
CHANGED
|
@@ -666,6 +666,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
666
666
|
if (eShared.message?.includes('already used by worktree') || eShared.message?.includes('already checked out')) {
|
|
667
667
|
const existingWtPath = await findExistingWorktree(rootDir, branchName);
|
|
668
668
|
if (existingWtPath && fs.existsSync(existingWtPath)) {
|
|
669
|
+
shared.assertWorktreeOutsideProject(existingWtPath, rootDir);
|
|
669
670
|
log('info', `Shared branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
670
671
|
worktreePath = existingWtPath;
|
|
671
672
|
} else { throw eShared; }
|
|
@@ -723,6 +724,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
723
724
|
log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
|
|
724
725
|
throw e2;
|
|
725
726
|
}
|
|
727
|
+
shared.assertWorktreeOutsideProject(existingWtPath, rootDir);
|
|
726
728
|
log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
727
729
|
worktreePath = existingWtPath;
|
|
728
730
|
} else if (existingWtPath && !fs.existsSync(existingWtPath)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1781",
|
|
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/prompts/cc-system.md
CHANGED
|
@@ -81,7 +81,7 @@ 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
|
|
|
@@ -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
|
|
163
|
-
`{"type":"<short-descriptor>","endpoint":"/api/...","params":{...}}`
|
|
164
|
-
The action runner
|
|
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.
|