@yemi33/minions 0.1.1772 → 0.1.1774

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.1774 (2026-05-07)
4
+
5
+ ### Features
6
+ - suppress stale doc-chat model errors
7
+
8
+ ### Fixes
9
+ - yemi33/minions#2168
10
+
3
11
  ## 0.1.1772 (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
@@ -281,12 +281,23 @@ function createWorkItemWithDedup(wiPath, item, options = {}) {
281
281
  return result || { created: false, item: null };
282
282
  }
283
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
+
284
295
  function resolveWorkItemsCreateTarget(projectName, projects = PROJECTS) {
285
296
  const project = String(projectName || '').trim();
286
297
  let targetProject = null;
287
298
  if (project) {
288
- targetProject = projects.find(p => p.name === project) || (projects.length > 0 ? projects[0] : null);
289
- if (!targetProject) return { error: 'No projects configured' };
299
+ targetProject = findProjectByName(projects, project);
300
+ if (!targetProject) return { error: formatUnknownProjectError(project, projects) };
290
301
  } else if (projects.length === 1) {
291
302
  targetProject = projects[0];
292
303
  }
@@ -326,7 +337,13 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
326
337
  throw err;
327
338
  }
328
339
  const projects = shared.getProjects(config);
329
- 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
+ }
330
347
  const prPath = targetProject ? shared.projectPrPath(targetProject) : path.join(MINIONS_DIR, 'pull-requests.json');
331
348
 
332
349
  const prNumMatch = url.match(/\/pull\/(\d+)|pullrequest\/(\d+)/);
@@ -2704,23 +2721,19 @@ function _formatDocChatContext({ document, title, filePath, selection, canEdit,
2704
2721
  return context;
2705
2722
  }
2706
2723
 
2707
- // Map errorClass codes from the runtime adapter to actionable user-facing messages.
2724
+ // Map runtime failures to user-facing messages.
2708
2725
  // sessionPreserved=true means ccCall preserved the session — user can retry immediately.
2709
2726
  // toolUses=[] from result.toolUses lets the message warn that tools may have already
2710
2727
  // modified files/state before the failure — the user shouldn't assume nothing happened.
2711
- // errorMessage is the runtime adapter's own remediation string (from parseError)
2712
- // authoritative because it knows whether the runtime is Claude, Copilot, or another;
2713
- // dashboard falls back to generic copy only when the adapter didn't supply one.
2728
+ // errorMessage is the runtime adapter's own text from parseError; when present it
2729
+ // is authoritative and is shown directly rather than replaced with dashboard copy.
2714
2730
  function _docChatErrorMessage(errorClass, sessionPreserved = false, toolUses = [], errorMessage = null) {
2715
2731
  const tools = Array.isArray(toolUses) ? toolUses : [];
2716
2732
  const toolHint = tools.length > 0
2717
2733
  ? ` (${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.)`
2718
2734
  : '';
2719
- if (errorClass === 'auth-failure') return (errorMessage || 'Runtime authentication failed — check your CLI auth or API key, then try again.') + toolHint;
2720
- if (errorClass === 'context-limit') return 'Session context is too long. Click "Clear" to start a fresh conversation.' + toolHint;
2721
- if (errorClass === 'budget-exceeded') return (errorMessage || 'Runtime budget exceeded — check your account or quota.') + toolHint;
2722
- if (errorClass === 'crash') return (errorMessage || 'Runtime crashed unexpectedly. Try again.') + toolHint;
2723
- if (errorClass === 'unknown-model') return (errorMessage || 'Configured model is not valid for the active runtime. Update engine.ccModel or engine.defaultModel.') + toolHint;
2735
+ const directError = typeof errorMessage === 'string' ? errorMessage.trim() : '';
2736
+ if (directError) return directError + toolHint;
2724
2737
  if (sessionPreserved) return 'Temporary connection issue — your conversation is intact, send your message again.' + toolHint;
2725
2738
  if (tools.length > 0) return 'The agent stopped responding before producing a final answer.' + toolHint;
2726
2739
  return 'Failed to process request. Try again.';
@@ -2729,14 +2742,26 @@ function _docChatErrorMessage(errorClass, sessionPreserved = false, toolUses = [
2729
2742
  // Secondary note rendered alongside a recovered partial answer — distinct from the
2730
2743
  // hard-failure message because the answer/actions/document-edit DID land. The user
2731
2744
  // just needs to know the run wasn't clean.
2732
- function _docChatPartialWarning(errorClass) {
2733
- if (errorClass === 'auth-failure') return 'Note: auth failed — answer recovered from partial output, but follow-up turns may not work.';
2734
- if (errorClass === 'context-limit') return 'Note: session context was too long — answer recovered. Click "Clear" before continuing.';
2735
- if (errorClass === 'budget-exceeded') return 'Note: runtime budget exceeded — answer recovered, but further calls may fail.';
2736
- if (errorClass === 'crash') return 'Note: runtime crashed before clean exit, but a complete response was recovered.';
2745
+ function _docChatPartialWarning(errorClass, errorMessage = null) {
2746
+ const directError = typeof errorMessage === 'string' ? errorMessage.trim() : '';
2747
+ if (directError) return `Note: ${directError}`;
2737
2748
  return 'Note: the agent exited unexpectedly. A complete response was recovered — verify any saved files or dispatched actions.';
2738
2749
  }
2739
2750
 
2751
+ function _docChatResultHasVisibleError(result) {
2752
+ if (!result) return false;
2753
+ if (result.errorClass) return true;
2754
+ if (typeof result.errorMessage === 'string' && result.errorMessage.trim()) return true;
2755
+ // stderr without a classified/runtime error is often CLI diagnostics; hard
2756
+ // failures with no answer still surface stderr through _docChatFailureResponse.
2757
+ return false;
2758
+ }
2759
+
2760
+ function _docChatResultLooksSuccessful(result) {
2761
+ if (!result || !result.text) return false;
2762
+ return result.code === 0 || !_docChatResultHasVisibleError(result);
2763
+ }
2764
+
2740
2765
  // Build the doc-chat extraContext for a single ccCall pass — refreshed on retry
2741
2766
  // so a fresh-session retry includes the full document instead of relying on the
2742
2767
  // dead session's prior turn for context.
@@ -2813,7 +2838,7 @@ function _recoverPartialDocChatResponse(result, sessionKey) {
2813
2838
  return {
2814
2839
  ...parsed,
2815
2840
  partial: true,
2816
- warning: _docChatPartialWarning(result.errorClass),
2841
+ warning: _docChatPartialWarning(result.errorClass, result.errorMessage || result.stderr || null),
2817
2842
  toolUses: Array.isArray(result.toolUses) ? result.toolUses : [],
2818
2843
  // Recovery path still attaches the raw runtime failure — the answer landed
2819
2844
  // despite a non-zero exit; users still benefit from seeing why.
@@ -2821,6 +2846,41 @@ function _recoverPartialDocChatResponse(result, sessionKey) {
2821
2846
  };
2822
2847
  }
2823
2848
 
2849
+ function _shouldSuppressDocChatPostPatchError(ccError, finalize) {
2850
+ if (!finalize || finalize.edited !== true) return false;
2851
+ if (!ccError || ccError.errorClass !== 'unknown-model') return false;
2852
+ return String(ccError.runtime || '').toLowerCase() === 'copilot';
2853
+ }
2854
+
2855
+ function _buildDocChatResponsePayload({
2856
+ answer,
2857
+ actions,
2858
+ actionResults,
2859
+ actionParseError,
2860
+ ccError,
2861
+ partial,
2862
+ warning,
2863
+ toolUses,
2864
+ finalize,
2865
+ } = {}) {
2866
+ const final = finalize || { edited: false, content: null, answerSuffix: '' };
2867
+ const suppressPostPatchError = _shouldSuppressDocChatPostPatchError(ccError, final);
2868
+ const visibleAnswer = suppressPostPatchError && !partial ? 'Updated the document.' : answer;
2869
+ const finalAnswer = final.answerSuffix ? visibleAnswer + final.answerSuffix : visibleAnswer;
2870
+ return {
2871
+ ok: !(ccError && !suppressPostPatchError),
2872
+ answer: finalAnswer,
2873
+ actions,
2874
+ ...(actionResults ? { actionResults } : {}),
2875
+ ...(actionParseError ? { actionParseError } : {}),
2876
+ ...(ccError && !suppressPostPatchError ? { error: ccError } : {}),
2877
+ ...(partial && !suppressPostPatchError ? { partial: true, warning } : {}),
2878
+ ...(Array.isArray(toolUses) && toolUses.length ? { toolUses } : {}),
2879
+ edited: final.edited,
2880
+ ...(final.edited && final.content !== null ? { content: final.content } : {}),
2881
+ };
2882
+ }
2883
+
2824
2884
 
2825
2885
  // True when the file is a meeting JSON whose status forbids edits. Loaded
2826
2886
  // fresh on each call because meeting status can change while a doc-chat is
@@ -2973,7 +3033,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
2973
3033
  // bleed into future interactions under the same key.
2974
3034
  docSessions.delete(sessionKey);
2975
3035
  schedulePersistDocSessions();
2976
- } else if (result.code === 0 && result.sessionId) {
3036
+ } else if (_docChatResultLooksSuccessful(result) && result.sessionId) {
2977
3037
  // Store doc hash for next call's unchanged check
2978
3038
  const session = resolveSession('doc', sessionKey);
2979
3039
  if (session) session._docHash = initialPass.docHash;
@@ -2983,7 +3043,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
2983
3043
  return { answer: result.text || result.stderr || 'Minions runtime is not installed or configured.', content: null, actions: [] };
2984
3044
  }
2985
3045
 
2986
- if (result.code !== 0 || !result.text) {
3046
+ if (!_docChatResultLooksSuccessful(result)) {
2987
3047
  // Try to salvage a parseable answer / action / document edit before failing.
2988
3048
  const recovered = _recoverPartialDocChatResponse(result, sessionKey);
2989
3049
  if (recovered) return recovered;
@@ -3034,7 +3094,7 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
3034
3094
  if (freshSession && sessionKey) {
3035
3095
  docSessions.delete(sessionKey);
3036
3096
  schedulePersistDocSessions();
3037
- } else if (result.code === 0 && result.sessionId) {
3097
+ } else if (_docChatResultLooksSuccessful(result) && result.sessionId) {
3038
3098
  const session = resolveSession('doc', sessionKey);
3039
3099
  if (session) session._docHash = initialPass.docHash;
3040
3100
  }
@@ -3043,7 +3103,7 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
3043
3103
  return { answer: result.text || result.stderr || 'Minions runtime is not installed or configured.', content: null, actions: [] };
3044
3104
  }
3045
3105
 
3046
- if (result.code !== 0 || !result.text) {
3106
+ if (!_docChatResultLooksSuccessful(result)) {
3047
3107
  const recovered = _recoverPartialDocChatResponse(result, sessionKey);
3048
3108
  if (recovered) return recovered;
3049
3109
  const sessionPreserved = !!(resolveSession('doc', sessionKey)?.sessionId);
@@ -5184,20 +5244,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5184
5244
  filePath: body.filePath, fullPath, isJson, canEdit,
5185
5245
  originalContent: currentContent, delimiterContent: content,
5186
5246
  });
5187
- const finalAnswer = finalize.answerSuffix ? answer + finalize.answerSuffix : answer;
5188
- _docDone = true;
5189
- return jsonReply(res, 200, {
5190
- ok: !ccError,
5191
- answer: finalAnswer,
5192
- actions,
5193
- ...(actionResults ? { actionResults } : {}),
5194
- ...(actionParseError ? { actionParseError } : {}),
5195
- ...(ccError ? { error: ccError } : {}),
5196
- ...(partial ? { partial: true, warning } : {}),
5197
- ...(Array.isArray(toolUses) && toolUses.length ? { toolUses } : {}),
5198
- edited: finalize.edited,
5199
- ...(finalize.edited && finalize.content !== null ? { content: finalize.content } : {}),
5247
+ const payload = _buildDocChatResponsePayload({
5248
+ answer, actions, actionResults, actionParseError, ccError, partial, warning, toolUses, finalize,
5200
5249
  });
5250
+ _docDone = true;
5251
+ return jsonReply(res, 200, payload);
5201
5252
  } finally { _docAbort = null; _docDone = true; docChatInFlight.delete(docKey); }
5202
5253
  } catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
5203
5254
  }
@@ -5286,18 +5337,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5286
5337
  filePath: body.filePath, fullPath, isJson, canEdit,
5287
5338
  originalContent: currentContent, delimiterContent: content,
5288
5339
  });
5289
- const finalAnswer = finalize.answerSuffix ? answer + finalize.answerSuffix : answer;
5340
+ const payload = _buildDocChatResponsePayload({
5341
+ answer, actions, actionResults, actionParseError, ccError, partial, warning, toolUses, finalize,
5342
+ });
5343
+ const { answer: finalAnswer, ...donePayload } = payload;
5290
5344
  writeDocEvent({
5291
5345
  type: 'done',
5292
5346
  text: finalAnswer,
5293
- actions,
5294
- ...(actionResults ? { actionResults } : {}),
5295
- ...(actionParseError ? { actionParseError } : {}),
5296
- ...(ccError ? { error: ccError } : {}),
5297
- ...(partial ? { partial: true, warning } : {}),
5298
- ...(Array.isArray(toolUses) && toolUses.length ? { toolUses } : {}),
5299
- edited: finalize.edited,
5300
- ...(finalize.edited && finalize.content !== null ? { content: finalize.content } : {}),
5347
+ ...donePayload,
5301
5348
  });
5302
5349
  _docStreamEnded = true;
5303
5350
  res.end();
@@ -7121,6 +7168,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7121
7168
  if (!url) return jsonReply(res, 400, { error: 'url required' });
7122
7169
 
7123
7170
  reloadConfig();
7171
+ const explicitProjectName = String(body.project || '').trim();
7172
+ if (explicitProjectName) {
7173
+ const projects = shared.getProjects(CONFIG);
7174
+ if (!findProjectByName(projects, explicitProjectName)) {
7175
+ return jsonReply(res, 400, { error: formatUnknownProjectError(explicitProjectName, projects) }, req);
7176
+ }
7177
+ }
7124
7178
  const adoTarget = parseAdoPrMetadataTarget(url);
7125
7179
  let initialPrData = null;
7126
7180
  if (adoTarget) {
@@ -7130,7 +7184,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7130
7184
  shared.log('warn', `ADO PR link metadata fetch failed for ${url}: ${e.message}`);
7131
7185
  }
7132
7186
  }
7133
- const { id: prId, prPath, prNum, created, linked } = linkPullRequestForTracking(body, CONFIG, { metadata: initialPrData });
7187
+ let linkResult;
7188
+ try {
7189
+ linkResult = linkPullRequestForTracking(body, CONFIG, { metadata: initialPrData });
7190
+ } catch (e) {
7191
+ return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
7192
+ }
7193
+ const { id: prId, prPath, prNum, created, linked } = linkResult;
7134
7194
  invalidateStatusCache();
7135
7195
  jsonReply(res, 200, { ok: true, id: prId, created, linked });
7136
7196
 
@@ -7268,6 +7328,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7268
7328
  { method: 'POST', path: /^\/api\/agent\/([\w-]+)\/kill$/, desc: 'Kill a running agent: stop process, clear dispatch, reset work items to pending', handler: handleAgentKill },
7269
7329
  { method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/, desc: 'SSE real-time live output streaming', handler: handleAgentLiveStream },
7270
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 },
7271
7332
  { method: 'GET', path: /^\/api\/agent\/([\w-]+)\/output(?:\?.*)?$/, desc: 'Fetch final output.log for an agent', handler: handleAgentOutput },
7272
7333
  { method: 'GET', path: /^\/api\/agent\/([\w-]+)$/, desc: 'Get detailed agent info', handler: handleAgentDetail },
7273
7334
  { method: 'GET', path: /^\/api\/dispatch\/([\w.-]+)\/completion-report$/, desc: 'Read structured completion report for a dispatch', handler: (req, res, match) => {
@@ -7722,6 +7783,10 @@ module.exports = {
7722
7783
  _docChatPartialWarning,
7723
7784
  _docChatFailureResponse,
7724
7785
  _recoverPartialDocChatResponse,
7786
+ _docChatResultHasVisibleError,
7787
+ _docChatResultLooksSuccessful,
7788
+ _shouldSuppressDocChatPostPatchError,
7789
+ _buildDocChatResponsePayload,
7725
7790
  _linkPullRequestForTracking: linkPullRequestForTracking,
7726
7791
  _resolveSkillReadPath,
7727
7792
  DOC_CHAT_DOCUMENT_DELIMITER,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-07T17:21:57.007Z"
4
+ "cachedAt": "2026-05-07T21:34:32.453Z"
5
5
  }
@@ -634,25 +634,77 @@ function parseStreamChunk(line) {
634
634
 
635
635
  // ── Error Normalization ─────────────────────────────────────────────────────
636
636
 
637
- function parseError(rawOutput) {
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
- if (/not authenticated|copilot login|please.*log.*in|401|403 forbidden|unauthorized/i.test(text)) {
643
- return { message: 'Copilot/GitHub authentication failed. Run `gh auth login` or provide GH_TOKEN/COPILOT_GITHUB_TOKEN with Copilot access.', code: 'auth-failure', retriable: false };
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: 'Copilot rate limit hit', code: 'rate-limit', retriable: true };
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: 'Copilot rejected the requested model', code: 'unknown-model', retriable: false };
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: 'Copilot premium-request budget exceeded — check your GitHub Copilot quota.', code: 'budget-exceeded', retriable: false };
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: 'Copilot CLI crashed unexpectedly. Try again.', code: 'crash', retriable: true };
707
+ return { message: text, code: 'crash', retriable: true };
656
708
  }
657
709
  return { message: '', code: null, retriable: true };
658
710
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1772",
3
+ "version": "0.1.1774",
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"