@yemi33/minions 0.1.1815 → 0.1.1817

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1817 (2026-05-09)
4
+
5
+ ### Features
6
+ - suppress duplicate inline ask dispatch (#2256)
7
+ - correlate CC action failures (#2254)
8
+
9
+ ### Other
10
+ - test(shared): add unit tests for project resolution and path comparison helpers (#2268)
11
+ - test(cli): add unit tests for pid/dispatch helpers and help formatter (#2267)
12
+
3
13
  ## 0.1.1815 (2026-05-09)
4
14
 
5
15
  ### Features
@@ -826,11 +826,15 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
826
826
  var failures = evt.actionResults.filter(function(r) { return r && r.error; });
827
827
  var warnings = evt.actionResults.filter(function(r) { return r && r.warning; });
828
828
  if (failures.length > 0) {
829
- var failHtml = failures.map(function(r) { return '<li>' + escHtml(r.type || 'action') + ': ' + escHtml(r.error) + '</li>'; }).join('');
829
+ var failHtml = failures.map(function(r) {
830
+ return '<li>' + escHtml(r.type || (r.actionContext && r.actionContext.type) || 'action') + ': ' + escHtml(r.error) + _ccActionContextSuffix(r.actionContext) + '</li>';
831
+ }).join('');
830
832
  addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--red);background:var(--surface2);border-radius:6px;margin:4px 0">⚠️ ' + failures.length + ' action' + (failures.length > 1 ? 's' : '') + ' failed:<ul style="margin:4px 0 0 16px;padding:0">' + failHtml + '</ul></div>', false, activeTabId);
831
833
  }
832
834
  if (warnings.length > 0) {
833
- var warnHtml = warnings.map(function(r) { return '<li>' + escHtml(r.type || 'action') + ': ' + escHtml(r.warning) + '</li>'; }).join('');
835
+ var warnHtml = warnings.map(function(r) {
836
+ return '<li>' + escHtml(r.type || (r.actionContext && r.actionContext.type) || 'action') + ': ' + escHtml(r.warning) + _ccActionContextSuffix(r.actionContext) + '</li>';
837
+ }).join('');
834
838
  addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--orange);background:var(--surface2);border-radius:6px;margin:4px 0">ℹ️ ' + warnings.length + ' action' + (warnings.length > 1 ? '' : '') + ' completed with warnings:<ul style="margin:4px 0 0 16px;padding:0">' + warnHtml + '</ul></div>', false, activeTabId);
835
839
  }
836
840
  }
@@ -1012,6 +1016,28 @@ async function _ccFetch(url, body, method) {
1012
1016
  return res;
1013
1017
  }
1014
1018
 
1019
+ var CC_ACTION_CONTEXT_STALE_MS = 2 * 60 * 1000;
1020
+ function _ccActionContextIsStale(ctx, nowMs) {
1021
+ if (!ctx || !ctx.requestedAt) return false;
1022
+ var ts = Date.parse(ctx.requestedAt);
1023
+ if (!Number.isFinite(ts)) return false;
1024
+ var now = Number.isFinite(nowMs) ? nowMs : Date.now();
1025
+ return now - ts > CC_ACTION_CONTEXT_STALE_MS;
1026
+ }
1027
+
1028
+ function _ccActionContextSuffix(ctx, nowMs) {
1029
+ if (!ctx || typeof ctx !== 'object') return '';
1030
+ var parts = [];
1031
+ if (ctx.title) parts.push('action "' + escHtml(ctx.title) + '"');
1032
+ if (ctx.request) parts.push('request "' + escHtml(ctx.request) + '"');
1033
+ if (ctx.requestedAt) parts.push('started ' + escHtml(ctx.requestedAt));
1034
+ if (parts.length === 0) return '';
1035
+ var stale = _ccActionContextIsStale(ctx, nowMs)
1036
+ ? ' <strong style="color:var(--orange)">possibly stale</strong>'
1037
+ : '';
1038
+ return '<div style="font-size:10px;color:var(--muted);margin-top:2px">Context: ' + parts.join(' | ') + stale + '</div>';
1039
+ }
1040
+
1015
1041
  // Tag actions that the server already executed so ccExecuteAction skips the API call
1016
1042
  function _tagServerExecuted(actions, actionResults) {
1017
1043
  if (!actionResults || !Array.isArray(actionResults)) return;
@@ -1022,10 +1048,12 @@ function _tagServerExecuted(actions, actionResults) {
1022
1048
  if (r.id) actions[i]._serverId = r.id;
1023
1049
  if (r.warning) actions[i]._serverWarning = r.warning;
1024
1050
  if (r.duplicate) actions[i]._serverDuplicate = true;
1051
+ if (r.actionContext) actions[i]._serverContext = r.actionContext;
1025
1052
  if (r.reusedFromAction !== undefined) actions[i]._serverHidden = true;
1026
1053
  } else if (r && r.error) {
1027
1054
  actions[i]._serverExecuted = true;
1028
1055
  actions[i]._serverError = r.error;
1056
+ if (r.actionContext) actions[i]._serverContext = r.actionContext;
1029
1057
  }
1030
1058
  // clientExecuted: false means server didn't handle it — frontend must execute
1031
1059
  }
@@ -1040,7 +1068,8 @@ async function ccExecuteAction(action, targetTabId) {
1040
1068
  if (action._serverExecuted) {
1041
1069
  if (action._serverHidden) return;
1042
1070
  if (action._serverError) {
1043
- status.innerHTML = '&#10007; ' + escHtml(action.type) + ' failed: ' + escHtml(action._serverError);
1071
+ status.innerHTML = '&#10007; ' + escHtml(action.type) + ' failed: ' + escHtml(action._serverError) +
1072
+ _ccActionContextSuffix(action._serverContext);
1044
1073
  status.style.color = 'var(--red)';
1045
1074
  } else {
1046
1075
  var label = action._serverId ? escHtml(action._serverId) : escHtml(action.title || action.type);
@@ -1048,7 +1077,8 @@ async function ccExecuteAction(action, targetTabId) {
1048
1077
  var successLabel = serverActionType === 'dispatch' ? 'Dispatched' : serverActionType;
1049
1078
  status.innerHTML = '&#10003; ' + escHtml(successLabel) + ': <strong>' + label + '</strong>' +
1050
1079
  (action._serverDuplicate ? '<div style="font-size:10px;color:var(--orange);margin-top:2px">Already existed from a previous request; no duplicate work item was created.</div>' : '') +
1051
- (action._serverWarning ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(action._serverWarning) + '</div>' : '');
1080
+ (action._serverWarning ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(action._serverWarning) + '</div>' : '') +
1081
+ _ccActionContextSuffix(action._serverContext);
1052
1082
  status.style.color = 'var(--green)';
1053
1083
  }
1054
1084
  ccAddMessage('action', status.outerHTML, false, targetTabId);
package/dashboard.js CHANGED
@@ -2163,7 +2163,8 @@ function _ccFallbackMissingTargetError(intent) {
2163
2163
 
2164
2164
  function _actionsWithIntentFallback(actions, opts = {}) {
2165
2165
  const { message = '', intentMetadata = null, source = 'command-center', filePath = null, title: docTitle = null, answerText = '', toolUses = [] } = opts;
2166
- const existing = Array.isArray(actions) ? actions.map(normalizeCCAction) : [];
2166
+ const existing = (Array.isArray(actions) ? actions.map(normalizeCCAction) : [])
2167
+ .filter(action => !_ccShouldSuppressAnsweredAskDispatch(action, { message, source, answerText, toolUses }));
2167
2168
  if (_messageRequestsDirectHandling(message)) return existing.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
2168
2169
  if (existing.some(a => normalizeCCAction(a)?.type === 'dispatch')) return existing;
2169
2170
  if (existing.length > 0) return existing;
@@ -2199,9 +2200,38 @@ function _actionsWithIntentFallback(actions, opts = {}) {
2199
2200
  ...common,
2200
2201
  };
2201
2202
  if (!hasTarget) action._intentFallbackError = _ccFallbackMissingTargetError(intent);
2203
+ if (_ccShouldSuppressAnsweredAskDispatch(action, { message, source, answerText, toolUses })) return existing;
2202
2204
  return [action];
2203
2205
  }
2204
2206
 
2207
+ function _ccActionContextValue(action) {
2208
+ if (!action || typeof action !== 'object') return '';
2209
+ const candidates = [action.title, action.id, action.file, action.target, action.pr, action.endpoint];
2210
+ for (const value of candidates) {
2211
+ const cleaned = _ccCleanIntentString(value, 160);
2212
+ if (cleaned) return cleaned;
2213
+ }
2214
+ return '';
2215
+ }
2216
+
2217
+ function _annotateCCActionResults(actions, actionResults, opts = {}) {
2218
+ if (!Array.isArray(actionResults)) return actionResults;
2219
+ const requestedAt = _ccCleanIntentString(opts.requestedAt || new Date().toISOString(), 80);
2220
+ const request = _ccCleanIntentString(opts.message, 180);
2221
+ return actionResults.map((result, idx) => {
2222
+ if (!result || typeof result !== 'object') return result;
2223
+ const action = Array.isArray(actions) ? normalizeCCAction(actions[idx]) : null;
2224
+ const context = {
2225
+ type: _ccCleanIntentString(result.type || action?.type || 'action', 80) || 'action',
2226
+ };
2227
+ const title = _ccActionContextValue(action);
2228
+ if (title) context.title = title;
2229
+ if (request) context.request = request;
2230
+ if (requestedAt) context.requestedAt = requestedAt;
2231
+ return { ...result, actionContext: context };
2232
+ });
2233
+ }
2234
+
2205
2235
  function parseCCActions(text) {
2206
2236
  let actions = [];
2207
2237
  let displayText = stripCCActionsForDisplay(text);
@@ -2258,6 +2288,18 @@ function parseCCActions(text) {
2258
2288
  }
2259
2289
 
2260
2290
  const DELEGATION_ACTION_TERMS = ['dispatch', 'delegate', 'assign', 'queue', 'enqueue'];
2291
+ const DELEGATION_REQUEST_PREFIX_TERMS = ['please', 'can', 'could', 'would', 'will', 'should', 'you', 'cc', 'minions', 'minion'];
2292
+ const DELEGATION_PERSON_ACTION_TERMS = ['ask', 'tell', 'have'];
2293
+ // Subset of delegation action terms that double as common nouns in status
2294
+ // questions (e.g. "queue status", "dispatch contents"). When followed by a
2295
+ // status-noun follow-on these are noun usage, not imperative delegation.
2296
+ const DELEGATION_AMBIGUOUS_NOUN_TERMS = new Set(['dispatch', 'queue']);
2297
+ const DELEGATION_NON_VERB_FOLLOW_ONS = new Set([
2298
+ 'status', 'contents', 'content', 'state', 'count', 'list', 'items', 'item',
2299
+ 'size', 'length', 'depth', 'health', 'info', 'order', 'queue', 'history',
2300
+ 'log', 'logs', 'summary', 'overview', 'report', 'is', 'was', 'are', 'were',
2301
+ 'has', 'have', 'looks', 'shows',
2302
+ ]);
2261
2303
  const DELEGATION_MINIONS_PHRASES = ['have minions', 'ask minions', 'tell minions', 'hand off', 'hand it off', 'hand this off', 'send to agent', 'send it to agent', 'send this to agent'];
2262
2304
  const MEDIUM_INVESTIGATION_TERMS = ['audit', 'investigate', 'research', 'explore', 'analyze', 'analyse'];
2263
2305
  const MEDIUM_INVESTIGATION_PHRASES = ['deep dive', 'root cause'];
@@ -2288,6 +2330,15 @@ const DIRECT_HANDLING_PHRASES = [
2288
2330
  'no dispatch', 'no delegate', 'no delegation', 'no work item',
2289
2331
  'without dispatching', 'without delegating', 'without creating work item',
2290
2332
  ];
2333
+ const ANSWERED_ASK_MIN_CHARS = 80;
2334
+ const ANSWERED_ASK_INCOMPLETE_PHRASES = [
2335
+ 'i will dispatch', 'ill dispatch', 'i will delegate', 'ill delegate',
2336
+ 'i will ask', 'ill ask', 'need to dispatch', 'needs to dispatch',
2337
+ 'need to delegate', 'needs to delegate', 'need an agent', 'needs an agent',
2338
+ 'hand off', 'hand this off', 'deeper investigation', 'further investigation',
2339
+ 'cannot answer', 'cant answer', 'could not answer', 'couldnt answer',
2340
+ 'not enough information',
2341
+ ];
2291
2342
 
2292
2343
  function _isIntentWordChar(ch) {
2293
2344
  const code = ch.charCodeAt(0);
@@ -2373,6 +2424,37 @@ function _messageHasDelegationIntent(message) {
2373
2424
  return _intentHasAnyPhrase(normalized, DELEGATION_MINIONS_PHRASES);
2374
2425
  }
2375
2426
 
2427
+ function _delegationTokenIsImperativeVerb(tokens, idx) {
2428
+ const term = tokens[idx];
2429
+ if (!term) return false;
2430
+ if (!DELEGATION_AMBIGUOUS_NOUN_TERMS.has(term)) return true;
2431
+ const next = tokens[idx + 1];
2432
+ if (!next) return true;
2433
+ return !DELEGATION_NON_VERB_FOLLOW_ONS.has(next);
2434
+ }
2435
+
2436
+ function _messageExplicitlyRequestsDelegation(message) {
2437
+ const normalized = _normalizeIntentText(message);
2438
+ const tokens = _intentTokens(normalized);
2439
+ if (!tokens.length) return false;
2440
+ if (_intentHasAnyPhrase(normalized, DELEGATION_MINIONS_PHRASES)) return true;
2441
+ if (_intentHasPhrase(normalized, 'work item') && _intentHasAnyToken(normalized, ['create', 'open', 'add'])) return true;
2442
+
2443
+ let i = 0;
2444
+ while (DELEGATION_REQUEST_PREFIX_TERMS.includes(tokens[i])) i++;
2445
+ if (DELEGATION_ACTION_TERMS.includes(tokens[i]) && _delegationTokenIsImperativeVerb(tokens, i)) return true;
2446
+ if (DELEGATION_PERSON_ACTION_TERMS.includes(tokens[i])) {
2447
+ return tokens.slice(i + 1, i + 7).includes('to');
2448
+ }
2449
+ for (let j = 0; j < tokens.length && j <= 6; j++) {
2450
+ if (!DELEGATION_ACTION_TERMS.includes(tokens[j])) continue;
2451
+ if (!_delegationTokenIsImperativeVerb(tokens, j)) continue;
2452
+ const prefix = tokens.slice(0, j);
2453
+ if (prefix.includes('you') || prefix.includes('minions') || prefix.includes('minion')) return true;
2454
+ }
2455
+ return false;
2456
+ }
2457
+
2376
2458
  function _messageRequestsDirectHandling(message) {
2377
2459
  const normalized = _normalizeIntentText(message);
2378
2460
  if (!normalized.trim()) return false;
@@ -2412,6 +2494,24 @@ function _inferDelegatedWorkType(message) {
2412
2494
  return 'ask';
2413
2495
  }
2414
2496
 
2497
+ function _ccAnswerLooksCompleteForAsk(answerText, toolUses = []) {
2498
+ if (!Array.isArray(toolUses) || toolUses.length === 0) return false;
2499
+ const answer = _collapseWhitespace(stripCCActionSyntax(answerText || ''));
2500
+ if (answer.length < ANSWERED_ASK_MIN_CHARS) return false;
2501
+ const normalized = _normalizeIntentText(answer);
2502
+ if (_intentHasAnyPhrase(normalized, ANSWERED_ASK_INCOMPLETE_PHRASES)) return false;
2503
+ if (_intentHasVerbObject(normalized, ['need', 'needs', 'require', 'requires'], ['agent', 'delegation', 'investigation', 'research'])) return false;
2504
+ return true;
2505
+ }
2506
+
2507
+ function _ccShouldSuppressAnsweredAskDispatch(action, opts = {}) {
2508
+ const normalized = normalizeCCAction(action);
2509
+ if (normalized?.type !== 'dispatch') return false;
2510
+ if (String(normalized.workType || '').trim().toLowerCase() !== 'ask') return false;
2511
+ if (_messageExplicitlyRequestsDelegation(opts.message)) return false;
2512
+ return _ccAnswerLooksCompleteForAsk(opts.answerText, opts.toolUses);
2513
+ }
2514
+
2415
2515
  function _collapseWhitespace(text) {
2416
2516
  let out = '';
2417
2517
  let lastWasSpace = true;
@@ -2504,11 +2604,13 @@ function _inferDelegationActionFromMessage(message, { source = 'command-center',
2504
2604
  }
2505
2605
 
2506
2606
  function _ensureDelegationForIntent(actions, opts = {}) {
2507
- const list = Array.isArray(actions) ? actions.map(normalizeCCAction) : [];
2607
+ const list = (Array.isArray(actions) ? actions.map(normalizeCCAction) : [])
2608
+ .filter(action => !_ccShouldSuppressAnsweredAskDispatch(action, opts));
2508
2609
  if (_messageRequestsDirectHandling(opts.message)) return list.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
2509
2610
  if (list.some(a => normalizeCCAction(a)?.type === 'dispatch')) return list;
2510
2611
  if (list.length > 0) return list;
2511
2612
  const inferred = _inferDelegationActionFromMessage(opts.message, opts);
2613
+ if (_ccShouldSuppressAnsweredAskDispatch(inferred, opts)) return list;
2512
2614
  return inferred ? [...list, inferred] : list;
2513
2615
  }
2514
2616
 
@@ -7100,6 +7202,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7100
7202
  try {
7101
7203
  const body = await readBody(req);
7102
7204
  if (!body.message) return jsonReply(res, 400, { error: 'message required' });
7205
+ const actionRequestedAt = new Date().toISOString();
7103
7206
 
7104
7207
  // Per-tab concurrency guard
7105
7208
  tabId = body.tabId || 'default';
@@ -7155,7 +7258,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7155
7258
  { message: body.message, intentMetadata: body.intentMetadata, source: 'command-center', answerText: parsed.text, toolUses }
7156
7259
  );
7157
7260
  if (parsed.actions.length > 0) {
7158
- parsed.actionResults = await executeCCActions(parsed.actions);
7261
+ parsed.actionResults = _annotateCCActionResults(
7262
+ parsed.actions,
7263
+ await executeCCActions(parsed.actions),
7264
+ { message: body.message, requestedAt: actionRequestedAt }
7265
+ );
7159
7266
  }
7160
7267
  // Mirror only user-facing text to Teams; never send the internal action block.
7161
7268
  if (!tabId.startsWith('teams-')) {
@@ -7259,6 +7366,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7259
7366
  const body = await readBody(req);
7260
7367
  if (!body.message && !body.reconnect) { res.statusCode = 400; res.end('message required'); return; }
7261
7368
  tabId = body.tabId || 'default';
7369
+ const actionRequestedAt = new Date().toISOString();
7262
7370
  if (body.reconnect) {
7263
7371
  const live = _getCcLiveStream(tabId);
7264
7372
  if (!live) { res.statusCode = 409; res.end('No live command-center response to reconnect'); return; }
@@ -7487,7 +7595,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7487
7595
  );
7488
7596
  let actionResults;
7489
7597
  if (actions.length > 0) {
7490
- actionResults = await executeCCActions(actions);
7598
+ actionResults = _annotateCCActionResults(
7599
+ actions,
7600
+ await executeCCActions(actions),
7601
+ { message: body.message, requestedAt: actionRequestedAt }
7602
+ );
7491
7603
  }
7492
7604
  const donePayload = { type: 'done', text: displayText, actions, actionResults, sessionId: responseSessionId, newSession: !wasResume };
7493
7605
  // Issue #1834: surface action JSON parse failures so the UI can warn
package/engine/cli.js CHANGED
@@ -1531,4 +1531,10 @@ module.exports = {
1531
1531
  _controlBelongsToOwner: controlBelongsToOwner,
1532
1532
  _markControlStoppingForOwner: markControlStoppingForOwner,
1533
1533
  _markControlStoppedForOwner: markControlStoppedForOwner,
1534
+ _isPidAlive: isPidAlive,
1535
+ _summarizeActiveDispatchPids: summarizeActiveDispatchPids,
1536
+ _dispatchSafeId: dispatchSafeId,
1537
+ _readDispatchPid: readDispatchPid,
1538
+ _normalizeSessionBranch: normalizeSessionBranch,
1539
+ _dispatchSessionBranch: dispatchSessionBranch,
1534
1540
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-09T15:24:57.566Z"
4
+ "cachedAt": "2026-05-09T15:29:07.860Z"
5
5
  }
package/engine/shared.js CHANGED
@@ -3451,6 +3451,10 @@ module.exports = {
3451
3451
  legacyProjectStatePath,
3452
3452
  ensureProjectStateFiles,
3453
3453
  sameResolvedPath,
3454
+ projectStateRecordKey, // exported for testing
3455
+ mergeProjectStateArrays, // exported for testing
3456
+ realPathForComparison, // exported for testing
3457
+ prPathComparisonCandidates, // exported for testing
3454
3458
  resolveProjectForPrPath, // exported for testing
3455
3459
  getPrLinks,
3456
3460
  addPrLink,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1815",
3
+ "version": "0.1.1817",
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"
@@ -98,6 +98,7 @@ I'll dispatch dallas to fix that bug.
98
98
  Core action types:
99
99
  - **dispatch**: title (REQUIRED), workType, priority (low/medium/high), agents[] or agent (optional — both shapes accepted), project (REQUIRED when multi-project unless `pr` resolves to a tracked PR), description, pr (optional PR number/id/url for work that targets an existing PR), scope (`"fan-out"` only when the user explicitly asks to fan out to all agents). Do not emit `type:"fix"` or `type:"implement"`; emit `type:"dispatch"` with `workType:"fix"` or emit `type:"dispatch"` with `workType:"implement"`.
100
100
  workTypes: `explore` (research/report only, NO PR), `ask` (answer/report, NO PR), `implement` (new code, PR REQUIRED), `fix` (standalone bug fix creates a PR; include `pr` when fixing review comments/build failures on an existing PR), `review` (code review, NO PR), `test` (tests, PR if new), `verify` (merge/build/maintenance, NO PR)
101
+ Do not dispatch an `ask` work item for a question you already answered inline in the same turn, especially after using tools. If the answer needs deeper investigation, give a brief handoff note and dispatch; do not both provide a complete answer and queue an agent to repeat it.
101
102
  If the user wants a design/architecture artifact committed through a PR, dispatch `implement` or `docs` rather than `explore`.
102
103
  When the user names a specific agent ("assign this to lambert"), put exactly that one name in `agents` (e.g. `"agents": ["lambert"]`). A single-agent assignment is hard-pinned by the server — it will queue for that agent only and skip the routing table. If the user explicitly asks for fan-out/all agents, set `scope: "fan-out"`.
103
104
  After emitting a dispatch-like action, return immediately; do not poll, monitor, watch, wait, or check until completion, and do not add follow-up status actions. Only create a watch, monitor, poll, or periodically check when the human explicitly asks for monitoring, watching, periodic checks, or notification on completion.