@yemi33/minions 0.1.911 → 0.1.913

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,8 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.911 (2026-04-13)
3
+ ## 0.1.913 (2026-04-13)
4
4
 
5
5
  ### Features
6
+ - fix dep re-merge failure on retry with existing commits (#977)
6
7
  - audit and harden log buffering implementation (#971)
7
8
  - replace magic string 'active' with PR_STATUS.ACTIVE in lifecycle.js (#969)
8
9
  - fix dashboard plan-pause nested lock violation (#968)
@@ -16,6 +17,7 @@
16
17
  - add red dot notification on CC tab when response completes (#934) (#946)
17
18
 
18
19
  ### Fixes
20
+ - CC queued messages sent to wrong tab after tab switch
19
21
  - pass sysPromptPath instead of steerPromptPath as system prompt in steering resume (#962)
20
22
  - PRD item display dispatched/in-progress status conflation (closes #950) (#955)
21
23
  - remove KB watchdog — checkpoint never written, restore never worked
@@ -368,6 +368,7 @@ async function ccSend() {
368
368
  input.value = '';
369
369
 
370
370
  var tab = _ccActiveTab();
371
+ var originTabId = _ccActiveTabId;
371
372
  if (!tab) return;
372
373
  if (!tab._queue) tab._queue = [];
373
374
 
@@ -377,16 +378,16 @@ async function ccSend() {
377
378
  _renderQueueIndicator();
378
379
  return;
379
380
  }
380
- var wasAborted = await _ccDoSend(message);
381
+ var wasAborted = await _ccDoSend(message, false, originTabId);
381
382
 
382
- // Flush queued messages one at a time, pausing after abort to let server release ccInFlight
383
+ // Flush queued messages to the ORIGINAL tab, even if user switched tabs
383
384
  while (tab._queue && tab._queue.length > 0) {
384
385
  if (wasAborted) {
385
386
  await new Promise(function(r) { setTimeout(r, 1500); });
386
387
  }
387
388
  var next = tab._queue.shift();
388
389
  _renderQueueIndicator();
389
- wasAborted = await _ccDoSend(next);
390
+ wasAborted = await _ccDoSend(next, false, originTabId);
390
391
  }
391
392
  }
392
393
 
@@ -407,29 +408,30 @@ function _renderQueueIndicator() {
407
408
  if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
408
409
  }
409
410
 
410
- async function _ccDoSend(message, skipUserMsg) {
411
+ async function _ccDoSend(message, skipUserMsg, forceTabId) {
411
412
  // Client-side /pin and /unpin — no LLM round-trip needed
412
413
  var pinMatch = message.match(/^\/(pin|unpin)\s+(.+)/i);
413
414
  if (pinMatch) {
414
- if (!skipUserMsg) ccAddMessage('user', escHtml(message));
415
+ if (!skipUserMsg) ccAddMessage('user', escHtml(message), false, forceTabId);
415
416
  var pinAction = pinMatch[1].toLowerCase();
416
417
  var pinQuery = pinMatch[2].toLowerCase().trim();
417
418
  var found = _ccFindPinTarget(pinQuery);
418
419
  if (found) {
419
420
  var wasPinned = isPinned(found.key);
420
- if (pinAction === 'pin' && !wasPinned) { togglePin(found.key); ccAddMessage('assistant', 'Pinned <strong>' + escHtml(found.label) + '</strong> to top'); }
421
- else if (pinAction === 'unpin' && wasPinned) { togglePin(found.key); ccAddMessage('assistant', 'Unpinned <strong>' + escHtml(found.label) + '</strong>'); }
422
- else { ccAddMessage('assistant', '<strong>' + escHtml(found.label) + '</strong> is already ' + (wasPinned ? 'pinned' : 'unpinned')); }
421
+ if (pinAction === 'pin' && !wasPinned) { togglePin(found.key); ccAddMessage('assistant', 'Pinned <strong>' + escHtml(found.label) + '</strong> to top', false, forceTabId); }
422
+ else if (pinAction === 'unpin' && wasPinned) { togglePin(found.key); ccAddMessage('assistant', 'Unpinned <strong>' + escHtml(found.label) + '</strong>', false, forceTabId); }
423
+ else { ccAddMessage('assistant', '<strong>' + escHtml(found.label) + '</strong> is already ' + (wasPinned ? 'pinned' : 'unpinned'), false, forceTabId); }
423
424
  showToast('cmd-toast', pinAction === 'pin' ? 'Pinned to top' : 'Unpinned', true);
424
425
  renderInbox(inboxData); renderKnowledgeBase();
425
426
  } else {
426
- ccAddMessage('assistant', 'No inbox or KB item matching "' + escHtml(pinQuery) + '"');
427
+ ccAddMessage('assistant', 'No inbox or KB item matching "' + escHtml(pinQuery) + '"', false, forceTabId);
427
428
  }
428
429
  return;
429
430
  }
430
431
 
431
- var activeTab = _ccActiveTab();
432
- var activeTabId = _ccActiveTabId;
432
+ // Use forced tab ID (from queue flush) or fall back to current active tab
433
+ var activeTabId = forceTabId || _ccActiveTabId;
434
+ var activeTab = _ccTabs.find(function(t) { return t.id === activeTabId; }) || _ccActiveTab();
433
435
  if (!activeTab) return;
434
436
  activeTab._sending = true;
435
437
  activeTab._sendStartedAt = Date.now();
package/engine.js CHANGED
@@ -524,7 +524,37 @@ async function spawnAgent(dispatchItem, config) {
524
524
  }
525
525
  // Merge successfully-fetched + recovered (local-only pushed) branches sequentially
526
526
  const fetched = fetchable.filter((_, i) => fetchResults[i].status === 'fulfilled' || recoveredBranches.has(fetchable[i].branch));
527
- if (!depMergeFailed) {
527
+ // Skip dep re-merge if worktree HEAD already contains all dep commits (#973)
528
+ let skipDepMerge = false;
529
+ if (!depMergeFailed && fetched.length > 0) {
530
+ const ancestorChecks = await Promise.all(
531
+ fetched.map(async ({ branch: depBranch }) => {
532
+ try {
533
+ await execAsync(`git merge-base --is-ancestor "origin/${depBranch}" HEAD`, { ..._gitOpts, cwd: worktreePath });
534
+ return true;
535
+ } catch (_) { return false; }
536
+ })
537
+ );
538
+ if (ancestorChecks.every(Boolean)) {
539
+ log('info', `All ${fetched.length} dep branch(es) already merged into ${branchName} — skipping dep re-merge`);
540
+ skipDepMerge = true;
541
+ }
542
+ }
543
+ // Stash uncommitted changes before dep merge if worktree is dirty (#973)
544
+ let stashed = false;
545
+ if (!depMergeFailed && !skipDepMerge && fetched.length > 0) {
546
+ try {
547
+ const statusOut = (await execAsync('git status --porcelain', { ..._gitOpts, cwd: worktreePath })).stdout.toString().trim();
548
+ if (statusOut) {
549
+ await execAsync('git stash push --include-untracked -m "engine: stash before dep re-merge"', { ..._gitOpts, cwd: worktreePath });
550
+ stashed = true;
551
+ log('info', `Stashed uncommitted changes in ${branchName} before dep merge`);
552
+ }
553
+ } catch (stashErr) {
554
+ log('warn', `Failed to stash changes in ${branchName} before dep merge: ${stashErr.message}`);
555
+ }
556
+ }
557
+ if (!depMergeFailed && !skipDepMerge) {
528
558
  for (const { branch: depBranch, prId } of fetched) {
529
559
  try {
530
560
  await execAsync(`git merge "origin/${depBranch}" --no-edit`, { ..._gitOpts, cwd: worktreePath });
@@ -573,6 +603,15 @@ async function spawnAgent(dispatchItem, config) {
573
603
  }
574
604
  }
575
605
  }
606
+ // Restore stashed changes after dep merge (#973)
607
+ if (stashed) {
608
+ try {
609
+ await execAsync('git stash pop', { ..._gitOpts, cwd: worktreePath });
610
+ log('info', `Restored stashed changes in ${branchName} after dep merge`);
611
+ } catch (popErr) {
612
+ log('warn', `git stash pop failed in ${branchName}: ${popErr.message} — stash preserved for agent`);
613
+ }
614
+ }
576
615
  if (depMergeFailed) {
577
616
  _cleanupPromptFiles();
578
617
  // Build actionable failReason identifying the conflicting branch and files
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.911",
3
+ "version": "0.1.913",
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"