@yemi33/minions 0.1.1752 → 0.1.1754

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1754 (2026-05-06)
4
+
5
+ ### Fixes
6
+ - canonical .minions home wins over nearest copied tree
7
+ - recover partial responses instead of always returning "Failed"
8
+ - surface adapter-supplied error messages instead of hardcoding Claude
9
+
10
+ ### Other
11
+ - refactor(doc-chat): dedupe error envelope shape into _buildDocChatErrorEnvelope
12
+
13
+ ## 0.1.1753 (2026-05-06)
14
+
15
+ ### Other
16
+ - refactor(doc-chat): drop brittle regex action gate in favour of sysprompt
17
+
3
18
  ## 0.1.1752 (2026-05-06)
4
19
 
5
20
  ### Features
package/bin/minions.js CHANGED
@@ -191,12 +191,14 @@ function resolveMinionsHome(forInit = false) {
191
191
 
192
192
  if (forInit) return path.join(process.cwd(), '.minions');
193
193
 
194
- const localRoot = findNearestLocalMinionsRoot(process.cwd());
195
- if (localRoot) return localRoot;
196
-
197
194
  const pointerRoot = readRootPointer();
198
195
  if (isInstalledRoot(pointerRoot)) return pointerRoot;
199
196
 
197
+ if (isInstalledRoot(DEFAULT_MINIONS_HOME)) return DEFAULT_MINIONS_HOME;
198
+
199
+ const localRoot = findNearestLocalMinionsRoot(process.cwd());
200
+ if (localRoot) return localRoot;
201
+
200
202
  return DEFAULT_MINIONS_HOME;
201
203
  }
202
204
 
@@ -205,6 +207,7 @@ let force = rest.includes('--force');
205
207
  const skipScan = rest.includes('--skip-scan');
206
208
  const skipStart = rest.includes('--skip-start') || rest.includes('--no-start');
207
209
  const MINIONS_HOME = resolveMinionsHome(cmd === 'init');
210
+ process.env.MINIONS_HOME = MINIONS_HOME;
208
211
  const POST_UPDATE_INIT_TIMEOUT_MS = 120000;
209
212
  const POST_UPDATE_RESTART_TIMEOUT_MS = 60000;
210
213
 
@@ -542,6 +545,7 @@ function delegate(script, args) {
542
545
  const child = spawn(process.execPath, [path.join(MINIONS_HOME, script), ...args], {
543
546
  stdio: 'inherit',
544
547
  cwd: MINIONS_HOME,
548
+ env: { ...process.env, MINIONS_HOME },
545
549
  });
546
550
  child.on('exit', code => process.exit(code || 0));
547
551
  }
@@ -184,6 +184,31 @@ function _qaBuildLoadingHtml(loadingId, queueCount) {
184
184
  qaQueueBadge + '</div>';
185
185
  }
186
186
 
187
+ // Render the raw error envelope (stderr + metadata) returned by ccDocCallStreaming
188
+ // when the runtime fails. Always shown — collapsed by default — so the user can
189
+ // inspect the actual CLI output instead of relying on the friendly summary.
190
+ function _qaBuildRawErrorHtml(err) {
191
+ if (!err) return '';
192
+ const meta = [];
193
+ if (err.runtime) meta.push('runtime: ' + escHtml(String(err.runtime)));
194
+ if (err.errorClass) meta.push('class: ' + escHtml(String(err.errorClass)));
195
+ if (err.code !== null && err.code !== undefined) meta.push('exit: ' + escHtml(String(err.code)));
196
+ const metaLine = meta.length
197
+ ? '<div style="font-size:10px;color:var(--muted);margin-bottom:4px">' + meta.join(' · ') + '</div>'
198
+ : '';
199
+ const adapterMessage = err.errorMessage
200
+ ? '<div style="font-size:11px;color:var(--text);margin-bottom:6px;white-space:pre-wrap">' + escHtml(String(err.errorMessage)) + '</div>'
201
+ : '';
202
+ const stderrText = err.stderr ? String(err.stderr) : '';
203
+ const stderrBlock = stderrText
204
+ ? '<pre style="margin:0;padding:6px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;font-size:11px;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto">' + escHtml(stderrText) + '</pre>'
205
+ : '<div style="font-size:11px;color:var(--muted)">(no stderr captured)</div>';
206
+ return '<details class="modal-qa-raw-error" style="margin:6px 0 12px;padding:6px 8px;border:1px solid var(--red);border-radius:4px;background:var(--surface)">' +
207
+ '<summary style="cursor:pointer;font-size:11px;color:var(--red)">Raw error output</summary>' +
208
+ '<div style="margin-top:6px">' + metaLine + adapterMessage + stderrBlock + '</div>' +
209
+ '</details>';
210
+ }
211
+
187
212
  function _qaBuildAssistantHtml(text, opts) {
188
213
  const body = opts?.isError ? escHtml(text) : renderMd(text);
189
214
  const style = opts?.isError
@@ -231,12 +256,14 @@ function _qaMutateThreadHtml(key, mutate) {
231
256
  mutate(tmp);
232
257
  const html = tmp.innerHTML;
233
258
  if (_qaIsActiveSession(key)) {
259
+ const wasCollapsed = _qaIsThreadCollapsed();
234
260
  const thread = _qaThreadEl();
235
261
  if (thread) {
236
262
  thread.innerHTML = html;
237
263
  thread.scrollTop = thread.scrollHeight;
238
264
  }
239
- _showThreadWrap();
265
+ if (wasCollapsed) _setQaThreadCollapsed(true);
266
+ else _showThreadWrap();
240
267
  }
241
268
  return html;
242
269
  }
@@ -544,17 +571,47 @@ async function _processQaMessage(message, selection, opts) {
544
571
  clearInterval(qaTimer);
545
572
  _clearQaStreamWatchdog();
546
573
  const qaElapsed = Math.round((Date.now() - qaStartTime) / 1000);
547
- const borderColor = evt.edited ? 'var(--green)' : 'var(--blue)';
574
+ // Pick the border that best reflects what actually happened:
575
+ // - partial recovery (work landed despite a non-zero exit) \u2192 orange
576
+ // (checked first because partial responses now also carry an error
577
+ // envelope so the raw stderr can be surfaced)
578
+ // - hard error (no recoverable answer) \u2192 red
579
+ // - successful edit \u2192 green
580
+ // - normal answer \u2192 blue
581
+ const borderColor = evt.partial
582
+ ? 'var(--orange)'
583
+ : evt.error
584
+ ? 'var(--red)'
585
+ : evt.edited ? 'var(--green)' : 'var(--blue)';
548
586
  const suffix = evt.edited ? '\n\n\u2713 Document saved.' : '';
549
- const answerHtml = _qaBuildAssistantHtml((evt.text || '') + suffix, { borderColor, elapsed: qaElapsed });
587
+ // Fall back to the live-streamed text when the backend produced no final
588
+ // answer \u2014 covers the "stream had visible chunks then returned empty" case.
589
+ const finalText = (evt.text && evt.text.trim()) ? evt.text : (streamedText || '');
590
+ let bodyText = finalText + suffix;
591
+ if (evt.partial && evt.warning) {
592
+ bodyText += '\n\n_' + evt.warning + '_';
593
+ }
594
+ // On hard failures, surface tool side-effects so the user knows what
595
+ // landed before the runtime gave up \u2014 silent destruction was the worst
596
+ // class of "Failed to process request" UX.
597
+ if (evt.error && Array.isArray(evt.toolUses) && evt.toolUses.length > 0) {
598
+ const names = evt.toolUses.slice(0, 8).map(t => t.name).join(', ');
599
+ const more = evt.toolUses.length > 8 ? '\u2026' : '';
600
+ bodyText += '\n\n_Tools that ran before the failure: ' + names + more + ' \u2014 files or state may have been modified._';
601
+ }
602
+ const answerHtml = _qaBuildAssistantHtml(bodyText, { borderColor, elapsed: qaElapsed });
603
+ // On any runtime failure, surface the raw error so users can debug it
604
+ // directly instead of guessing what the friendly summary was hiding.
605
+ const rawErrorHtml = evt.error ? _qaBuildRawErrorHtml(evt.error) : '';
550
606
  let updatedThreadHtml = _qaMutateThreadHtml(sessionKey, tmp => {
551
607
  const loadingEl = tmp.querySelector('#' + loadingId);
552
608
  if (loadingEl) loadingEl.remove();
553
609
  tmp.insertAdjacentHTML('beforeend', answerHtml);
610
+ if (rawErrorHtml) tmp.insertAdjacentHTML('beforeend', rawErrorHtml);
554
611
  });
555
612
 
556
613
  runtime.history.push({ role: 'user', text: message });
557
- runtime.history.push({ role: 'assistant', text: evt.text || '' });
614
+ runtime.history.push({ role: 'assistant', text: finalText || '' });
558
615
  if (_qaIsActiveSession(sessionKey)) _qaHistory = runtime.history.slice();
559
616
 
560
617
  _qaNotifySidebar(capturedFilePath);
@@ -710,18 +767,26 @@ function qaAbort() {
710
767
 
711
768
  function toggleDocChat() {
712
769
  var wrap = document.getElementById('modal-qa-thread-wrap');
713
- var expandBar = document.getElementById('qa-expand-bar');
714
770
  if (!wrap) return;
715
771
  var visible = wrap.style.display !== 'none';
716
- wrap.style.display = visible ? 'none' : '';
717
- if (expandBar) expandBar.style.display = visible ? '' : 'none';
772
+ _setQaThreadCollapsed(visible);
718
773
  }
719
774
 
720
- function _showThreadWrap() {
775
+ function _qaIsThreadCollapsed() {
721
776
  var wrap = document.getElementById('modal-qa-thread-wrap');
722
777
  var expandBar = document.getElementById('qa-expand-bar');
723
- if (wrap) wrap.style.display = '';
724
- if (expandBar) expandBar.style.display = 'none';
778
+ return !!(wrap && wrap.style.display === 'none' && expandBar && expandBar.style.display !== 'none');
779
+ }
780
+
781
+ function _setQaThreadCollapsed(collapsed) {
782
+ var wrap = document.getElementById('modal-qa-thread-wrap');
783
+ var expandBar = document.getElementById('qa-expand-bar');
784
+ if (wrap) wrap.style.display = collapsed ? 'none' : '';
785
+ if (expandBar) expandBar.style.display = collapsed ? '' : 'none';
786
+ }
787
+
788
+ function _showThreadWrap() {
789
+ _setQaThreadCollapsed(false);
725
790
  }
726
791
 
727
792
  // ── Drag-to-resize doc chat thread ──────────────────────────────────────────
package/dashboard.js CHANGED
@@ -1510,34 +1510,6 @@ function stripCCActionSyntax(text) {
1510
1510
  return displayText.replace(/`{3,}\s*action\s*\r?\n[\s\S]*?`{3,}\n?/g, '').trim();
1511
1511
  }
1512
1512
 
1513
- function _messageRequestsOrchestration(message) {
1514
- const text = String(message || '').toLowerCase();
1515
- if (!text.trim()) return false;
1516
-
1517
- const docTarget = '\\b(document|doc|text|selection|selected text|selected paragraph|selected section|paragraph|section|wording|copy|markdown|plan)\\b';
1518
- const docEditVerb = '\\b(edit|rewrite|revise|update|change|rephrase|polish|format|shorten|expand|summarize|correct|add|write)\\b';
1519
- const explicitDocEdit = new RegExp(`${docEditVerb}[\\s\\S]{0,120}${docTarget}|${docTarget}[\\s\\S]{0,120}${docEditVerb}`).test(text)
1520
- || /\bfix\b[\s\S]{0,80}\b(typo|typos|grammar|spelling|wording|copy|markdown)\b[\s\S]{0,80}\b(document|doc|text|selection|paragraph|section|plan)\b/.test(text);
1521
- const actionTerm = '\\b(dispatch|delegate|assign|orchestrate|hand off|handoff|work item|ticket|agent|minions|watch|monitor|schedule|pipeline|meeting)\\b';
1522
- const untrustedActionMention = new RegExp(
1523
- `${docTarget}[\\s\\S]{0,120}\\b(says|contains|mentions|includes|reads|states|instructs|asks|tells|literal|literally)\\b[\\s\\S]{0,160}${actionTerm}`
1524
- ).test(text)
1525
- || new RegExp(`\\b(summarize|explain|quote|describe|analyze|extract)\\b[\\s\\S]{0,160}${docTarget}[\\s\\S]{0,160}${actionTerm}`).test(text);
1526
- const explicitFollowupAction = /\b(and|then|also)\b[\s\S]{0,80}\b(dispatch|delegate|assign|orchestrate|hand off|handoff|work item|ticket|agent|minions|watch|monitor|schedule|pipeline|meeting)\b/.test(text);
1527
- if (untrustedActionMention && !explicitFollowupAction) return false;
1528
-
1529
- const dispatchAction = /\b(dispatch|delegate|assign|orchestrate|hand off|handoff)\b[\s\S]{0,120}\b(agent|dallas|ripley|lambert|rebecca|ralph|work item|task|fix|implement|explore|investigate|audit|review|test|verify|build)\b/.test(text);
1530
- const workItemAction = /\b(create|open|file|add)\b[\s\S]{0,80}\b(work item|task|ticket)\b/.test(text);
1531
- const stateAction = /\b(create|add|set up|start)\b[\s\S]{0,80}\b(watch|monitor|schedule|pipeline|meeting)\b/.test(text)
1532
- || /\b(watch|monitor|keep an eye on)\b[\s\S]{0,100}\b(pr|pull request|work item|build)\b/.test(text)
1533
- || /\b(cancel|retry|reopen|archive|pause|approve|reject|execute|resume|steer)\b[\s\S]{0,100}\b(plan|work item|agent|pr|pull request|schedule|pipeline)\b/.test(text);
1534
- const agentEngineeringAction = /\b(minions|agent|dallas|ripley|lambert|rebecca|ralph)\b[\s\S]{0,120}\b(fix|debug|repair|investigate|audit|review|test|verify|build|refactor|implement)\b/.test(text)
1535
- || /\b(fix|debug|repair|investigate|audit|review|test|verify|build|refactor|implement)\b[\s\S]{0,120}\b(minions|agent|dallas|ripley|lambert|rebecca|ralph)\b/.test(text);
1536
- const explicitActionIntent = dispatchAction || workItemAction || stateAction || agentEngineeringAction;
1537
- if (explicitDocEdit && !explicitActionIntent) return false;
1538
- return explicitActionIntent;
1539
- }
1540
-
1541
1513
  function _escapeRegExp(str) {
1542
1514
  return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1543
1515
  }
@@ -2517,13 +2489,11 @@ function contentFingerprint(str) {
2517
2489
  return str.length + ':' + str.charCodeAt(0) + ':' + str.charCodeAt(mid) + ':' + str.charCodeAt(str.length - 1);
2518
2490
  }
2519
2491
 
2520
- function _parseDocChatResultText(text, { allowActions = false } = {}) {
2492
+ function _parseDocChatResultText(text) {
2521
2493
  const docDelimiter = findDocChatDocumentDelimiter(text);
2522
2494
  if (docDelimiter) {
2523
2495
  const answerPart = text.slice(0, docDelimiter.index).trim();
2524
- const parsedActions = allowActions
2525
- ? parseCCActions(answerPart)
2526
- : { text: stripCCActionSyntax(answerPart), actions: [] };
2496
+ const parsedActions = parseCCActions(answerPart);
2527
2497
  const { text: answer, actions } = parsedActions;
2528
2498
  let content = text.slice(docDelimiter.index + docDelimiter.length).trim();
2529
2499
  content = content.replace(/^```\w*\n?/, '').replace(/\n?```$/, '').trim();
@@ -2534,9 +2504,7 @@ function _parseDocChatResultText(text, { allowActions = false } = {}) {
2534
2504
  ...(parsedActions._actionParseError ? { actionParseError: parsedActions._actionParseError } : {}),
2535
2505
  };
2536
2506
  }
2537
- const parsedActions = allowActions
2538
- ? parseCCActions(text)
2539
- : { text: stripCCActionSyntax(text), actions: [] };
2507
+ const parsedActions = parseCCActions(text);
2540
2508
  const { text: stripped, actions } = parsedActions;
2541
2509
  return {
2542
2510
  answer: stripped,
@@ -2546,8 +2514,8 @@ function _parseDocChatResultText(text, { allowActions = false } = {}) {
2546
2514
  };
2547
2515
  }
2548
2516
 
2549
- function _docChatDisplayText(text, opts) {
2550
- return _parseDocChatResultText(text, opts).answer;
2517
+ function _docChatDisplayText(text) {
2518
+ return _parseDocChatResultText(text).answer;
2551
2519
  }
2552
2520
 
2553
2521
  function _formatDocChatContext({ document, title, filePath, selection, canEdit, isJson, docUnchanged }) {
@@ -2570,15 +2538,36 @@ function _formatDocChatContext({ document, title, filePath, selection, canEdit,
2570
2538
 
2571
2539
  // Map errorClass codes from the runtime adapter to actionable user-facing messages.
2572
2540
  // sessionPreserved=true means ccCall preserved the session — user can retry immediately.
2573
- function _docChatErrorMessage(errorClass, sessionPreserved = false) {
2574
- if (errorClass === 'auth-failure') return 'Claude authentication failed run `claude auth` or check your API key, then try again.';
2575
- if (errorClass === 'context-limit') return 'Session context is too long. Click "Clear" to start a fresh conversation.';
2576
- if (errorClass === 'budget-exceeded') return 'API budget exceeded check your Claude account spending limit.';
2577
- if (errorClass === 'crash') return 'Claude runtime crashed unexpectedly. Try again.';
2578
- if (sessionPreserved) return 'Temporary connection issue — your conversation is intact, send your message again.';
2541
+ // toolUses=[] from result.toolUses lets the message warn that tools may have already
2542
+ // modified files/state before the failurethe user shouldn't assume nothing happened.
2543
+ // errorMessage is the runtime adapter's own remediation string (from parseError)
2544
+ // authoritative because it knows whether the runtime is Claude, Copilot, or another;
2545
+ // dashboard falls back to generic copy only when the adapter didn't supply one.
2546
+ function _docChatErrorMessage(errorClass, sessionPreserved = false, toolUses = [], errorMessage = null) {
2547
+ const tools = Array.isArray(toolUses) ? toolUses : [];
2548
+ const toolHint = tools.length > 0
2549
+ ? ` (${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.)`
2550
+ : '';
2551
+ if (errorClass === 'auth-failure') return (errorMessage || 'Runtime authentication failed — check your CLI auth or API key, then try again.') + toolHint;
2552
+ if (errorClass === 'context-limit') return 'Session context is too long. Click "Clear" to start a fresh conversation.' + toolHint;
2553
+ if (errorClass === 'budget-exceeded') return (errorMessage || 'Runtime budget exceeded — check your account or quota.') + toolHint;
2554
+ if (errorClass === 'crash') return (errorMessage || 'Runtime crashed unexpectedly. Try again.') + toolHint;
2555
+ if (sessionPreserved) return 'Temporary connection issue — your conversation is intact, send your message again.' + toolHint;
2556
+ if (tools.length > 0) return 'The agent stopped responding before producing a final answer.' + toolHint;
2579
2557
  return 'Failed to process request. Try again.';
2580
2558
  }
2581
2559
 
2560
+ // Secondary note rendered alongside a recovered partial answer — distinct from the
2561
+ // hard-failure message because the answer/actions/document-edit DID land. The user
2562
+ // just needs to know the run wasn't clean.
2563
+ function _docChatPartialWarning(errorClass) {
2564
+ if (errorClass === 'auth-failure') return 'Note: auth failed — answer recovered from partial output, but follow-up turns may not work.';
2565
+ if (errorClass === 'context-limit') return 'Note: session context was too long — answer recovered. Click "Clear" before continuing.';
2566
+ if (errorClass === 'budget-exceeded') return 'Note: runtime budget exceeded — answer recovered, but further calls may fail.';
2567
+ if (errorClass === 'crash') return 'Note: runtime crashed before clean exit, but a complete response was recovered.';
2568
+ return 'Note: the agent exited unexpectedly. A complete response was recovered — verify any saved files or dispatched actions.';
2569
+ }
2570
+
2582
2571
  // Build the doc-chat extraContext for a single ccCall pass — refreshed on retry
2583
2572
  // so a fresh-session retry includes the full document instead of relying on the
2584
2573
  // dead session's prior turn for context.
@@ -2612,20 +2601,54 @@ async function _retryDocChatAfterResumeFailure({ result, initialPass, freshSessi
2612
2601
  // Build the {error} envelope returned to the dashboard when doc-chat ultimately
2613
2602
  // fails. Surfaces an actionable user-facing message (via _docChatErrorMessage)
2614
2603
  // plus the runtime's real stderr / exit code / errorClass so the UI can render
2615
- // the cause and so future failures are debuggable from logs.
2604
+ // the cause and so future failures are debuggable from logs. toolUses lets the
2605
+ // client surface what side-effects already landed despite the failure.
2606
+ // Shape the per-failure debug envelope (raw stderr + classification metadata)
2607
+ // shared by hard failures and partial recoveries — keeps the wire shape in lockstep.
2608
+ function _buildDocChatErrorEnvelope(result) {
2609
+ return {
2610
+ code: result.code ?? null,
2611
+ stderr: (result.stderr || '').slice(-2048),
2612
+ errorClass: result.errorClass || null,
2613
+ errorMessage: result.errorMessage || null,
2614
+ runtime: result.runtime || null,
2615
+ };
2616
+ }
2617
+
2616
2618
  function _docChatFailureResponse(label, filePath, result, sessionPreserved = false) {
2617
- const stderrTail = (result.stderr || '').slice(-2048);
2618
- console.error(`[${label}] Failed: code=${result.code}, errorClass=${result.errorClass || 'null'}, sessionPreserved=${sessionPreserved}, empty=${!result.text}, filePath=${filePath}, stderr=${stderrTail.slice(0, 200)}`);
2619
+ const envelope = _buildDocChatErrorEnvelope(result);
2620
+ const toolUses = Array.isArray(result.toolUses) ? result.toolUses : [];
2621
+ console.error(`[${label}] Failed: code=${result.code}, errorClass=${result.errorClass || 'null'}, sessionPreserved=${sessionPreserved}, empty=${!result.text}, tools=${toolUses.length}, filePath=${filePath}, stderr=${envelope.stderr.slice(0, 200)}`);
2619
2622
  return {
2620
- answer: _docChatErrorMessage(result.errorClass, sessionPreserved),
2623
+ answer: _docChatErrorMessage(result.errorClass, sessionPreserved, toolUses, result.errorMessage || null),
2621
2624
  content: null,
2622
2625
  actions: [],
2623
- error: {
2624
- code: result.code ?? null,
2625
- stderr: stderrTail,
2626
- errorClass: result.errorClass || null,
2627
- runtime: result.runtime || null,
2628
- },
2626
+ toolUses,
2627
+ error: envelope,
2628
+ };
2629
+ }
2630
+
2631
+ // Try to salvage useful work from a non-zero / empty-text result before falling
2632
+ // through to the failure response. Even when the runtime exits non-zero, the
2633
+ // model often produced a complete answer / action JSON / document edit in
2634
+ // result.text — discarding it just to show "Failed to process request" wastes
2635
+ // successful work and confuses users who watched tools run during streaming.
2636
+ // Returns null when there's nothing parseable; caller falls through to failure.
2637
+ function _recoverPartialDocChatResponse(result, sessionKey) {
2638
+ if (!result || !result.text || !result.text.trim()) return null;
2639
+ const parsed = _parseDocChatResultText(result.text);
2640
+ const hasActions = Array.isArray(parsed.actions) && parsed.actions.length > 0;
2641
+ const hasAnswer = typeof parsed.answer === 'string' && !!parsed.answer.trim();
2642
+ const hasContent = typeof parsed.content === 'string' && !!parsed.content.trim();
2643
+ if (!hasAnswer && !hasContent && !hasActions) return null;
2644
+ return {
2645
+ ...parsed,
2646
+ partial: true,
2647
+ warning: _docChatPartialWarning(result.errorClass),
2648
+ toolUses: Array.isArray(result.toolUses) ? result.toolUses : [],
2649
+ // Recovery path still attaches the raw runtime failure — the answer landed
2650
+ // despite a non-zero exit; users still benefit from seeing why.
2651
+ error: _buildDocChatErrorEnvelope(result),
2629
2652
  };
2630
2653
  }
2631
2654
 
@@ -2643,8 +2666,6 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
2643
2666
  // Skip persistDocSessions() here — the post-call cleanup below handles persistence.
2644
2667
  }
2645
2668
 
2646
- const allowActions = _messageRequestsOrchestration(message);
2647
-
2648
2669
  const runOnce = async () => {
2649
2670
  const { extraContext } = _buildDocChatPass({
2650
2671
  docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession,
@@ -2687,11 +2708,14 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
2687
2708
  }
2688
2709
 
2689
2710
  if (result.code !== 0 || !result.text) {
2711
+ // Try to salvage a parseable answer / action / document edit before failing.
2712
+ const recovered = _recoverPartialDocChatResponse(result, sessionKey);
2713
+ if (recovered) return recovered;
2690
2714
  const sessionPreserved = !!(resolveSession('doc', sessionKey)?.sessionId);
2691
2715
  return _docChatFailureResponse('doc-chat', filePath, result, sessionPreserved);
2692
2716
  }
2693
2717
 
2694
- return _parseDocChatResultText(result.text, { allowActions });
2718
+ return _parseDocChatResultText(result.text);
2695
2719
  }
2696
2720
 
2697
2721
  async function ccDocCallStreaming({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, onAbortReady, onChunk, onToolUse, onRetry }) {
@@ -2702,8 +2726,6 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
2702
2726
  docSessions.delete(sessionKey);
2703
2727
  }
2704
2728
 
2705
- const allowActions = _messageRequestsOrchestration(message);
2706
-
2707
2729
  const runOnce = async () => {
2708
2730
  const { extraContext } = _buildDocChatPass({
2709
2731
  docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession,
@@ -2720,7 +2742,7 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
2720
2742
  systemPrompt: DOC_CHAT_SYSTEM_PROMPT,
2721
2743
  ...(model ? { model } : {}),
2722
2744
  onAbortReady,
2723
- onChunk: (text) => { if (onChunk) onChunk(_docChatDisplayText(text, { allowActions })); },
2745
+ onChunk: (text) => { if (onChunk) onChunk(_docChatDisplayText(text)); },
2724
2746
  onToolUse,
2725
2747
  onRetry,
2726
2748
  });
@@ -2745,11 +2767,13 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
2745
2767
  }
2746
2768
 
2747
2769
  if (result.code !== 0 || !result.text) {
2770
+ const recovered = _recoverPartialDocChatResponse(result, sessionKey);
2771
+ if (recovered) return recovered;
2748
2772
  const sessionPreserved = !!(resolveSession('doc', sessionKey)?.sessionId);
2749
2773
  return _docChatFailureResponse('doc-chat-stream', filePath, result, sessionPreserved);
2750
2774
  }
2751
2775
 
2752
- return _parseDocChatResultText(result.text, { allowActions });
2776
+ return _parseDocChatResultText(result.text);
2753
2777
  }
2754
2778
 
2755
2779
  // -- POST helpers --
@@ -4858,7 +4882,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4858
4882
  }
4859
4883
  }
4860
4884
 
4861
- const { answer, content, actions, actionParseError, error: ccError } = await ccDocCall({
4885
+ const { answer, content, actions, actionParseError, partial, warning, toolUses, error: ccError } = await ccDocCall({
4862
4886
  message: body.message, document: currentContent, title: body.title,
4863
4887
  filePath: body.filePath, selection: body.selection, canEdit, isJson,
4864
4888
  model: body.model || undefined,
@@ -4873,6 +4897,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4873
4897
  ...(actionResults ? { actionResults } : {}),
4874
4898
  ...(actionParseError ? { actionParseError } : {}),
4875
4899
  ...(ccError ? { error: ccError } : {}),
4900
+ ...(partial ? { partial: true, warning } : {}),
4901
+ ...(Array.isArray(toolUses) && toolUses.length ? { toolUses } : {}),
4876
4902
  ...extra,
4877
4903
  });
4878
4904
 
@@ -4974,7 +5000,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4974
5000
 
4975
5001
  try {
4976
5002
 
4977
- const { answer, content, actions, actionParseError, error: ccError } = await ccDocCallStreaming({
5003
+ const { answer, content, actions, actionParseError, partial, warning, toolUses, error: ccError } = await ccDocCallStreaming({
4978
5004
  message: body.message, document: currentContent, title: body.title,
4979
5005
  filePath: body.filePath, selection: body.selection, canEdit, isJson,
4980
5006
  model: body.model || undefined,
@@ -4992,6 +5018,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4992
5018
  ...(actionResults ? { actionResults } : {}),
4993
5019
  ...(actionParseError ? { actionParseError } : {}),
4994
5020
  ...(ccError ? { error: ccError } : {}),
5021
+ ...(partial ? { partial: true, warning } : {}),
5022
+ ...(Array.isArray(toolUses) && toolUses.length ? { toolUses } : {}),
4995
5023
  ...extra,
4996
5024
  });
4997
5025
 
@@ -7435,8 +7463,11 @@ module.exports = {
7435
7463
  _meetingParticipantsFromAction: meetingParticipantsFromAction,
7436
7464
  parsePinnedEntries,
7437
7465
  _parseDocChatResultText,
7438
- _messageRequestsOrchestration,
7439
7466
  _formatDocChatContext,
7467
+ _docChatErrorMessage,
7468
+ _docChatPartialWarning,
7469
+ _docChatFailureResponse,
7470
+ _recoverPartialDocChatResponse,
7440
7471
  _linkPullRequestForTracking: linkPullRequestForTracking,
7441
7472
  _resolveSkillReadPath,
7442
7473
  DOC_CHAT_DOCUMENT_DELIMITER,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-06T20:30:44.726Z"
4
+ "cachedAt": "2026-05-06T21:43:07.445Z"
5
5
  }
package/engine/llm.js CHANGED
@@ -232,6 +232,7 @@ function _missingRuntimeResult(runtimeName, runtime, reason) {
232
232
  toolUses: [],
233
233
  runtime: runtime?.name || runtimeName || null,
234
234
  errorClass: shared.FAILURE_CLASS.CONFIG_ERROR,
235
+ errorMessage: message,
235
236
  missingRuntime: true,
236
237
  };
237
238
  }
@@ -689,6 +690,7 @@ function callLLM(promptText, sysPromptText, opts = {}) {
689
690
  toolUses: parsed.toolUses,
690
691
  runtime: runtime.name,
691
692
  errorClass: errInfo.code,
693
+ errorMessage: errInfo.message || null,
692
694
  });
693
695
  };
694
696
 
@@ -711,7 +713,7 @@ function callLLM(promptText, sysPromptText, opts = {}) {
711
713
  resolve({
712
714
  text: '', usage: null, sessionId: null, code: 1,
713
715
  stderr: err.message, raw: '', toolUses: [],
714
- runtime: runtime.name, errorClass: null,
716
+ runtime: runtime.name, errorClass: null, errorMessage: null,
715
717
  });
716
718
  });
717
719
  });
@@ -814,6 +816,7 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
814
816
  toolUses: parsed.toolUses,
815
817
  runtime: runtime.name,
816
818
  errorClass: errInfo.code,
819
+ errorMessage: errInfo.message || null,
817
820
  });
818
821
  };
819
822
 
@@ -836,7 +839,7 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
836
839
  resolve({
837
840
  text: '', usage: null, sessionId: null, code: 1,
838
841
  stderr: err.message, raw: '', toolUses: [],
839
- runtime: runtime.name, errorClass: null,
842
+ runtime: runtime.name, errorClass: null, errorMessage: null,
840
843
  });
841
844
  });
842
845
  });
@@ -493,16 +493,16 @@ function parseError(rawOutput) {
493
493
  const hasExplicitAuthFailure = /invalid api key|api key.*invalid|authentication.*fail|\bunauthorized\b|please.*log.*in|claude\.ai\/login/i.test(text);
494
494
  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);
495
495
  if (hasExplicitAuthFailure || hasAuthStatusCode) {
496
- return { message: 'Claude authentication failed', code: 'auth-failure', retriable: false };
496
+ return { message: 'Claude authentication failed — run `claude auth` or check your API key, then try again.', code: 'auth-failure', retriable: false };
497
497
  }
498
498
  if (/prompt is too long|context window|context.*length.*exceeded|token limit|conversation.*too long/i.test(text)) {
499
499
  return { message: 'Claude context window exhausted', code: 'context-limit', retriable: false };
500
500
  }
501
501
  if (/budget.*exceed|max.budget.usd.*reach|cost.*limit.*exceed/i.test(lower)) {
502
- return { message: 'Claude budget cap exceeded', code: 'budget-exceeded', retriable: false };
502
+ return { message: 'Claude budget cap exceeded — check your Claude account spending limit.', code: 'budget-exceeded', retriable: false };
503
503
  }
504
504
  if (/internal error|panic|segmentation fault|claude.*crashed|fatal: claude/i.test(lower)) {
505
- return { message: 'Claude CLI crashed', code: 'crash', retriable: true };
505
+ return { message: 'Claude CLI crashed unexpectedly. Try again.', code: 'crash', retriable: true };
506
506
  }
507
507
  return { message: '', code: null, retriable: true };
508
508
  }
@@ -576,10 +576,10 @@ function parseError(rawOutput) {
576
576
  return { message: 'Copilot rejected the requested model', code: 'unknown-model', retriable: false };
577
577
  }
578
578
  if (/budget.*exceed|premium.*limit.*reach|quota.*exceed/i.test(lower)) {
579
- return { message: 'Copilot premium-request budget exceeded', code: 'budget-exceeded', retriable: false };
579
+ return { message: 'Copilot premium-request budget exceeded — check your GitHub Copilot quota.', code: 'budget-exceeded', retriable: false };
580
580
  }
581
581
  if (/internal error|panic|uncaught|copilot.*crashed|fatal: copilot/i.test(lower)) {
582
- return { message: 'Copilot CLI crashed', code: 'crash', retriable: true };
582
+ return { message: 'Copilot CLI crashed unexpectedly. Try again.', code: 'crash', retriable: true };
583
583
  }
584
584
  return { message: '', code: null, retriable: true };
585
585
  }
package/engine/shared.js CHANGED
@@ -7,7 +7,7 @@ const fs = require('fs');
7
7
  const path = require('path');
8
8
  const crypto = require('crypto');
9
9
 
10
- const MINIONS_DIR = process.env.MINIONS_TEST_DIR || path.resolve(__dirname, '..');
10
+ const MINIONS_DIR = process.env.MINIONS_TEST_DIR || (process.env.MINIONS_HOME ? path.resolve(process.env.MINIONS_HOME) : path.resolve(__dirname, '..'));
11
11
  const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
12
12
  const CONTROL_PATH = path.join(ENGINE_DIR, 'control.json');
13
13
  const COOLDOWNS_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
package/minions.js CHANGED
@@ -19,7 +19,7 @@ const { execSync } = require('child_process');
19
19
  const { ENGINE_DEFAULTS, DEFAULT_AGENTS, DEFAULT_CLAUDE } = require('./engine/shared');
20
20
  const projectDiscovery = require('./engine/project-discovery');
21
21
 
22
- const MINIONS_HOME = __dirname;
22
+ const MINIONS_HOME = process.env.MINIONS_HOME ? path.resolve(process.env.MINIONS_HOME) : __dirname;
23
23
  const CONFIG_PATH = path.join(MINIONS_HOME, 'config.json');
24
24
 
25
25
  function loadConfig() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1752",
3
+ "version": "0.1.1754",
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"