@yemi33/minions 0.1.1740 → 0.1.1742

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,14 +1,23 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1740 (2026-05-06)
3
+ ## 0.1.1742 (2026-05-06)
4
+
5
+ ### Fixes
6
+ - pending-reassignment double-books agent with own pending dispatch (#2108)
7
+
8
+ ## 0.1.1741 (2026-05-06)
4
9
 
5
10
  ### Features
6
- - doc-chat resume retry + 1h timeout + surfaced stderr (#2101)
7
- - make Command Center chat sessions non-expiring (#2100)
11
+ - hard-timeout backstop prevents permanent round stalls
12
+ - auto-index API + CLI catalog into preamble; threshold-based delegation gate
8
13
 
9
- ### Fixes
10
- - invalidate stale session IDs on runtime switch (#2102)
11
- - drop 5s setTimeout that nuked sysprompt .tmp before Claude could read it (#2105)
14
+ ### Other
15
+ - chore(gitignore): ignore .vscode/, engine/completions/, pull-requests.json.backup
16
+
17
+ ## 0.1.1739 (2026-05-06)
18
+
19
+ ### Features
20
+ - make Command Center chat sessions non-expiring (#2100)
12
21
 
13
22
  ## 0.1.1738 (2026-05-06)
14
23
 
package/dashboard.js CHANGED
@@ -1176,6 +1176,50 @@ let _preambleCache = null;
1176
1176
  let _preambleCacheTs = 0;
1177
1177
  const PREAMBLE_TTL = 30000; // 30s — longer TTL since preamble is lightweight orientation, not real-time data
1178
1178
 
1179
+ // SoT for CC's runtime API index. Captured lazily on the first HTTP request
1180
+ // because ROUTES is closed over inside the request handler. Subsequent
1181
+ // captures no-op via the truthy guard.
1182
+ let _ccApiRoutesMeta = null;
1183
+
1184
+ function _routesAsMeta(routes) {
1185
+ return routes.map(r => ({
1186
+ method: r.method,
1187
+ path: typeof r.path === 'string' ? r.path : String(r.path),
1188
+ desc: r.desc || '',
1189
+ params: r.params || null,
1190
+ }));
1191
+ }
1192
+
1193
+ function _captureApiRoutesMeta(routes) {
1194
+ if (_ccApiRoutesMeta || !Array.isArray(routes)) return;
1195
+ _ccApiRoutesMeta = _routesAsMeta(routes);
1196
+ }
1197
+
1198
+ function _formatCcApiRoutesIndex() {
1199
+ if (!Array.isArray(_ccApiRoutesMeta) || _ccApiRoutesMeta.length === 0) return '';
1200
+ return _ccApiRoutesMeta
1201
+ .filter(r => r.path.startsWith('/api/'))
1202
+ .map(r => {
1203
+ const params = r.params ? ` — params: ${r.params}` : '';
1204
+ return `- \`${r.method} ${r.path}\` — ${r.desc}${params}`;
1205
+ })
1206
+ .join('\n');
1207
+ }
1208
+
1209
+ const { CLI_COMMAND_DOCS: _CC_CLI_DOCS } = require('./engine/cli');
1210
+
1211
+ function _formatCcCliCommandsIndex() {
1212
+ if (!_CC_CLI_DOCS) return '';
1213
+ return Object.entries(_CC_CLI_DOCS)
1214
+ .map(([name, { args, summary }]) => `- \`minions ${name}${args ? ' ' + args : ''}\` — ${summary}`)
1215
+ .join('\n');
1216
+ }
1217
+
1218
+ function _resetPreambleCache() {
1219
+ _preambleCache = null;
1220
+ _preambleCacheTs = 0;
1221
+ }
1222
+
1179
1223
  function buildCCStatePreamble() {
1180
1224
  const now = Date.now();
1181
1225
  if (_preambleCache && now - _preambleCacheTs < PREAMBLE_TTL) return _preambleCache;
@@ -1198,6 +1242,19 @@ function buildCCStatePreamble() {
1198
1242
  let pipelineCount = 0;
1199
1243
  try { pipelineCount = require('./engine/pipeline').getPipelines().length; } catch {}
1200
1244
 
1245
+ const apiIndex = _formatCcApiRoutesIndex();
1246
+ const cliIndex = _formatCcCliCommandsIndex();
1247
+ const indexSection = (apiIndex || cliIndex) ? `
1248
+
1249
+ ### API Index (auto-generated from dashboard.js ROUTES — single source of truth)
1250
+ ${apiIndex || '(routes not yet captured — first request still pending)'}
1251
+
1252
+ ### CLI Index (auto-generated from engine/cli.js CLI_COMMAND_DOCS — single source of truth)
1253
+ ${cliIndex || '(unavailable)'}
1254
+
1255
+ For any \`/api/...\` endpoint not covered by a named CC action, use the generic fallback:
1256
+ \`{"type":"<descriptive>","endpoint":"/api/...","params":{...}}\`.` : '';
1257
+
1201
1258
  const result = `### Agents
1202
1259
  ${agents}
1203
1260
 
@@ -1211,8 +1268,8 @@ PRs: ${prCount} | Work items: ${wiCount} | Plans/PRDs: ${planFiles.length} | Sch
1211
1268
  ### Projects
1212
1269
  ${projects}
1213
1270
 
1214
- Use tools to read \`config.json\` (schedules), \`pipelines/\` dir, or \`curl http://localhost:7331/api/routes\` for details.
1215
- For all state files, look under \`${MINIONS_DIR}\`.`;
1271
+ Use tools to read \`config.json\` (schedules), \`pipelines/\` dir for details.
1272
+ For all state files, look under \`${MINIONS_DIR}\`.${indexSection}`;
1216
1273
  _preambleCache = result;
1217
1274
  _preambleCacheTs = now;
1218
1275
  return result;
@@ -6546,12 +6603,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6546
6603
  });
6547
6604
  }},
6548
6605
  { method: 'GET', path: '/api/routes', desc: 'List all available API endpoints', handler: (req, res) => {
6549
- const list = ROUTES.map(r => ({
6550
- method: r.method,
6551
- path: typeof r.path === 'string' ? r.path : r.path.toString(),
6552
- description: r.desc,
6553
- params: r.params || null
6554
- }));
6606
+ const list = _routesAsMeta(ROUTES).map(({ desc, ...rest }) => ({ ...rest, description: desc }));
6555
6607
  return jsonReply(res, 200, { routes: list });
6556
6608
  }},
6557
6609
 
@@ -7184,6 +7236,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7184
7236
 
7185
7237
  // ── Route Dispatcher ────────────────────────────────────────────────────────
7186
7238
 
7239
+ // Capture ROUTES metadata into the module-level snapshot so CC's preamble
7240
+ // renders the live API surface (single source of truth — adding any new
7241
+ // route to ROUTES above auto-surfaces it in CC's index).
7242
+ _captureApiRoutesMeta(ROUTES);
7243
+
7187
7244
  const pathname = req.url.split('?')[0];
7188
7245
  const _reqStart = Date.now();
7189
7246
  for (const route of ROUTES) {
@@ -7269,6 +7326,11 @@ module.exports = {
7269
7326
  _resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
7270
7327
  _createPipelineFromAction: createPipelineFromAction,
7271
7328
  executeCCActions,
7329
+ buildCCStatePreamble,
7330
+ _captureApiRoutesMeta,
7331
+ _formatCcApiRoutesIndex,
7332
+ _formatCcCliCommandsIndex,
7333
+ _resetPreambleCache,
7272
7334
  };
7273
7335
 
7274
7336
  // Start the HTTP server only when run directly (node dashboard.js).
package/engine/cli.js CHANGED
@@ -97,27 +97,41 @@ function handleCommand(cmd, args) {
97
97
  } else {
98
98
  console.log(`Unknown command: ${cmd}`);
99
99
  console.log('Commands:');
100
- console.log(' start [--cli R] [--model M] Start engine daemon (R = registered runtime)');
101
- console.log(' stop Stop engine');
102
- console.log(' pause / resume Pause/resume dispatching');
103
- console.log(' status Show engine + agent state + fleet config');
104
- console.log(' queue Show dispatch queue');
105
- console.log(' sources Show work source status');
106
- console.log(' discover Dry-run work discovery');
107
- console.log(' dispatch Force a dispatch cycle');
108
- console.log(' spawn <a> <p> Manually spawn agent with prompt');
109
- console.log(' work <title> [o] Add to work-items.json queue');
110
- console.log(' plan <src> [p] Generate PRD from a plan (file or text)');
111
- console.log(' kill Kill all active agents, reset to pending');
112
- console.log(' complete <id> Mark a dispatch as done');
113
- console.log(' cleanup Clean temp files, worktrees, zombies');
114
- console.log(' mcp-sync Sync MCP servers from ~/.claude.json');
115
- console.log(' doctor Check prerequisites and runtime health');
116
- console.log(' config set-cli <R> [--model M] Persist defaultCli/defaultModel without starting');
100
+ for (const line of formatCliCommandHelpLines()) console.log(line);
117
101
  process.exit(1);
118
102
  }
119
103
  }
120
104
 
105
+ // SoT for engine-CLI metadata: drives handleCommand's help text and the
106
+ // CC preamble's CLI index in dashboard.js. Drift-checked against `commands`.
107
+ const CLI_COMMAND_DOCS = Object.freeze({
108
+ start: { args: '[--cli R] [--model M]', summary: 'Start engine daemon (R = registered runtime)' },
109
+ stop: { args: '', summary: 'Stop engine' },
110
+ pause: { args: '', summary: 'Pause dispatching' },
111
+ resume: { args: '', summary: 'Resume dispatching' },
112
+ status: { args: '', summary: 'Show engine + agent state + fleet config' },
113
+ queue: { args: '', summary: 'Show dispatch queue' },
114
+ sources: { args: '', summary: 'Show work source status per project' },
115
+ discover: { args: '', summary: 'Dry-run work discovery' },
116
+ dispatch: { args: '', summary: 'Force a dispatch cycle' },
117
+ spawn: { args: '<agent> <prompt>', summary: 'Manually spawn an agent with prompt' },
118
+ work: { args: '<title> [opts]', summary: 'Add to work-items.json queue' },
119
+ plan: { args: '<file|text> [project]', summary: 'Generate PRD from a plan (file or text)' },
120
+ kill: { args: '', summary: 'Kill all active agents, reset to pending' },
121
+ complete: { args: '<dispatch-id>', summary: 'Mark a dispatch as done' },
122
+ cleanup: { args: '', summary: 'Clean temp files, worktrees, zombies' },
123
+ 'mcp-sync': { args: '', summary: 'Sync MCP servers from ~/.claude.json' },
124
+ doctor: { args: '', summary: 'Check prerequisites and runtime health' },
125
+ config: { args: 'set-cli <R> [--model M]', summary: 'Persist defaultCli/defaultModel without starting' },
126
+ });
127
+
128
+ function formatCliCommandHelpLines() {
129
+ const entries = Object.entries(CLI_COMMAND_DOCS);
130
+ const lefts = entries.map(([name, { args }]) => ' ' + name + (args ? ' ' + args : ''));
131
+ const padTo = Math.max(...lefts.map(s => s.length)) + 2;
132
+ return entries.map(([, { summary }], i) => lefts[i].padEnd(padTo) + summary);
133
+ }
134
+
121
135
  // ─── Runtime fleet flags (--cli / --model / --effort) ────────────────────────
122
136
  //
123
137
  // Shared by `start`, `restart`, and `config set-cli`. Single source of truth
@@ -1461,7 +1475,11 @@ const commands = {
1461
1475
 
1462
1476
  module.exports = {
1463
1477
  handleCommand,
1478
+ // exported for downstream indexing (dashboard CC preamble)
1479
+ CLI_COMMAND_DOCS,
1480
+ formatCliCommandHelpLines,
1464
1481
  // exported for testing
1482
+ _listCommandKeys: () => Object.keys(commands),
1465
1483
  _parseRuntimeFlags,
1466
1484
  _modelLooksIncompatible,
1467
1485
  _applyRuntimeFlags,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-06T00:41:30.276Z"
4
+ "cachedAt": "2026-05-06T01:07:10.189Z"
5
5
  }
package/engine/meeting.js CHANGED
@@ -730,6 +730,8 @@ function checkMeetingTimeouts(config) {
730
730
  const meetings = getMeetings();
731
731
  const timeout = (config.engine || {}).meetingRoundTimeout
732
732
  || ENGINE_DEFAULTS.meetingRoundTimeout;
733
+ const hardTimeout = (config.engine || {}).meetingRoundHardTimeout
734
+ || ENGINE_DEFAULTS.meetingRoundHardTimeout;
733
735
 
734
736
  for (const meeting of meetings) {
735
737
  if (isTerminalMeetingStatus(meeting.status)) continue;
@@ -760,11 +762,34 @@ function checkMeetingTimeouts(config) {
760
762
  meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round ${meeting.round} timed out after all participants finished`, at: ts() });
761
763
  advanceMeetingIfRoundComplete(meeting, roundName, meeting.id, config);
762
764
  saveMeeting(meeting);
765
+ } else if (elapsed >= hardTimeout) {
766
+ const failures = getRoundFailures(meeting, roundName, meeting.round, true);
767
+ const stalled = (meeting.participants || []).filter(p => !hasRoundTerminalOutcome(meeting, roundName, p, meeting.round));
768
+ const reason = `Hard meeting timeout after ${Math.round(elapsed / 60000)}min — agent did not produce ${roundName} output`;
769
+ for (const agentId of stalled) {
770
+ failures[agentId] = { reason, content: '', submittedAt: ts() };
771
+ meeting.transcript.push({ round: meeting.round, agent: agentId, type: 'failure', content: reason, at: ts() });
772
+ }
773
+ log('warn', `Meeting ${meeting.id}: round ${meeting.round} hit hard timeout after ${Math.round(elapsed / 60000)}min — marking ${stalled.length}/${totalCount} non-responders as failed and advancing`);
774
+ meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round ${meeting.round} hard timeout — ${stalled.length} non-responder(s) marked failed`, at: ts() });
775
+ advanceMeetingIfRoundComplete(meeting, roundName, meeting.id, config);
776
+ saveMeeting(meeting);
763
777
  } else {
764
778
  log('warn', `Meeting ${meeting.id}: round ${meeting.round} timed out after ${Math.round(elapsed / 60000)}min — waiting for all participants to finish (${respondedCount}/${totalCount} succeeded)`);
765
779
  }
766
780
  } else if (meeting.status === 'concluding') {
767
- log('warn', `Meeting ${meeting.id}: conclusion round timed out after ${Math.round(elapsed / 60000)}min — waiting for the conclusion agent to finish`);
781
+ if (elapsed >= hardTimeout) {
782
+ const reason = `Hard meeting timeout after ${Math.round(elapsed / 60000)}min — conclusion agent did not produce output`;
783
+ const failures = getRoundFailures(meeting, 'conclude', meeting.round, true);
784
+ const conclusionAgent = (meeting.participants || []).find(p => !hasRoundTerminalOutcome(meeting, 'conclude', p, meeting.round)) || meeting.participants?.[0] || 'system';
785
+ failures[conclusionAgent] = { reason, content: '', submittedAt: ts() };
786
+ meeting.transcript.push({ round: meeting.round, agent: conclusionAgent, type: 'failure', content: reason, at: ts() });
787
+ log('warn', `Meeting ${meeting.id}: conclusion round hit hard timeout after ${Math.round(elapsed / 60000)}min — synthesising fallback conclusion`);
788
+ advanceMeetingIfRoundComplete(meeting, 'conclude', meeting.id, config);
789
+ saveMeeting(meeting);
790
+ } else {
791
+ log('warn', `Meeting ${meeting.id}: conclusion round timed out after ${Math.round(elapsed / 60000)}min — waiting for the conclusion agent to finish`);
792
+ }
768
793
  }
769
794
  }
770
795
  }
package/engine/shared.js CHANGED
@@ -861,7 +861,8 @@ const ENGINE_DEFAULTS = {
861
861
  prNoOpFixPauseAttempts: 2, // pause one PR automation cause after repeated no-op fixes for unchanged evidence
862
862
  completionReportRetentionDays: 90, // retain completion report sidecars beyond capped dispatch history
863
863
  completionReportMaxFiles: 5000, // hard cap for completion report sidecars during cleanup
864
- meetingRoundTimeout: 900000, // 15min per meeting round before auto-advance
864
+ meetingRoundTimeout: 900000, // 15min per meeting round soft signal; logs a "still waiting" warning each tick
865
+ meetingRoundHardTimeout: 3600000, // 60min hard backstop — non-terminal participants are marked failed and the round advances. Prevents permanent stalls if an agent's dispatch never spawns or its completion gets dropped.
865
866
  evalLoop: true, // enable review→fix loop after implementation completes
866
867
  evalMaxIterations: 3, // legacy UI/config field; engine discovery no longer enforces review→fix cycle caps
867
868
  evalMaxCost: null, // USD ceiling per work item across all eval iterations; null = no limit (gather baseline data first)
package/engine.js CHANGED
@@ -4326,6 +4326,17 @@ async function tickInner() {
4326
4326
 
4327
4327
  // Build set of agents currently active (one task per agent at a time).
4328
4328
  const busyAgents = new Set((dispatch.active || []).map(d => d.agent));
4329
+ // W-motc4y1n000t1a5f: Track agents that already have a pending dispatch in
4330
+ // the queue. Reassignment routes (busy / unspawned-temp) must not move a
4331
+ // dispatch onto an agent who already has their own pending entry — order-
4332
+ // dependent iteration would otherwise double-book that agent. Seeded once
4333
+ // from the snapshot of pending and updated on successful reassignment.
4334
+ const pendingAgents = new Set();
4335
+ for (const p of (dispatch.pending || [])) {
4336
+ if (typeof p.agent === 'string' && p.agent && p.agent !== routing.ANY_AGENT) {
4337
+ pendingAgents.add(p.agent);
4338
+ }
4339
+ }
4329
4340
  // Branch mutex: track branches locked by active dispatches to prevent concurrent writes
4330
4341
  const lockedBranches = new Set();
4331
4342
  for (const d of (dispatch.active || [])) {
@@ -4409,9 +4420,12 @@ async function tickInner() {
4409
4420
  const isUnspawnedTemp = item.agent?.startsWith('temp-') && !busyAgents.has(item.agent);
4410
4421
  if (isUnspawnedTemp) {
4411
4422
  const altAgent = resolvePendingDispatchAgent(item, config);
4412
- if (altAgent && altAgent !== item.agent) {
4423
+ // W-motc4y1n000t1a5f: don't reassign onto an agent who already has
4424
+ // their own pending dispatch — that would double-book the alt.
4425
+ if (altAgent && altAgent !== item.agent && !pendingAgents.has(altAgent)) {
4413
4426
  const prevAgent = item.agent;
4414
4427
  assignPendingDispatchAgent(item, altAgent, config);
4428
+ pendingAgents.add(altAgent);
4415
4429
  log('info', `Reassigning ${item.id} from unspawned temp ${prevAgent} to ${altAgent} — temp agent never spawned`);
4416
4430
  // Persist reassignment to dispatch.json so it survives restarts/ticks
4417
4431
  persistPendingDispatchAgent(item);
@@ -4427,10 +4441,14 @@ async function tickInner() {
4427
4441
  continue; // Valid hard pin — keep waiting for pinned agent
4428
4442
  }
4429
4443
  // agent busy and idle alternative available — reroute immediately (no threshold)
4444
+ // W-motc4y1n000t1a5f: pendingAgents check prevents reassigning onto an
4445
+ // agent who already has their own pending dispatch (order-dependent
4446
+ // double-book in the meeting fan-out scenario).
4430
4447
  const altAgent = resolvePendingDispatchAgent(item, config);
4431
- if (altAgent && altAgent !== originalAgent && !busyAgents.has(altAgent)) {
4448
+ if (altAgent && altAgent !== originalAgent && !busyAgents.has(altAgent) && !pendingAgents.has(altAgent)) {
4432
4449
  log('info', `Reassigning ${item.id} from ${originalAgent} to ${altAgent} — agent busy and idle alternative available`);
4433
4450
  assignPendingDispatchAgent(item, altAgent, config);
4451
+ pendingAgents.add(altAgent);
4434
4452
  persistPendingDispatchAgent(item);
4435
4453
  // Fall through to branch mutex / concurrency checks below
4436
4454
  } else if (isSoftFixDispatch(item)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1740",
3
+ "version": "0.1.1742",
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"
@@ -30,30 +30,35 @@ CAN modify: notes, plans, knowledge, work items, pull-requests.json, routing.md,
30
30
  Minions state lives in `{{minions_dir}}/`. Key paths: `config.json` (config), `routing.md` (dispatch rules), `projects/{name}/work-items.json` & `pull-requests.json` (per-project), `agents/{id}/` (charters, output), `plans/` & `prd/` (plans), `knowledge/` (KB), `notes/inbox/` (inbox), `engine/dispatch.json` (queue), `playbooks/` (templates). Use tools to read specifics.
31
31
 
32
32
  ## Role: Orchestrator
33
- Default: **delegate to agents**. Agents have full Claude Code + worktrees + MCP tools.
33
+ You are primarily a dispatcher. Agents have full Claude Code + worktrees + MCP tools and are better suited for real work — but you are not hard-stopped from handling small requests yourself.
34
34
 
35
- ### When to delegate (ALWAYS)
35
+ ### Step 1 Estimate difficulty before responding
36
+ State the size in 3-4 words to yourself, then act:
37
+ - **Small** (≤3 tool calls, 1-2 files, no cross-module reasoning): you MAY do it yourself.
38
+ - **Medium** (4-10 tool calls, 3+ files, multi-file reasoning, real refactor): you MUST delegate.
39
+ - **Large** (10+ tool calls, cross-cutting, multi-stage): you MUST delegate, consider a plan with decomposition.
40
+
41
+ ### Step 2 — Delegate when ≥ Medium (the hard stop)
42
+ Always delegate these to an agent — do not attempt them yourself even if they look small at first:
36
43
  - Code changes, fixes, refactors, new features → `implement` or `fix`
37
44
  - Exploration, investigation, research, audits → `explore`
38
45
  - Code reviews → `review`
39
46
  - Testing → `test`
40
47
  - Architecture analysis → `explore`
41
- - Any task that would require **more than 3 tool calls** or touching **more than 2 files**
48
+ - Anything Medium per Step 1
42
49
 
43
- ### When to do it yourself (ONLY these)
50
+ ### Step 3 — Small tasks: do them yourself when it's faster than dispatching
51
+ Examples (not an exhaustive whitelist — apply Step 1 to anything not listed):
44
52
  - Quick status lookups (reading 1-2 state files)
45
53
  - Notes, plan edits, KB entries, routing updates
46
54
  - Git ops the user explicitly asked CC to do
47
55
  - Simple config changes (`set-config`)
48
56
  - Answering questions from context you already have
57
+ - One-line edits to non-protected files when the change is unambiguous
49
58
 
50
- ### Size estimation rule
51
- Before responding, estimate the task size:
52
- - **Small** (≤3 tool calls, 1-2 files): do it yourself
53
- - **Medium** (4-10 tool calls, 3+ files): DELEGATE to an agent
54
- - **Large** (10+ tool calls, cross-cutting): DELEGATE, consider a plan with decomposition
59
+ If you start a small task and discover it's actually Medium (3+ files, more tool calls than expected, surprising complexity), STOP and delegate instead of pushing through.
55
60
 
56
- When in doubt, delegate. You are the dispatcher, not the worker. Agents have isolated worktrees, full tool access, and no turn limits — they are better suited for real work.
61
+ When genuinely in doubt about the size, delegate agents have isolated worktrees, full tool access, and no turn limits.
57
62
 
58
63
  ## Actions
59
64
  Append actions at the END of your response. Write your response first, then `===ACTIONS===` on its own line, then a JSON array. No text after the JSON. Omit entirely if no actions needed.
@@ -149,4 +154,13 @@ Terms like schedules, pipelines, agents, inbox, work items, plans, PRD, PRs, dis
149
154
  1. Answer from the state preamble and context first. Only use tools for specific file lookups the user asked about — not to explore or investigate.
150
155
  2. Be specific — cite IDs, names, filenames, line numbers.
151
156
  3. Never modify engine source. Never push to git without user confirmation.
152
- 4. Delegate, don't do. If a task involves code changes, multi-file reads, debugging, or any real engineering work dispatch an agent. Your tools are for quick lookups, not for doing the work.
157
+ 4. Estimate first, then act (see Role: Orchestrator). For Medium-and-above tasks, your tools are for orientation; the agent does the work. For Small tasks, you may do them yourself.
158
+
159
+ ## API & CLI Index (auto-injected)
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
+
162
+ For any `/api/...` endpoint that doesn't have a matching named action above, emit the generic fallback shape:
163
+ `{"type":"<short-descriptor>","endpoint":"/api/...","params":{...}}`
164
+ The action runner accepts any local `/api/` path and POSTs `params` as JSON.
165
+
166
+ For CLI commands (`minions <cmd>`), use Bash to invoke them when delegating would be heavier than just running the command.