@yemi33/minions 0.1.1773 → 0.1.1775
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 +13 -4
- package/bin/minions.js +3 -21
- package/dashboard.js +207 -63
- package/engine/copilot-models.json +1 -1
- package/engine/runtimes/copilot.js +59 -7
- package/engine/shared.js +52 -1
- package/engine.js +9 -6
- package/package.json +1 -1
- package/prompts/cc-system.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1775 (2026-05-07)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- Improve dashboard API route metadata
|
|
7
|
+
- suppress stale doc-chat model errors
|
|
8
|
+
|
|
9
|
+
### Fixes
|
|
10
|
+
- yemi33/minions#2168
|
|
11
|
+
- honor central plan work item project
|
|
12
|
+
- restore canonical-home shared helpers
|
|
13
|
+
- yemi33/minions#2170
|
|
14
|
+
|
|
15
|
+
## 0.1.1772 (2026-05-07)
|
|
4
16
|
|
|
5
17
|
### Features
|
|
6
|
-
- fix doc chat sticky scroll (#2176)
|
|
7
|
-
- fix live output route and CLI docs (#2172)
|
|
8
|
-
- make API project resolution strict (#2169)
|
|
9
18
|
- fix copilot cc resume context (#2166)
|
|
10
19
|
|
|
11
20
|
## 0.1.1771 (2026-05-07)
|
package/bin/minions.js
CHANGED
|
@@ -39,6 +39,7 @@ const os = require('os');
|
|
|
39
39
|
const { spawn, spawnSync, execSync } = require('child_process');
|
|
40
40
|
|
|
41
41
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
42
|
+
const shared = require(path.join(PKG_ROOT, 'engine', 'shared'));
|
|
42
43
|
const DASH_PORT = 7331;
|
|
43
44
|
const DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS = 45000;
|
|
44
45
|
|
|
@@ -152,8 +153,6 @@ function spawnDashboard(suppressOpen) {
|
|
|
152
153
|
return proc;
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
const DEFAULT_MINIONS_HOME = path.join(os.homedir(), '.minions');
|
|
156
|
-
const ROOT_POINTER_PATH = path.join(os.homedir(), '.minions-root');
|
|
157
156
|
const LEGACY_DEFAULT_SQUAD_HOME = path.join(os.homedir(), '.squad');
|
|
158
157
|
const LEGACY_ROOT_POINTER_PATH = path.join(os.homedir(), '.squad-root');
|
|
159
158
|
|
|
@@ -171,13 +170,6 @@ function isLegacyInstalledRoot(dir) {
|
|
|
171
170
|
fs.existsSync(path.join(dir, 'squad.js'));
|
|
172
171
|
}
|
|
173
172
|
|
|
174
|
-
function readRootPointer() {
|
|
175
|
-
try {
|
|
176
|
-
const p = fs.readFileSync(ROOT_POINTER_PATH, 'utf8').trim();
|
|
177
|
-
return p ? path.resolve(p) : null;
|
|
178
|
-
} catch { return null; }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
173
|
function readLegacyRootPointer() {
|
|
182
174
|
try {
|
|
183
175
|
const p = fs.readFileSync(LEGACY_ROOT_POINTER_PATH, 'utf8').trim();
|
|
@@ -186,7 +178,7 @@ function readLegacyRootPointer() {
|
|
|
186
178
|
}
|
|
187
179
|
|
|
188
180
|
function saveRootPointer(root) {
|
|
189
|
-
|
|
181
|
+
shared.saveMinionsRootPointer(root);
|
|
190
182
|
}
|
|
191
183
|
|
|
192
184
|
function findLegacySquadRoot() {
|
|
@@ -254,17 +246,7 @@ function migrateLegacyInstallIfNeeded(targetHome) {
|
|
|
254
246
|
}
|
|
255
247
|
|
|
256
248
|
function resolveMinionsHome(forInit = false) {
|
|
257
|
-
|
|
258
|
-
if (envHome) return envHome;
|
|
259
|
-
|
|
260
|
-
if (forInit) return DEFAULT_MINIONS_HOME;
|
|
261
|
-
|
|
262
|
-
const pointerRoot = readRootPointer();
|
|
263
|
-
if (isInstalledRoot(pointerRoot)) return pointerRoot;
|
|
264
|
-
|
|
265
|
-
if (isInstalledRoot(DEFAULT_MINIONS_HOME)) return DEFAULT_MINIONS_HOME;
|
|
266
|
-
|
|
267
|
-
return DEFAULT_MINIONS_HOME;
|
|
249
|
+
return shared.resolveMinionsHome(forInit);
|
|
268
250
|
}
|
|
269
251
|
|
|
270
252
|
const [cmd, ...rest] = process.argv.slice(2);
|
package/dashboard.js
CHANGED
|
@@ -1334,13 +1334,116 @@ const PREAMBLE_TTL = 30000; // 30s — longer TTL since preamble is lightweight
|
|
|
1334
1334
|
// captures no-op via the truthy guard.
|
|
1335
1335
|
let _ccApiRoutesMeta = null;
|
|
1336
1336
|
|
|
1337
|
+
function _routePathForMeta(route) {
|
|
1338
|
+
if (!route) return '';
|
|
1339
|
+
if (typeof route.path === 'string') return route.path;
|
|
1340
|
+
return route.template || route.pathTemplate || String(route.path);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function _parseRouteParamHint(paramHint, method) {
|
|
1344
|
+
if (!paramHint || typeof paramHint !== 'string') return [];
|
|
1345
|
+
const paramLocation = ['GET', 'HEAD', 'DELETE'].includes(String(method || '').toUpperCase()) ? 'query' : 'body';
|
|
1346
|
+
const parts = [];
|
|
1347
|
+
let current = '';
|
|
1348
|
+
let parenDepth = 0;
|
|
1349
|
+
for (const ch of paramHint) {
|
|
1350
|
+
if (ch === '(') parenDepth++;
|
|
1351
|
+
if (ch === ')' && parenDepth > 0) parenDepth--;
|
|
1352
|
+
if (ch === ',' && parenDepth === 0) {
|
|
1353
|
+
parts.push(current);
|
|
1354
|
+
current = '';
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
current += ch;
|
|
1358
|
+
}
|
|
1359
|
+
if (current) parts.push(current);
|
|
1360
|
+
return parts
|
|
1361
|
+
.map(part => {
|
|
1362
|
+
const raw = part.trim();
|
|
1363
|
+
if (!raw) return null;
|
|
1364
|
+
let text = raw;
|
|
1365
|
+
let description = '';
|
|
1366
|
+
text = text.replace(/\(([^)]*)\)/g, (_, desc) => {
|
|
1367
|
+
description = desc.trim();
|
|
1368
|
+
return '';
|
|
1369
|
+
}).trim();
|
|
1370
|
+
const required = !text.includes('?');
|
|
1371
|
+
const array = text.includes('[]');
|
|
1372
|
+
const name = text
|
|
1373
|
+
.replace(/\?/g, '')
|
|
1374
|
+
.replace(/\[\]/g, '')
|
|
1375
|
+
.trim()
|
|
1376
|
+
.split(/\s+/)[0];
|
|
1377
|
+
if (!name) return null;
|
|
1378
|
+
return {
|
|
1379
|
+
name,
|
|
1380
|
+
in: paramLocation,
|
|
1381
|
+
required,
|
|
1382
|
+
...(description ? { description } : {}),
|
|
1383
|
+
...(array ? { type: 'array' } : {}),
|
|
1384
|
+
};
|
|
1385
|
+
})
|
|
1386
|
+
.filter(Boolean);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function _routePathParams(pathTemplate) {
|
|
1390
|
+
if (!pathTemplate || typeof pathTemplate !== 'string') return [];
|
|
1391
|
+
const params = [];
|
|
1392
|
+
for (const match of pathTemplate.matchAll(/:([A-Za-z][A-Za-z0-9_]*)/g)) {
|
|
1393
|
+
params.push({ name: match[1], in: 'path', required: true });
|
|
1394
|
+
}
|
|
1395
|
+
return params;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function _routeParametersForMeta(route, pathTemplate) {
|
|
1399
|
+
const seen = new Set();
|
|
1400
|
+
const params = [..._routePathParams(pathTemplate), ..._parseRouteParamHint(route.params, route.method)];
|
|
1401
|
+
return params.filter(param => {
|
|
1402
|
+
const key = `${param.in}:${param.name}`;
|
|
1403
|
+
if (seen.has(key)) return false;
|
|
1404
|
+
seen.add(key);
|
|
1405
|
+
return true;
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function _routeReadOnly(route) {
|
|
1410
|
+
if (typeof route.readOnly === 'boolean') return route.readOnly;
|
|
1411
|
+
return ['GET', 'HEAD', 'OPTIONS'].includes(String(route.method || '').toUpperCase());
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function _routeDestructive(route, pathTemplate, readOnly) {
|
|
1415
|
+
if (typeof route.destructive === 'boolean') return route.destructive;
|
|
1416
|
+
if (readOnly) return false;
|
|
1417
|
+
if (String(route.method || '').toUpperCase() === 'DELETE') return true;
|
|
1418
|
+
const haystack = `${pathTemplate || ''} ${route.desc || ''}`.toLowerCase();
|
|
1419
|
+
return /\b(delete|remove|cancel|kill|reset|restart|archive|unarchive|abort|purge|reject|pause|unlink|clear)\b/.test(haystack);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function _routeGenericFallback(route, pathTemplate) {
|
|
1423
|
+
if (typeof route.genericFallback === 'boolean') return route.genericFallback;
|
|
1424
|
+
if (!pathTemplate || !pathTemplate.startsWith('/api/')) return false;
|
|
1425
|
+
if (String(route.method || '').toUpperCase() !== 'POST') return false;
|
|
1426
|
+
const haystack = `${pathTemplate} ${route.desc || ''}`.toLowerCase();
|
|
1427
|
+
if (haystack.includes('sse') || /\bstreaming\b/.test(haystack) || /\/stream(?:$|[/?#])/.test(pathTemplate)) return false;
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1337
1431
|
function _routesAsMeta(routes) {
|
|
1338
|
-
return routes.map(r =>
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1432
|
+
return routes.map(r => {
|
|
1433
|
+
const pathTemplate = _routePathForMeta(r);
|
|
1434
|
+
const readOnly = _routeReadOnly(r);
|
|
1435
|
+
const destructive = _routeDestructive(r, pathTemplate, readOnly);
|
|
1436
|
+
return {
|
|
1437
|
+
method: r.method,
|
|
1438
|
+
path: pathTemplate,
|
|
1439
|
+
desc: r.desc || '',
|
|
1440
|
+
params: r.params || null,
|
|
1441
|
+
parameters: _routeParametersForMeta(r, pathTemplate),
|
|
1442
|
+
readOnly,
|
|
1443
|
+
destructive,
|
|
1444
|
+
genericFallback: _routeGenericFallback(r, pathTemplate),
|
|
1445
|
+
};
|
|
1446
|
+
});
|
|
1344
1447
|
}
|
|
1345
1448
|
|
|
1346
1449
|
function _captureApiRoutesMeta(routes) {
|
|
@@ -1354,7 +1457,13 @@ function _formatCcApiRoutesIndex() {
|
|
|
1354
1457
|
.filter(r => r.path.startsWith('/api/'))
|
|
1355
1458
|
.map(r => {
|
|
1356
1459
|
const params = r.params ? ` — params: ${r.params}` : '';
|
|
1357
|
-
|
|
1460
|
+
const flags = [
|
|
1461
|
+
r.readOnly ? 'read-only' : null,
|
|
1462
|
+
r.destructive ? 'destructive' : null,
|
|
1463
|
+
r.genericFallback ? 'generic-fallback' : null,
|
|
1464
|
+
].filter(Boolean);
|
|
1465
|
+
const flagText = flags.length ? ` — flags: ${flags.join(', ')}` : '';
|
|
1466
|
+
return `- \`${r.method} ${r.path}\` — ${r.desc}${params}${flagText}`;
|
|
1358
1467
|
})
|
|
1359
1468
|
.join('\n');
|
|
1360
1469
|
}
|
|
@@ -1405,7 +1514,7 @@ ${apiIndex || '(routes not yet captured — first request still pending)'}
|
|
|
1405
1514
|
### CLI Index (auto-generated from engine/cli.js CLI_COMMAND_DOCS — single source of truth)
|
|
1406
1515
|
${cliIndex || '(unavailable)'}
|
|
1407
1516
|
|
|
1408
|
-
For
|
|
1517
|
+
For \`POST /api/...\` endpoints marked \`generic-fallback\` and not covered by a named CC action, use the generic fallback:
|
|
1409
1518
|
\`{"type":"<descriptive>","endpoint":"/api/...","params":{...}}\`.` : '';
|
|
1410
1519
|
|
|
1411
1520
|
const result = `### Agents
|
|
@@ -2721,23 +2830,19 @@ function _formatDocChatContext({ document, title, filePath, selection, canEdit,
|
|
|
2721
2830
|
return context;
|
|
2722
2831
|
}
|
|
2723
2832
|
|
|
2724
|
-
// Map
|
|
2833
|
+
// Map runtime failures to user-facing messages.
|
|
2725
2834
|
// sessionPreserved=true means ccCall preserved the session — user can retry immediately.
|
|
2726
2835
|
// toolUses=[] from result.toolUses lets the message warn that tools may have already
|
|
2727
2836
|
// modified files/state before the failure — the user shouldn't assume nothing happened.
|
|
2728
|
-
// errorMessage is the runtime adapter's own
|
|
2729
|
-
// authoritative
|
|
2730
|
-
// dashboard falls back to generic copy only when the adapter didn't supply one.
|
|
2837
|
+
// errorMessage is the runtime adapter's own text from parseError; when present it
|
|
2838
|
+
// is authoritative and is shown directly rather than replaced with dashboard copy.
|
|
2731
2839
|
function _docChatErrorMessage(errorClass, sessionPreserved = false, toolUses = [], errorMessage = null) {
|
|
2732
2840
|
const tools = Array.isArray(toolUses) ? toolUses : [];
|
|
2733
2841
|
const toolHint = tools.length > 0
|
|
2734
2842
|
? ` (${tools.length} tool${tools.length === 1 ? '' : 's'} ran before the failure: ${tools.slice(0, 5).map(t => t.name).join(', ')}${tools.length > 5 ? '…' : ''} — files or state may have been modified.)`
|
|
2735
2843
|
: '';
|
|
2736
|
-
|
|
2737
|
-
if (
|
|
2738
|
-
if (errorClass === 'budget-exceeded') return (errorMessage || 'Runtime budget exceeded — check your account or quota.') + toolHint;
|
|
2739
|
-
if (errorClass === 'crash') return (errorMessage || 'Runtime crashed unexpectedly. Try again.') + toolHint;
|
|
2740
|
-
if (errorClass === 'unknown-model') return (errorMessage || 'Configured model is not valid for the active runtime. Update engine.ccModel or engine.defaultModel.') + toolHint;
|
|
2844
|
+
const directError = typeof errorMessage === 'string' ? errorMessage.trim() : '';
|
|
2845
|
+
if (directError) return directError + toolHint;
|
|
2741
2846
|
if (sessionPreserved) return 'Temporary connection issue — your conversation is intact, send your message again.' + toolHint;
|
|
2742
2847
|
if (tools.length > 0) return 'The agent stopped responding before producing a final answer.' + toolHint;
|
|
2743
2848
|
return 'Failed to process request. Try again.';
|
|
@@ -2746,14 +2851,26 @@ function _docChatErrorMessage(errorClass, sessionPreserved = false, toolUses = [
|
|
|
2746
2851
|
// Secondary note rendered alongside a recovered partial answer — distinct from the
|
|
2747
2852
|
// hard-failure message because the answer/actions/document-edit DID land. The user
|
|
2748
2853
|
// just needs to know the run wasn't clean.
|
|
2749
|
-
function _docChatPartialWarning(errorClass) {
|
|
2750
|
-
|
|
2751
|
-
if (
|
|
2752
|
-
if (errorClass === 'budget-exceeded') return 'Note: runtime budget exceeded — answer recovered, but further calls may fail.';
|
|
2753
|
-
if (errorClass === 'crash') return 'Note: runtime crashed before clean exit, but a complete response was recovered.';
|
|
2854
|
+
function _docChatPartialWarning(errorClass, errorMessage = null) {
|
|
2855
|
+
const directError = typeof errorMessage === 'string' ? errorMessage.trim() : '';
|
|
2856
|
+
if (directError) return `Note: ${directError}`;
|
|
2754
2857
|
return 'Note: the agent exited unexpectedly. A complete response was recovered — verify any saved files or dispatched actions.';
|
|
2755
2858
|
}
|
|
2756
2859
|
|
|
2860
|
+
function _docChatResultHasVisibleError(result) {
|
|
2861
|
+
if (!result) return false;
|
|
2862
|
+
if (result.errorClass) return true;
|
|
2863
|
+
if (typeof result.errorMessage === 'string' && result.errorMessage.trim()) return true;
|
|
2864
|
+
// stderr without a classified/runtime error is often CLI diagnostics; hard
|
|
2865
|
+
// failures with no answer still surface stderr through _docChatFailureResponse.
|
|
2866
|
+
return false;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
function _docChatResultLooksSuccessful(result) {
|
|
2870
|
+
if (!result || !result.text) return false;
|
|
2871
|
+
return result.code === 0 || !_docChatResultHasVisibleError(result);
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2757
2874
|
// Build the doc-chat extraContext for a single ccCall pass — refreshed on retry
|
|
2758
2875
|
// so a fresh-session retry includes the full document instead of relying on the
|
|
2759
2876
|
// dead session's prior turn for context.
|
|
@@ -2830,7 +2947,7 @@ function _recoverPartialDocChatResponse(result, sessionKey) {
|
|
|
2830
2947
|
return {
|
|
2831
2948
|
...parsed,
|
|
2832
2949
|
partial: true,
|
|
2833
|
-
warning: _docChatPartialWarning(result.errorClass),
|
|
2950
|
+
warning: _docChatPartialWarning(result.errorClass, result.errorMessage || result.stderr || null),
|
|
2834
2951
|
toolUses: Array.isArray(result.toolUses) ? result.toolUses : [],
|
|
2835
2952
|
// Recovery path still attaches the raw runtime failure — the answer landed
|
|
2836
2953
|
// despite a non-zero exit; users still benefit from seeing why.
|
|
@@ -2838,6 +2955,41 @@ function _recoverPartialDocChatResponse(result, sessionKey) {
|
|
|
2838
2955
|
};
|
|
2839
2956
|
}
|
|
2840
2957
|
|
|
2958
|
+
function _shouldSuppressDocChatPostPatchError(ccError, finalize) {
|
|
2959
|
+
if (!finalize || finalize.edited !== true) return false;
|
|
2960
|
+
if (!ccError || ccError.errorClass !== 'unknown-model') return false;
|
|
2961
|
+
return String(ccError.runtime || '').toLowerCase() === 'copilot';
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
function _buildDocChatResponsePayload({
|
|
2965
|
+
answer,
|
|
2966
|
+
actions,
|
|
2967
|
+
actionResults,
|
|
2968
|
+
actionParseError,
|
|
2969
|
+
ccError,
|
|
2970
|
+
partial,
|
|
2971
|
+
warning,
|
|
2972
|
+
toolUses,
|
|
2973
|
+
finalize,
|
|
2974
|
+
} = {}) {
|
|
2975
|
+
const final = finalize || { edited: false, content: null, answerSuffix: '' };
|
|
2976
|
+
const suppressPostPatchError = _shouldSuppressDocChatPostPatchError(ccError, final);
|
|
2977
|
+
const visibleAnswer = suppressPostPatchError && !partial ? 'Updated the document.' : answer;
|
|
2978
|
+
const finalAnswer = final.answerSuffix ? visibleAnswer + final.answerSuffix : visibleAnswer;
|
|
2979
|
+
return {
|
|
2980
|
+
ok: !(ccError && !suppressPostPatchError),
|
|
2981
|
+
answer: finalAnswer,
|
|
2982
|
+
actions,
|
|
2983
|
+
...(actionResults ? { actionResults } : {}),
|
|
2984
|
+
...(actionParseError ? { actionParseError } : {}),
|
|
2985
|
+
...(ccError && !suppressPostPatchError ? { error: ccError } : {}),
|
|
2986
|
+
...(partial && !suppressPostPatchError ? { partial: true, warning } : {}),
|
|
2987
|
+
...(Array.isArray(toolUses) && toolUses.length ? { toolUses } : {}),
|
|
2988
|
+
edited: final.edited,
|
|
2989
|
+
...(final.edited && final.content !== null ? { content: final.content } : {}),
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2841
2993
|
|
|
2842
2994
|
// True when the file is a meeting JSON whose status forbids edits. Loaded
|
|
2843
2995
|
// fresh on each call because meeting status can change while a doc-chat is
|
|
@@ -2990,7 +3142,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
2990
3142
|
// bleed into future interactions under the same key.
|
|
2991
3143
|
docSessions.delete(sessionKey);
|
|
2992
3144
|
schedulePersistDocSessions();
|
|
2993
|
-
} else if (result
|
|
3145
|
+
} else if (_docChatResultLooksSuccessful(result) && result.sessionId) {
|
|
2994
3146
|
// Store doc hash for next call's unchanged check
|
|
2995
3147
|
const session = resolveSession('doc', sessionKey);
|
|
2996
3148
|
if (session) session._docHash = initialPass.docHash;
|
|
@@ -3000,7 +3152,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
3000
3152
|
return { answer: result.text || result.stderr || 'Minions runtime is not installed or configured.', content: null, actions: [] };
|
|
3001
3153
|
}
|
|
3002
3154
|
|
|
3003
|
-
if (
|
|
3155
|
+
if (!_docChatResultLooksSuccessful(result)) {
|
|
3004
3156
|
// Try to salvage a parseable answer / action / document edit before failing.
|
|
3005
3157
|
const recovered = _recoverPartialDocChatResponse(result, sessionKey);
|
|
3006
3158
|
if (recovered) return recovered;
|
|
@@ -3051,7 +3203,7 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
|
|
|
3051
3203
|
if (freshSession && sessionKey) {
|
|
3052
3204
|
docSessions.delete(sessionKey);
|
|
3053
3205
|
schedulePersistDocSessions();
|
|
3054
|
-
} else if (result
|
|
3206
|
+
} else if (_docChatResultLooksSuccessful(result) && result.sessionId) {
|
|
3055
3207
|
const session = resolveSession('doc', sessionKey);
|
|
3056
3208
|
if (session) session._docHash = initialPass.docHash;
|
|
3057
3209
|
}
|
|
@@ -3060,7 +3212,7 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
|
|
|
3060
3212
|
return { answer: result.text || result.stderr || 'Minions runtime is not installed or configured.', content: null, actions: [] };
|
|
3061
3213
|
}
|
|
3062
3214
|
|
|
3063
|
-
if (
|
|
3215
|
+
if (!_docChatResultLooksSuccessful(result)) {
|
|
3064
3216
|
const recovered = _recoverPartialDocChatResponse(result, sessionKey);
|
|
3065
3217
|
if (recovered) return recovered;
|
|
3066
3218
|
const sessionPreserved = !!(resolveSession('doc', sessionKey)?.sessionId);
|
|
@@ -5201,20 +5353,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5201
5353
|
filePath: body.filePath, fullPath, isJson, canEdit,
|
|
5202
5354
|
originalContent: currentContent, delimiterContent: content,
|
|
5203
5355
|
});
|
|
5204
|
-
const
|
|
5205
|
-
|
|
5206
|
-
return jsonReply(res, 200, {
|
|
5207
|
-
ok: !ccError,
|
|
5208
|
-
answer: finalAnswer,
|
|
5209
|
-
actions,
|
|
5210
|
-
...(actionResults ? { actionResults } : {}),
|
|
5211
|
-
...(actionParseError ? { actionParseError } : {}),
|
|
5212
|
-
...(ccError ? { error: ccError } : {}),
|
|
5213
|
-
...(partial ? { partial: true, warning } : {}),
|
|
5214
|
-
...(Array.isArray(toolUses) && toolUses.length ? { toolUses } : {}),
|
|
5215
|
-
edited: finalize.edited,
|
|
5216
|
-
...(finalize.edited && finalize.content !== null ? { content: finalize.content } : {}),
|
|
5356
|
+
const payload = _buildDocChatResponsePayload({
|
|
5357
|
+
answer, actions, actionResults, actionParseError, ccError, partial, warning, toolUses, finalize,
|
|
5217
5358
|
});
|
|
5359
|
+
_docDone = true;
|
|
5360
|
+
return jsonReply(res, 200, payload);
|
|
5218
5361
|
} finally { _docAbort = null; _docDone = true; docChatInFlight.delete(docKey); }
|
|
5219
5362
|
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
5220
5363
|
}
|
|
@@ -5303,18 +5446,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5303
5446
|
filePath: body.filePath, fullPath, isJson, canEdit,
|
|
5304
5447
|
originalContent: currentContent, delimiterContent: content,
|
|
5305
5448
|
});
|
|
5306
|
-
const
|
|
5449
|
+
const payload = _buildDocChatResponsePayload({
|
|
5450
|
+
answer, actions, actionResults, actionParseError, ccError, partial, warning, toolUses, finalize,
|
|
5451
|
+
});
|
|
5452
|
+
const { answer: finalAnswer, ...donePayload } = payload;
|
|
5307
5453
|
writeDocEvent({
|
|
5308
5454
|
type: 'done',
|
|
5309
5455
|
text: finalAnswer,
|
|
5310
|
-
|
|
5311
|
-
...(actionResults ? { actionResults } : {}),
|
|
5312
|
-
...(actionParseError ? { actionParseError } : {}),
|
|
5313
|
-
...(ccError ? { error: ccError } : {}),
|
|
5314
|
-
...(partial ? { partial: true, warning } : {}),
|
|
5315
|
-
...(Array.isArray(toolUses) && toolUses.length ? { toolUses } : {}),
|
|
5316
|
-
edited: finalize.edited,
|
|
5317
|
-
...(finalize.edited && finalize.content !== null ? { content: finalize.content } : {}),
|
|
5456
|
+
...donePayload,
|
|
5318
5457
|
});
|
|
5319
5458
|
_docStreamEnded = true;
|
|
5320
5459
|
res.end();
|
|
@@ -7122,8 +7261,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7122
7261
|
{ method: 'POST', path: '/api/plans/unarchive', desc: 'Restore a plan/PRD from archive', params: 'file', handler: handlePlansUnarchive },
|
|
7123
7262
|
{ method: 'POST', path: '/api/plans/revise', desc: 'Request revision with feedback, dispatches agent to revise', params: 'file, feedback, requestedBy?', handler: handlePlansRevise },
|
|
7124
7263
|
{ method: 'POST', path: '/api/plans/discuss', desc: 'Generate a plan discussion session script for Claude CLI', params: 'file', handler: handlePlansDiscuss },
|
|
7125
|
-
{ method: 'GET', path: /^\/api\/plans\/archive\/([^?]+)$/, desc: 'Read an archived plan file', handler: handlePlansArchiveRead },
|
|
7126
|
-
{ method: 'GET', path: /^\/api\/plans\/([^?]+)$/, desc: 'Read a full plan (JSON from prd/ or markdown from plans/)', handler: handlePlansRead },
|
|
7264
|
+
{ method: 'GET', path: /^\/api\/plans\/archive\/([^?]+)$/, template: '/api/plans/archive/:file', desc: 'Read an archived plan file', handler: handlePlansArchiveRead },
|
|
7265
|
+
{ method: 'GET', path: /^\/api\/plans\/([^?]+)$/, template: '/api/plans/:file', desc: 'Read a full plan (JSON from prd/ or markdown from plans/)', handler: handlePlansRead },
|
|
7127
7266
|
|
|
7128
7267
|
// PRD items
|
|
7129
7268
|
{ method: 'POST', path: '/api/prd-items', desc: 'Create a PRD item as a plan file in prd/ (auto-approved)', params: 'name, description?, priority?, estimated_complexity?, project?, id?', handler: handlePrdItemsCreate },
|
|
@@ -7295,13 +7434,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7295
7434
|
});
|
|
7296
7435
|
}},
|
|
7297
7436
|
{ method: 'POST', path: '/api/agents/cancel', desc: 'Cancel an active agent by ID or task substring', params: 'agent?, task?', handler: handleAgentsCancel },
|
|
7298
|
-
{ method: 'POST', path: /^\/api\/agent\/([\w-]+)\/kill$/, desc: 'Kill a running agent: stop process, clear dispatch, reset work items to pending', handler: handleAgentKill },
|
|
7299
|
-
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/, desc: 'SSE real-time live output streaming', handler: handleAgentLiveStream },
|
|
7300
|
-
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live(?:\?.*)?$/, desc: 'Tail live output for a working agent', params: 'tail? (bytes, default 8192)', handler: handleAgentLive },
|
|
7301
|
-
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-output(?:\?.*)?$/, desc: 'Tail live output for a working agent (alias for /live)', params: 'tail? (bytes, default 8192)', handler: handleAgentLive },
|
|
7302
|
-
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/output(?:\?.*)?$/, desc: 'Fetch final output.log for an agent', handler: handleAgentOutput },
|
|
7303
|
-
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)$/, desc: 'Get detailed agent info', handler: handleAgentDetail },
|
|
7304
|
-
{ method: 'GET', path: /^\/api\/dispatch\/([\w.-]+)\/completion-report$/, desc: 'Read structured completion report for a dispatch', handler: (req, res, match) => {
|
|
7437
|
+
{ 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 },
|
|
7438
|
+
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/, template: '/api/agent/:id/live-stream', desc: 'SSE real-time live output streaming', handler: handleAgentLiveStream },
|
|
7439
|
+
{ 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 },
|
|
7440
|
+
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-output(?:\?.*)?$/, template: '/api/agent/:id/live-output', desc: 'Tail live output for a working agent (alias for /live)', params: 'tail? (bytes, default 8192)', handler: handleAgentLive },
|
|
7441
|
+
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/output(?:\?.*)?$/, template: '/api/agent/:id/output', desc: 'Fetch final output.log for an agent', handler: handleAgentOutput },
|
|
7442
|
+
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)$/, template: '/api/agent/:id', desc: 'Get detailed agent info', handler: handleAgentDetail },
|
|
7443
|
+
{ method: 'GET', path: /^\/api\/dispatch\/([\w.-]+)\/completion-report$/, template: '/api/dispatch/:id/completion-report', desc: 'Read structured completion report for a dispatch', handler: (req, res, match) => {
|
|
7305
7444
|
const id = match && match[1];
|
|
7306
7445
|
const payload = queries.getDispatchCompletionReport(id);
|
|
7307
7446
|
if (!payload) return jsonReply(res, 404, { error: 'completion report not found' }, req);
|
|
@@ -7337,7 +7476,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7337
7476
|
}},
|
|
7338
7477
|
{ method: 'POST', path: '/api/knowledge/sweep', desc: 'Trigger async KB sweep (returns 202)', handler: handleKnowledgeSweep },
|
|
7339
7478
|
{ method: 'GET', path: '/api/knowledge/sweep/status', desc: 'Poll KB sweep status', handler: handleKnowledgeSweepStatus },
|
|
7340
|
-
{ method: 'GET', path: /^\/api\/knowledge\/([^/]+)\/([^?]+)/, desc: 'Read a specific knowledge base entry', handler: handleKnowledgeRead },
|
|
7479
|
+
{ method: 'GET', path: /^\/api\/knowledge\/([^/]+)\/([^?]+)/, template: '/api/knowledge/:category/:file', desc: 'Read a specific knowledge base entry', handler: handleKnowledgeRead },
|
|
7341
7480
|
|
|
7342
7481
|
// Doc chat
|
|
7343
7482
|
{ 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 },
|
|
@@ -7368,7 +7507,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7368
7507
|
{ method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, sessionId?', handler: handleCommandCenter },
|
|
7369
7508
|
{ method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message, tabId?', handler: handleCommandCenterStream },
|
|
7370
7509
|
{ method: 'GET', path: '/api/cc-sessions', desc: 'List CC session metadata for all tabs', handler: handleCCSessionsList },
|
|
7371
|
-
{ method: 'DELETE', path: /^\/api\/cc-sessions\/([\w-]+)$/, desc: 'Delete a CC session by tab ID', handler: handleCCSessionDelete },
|
|
7510
|
+
{ method: 'DELETE', path: /^\/api\/cc-sessions\/([\w-]+)$/, template: '/api/cc-sessions/:id', desc: 'Delete a CC session by tab ID', handler: handleCCSessionDelete },
|
|
7372
7511
|
|
|
7373
7512
|
// Schedules
|
|
7374
7513
|
{ method: 'POST', path: '/api/schedules/parse-natural', desc: 'Parse natural language schedule text into cron expression', params: 'text', handler: handleSchedulesParseNatural },
|
|
@@ -7514,7 +7653,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7514
7653
|
return jsonReply(res, 200, { meetings: getMeetings() });
|
|
7515
7654
|
}},
|
|
7516
7655
|
|
|
7517
|
-
{ method: 'GET', path: /^\/api\/meetings\/(MTG-[\w]+)$/, desc: 'Get meeting detail', handler: async (req, res, match) => {
|
|
7656
|
+
{ method: 'GET', path: /^\/api\/meetings\/(MTG-[\w]+)$/, template: '/api/meetings/:id', desc: 'Get meeting detail', handler: async (req, res, match) => {
|
|
7518
7657
|
const { getMeeting } = require('./engine/meeting');
|
|
7519
7658
|
const meeting = getMeeting(match[1]);
|
|
7520
7659
|
if (!meeting) return jsonReply(res, 404, { error: 'Meeting not found' });
|
|
@@ -7589,7 +7728,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7589
7728
|
const md = require('./engine/model-discovery');
|
|
7590
7729
|
return jsonReply(res, 200, { runtimes: md.listAllRuntimes() }, req);
|
|
7591
7730
|
}},
|
|
7592
|
-
{ method: 'POST', path: /^\/api\/runtimes\/([\w-]+)\/models\/refresh$/, desc: 'Invalidate the models cache for a runtime and re-fetch', handler: async (req, res, match) => {
|
|
7731
|
+
{ method: 'POST', path: /^\/api\/runtimes\/([\w-]+)\/models\/refresh$/, template: '/api/runtimes/:runtime/models/refresh', desc: 'Invalidate the models cache for a runtime and re-fetch', handler: async (req, res, match) => {
|
|
7593
7732
|
const md = require('./engine/model-discovery');
|
|
7594
7733
|
const name = match[1];
|
|
7595
7734
|
try {
|
|
@@ -7608,7 +7747,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7608
7747
|
}
|
|
7609
7748
|
return jsonReply(res, 200, payload, req);
|
|
7610
7749
|
}},
|
|
7611
|
-
{ method: 'GET', path: /^\/api\/runtimes\/([\w-]+)\/models$/, desc: 'Get cached or fresh model list for a runtime', handler: async (req, res, match) => {
|
|
7750
|
+
{ method: 'GET', path: /^\/api\/runtimes\/([\w-]+)\/models$/, template: '/api/runtimes/:runtime/models', desc: 'Get cached or fresh model list for a runtime', handler: async (req, res, match) => {
|
|
7612
7751
|
const md = require('./engine/model-discovery');
|
|
7613
7752
|
const name = match[1];
|
|
7614
7753
|
let payload;
|
|
@@ -7753,6 +7892,10 @@ module.exports = {
|
|
|
7753
7892
|
_docChatPartialWarning,
|
|
7754
7893
|
_docChatFailureResponse,
|
|
7755
7894
|
_recoverPartialDocChatResponse,
|
|
7895
|
+
_docChatResultHasVisibleError,
|
|
7896
|
+
_docChatResultLooksSuccessful,
|
|
7897
|
+
_shouldSuppressDocChatPostPatchError,
|
|
7898
|
+
_buildDocChatResponsePayload,
|
|
7756
7899
|
_linkPullRequestForTracking: linkPullRequestForTracking,
|
|
7757
7900
|
_resolveSkillReadPath,
|
|
7758
7901
|
DOC_CHAT_DOCUMENT_DELIMITER,
|
|
@@ -7764,6 +7907,7 @@ module.exports = {
|
|
|
7764
7907
|
_createPipelineFromAction: createPipelineFromAction,
|
|
7765
7908
|
executeCCActions,
|
|
7766
7909
|
buildCCStatePreamble,
|
|
7910
|
+
_routesAsMeta,
|
|
7767
7911
|
_buildTranscriptCarryover,
|
|
7768
7912
|
_ccRuntimeNeedsResumeCarryover,
|
|
7769
7913
|
_joinCcPromptParts,
|
|
@@ -634,25 +634,77 @@ function parseStreamChunk(line) {
|
|
|
634
634
|
|
|
635
635
|
// ── Error Normalization ─────────────────────────────────────────────────────
|
|
636
636
|
|
|
637
|
-
function
|
|
637
|
+
function _collectErrorSignal(rawOutput) {
|
|
638
638
|
const text = rawOutput == null ? '' : String(rawOutput);
|
|
639
|
+
if (!text) return '';
|
|
640
|
+
|
|
641
|
+
const signals = [];
|
|
642
|
+
let sawJsonLine = false;
|
|
643
|
+
for (const rawLine of text.split('\n')) {
|
|
644
|
+
const line = rawLine.trim();
|
|
645
|
+
if (!line) continue;
|
|
646
|
+
if (!line.startsWith('{')) {
|
|
647
|
+
signals.push(line);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
let obj;
|
|
652
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
653
|
+
if (!obj || typeof obj !== 'object') continue;
|
|
654
|
+
sawJsonLine = true;
|
|
655
|
+
|
|
656
|
+
const type = String(obj.type || '');
|
|
657
|
+
const subtype = String(obj.subtype || '');
|
|
658
|
+
const eventType = String(obj.event?.type || '');
|
|
659
|
+
const isErrorEvent = type === 'error'
|
|
660
|
+
|| eventType === 'error'
|
|
661
|
+
|| subtype.startsWith('error')
|
|
662
|
+
|| obj.is_error === true
|
|
663
|
+
|| obj.error != null
|
|
664
|
+
|| obj.data?.error != null;
|
|
665
|
+
if (!isErrorEvent) continue;
|
|
666
|
+
|
|
667
|
+
for (const value of [
|
|
668
|
+
obj.error,
|
|
669
|
+
obj.message,
|
|
670
|
+
obj.stderr,
|
|
671
|
+
obj.result,
|
|
672
|
+
obj.data?.error,
|
|
673
|
+
obj.data?.message,
|
|
674
|
+
obj.data?.stderr,
|
|
675
|
+
obj.event?.error,
|
|
676
|
+
obj.event?.message,
|
|
677
|
+
subtype,
|
|
678
|
+
]) {
|
|
679
|
+
if (typeof value === 'string' && value.trim()) signals.push(value.trim());
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (signals.length > 0) return signals.join('\n');
|
|
684
|
+
return sawJsonLine ? '' : text;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function parseError(rawOutput) {
|
|
688
|
+
const text = _collectErrorSignal(rawOutput);
|
|
639
689
|
if (!text) return { message: '', code: null, retriable: true };
|
|
640
690
|
const lower = text.toLowerCase();
|
|
641
691
|
|
|
642
|
-
|
|
643
|
-
|
|
692
|
+
const hasExplicitAuthFailure = /not authenticated|copilot login|please.*log.*in|\bunauthorized\b/i.test(text);
|
|
693
|
+
const hasAuthStatusCode = /\b(?:http(?:\/\d(?:\.\d)?)?|status(?:\s+code)?|statuscode|response(?:\s+status)?|api(?:\s+(?:error|response|status))?)\s*[:=]?\s*(?:401|403)\b|\b(?:401\s+unauthorized|403\s+forbidden)\b/i.test(text);
|
|
694
|
+
if (hasExplicitAuthFailure || hasAuthStatusCode) {
|
|
695
|
+
return { message: text, code: 'auth-failure', retriable: false };
|
|
644
696
|
}
|
|
645
697
|
if (/rate limit|too many requests|\b429\b/i.test(text)) {
|
|
646
|
-
return { message:
|
|
698
|
+
return { message: text, code: 'rate-limit', retriable: true };
|
|
647
699
|
}
|
|
648
700
|
if (/unknown model|model not found|model.*invalid|invalid model/i.test(text)) {
|
|
649
|
-
return { message:
|
|
701
|
+
return { message: text, code: 'unknown-model', retriable: false };
|
|
650
702
|
}
|
|
651
703
|
if (/budget.*exceed|premium.*limit.*reach|quota.*exceed/i.test(lower)) {
|
|
652
|
-
return { message:
|
|
704
|
+
return { message: text, code: 'budget-exceeded', retriable: false };
|
|
653
705
|
}
|
|
654
706
|
if (/internal error|panic|uncaught|copilot.*crashed|fatal: copilot/i.test(lower)) {
|
|
655
|
-
return { message:
|
|
707
|
+
return { message: text, code: 'crash', retriable: true };
|
|
656
708
|
}
|
|
657
709
|
return { message: '', code: null, retriable: true };
|
|
658
710
|
}
|
package/engine/shared.js
CHANGED
|
@@ -5,9 +5,58 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
8
9
|
const crypto = require('crypto');
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
12
|
+
const DEFAULT_MINIONS_HOME = path.join(os.homedir(), '.minions');
|
|
13
|
+
const ROOT_POINTER_PATH = path.join(os.homedir(), '.minions-root');
|
|
14
|
+
|
|
15
|
+
function isInstalledRoot(dir) {
|
|
16
|
+
if (!dir) return false;
|
|
17
|
+
return fs.existsSync(path.join(dir, 'engine.js')) &&
|
|
18
|
+
fs.existsSync(path.join(dir, 'dashboard.js')) &&
|
|
19
|
+
fs.existsSync(path.join(dir, 'minions.js'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isSourceCheckoutRoot(dir) {
|
|
23
|
+
return !!dir && fs.existsSync(path.join(dir, '.git'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readMinionsRootPointer() {
|
|
27
|
+
try {
|
|
28
|
+
const p = fs.readFileSync(ROOT_POINTER_PATH, 'utf8').trim();
|
|
29
|
+
return p ? path.resolve(p) : null;
|
|
30
|
+
} catch { return null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function saveMinionsRootPointer(root) {
|
|
34
|
+
try { fs.writeFileSync(ROOT_POINTER_PATH, path.resolve(root)); } catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveMinionsHome(forInit = false, options = {}) {
|
|
38
|
+
const envHome = process.env.MINIONS_HOME ? path.resolve(process.env.MINIONS_HOME) : null;
|
|
39
|
+
if (envHome) return envHome;
|
|
40
|
+
|
|
41
|
+
if (forInit) return DEFAULT_MINIONS_HOME;
|
|
42
|
+
|
|
43
|
+
const packageRoot = options.packageRoot ? path.resolve(options.packageRoot) : PACKAGE_ROOT;
|
|
44
|
+
if (options.preferSourceCheckout && isSourceCheckoutRoot(packageRoot)) return packageRoot;
|
|
45
|
+
|
|
46
|
+
const pointerRoot = readMinionsRootPointer();
|
|
47
|
+
if (isInstalledRoot(pointerRoot)) return pointerRoot;
|
|
48
|
+
|
|
49
|
+
if (isInstalledRoot(DEFAULT_MINIONS_HOME)) return DEFAULT_MINIONS_HOME;
|
|
50
|
+
|
|
51
|
+
if (options.allowPackageRootFallback && isInstalledRoot(packageRoot)) return packageRoot;
|
|
52
|
+
|
|
53
|
+
return DEFAULT_MINIONS_HOME;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const MINIONS_DIR = process.env.MINIONS_TEST_DIR || resolveMinionsHome(false, {
|
|
57
|
+
allowPackageRootFallback: true,
|
|
58
|
+
preferSourceCheckout: true,
|
|
59
|
+
});
|
|
11
60
|
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
12
61
|
const CONTROL_PATH = path.join(ENGINE_DIR, 'control.json');
|
|
13
62
|
const COOLDOWNS_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
|
|
@@ -3055,6 +3104,8 @@ module.exports = {
|
|
|
3055
3104
|
safeJson, safeJsonObj, safeJsonArr, safeJsonNoRestore,
|
|
3056
3105
|
safeWrite,
|
|
3057
3106
|
safeUnlink,
|
|
3107
|
+
resolveMinionsHome,
|
|
3108
|
+
saveMinionsRootPointer,
|
|
3058
3109
|
neutralizeJsonBackupSidecar,
|
|
3059
3110
|
PROMPT_CONTEXTS_DIR,
|
|
3060
3111
|
dispatchPromptSidecarPath,
|
package/engine.js
CHANGED
|
@@ -3543,6 +3543,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3543
3543
|
const items = safeJson(centralPath) || [];
|
|
3544
3544
|
const projects = getProjects(config);
|
|
3545
3545
|
const dispatchProjects = getCentralDispatchProjects(projects);
|
|
3546
|
+
const projectsByName = new Map(dispatchProjects.map(p => [p.name, p]));
|
|
3546
3547
|
const newWork = [];
|
|
3547
3548
|
// Collect mutations to apply atomically inside lock callback (avoids TOCTOU)
|
|
3548
3549
|
const mutations = new Map(); // item.id → { field: value, ... }
|
|
@@ -3673,6 +3674,8 @@ function discoverCentralWorkItems(config) {
|
|
|
3673
3674
|
const agentName = config.agents[agentId]?.name || agentId;
|
|
3674
3675
|
const agentRole = config.agents[agentId]?.role || 'Agent';
|
|
3675
3676
|
const firstProject = dispatchProjects[0];
|
|
3677
|
+
const requestedProjectName = typeof item.project === 'string' ? item.project : item.project?.name;
|
|
3678
|
+
const targetProject = (requestedProjectName && projectsByName.get(requestedProjectName)) || firstProject;
|
|
3676
3679
|
|
|
3677
3680
|
// Branch mutex: skip if target branch is locked by an active dispatch
|
|
3678
3681
|
const centralBranch = item.branch || item.featureBranch || `work/${item.id}`;
|
|
@@ -3683,7 +3686,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3683
3686
|
}
|
|
3684
3687
|
|
|
3685
3688
|
const vars = {
|
|
3686
|
-
...buildBaseVars(agentId, config,
|
|
3689
|
+
...buildBaseVars(agentId, config, targetProject),
|
|
3687
3690
|
item_id: item.id,
|
|
3688
3691
|
item_name: item.title || item.id,
|
|
3689
3692
|
item_priority: item.priority || 'medium',
|
|
@@ -3694,11 +3697,11 @@ function discoverCentralWorkItems(config) {
|
|
|
3694
3697
|
work_type: workType,
|
|
3695
3698
|
additional_context: item.prompt ? `## Additional Context\n\n${item.prompt}` : '',
|
|
3696
3699
|
scope_section: buildProjectContext(dispatchProjects, null, false, agentName, agentRole),
|
|
3697
|
-
project_path:
|
|
3700
|
+
project_path: targetProject?.localPath || '',
|
|
3698
3701
|
branch_name: centralBranch,
|
|
3699
3702
|
};
|
|
3700
|
-
const centralWtPath =
|
|
3701
|
-
? path.resolve(
|
|
3703
|
+
const centralWtPath = targetProject?.localPath
|
|
3704
|
+
? path.resolve(targetProject.localPath, config.engine?.worktreeRoot || '../worktrees', centralBranch)
|
|
3702
3705
|
: '';
|
|
3703
3706
|
const cpResult = buildWorkItemDispatchVars(item, vars, config, {
|
|
3704
3707
|
worktreePath: centralWtPath || undefined,
|
|
@@ -3749,7 +3752,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3749
3752
|
}
|
|
3750
3753
|
vars.plan_summary = (item.title || item.planFile).substring(0, 80);
|
|
3751
3754
|
vars.plan_file = item.planFile || '';
|
|
3752
|
-
vars.project_name_lower = (
|
|
3755
|
+
vars.project_name_lower = (targetProject?.name || 'project').toLowerCase();
|
|
3753
3756
|
// Default empty string so the {{existing_prd_json}} token always resolves —
|
|
3754
3757
|
// playbook treats empty as "no existing PRD, fresh run". Without this default
|
|
3755
3758
|
// the renderPlaybook pass logs an "unresolved template variables" warning
|
|
@@ -3804,7 +3807,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3804
3807
|
agentRole,
|
|
3805
3808
|
task: item.title || item.description?.slice(0, 80) || item.id,
|
|
3806
3809
|
prompt,
|
|
3807
|
-
meta: { dispatchKey: key, source: 'central-work-item', item: { ...item, ...mutations.get(item.id) }, planFileName: item.planFile || mutations.get(item.id)?._planFileName || null, branch: item.branch || item.featureBranch || `work/${item.id}`, project: { name:
|
|
3810
|
+
meta: { dispatchKey: key, source: 'central-work-item', item: { ...item, ...mutations.get(item.id) }, planFileName: item.planFile || mutations.get(item.id)?._planFileName || null, branch: item.branch || item.featureBranch || `work/${item.id}`, project: { name: targetProject.name, localPath: targetProject.localPath } }
|
|
3808
3811
|
});
|
|
3809
3812
|
|
|
3810
3813
|
setCooldown(key);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1775",
|
|
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
|
@@ -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
|
|
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
163
|
`{"type":"<short-descriptor>","endpoint":"/api/...","params":{...}}`
|
|
164
|
-
The action runner
|
|
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.
|
|
165
165
|
|
|
166
166
|
For CLI commands (`minions <cmd>`), use Bash to invoke them when delegating would be heavier than just running the command.
|