@yemi33/minions 0.1.1774 → 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 CHANGED
@@ -1,12 +1,16 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1774 (2026-05-07)
3
+ ## 0.1.1775 (2026-05-07)
4
4
 
5
5
  ### Features
6
+ - Improve dashboard API route metadata
6
7
  - suppress stale doc-chat model errors
7
8
 
8
9
  ### Fixes
9
10
  - yemi33/minions#2168
11
+ - honor central plan work item project
12
+ - restore canonical-home shared helpers
13
+ - yemi33/minions#2170
10
14
 
11
15
  ## 0.1.1772 (2026-05-07)
12
16
 
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
- try { fs.writeFileSync(ROOT_POINTER_PATH, root); } catch {}
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
- const envHome = process.env.MINIONS_HOME ? path.resolve(process.env.MINIONS_HOME) : null;
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
- method: r.method,
1340
- path: typeof r.path === 'string' ? r.path : String(r.path),
1341
- desc: r.desc || '',
1342
- params: r.params || null,
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
- return `- \`${r.method} ${r.path}\` — ${r.desc}${params}`;
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 any \`/api/...\` endpoint not covered by a named CC action, use the generic fallback:
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
@@ -7152,8 +7261,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7152
7261
  { method: 'POST', path: '/api/plans/unarchive', desc: 'Restore a plan/PRD from archive', params: 'file', handler: handlePlansUnarchive },
7153
7262
  { method: 'POST', path: '/api/plans/revise', desc: 'Request revision with feedback, dispatches agent to revise', params: 'file, feedback, requestedBy?', handler: handlePlansRevise },
7154
7263
  { method: 'POST', path: '/api/plans/discuss', desc: 'Generate a plan discussion session script for Claude CLI', params: 'file', handler: handlePlansDiscuss },
7155
- { method: 'GET', path: /^\/api\/plans\/archive\/([^?]+)$/, desc: 'Read an archived plan file', handler: handlePlansArchiveRead },
7156
- { 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 },
7157
7266
 
7158
7267
  // PRD items
7159
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 },
@@ -7325,13 +7434,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7325
7434
  });
7326
7435
  }},
7327
7436
  { method: 'POST', path: '/api/agents/cancel', desc: 'Cancel an active agent by ID or task substring', params: 'agent?, task?', handler: handleAgentsCancel },
7328
- { method: 'POST', path: /^\/api\/agent\/([\w-]+)\/kill$/, desc: 'Kill a running agent: stop process, clear dispatch, reset work items to pending', handler: handleAgentKill },
7329
- { method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/, desc: 'SSE real-time live output streaming', handler: handleAgentLiveStream },
7330
- { method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live(?:\?.*)?$/, desc: 'Tail live output for a working agent', params: 'tail? (bytes, default 8192)', handler: handleAgentLive },
7331
- { 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 },
7332
- { method: 'GET', path: /^\/api\/agent\/([\w-]+)\/output(?:\?.*)?$/, desc: 'Fetch final output.log for an agent', handler: handleAgentOutput },
7333
- { method: 'GET', path: /^\/api\/agent\/([\w-]+)$/, desc: 'Get detailed agent info', handler: handleAgentDetail },
7334
- { 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) => {
7335
7444
  const id = match && match[1];
7336
7445
  const payload = queries.getDispatchCompletionReport(id);
7337
7446
  if (!payload) return jsonReply(res, 404, { error: 'completion report not found' }, req);
@@ -7367,7 +7476,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7367
7476
  }},
7368
7477
  { method: 'POST', path: '/api/knowledge/sweep', desc: 'Trigger async KB sweep (returns 202)', handler: handleKnowledgeSweep },
7369
7478
  { method: 'GET', path: '/api/knowledge/sweep/status', desc: 'Poll KB sweep status', handler: handleKnowledgeSweepStatus },
7370
- { 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 },
7371
7480
 
7372
7481
  // Doc chat
7373
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 },
@@ -7398,7 +7507,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7398
7507
  { method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, sessionId?', handler: handleCommandCenter },
7399
7508
  { method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message, tabId?', handler: handleCommandCenterStream },
7400
7509
  { method: 'GET', path: '/api/cc-sessions', desc: 'List CC session metadata for all tabs', handler: handleCCSessionsList },
7401
- { 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 },
7402
7511
 
7403
7512
  // Schedules
7404
7513
  { method: 'POST', path: '/api/schedules/parse-natural', desc: 'Parse natural language schedule text into cron expression', params: 'text', handler: handleSchedulesParseNatural },
@@ -7544,7 +7653,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7544
7653
  return jsonReply(res, 200, { meetings: getMeetings() });
7545
7654
  }},
7546
7655
 
7547
- { 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) => {
7548
7657
  const { getMeeting } = require('./engine/meeting');
7549
7658
  const meeting = getMeeting(match[1]);
7550
7659
  if (!meeting) return jsonReply(res, 404, { error: 'Meeting not found' });
@@ -7619,7 +7728,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7619
7728
  const md = require('./engine/model-discovery');
7620
7729
  return jsonReply(res, 200, { runtimes: md.listAllRuntimes() }, req);
7621
7730
  }},
7622
- { 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) => {
7623
7732
  const md = require('./engine/model-discovery');
7624
7733
  const name = match[1];
7625
7734
  try {
@@ -7638,7 +7747,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7638
7747
  }
7639
7748
  return jsonReply(res, 200, payload, req);
7640
7749
  }},
7641
- { 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) => {
7642
7751
  const md = require('./engine/model-discovery');
7643
7752
  const name = match[1];
7644
7753
  let payload;
@@ -7798,6 +7907,7 @@ module.exports = {
7798
7907
  _createPipelineFromAction: createPipelineFromAction,
7799
7908
  executeCCActions,
7800
7909
  buildCCStatePreamble,
7910
+ _routesAsMeta,
7801
7911
  _buildTranscriptCarryover,
7802
7912
  _ccRuntimeNeedsResumeCarryover,
7803
7913
  _joinCcPromptParts,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-07T21:34:32.453Z"
4
+ "cachedAt": "2026-05-07T21:35:37.002Z"
5
5
  }
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 MINIONS_DIR = process.env.MINIONS_TEST_DIR || (process.env.MINIONS_HOME ? path.resolve(process.env.MINIONS_HOME) : path.resolve(__dirname, '..'));
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, firstProject),
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: firstProject?.localPath || '',
3700
+ project_path: targetProject?.localPath || '',
3698
3701
  branch_name: centralBranch,
3699
3702
  };
3700
- const centralWtPath = firstProject?.localPath
3701
- ? path.resolve(firstProject.localPath, config.engine?.worktreeRoot || '../worktrees', centralBranch)
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 = (firstProject?.name || 'project').toLowerCase();
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: firstProject.name, localPath: firstProject.localPath } }
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.1774",
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"
@@ -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 any `/api/...` endpoint that doesn't have a matching named action above, emit the generic fallback shape:
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 accepts any local `/api/` path and POSTs `params` as JSON.
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.