@yemi33/minions 0.1.1771 → 0.1.1773

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1773 (2026-05-07)
4
+
5
+ ### 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
+ - fix copilot cc resume context (#2166)
10
+
3
11
  ## 0.1.1771 (2026-05-07)
4
12
 
5
13
  ### Features
package/README.md CHANGED
@@ -127,21 +127,31 @@ minions work "Explore the codebase and document the architecture"
127
127
  | `minions init` | Bootstrap `~/.minions/` with default agents and config |
128
128
  | `minions update` | Update to latest version (npm update + apply) |
129
129
  | `minions version` | Show installed vs package version |
130
+ | `minions doctor` | Check prerequisites and runtime health |
130
131
  | `minions scan [dir] [depth]` | Scan for git repos and multi-select to add (default: ~, depth 3) |
131
132
  | `minions add <dir>` | Link a single project (auto-detects settings from git, prompts to confirm) |
132
133
  | `minions remove <dir-or-name> [--keep-data \| --purge --force]` | Unlink a project: cancels pending work items, drains dispatch + kills active agents, cleans worktrees, disables linked schedules, archives `projects/<name>/` to `projects/.archived/<name>-YYYYMMDD/`. Use `--keep-data` to leave the data dir in place, or `--purge --force` to delete it. |
133
134
  | `minions list` | List all linked projects with descriptions |
135
+ | `minions restart` | Start engine and dashboard together (recommended after reboot) |
134
136
  | `minions start` | Start engine daemon (ticks every 60s, auto-syncs MCP servers) |
135
137
  | `minions stop` | Stop the engine |
136
138
  | `minions status` | Show agents, projects, dispatch queue, quality metrics |
137
139
  | `minions pause` / `resume` | Pause/resume dispatching |
138
140
  | `minions dispatch` | Force a dispatch cycle |
139
141
  | `minions discover` | Dry-run work discovery |
142
+ | `minions queue` | Show dispatch queue (pending, active, completed) |
143
+ | `minions sources` | Show work source status per project |
140
144
  | `minions work <title> [opts]` | Add to central work queue |
141
145
  | `minions spawn <agent> <prompt>` | Manually spawn an agent |
142
146
  | `minions plan <file\|text> [proj]` | Run a plan |
147
+ | `minions kill` | Kill all active agents and reset their dispatches to pending |
148
+ | `minions complete <dispatch-id>` | Manually mark a dispatch as completed |
149
+ | `minions config set-cli <R> [--model M]` | Persist the default runtime/model without starting the engine |
150
+ | `minions mcp-sync` | Sync MCP servers from `~/.claude.json` |
143
151
  | `minions cleanup` | Run cleanup manually (temp files, worktrees, zombies) |
144
152
  | `minions dash` | Open dashboard (starts if not already running, port 7331) |
153
+ | `minions nuke --confirm` | Factory reset runtime state and reset config to defaults |
154
+ | `minions uninstall --confirm` | Remove Minions state and uninstall the npm package |
145
155
 
146
156
  You can also run scripts directly: `node ~/.minions/engine.js start`, `node ~/.minions/dashboard.js`, etc.
147
157
 
package/bin/minions.js CHANGED
@@ -8,18 +8,29 @@
8
8
  * minions add <project-dir> Link a project (interactive)
9
9
  * minions remove <project-dir> Unlink a project
10
10
  * minions list List linked projects
11
+ * minions update Update to latest version
12
+ * minions version Show installed and package versions
13
+ * minions doctor Check prerequisites and runtime health
14
+ * minions restart Start engine + dashboard
11
15
  * minions start Start the engine
12
16
  * minions stop Stop the engine
13
17
  * minions status Show engine status
14
18
  * minions pause / resume Pause/resume dispatching
19
+ * minions queue Show dispatch queue
20
+ * minions sources Show work source status
15
21
  * minions dash Start the dashboard
16
22
  * minions work <title> [opts-json] Add a work item
17
23
  * minions spawn <agent> <prompt> Manually spawn an agent
18
24
  * minions dispatch Force a dispatch cycle
19
25
  * minions discover Dry-run work discovery
20
26
  * minions cleanup Run cleanup manually
27
+ * minions kill Kill active agents and reset to pending
28
+ * minions complete <dispatch-id> Mark a dispatch completed
29
+ * minions config set-cli <R> [--model M] Persist default runtime/model
21
30
  * minions plan <file|text> [proj] Run a plan
22
- * minions version Show installed and package versions
31
+ * minions mcp-sync Sync MCP servers from ~/.claude.json
32
+ * minions nuke --confirm Factory reset runtime state/config
33
+ * minions uninstall --confirm Remove Minions and uninstall package
23
34
  */
24
35
 
25
36
  const fs = require('fs');
@@ -642,6 +653,9 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
642
653
  minions plan <file|text> [proj] Run a plan
643
654
  minions kill Kill all active agents and reset to pending
644
655
  minions complete <dispatch-id> Manually mark a dispatch as completed
656
+ minions config set-cli <R> [--model M]
657
+ Persist default runtime/model without starting
658
+ minions mcp-sync Sync MCP servers from ~/.claude.json
645
659
  minions cleanup Clean temp files, worktrees, zombies
646
660
  minions nuke --confirm Factory reset (delete state, reset config to defaults)
647
661
  minions uninstall --confirm Remove everything + uninstall npm package
@@ -30,6 +30,32 @@ const QA_QUEUE_CAP = 10; // max queued messages
30
30
  const QA_STREAM_STALL_MS = 6 * 60 * 1000; // allow the full doc-chat timeout before treating heartbeat-only streams as stalled
31
31
  let _qaSessionKey = ''; // key for current conversation (title or filePath)
32
32
 
33
+ const QA_STICKY_BOTTOM_PX = 80;
34
+ let _qaThreadShouldFollow = true;
35
+
36
+ function _qaIsNearThreadBottom(thread) {
37
+ if (!thread) return true;
38
+ return thread.scrollHeight - thread.scrollTop - thread.clientHeight <= QA_STICKY_BOTTOM_PX;
39
+ }
40
+
41
+ function _qaShouldFollowThread(thread) {
42
+ return _qaThreadShouldFollow && _qaIsNearThreadBottom(thread);
43
+ }
44
+
45
+ function _qaSetThreadFollowFromScroll(thread) {
46
+ _qaThreadShouldFollow = _qaIsNearThreadBottom(thread);
47
+ }
48
+
49
+ function _qaScrollThreadToBottom(thread) {
50
+ if (!thread) return;
51
+ thread.scrollTop = thread.scrollHeight;
52
+ _qaThreadShouldFollow = true;
53
+ }
54
+
55
+ function _qaMaybeScrollThreadToBottom(thread, shouldFollow) {
56
+ if (shouldFollow) _qaScrollThreadToBottom(thread);
57
+ }
58
+
33
59
  // Insert html at the bottom of the thread but above any pending "Queued: ..."
34
60
  // strips, so queued messages always remain visually below the active progress
35
61
  // UX / answer / errors.
@@ -47,7 +73,7 @@ function _renderQaUserMessage(thread, message, selection) {
47
73
  }
48
74
  qHtml += '</div>';
49
75
  _qaInsertBeforeQueued(thread, qHtml);
50
- thread.scrollTop = thread.scrollHeight;
76
+ _qaScrollThreadToBottom(thread);
51
77
  _showThreadWrap();
52
78
  }
53
79
  const _qaSessions = new Map(); // persist conversations across modal open/close {key → {history, threadHtml}}
@@ -91,6 +117,10 @@ function _qaThreadEl() {
91
117
  return document.getElementById('modal-qa-thread');
92
118
  }
93
119
 
120
+ document.addEventListener('scroll', function(e) {
121
+ if (e.target && e.target.id === 'modal-qa-thread') _qaSetThreadFollowFromScroll(e.target);
122
+ }, true);
123
+
94
124
  function _qaThreadHtml() {
95
125
  return (_qaThreadEl() || {}).innerHTML || '';
96
126
  }
@@ -308,8 +338,9 @@ function _qaMutateThreadHtml(key, mutate) {
308
338
  const wasCollapsed = _qaIsThreadCollapsed();
309
339
  const thread = _qaThreadEl();
310
340
  if (thread) {
341
+ const shouldFollow = _qaShouldFollowThread(thread);
311
342
  thread.innerHTML = html;
312
- thread.scrollTop = thread.scrollHeight;
343
+ _qaMaybeScrollThreadToBottom(thread, shouldFollow);
313
344
  }
314
345
  if (wasCollapsed) _setQaThreadCollapsed(true);
315
346
  else _showThreadWrap();
@@ -386,7 +417,7 @@ function _initQaSession() {
386
417
  _showThreadWrap();
387
418
  requestAnimationFrame(function() {
388
419
  var thread = document.getElementById('modal-qa-thread');
389
- if (thread) thread.scrollTop = thread.scrollHeight;
420
+ _qaScrollThreadToBottom(thread);
390
421
  });
391
422
  if (_qaQueue.length > 0 && !_qaProcessing) {
392
423
  setTimeout(_qaResumeQueuedMessages, 0);
@@ -394,6 +425,7 @@ function _initQaSession() {
394
425
  } else {
395
426
  _qaHistory = [];
396
427
  document.getElementById('modal-qa-thread').innerHTML = '';
428
+ _qaThreadShouldFollow = true;
397
429
  var wrap = document.getElementById('modal-qa-thread-wrap');
398
430
  var expandBar = document.getElementById('qa-expand-bar');
399
431
  if (wrap) wrap.style.display = 'none';
@@ -410,6 +442,7 @@ function clearQaConversation() {
410
442
  _qaProcessing = false;
411
443
  _qaAbortController = null;
412
444
  document.getElementById('modal-qa-thread').innerHTML = '';
445
+ _qaThreadShouldFollow = true;
413
446
  var wrap = document.getElementById('modal-qa-thread-wrap');
414
447
  var expandBar = document.getElementById('qa-expand-bar');
415
448
  if (wrap) wrap.style.display = 'none';
@@ -455,7 +488,7 @@ function modalSend() {
455
488
  }
456
489
  _qaQueue.push({ message, selection });
457
490
  thread.insertAdjacentHTML('beforeend', _qaBuildQueuedHtml(message));
458
- thread.scrollTop = thread.scrollHeight;
491
+ _qaScrollThreadToBottom(thread);
459
492
  _showThreadWrap();
460
493
  _qaSaveActiveSessionState();
461
494
  return;
package/dashboard.js CHANGED
@@ -10,6 +10,7 @@ const zlib = require('zlib');
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
  const llm = require('./engine/llm');
13
+ const { resolveRuntime } = require('./engine/runtimes');
13
14
 
14
15
  // Dashboard version stamp — captured at module load so it reflects the code actually running
15
16
  const _dashboardVersion = {
@@ -280,12 +281,23 @@ function createWorkItemWithDedup(wiPath, item, options = {}) {
280
281
  return result || { created: false, item: null };
281
282
  }
282
283
 
284
+ function formatUnknownProjectError(projectName, projects = []) {
285
+ const known = projects.map(p => p.name).filter(Boolean).join(', ') || '(none configured)';
286
+ return `Project "${projectName}" not found. Known projects: ${known}`;
287
+ }
288
+
289
+ function findProjectByName(projects, projectName) {
290
+ const name = String(projectName || '').trim().toLowerCase();
291
+ if (!name) return null;
292
+ return projects.find(p => p.name?.toLowerCase() === name) || null;
293
+ }
294
+
283
295
  function resolveWorkItemsCreateTarget(projectName, projects = PROJECTS) {
284
296
  const project = String(projectName || '').trim();
285
297
  let targetProject = null;
286
298
  if (project) {
287
- targetProject = projects.find(p => p.name === project) || (projects.length > 0 ? projects[0] : null);
288
- if (!targetProject) return { error: 'No projects configured' };
299
+ targetProject = findProjectByName(projects, project);
300
+ if (!targetProject) return { error: formatUnknownProjectError(project, projects) };
289
301
  } else if (projects.length === 1) {
290
302
  targetProject = projects[0];
291
303
  }
@@ -325,7 +337,13 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
325
337
  throw err;
326
338
  }
327
339
  const projects = shared.getProjects(config);
328
- const targetProject = projectName ? projects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) : (projects[0] || null);
340
+ const explicitProjectName = String(projectName || '').trim();
341
+ const targetProject = explicitProjectName ? findProjectByName(projects, explicitProjectName) : (projects[0] || null);
342
+ if (explicitProjectName && !targetProject) {
343
+ const err = new Error(formatUnknownProjectError(explicitProjectName, projects));
344
+ err.statusCode = 400;
345
+ throw err;
346
+ }
329
347
  const prPath = targetProject ? shared.projectPrPath(targetProject) : path.join(MINIONS_DIR, 'pull-requests.json');
330
348
 
331
349
  const prNumMatch = url.match(/\/pull\/(\d+)|pullrequest\/(\d+)/);
@@ -1262,9 +1280,14 @@ function _readCcTabSessions({ prune = true } = {}) {
1262
1280
  const CC_CARRYOVER_MAX_TURNS = 20;
1263
1281
  const CC_CARRYOVER_PER_MSG_CHARS = 2000;
1264
1282
 
1265
- function _buildTranscriptCarryover(transcript, { previousRuntime } = {}) {
1283
+ function _buildTranscriptCarryover(transcript, { previousRuntime, currentMessage } = {}) {
1266
1284
  if (!Array.isArray(transcript) || transcript.length === 0) return '';
1267
- const filtered = transcript.filter(m => m && (m.role === 'user' || m.role === 'assistant') && typeof m.text === 'string' && m.text.trim());
1285
+ let filtered = transcript.filter(m => m && (m.role === 'user' || m.role === 'assistant') && typeof m.text === 'string' && m.text.trim());
1286
+ const current = typeof currentMessage === 'string' ? currentMessage.trim() : '';
1287
+ if (current && filtered.length > 0) {
1288
+ const last = filtered[filtered.length - 1];
1289
+ if (last.role === 'user' && last.text.trim() === current) filtered = filtered.slice(0, -1);
1290
+ }
1268
1291
  if (filtered.length === 0) return '';
1269
1292
  const recent = filtered.slice(-CC_CARRYOVER_MAX_TURNS);
1270
1293
  const truncated = filtered.length > recent.length;
@@ -1281,6 +1304,19 @@ function _buildTranscriptCarryover(transcript, { previousRuntime } = {}) {
1281
1304
  return `${header}\n\n${truncationNote}${lines.join('\n\n')}\n\n--- Current message follows ---`;
1282
1305
  }
1283
1306
 
1307
+ function _ccRuntimeNeedsResumeCarryover(runtimeName) {
1308
+ try {
1309
+ const runtime = resolveRuntime(runtimeName);
1310
+ return !!runtime?.capabilities?.resumePromptCarryover;
1311
+ } catch {
1312
+ return false;
1313
+ }
1314
+ }
1315
+
1316
+ function _joinCcPromptParts(...parts) {
1317
+ return parts.filter(Boolean).join('\n\n---\n\n');
1318
+ }
1319
+
1284
1320
  // Load persisted CC session on startup. CC chat sessions are non-expiring;
1285
1321
  // only restore-time validity checks here are sessionId presence (anything
1286
1322
  // else would auto-expire the user's chat without their consent).
@@ -2415,7 +2451,7 @@ async function _preflightModelCheck({ runtime: cliOverride, model: modelOverride
2415
2451
  * @param {number} opts.maxTurns - Max tool-use turns
2416
2452
  * @param {string} opts.allowedTools - Comma-separated tool list
2417
2453
  */
2418
- async function ccCall(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, systemPrompt = CC_STATIC_SYSTEM_PROMPT } = {}) {
2454
+ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, systemPrompt = CC_STATIC_SYSTEM_PROMPT, transcript } = {}) {
2419
2455
  if (!maxTurns) maxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
2420
2456
  if (!model) model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
2421
2457
  const ccEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
@@ -2428,10 +2464,15 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
2428
2464
 
2429
2465
  const existing = resolveSession(store, sessionKey);
2430
2466
  let sessionId = existing ? existing.sessionId : null;
2467
+ const resumeNeedsCarryover = !!sessionId && _ccRuntimeNeedsResumeCarryover(shared.resolveCcCli(CONFIG.engine));
2431
2468
 
2432
- function buildPrompt({ includePreamble = true } = {}) {
2469
+ function buildPrompt({ includePreamble = true, includeCarryover = false } = {}) {
2433
2470
  const parts = (!skipStatePreamble && includePreamble) ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
2434
2471
  if (extraContext) parts.push(extraContext);
2472
+ if (includeCarryover) {
2473
+ const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message });
2474
+ if (carryover) parts.push(carryover);
2475
+ }
2435
2476
  parts.push(message);
2436
2477
  return parts.join('\n\n---\n\n');
2437
2478
  }
@@ -2440,7 +2481,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
2440
2481
 
2441
2482
  // Attempt 1: resume existing session — skip preamble (session already has context)
2442
2483
  if (sessionId && maxTurns > 1) {
2443
- const p1 = llm.callLLM(buildPrompt({ includePreamble: false }), '', {
2484
+ const p1 = llm.callLLM(buildPrompt({ includePreamble: false, includeCarryover: resumeNeedsCarryover }), '', {
2444
2485
  timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
2445
2486
  engineConfig: CONFIG.engine,
2446
2487
  });
@@ -2477,7 +2518,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
2477
2518
  }
2478
2519
 
2479
2520
  // Attempt 2: fresh session (include preamble for full context)
2480
- const freshPrompt = buildPrompt();
2521
+ const freshPrompt = buildPrompt({ includeCarryover: resumeNeedsCarryover });
2481
2522
  const p2 = llm.callLLM(freshPrompt, systemPrompt, {
2482
2523
  timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
2483
2524
  engineConfig: CONFIG.engine,
@@ -2511,7 +2552,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
2511
2552
  return result;
2512
2553
  }
2513
2554
 
2514
- async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, onChunk, onToolUse, onRetry, systemPrompt = CC_STATIC_SYSTEM_PROMPT } = {}) {
2555
+ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext, label = 'command-center', timeout = CC_CALL_TIMEOUT_MS, maxTurns, allowedTools = 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch', skipStatePreamble = false, model, onAbortReady, onChunk, onToolUse, onRetry, systemPrompt = CC_STATIC_SYSTEM_PROMPT, transcript } = {}) {
2515
2556
  if (!maxTurns) maxTurns = CONFIG.engine?.ccMaxTurns || shared.ENGINE_DEFAULTS.ccMaxTurns;
2516
2557
  if (!model) model = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
2517
2558
  const ccEffort = CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort;
@@ -2524,10 +2565,15 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
2524
2565
 
2525
2566
  const existing = resolveSession(store, sessionKey);
2526
2567
  let sessionId = existing ? existing.sessionId : null;
2568
+ const resumeNeedsCarryover = !!sessionId && _ccRuntimeNeedsResumeCarryover(shared.resolveCcCli(CONFIG.engine));
2527
2569
 
2528
- function buildPrompt({ includePreamble = true } = {}) {
2570
+ function buildPrompt({ includePreamble = true, includeCarryover = false } = {}) {
2529
2571
  const parts = (!skipStatePreamble && includePreamble) ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
2530
2572
  if (extraContext) parts.push(extraContext);
2573
+ if (includeCarryover) {
2574
+ const carryover = _buildTranscriptCarryover(transcript, { currentMessage: message });
2575
+ if (carryover) parts.push(carryover);
2576
+ }
2531
2577
  parts.push(message);
2532
2578
  return parts.join('\n\n---\n\n');
2533
2579
  }
@@ -2535,7 +2581,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
2535
2581
  let result;
2536
2582
 
2537
2583
  if (sessionId && maxTurns > 1) {
2538
- const p1 = llm.callLLMStreaming(buildPrompt({ includePreamble: false }), '', {
2584
+ const p1 = llm.callLLMStreaming(buildPrompt({ includePreamble: false, includeCarryover: resumeNeedsCarryover }), '', {
2539
2585
  timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
2540
2586
  engineConfig: CONFIG.engine,
2541
2587
  onChunk,
@@ -2572,7 +2618,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
2572
2618
  }
2573
2619
 
2574
2620
  if (onRetry) onRetry(2);
2575
- const freshPrompt = buildPrompt();
2621
+ const freshPrompt = buildPrompt({ includeCarryover: resumeNeedsCarryover });
2576
2622
  const p2 = llm.callLLMStreaming(freshPrompt, systemPrompt, {
2577
2623
  timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
2578
2624
  engineConfig: CONFIG.engine,
@@ -5754,7 +5800,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5754
5800
  }
5755
5801
  const wasResume = !!(body.sessionId && body.sessionId === ccSession.sessionId && ccSessionValid());
5756
5802
 
5757
- const result = await ccCall(body.message, { store: 'cc' });
5803
+ const result = await ccCall(body.message, { store: 'cc', transcript: body.transcript });
5758
5804
 
5759
5805
  // Non-zero exit with text = max_turns or partial success — still usable
5760
5806
  if (!result.text) {
@@ -5989,13 +6035,21 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5989
6035
  sessionReset = true;
5990
6036
  sessionResetReason = 'runtimeChanged';
5991
6037
  previousRuntime = tabEntry.runtime;
6038
+ } else if (tabEntry.sessionId && tabEntry.sessionId !== tabSessionId) {
6039
+ tabSessionId = tabEntry.sessionId;
5992
6040
  }
5993
6041
  }
5994
6042
  const wasResume = !!tabSessionId;
5995
6043
  const sessionId = tabSessionId || null;
6044
+ const resumeNeedsCarryover = wasResume && _ccRuntimeNeedsResumeCarryover(currentRuntime);
5996
6045
  const preamble = wasResume ? '' : buildCCStatePreamble();
5997
- const carryover = sessionReset ? _buildTranscriptCarryover(body.transcript, { previousRuntime }) : '';
5998
- const prompt = (preamble ? preamble + '\n\n---\n\n' : '') + (carryover ? carryover + '\n\n---\n\n' : '') + body.message;
6046
+ const carryover = (sessionReset || resumeNeedsCarryover)
6047
+ ? _buildTranscriptCarryover(body.transcript, {
6048
+ previousRuntime: sessionReset ? previousRuntime : null,
6049
+ currentMessage: body.message,
6050
+ })
6051
+ : '';
6052
+ const prompt = _joinCcPromptParts(preamble, carryover, body.message);
5999
6053
 
6000
6054
  const { trackEngineUsage: trackUsage } = require('./engine/llm');
6001
6055
  const streamModel = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
@@ -6023,7 +6077,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6023
6077
  // Resume failed (stale/expired session) — auto-retry as fresh session (skip if client already disconnected)
6024
6078
  console.log(`[CC-stream] Resume failed (code=${result.code}) — retrying fresh`);
6025
6079
  const freshPreamble = buildCCStatePreamble();
6026
- const freshPrompt = (freshPreamble ? freshPreamble + '\n\n---\n\n' : '') + body.message;
6080
+ const freshCarryover = _buildTranscriptCarryover(body.transcript, { currentMessage: body.message });
6081
+ const freshPrompt = _joinCcPromptParts(freshPreamble, freshCarryover, body.message);
6027
6082
  toolUses = []; // discard stale metadata from the failed resume attempt
6028
6083
  const retryPromise = _invokeCcStream({
6029
6084
  prompt: freshPrompt, sessionId: undefined, liveState, toolUses,
@@ -7083,6 +7138,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7083
7138
  if (!url) return jsonReply(res, 400, { error: 'url required' });
7084
7139
 
7085
7140
  reloadConfig();
7141
+ const explicitProjectName = String(body.project || '').trim();
7142
+ if (explicitProjectName) {
7143
+ const projects = shared.getProjects(CONFIG);
7144
+ if (!findProjectByName(projects, explicitProjectName)) {
7145
+ return jsonReply(res, 400, { error: formatUnknownProjectError(explicitProjectName, projects) }, req);
7146
+ }
7147
+ }
7086
7148
  const adoTarget = parseAdoPrMetadataTarget(url);
7087
7149
  let initialPrData = null;
7088
7150
  if (adoTarget) {
@@ -7092,7 +7154,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7092
7154
  shared.log('warn', `ADO PR link metadata fetch failed for ${url}: ${e.message}`);
7093
7155
  }
7094
7156
  }
7095
- const { id: prId, prPath, prNum, created, linked } = linkPullRequestForTracking(body, CONFIG, { metadata: initialPrData });
7157
+ let linkResult;
7158
+ try {
7159
+ linkResult = linkPullRequestForTracking(body, CONFIG, { metadata: initialPrData });
7160
+ } catch (e) {
7161
+ return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
7162
+ }
7163
+ const { id: prId, prPath, prNum, created, linked } = linkResult;
7096
7164
  invalidateStatusCache();
7097
7165
  jsonReply(res, 200, { ok: true, id: prId, created, linked });
7098
7166
 
@@ -7230,6 +7298,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7230
7298
  { method: 'POST', path: /^\/api\/agent\/([\w-]+)\/kill$/, desc: 'Kill a running agent: stop process, clear dispatch, reset work items to pending', handler: handleAgentKill },
7231
7299
  { method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/, desc: 'SSE real-time live output streaming', handler: handleAgentLiveStream },
7232
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 },
7233
7302
  { method: 'GET', path: /^\/api\/agent\/([\w-]+)\/output(?:\?.*)?$/, desc: 'Fetch final output.log for an agent', handler: handleAgentOutput },
7234
7303
  { method: 'GET', path: /^\/api\/agent\/([\w-]+)$/, desc: 'Get detailed agent info', handler: handleAgentDetail },
7235
7304
  { method: 'GET', path: /^\/api\/dispatch\/([\w.-]+)\/completion-report$/, desc: 'Read structured completion report for a dispatch', handler: (req, res, match) => {
@@ -7695,6 +7764,9 @@ module.exports = {
7695
7764
  _createPipelineFromAction: createPipelineFromAction,
7696
7765
  executeCCActions,
7697
7766
  buildCCStatePreamble,
7767
+ _buildTranscriptCarryover,
7768
+ _ccRuntimeNeedsResumeCarryover,
7769
+ _joinCcPromptParts,
7698
7770
  _captureApiRoutesMeta,
7699
7771
  _formatCcApiRoutesIndex,
7700
7772
  _formatCcCliCommandsIndex,
@@ -17,6 +17,7 @@
17
17
  | `capabilities.modelDiscovery` | **`true`** | `GET https://api.githubcopilot.com/models` with a `gh auth token` Bearer returns HTTP 200 + a 24-model JSON catalog. |
18
18
  | `capabilities.streaming` | **`true`** | `--stream on` (default) emits `assistant.message_delta` events incrementally; `--stream off` suppresses deltas but the final `assistant.message` always arrives. |
19
19
  | `capabilities.sessionResume` | **`true`** | `--resume <session-id>` documented, and every `result` event emits `sessionId`. |
20
+ | `capabilities.resumePromptCarryover` | **`true`** | Command Center resume turns should prepend the browser's recent Q&A transcript because Copilot's session store is opaque to Minions and can resume without enough conversational context. |
20
21
  | `capabilities.systemPromptFile` | **`false`** | No `--system-prompt-file` flag exists. Inject system prompt via a `<system>` block prepended to stdin. |
21
22
  | `capabilities.effortLevels` | **`true`** | `--effort` accepts `low|medium|high|xhigh` (no `max`). Adapter must map `'max' → 'xhigh'`. |
22
23
  | `capabilities.costTracking` | **`false`** | `result.usage` contains `premiumRequests` (count, not USD), no token counts, no cost. |
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-07T16:58:12.762Z"
4
+ "cachedAt": "2026-05-07T17:23:11.001Z"
5
5
  }
@@ -717,6 +717,8 @@ const capabilities = {
717
717
  fallbackModel: true,
718
718
  // Engine controls session persistence (writes session.json on completion)
719
719
  sessionPersistenceControl: true,
720
+ // Claude resume reliably restores prior turns; do not duplicate browser transcript.
721
+ resumePromptCarryover: false,
720
722
  // Adapter implements createStreamConsumer(ctx) — required by llm.js accumulator
721
723
  streamConsumer: true,
722
724
  };
@@ -874,6 +874,9 @@ const capabilities = {
874
874
  fallbackModel: false,
875
875
  // Copilot manages session state internally in ~/.copilot/session-state/
876
876
  sessionPersistenceControl: false,
877
+ // CC resumes should include recent visible Q&A in stdin because Minions cannot
878
+ // inspect or repair Copilot's opaque session-state store when it drops context.
879
+ resumePromptCarryover: true,
877
880
  // Adapter implements createStreamConsumer(ctx) — required by llm.js accumulator
878
881
  streamConsumer: true,
879
882
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1771",
3
+ "version": "0.1.1773",
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"