@yemi33/minions 0.1.1972 → 0.1.1974

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.
@@ -485,17 +485,103 @@ function ccRenderTabBar() {
485
485
  for (var i = 0; i < _ccTabs.length; i++) {
486
486
  var t = _ccTabs[i];
487
487
  var isActive = t.id === _ccActiveTabId;
488
- html += '<div class="cc-tab' + (isActive ? ' active' : '') + (t._sending ? ' working' : '') + '" onclick="ccSwitchTab(\'' + t.id + '\')" title="' + escHtml(t.title) + '">';
488
+ // draggable="true" + DnD handlers enable click-and-drag reorder of tabs.
489
+ // The handlers below splice _ccTabs in place (preserving per-tab in-flight
490
+ // state: _sending, _queue, _abortController, _streamedText, _toolsUsed)
491
+ // and persist via ccSaveState. The close X / new-tab + opt out of drag
492
+ // with draggable="false" + ondragstart preventDefault so the affordances
493
+ // don't accidentally start a drag.
494
+ html += '<div class="cc-tab' + (isActive ? ' active' : '') + (t._sending ? ' working' : '') + '" draggable="true"'
495
+ + ' ondragstart="ccTabDragStart(event, \'' + t.id + '\')"'
496
+ + ' ondragover="ccTabDragOver(event, \'' + t.id + '\')"'
497
+ + ' ondragleave="ccTabDragLeave(event)"'
498
+ + ' ondrop="ccTabDrop(event, \'' + t.id + '\')"'
499
+ + ' ondragend="ccTabDragEnd(event)"'
500
+ + ' onclick="ccSwitchTab(\'' + t.id + '\')" title="' + escHtml(t.title) + '">';
489
501
  html += '<span class="cc-tab-text">' + escHtml(t.title) + '</span>';
490
502
  if (t._unread) html += '<span class="notif-badge done"></span>';
491
- html += '<span class="cc-tab-close" onclick="event.stopPropagation();ccCloseTab(\'' + t.id + '\')">&times;</span>';
503
+ html += '<span class="cc-tab-close" draggable="false" ondragstart="event.preventDefault();event.stopPropagation();" onmousedown="event.stopPropagation();" onclick="event.stopPropagation();ccCloseTab(\'' + t.id + '\')">&times;</span>';
492
504
  html += '</div>';
493
505
  }
494
- html += '<div class="cc-tab cc-tab-new" onclick="ccNewTab()" title="New tab">+</div>';
506
+ html += '<div class="cc-tab cc-tab-new" draggable="false" ondragstart="event.preventDefault();event.stopPropagation();" onclick="ccNewTab()" title="New tab">+</div>';
495
507
  html += '</div>';
496
508
  bar.innerHTML = html;
497
509
  }
498
510
 
511
+ // ── Tab drag-to-reorder ─────────────────────────────────────────────────────
512
+ // Native HTML5 DnD: dragstart records the source tab id, dragover on a peer
513
+ // tab calls preventDefault to allow drop and adds a drop-target indicator,
514
+ // drop splices _ccTabs in place so per-tab references (and their in-flight
515
+ // state) are preserved, and dragend clears any leftover visual state.
516
+ var _ccDragSourceId = null;
517
+
518
+ function ccTabDragStart(ev, id) {
519
+ _ccDragSourceId = id;
520
+ try {
521
+ ev.dataTransfer.effectAllowed = 'move';
522
+ // setData is required for Firefox to actually start the drag.
523
+ ev.dataTransfer.setData('text/plain', id);
524
+ } catch {}
525
+ var el = ev.currentTarget;
526
+ if (el && el.classList) el.classList.add('cc-tab-dragging');
527
+ }
528
+
529
+ function ccTabDragOver(ev, id) {
530
+ if (!_ccDragSourceId) return;
531
+ // preventDefault must fire on every dragover (including the source) so the
532
+ // drop event actually fires — required to detect "released on same tab".
533
+ ev.preventDefault();
534
+ try { ev.dataTransfer.dropEffect = 'move'; } catch {}
535
+ if (_ccDragSourceId === id) return;
536
+ var el = ev.currentTarget;
537
+ if (el && el.classList) el.classList.add('cc-tab-drop-target');
538
+ }
539
+
540
+ function ccTabDragLeave(ev) {
541
+ var el = ev.currentTarget;
542
+ if (el && el.classList) el.classList.remove('cc-tab-drop-target');
543
+ }
544
+
545
+ function ccTabDrop(ev, targetId) {
546
+ ev.preventDefault();
547
+ var srcId = _ccDragSourceId;
548
+ // Clear all visual indicators before any early-return.
549
+ var nodes = document.querySelectorAll('.cc-tab-drop-target, .cc-tab-dragging');
550
+ for (var i = 0; i < nodes.length; i++) {
551
+ nodes[i].classList.remove('cc-tab-drop-target');
552
+ nodes[i].classList.remove('cc-tab-dragging');
553
+ }
554
+ // Released on the source tab (or no source): treat as a click — activate
555
+ // the tab and skip any reorder/state churn. ccSwitchTab is a no-op when
556
+ // the target is already active.
557
+ if (!srcId || srcId === targetId) {
558
+ if (targetId) ccSwitchTab(targetId);
559
+ return;
560
+ }
561
+ var fromIdx = _ccTabs.findIndex(function(t) { return t.id === srcId; });
562
+ var toIdx = _ccTabs.findIndex(function(t) { return t.id === targetId; });
563
+ if (fromIdx === -1 || toIdx === -1) return;
564
+ // Splice the existing tab reference so per-tab in-flight state survives
565
+ // (_sending, _queue, _abortController, _streamedText, _toolsUsed,
566
+ // _retryRequests, _sendStartedAt, etc.). Do NOT clone/replace the object.
567
+ var moved = _ccTabs.splice(fromIdx, 1)[0];
568
+ _ccTabs.splice(toIdx, 0, moved);
569
+ // _ccActiveTabId is intentionally unchanged: the same tab stays active,
570
+ // just at a new position.
571
+ ccRenderTabBar();
572
+ ccSaveState();
573
+ }
574
+
575
+ function ccTabDragEnd(_ev) {
576
+ _ccDragSourceId = null;
577
+ // Defensive cleanup: dragend always fires, even if drop didn't.
578
+ var nodes = document.querySelectorAll('.cc-tab-dragging, .cc-tab-drop-target');
579
+ for (var i = 0; i < nodes.length; i++) {
580
+ nodes[i].classList.remove('cc-tab-dragging');
581
+ nodes[i].classList.remove('cc-tab-drop-target');
582
+ }
583
+ }
584
+
499
585
  function ccRestoreMessages() {
500
586
  var el = document.getElementById('cc-messages');
501
587
  var tab = _ccActiveTab();
@@ -654,8 +654,11 @@
654
654
 
655
655
  /* Command Center tab bar */
656
656
  .cc-tab-scroll { display: flex; gap: 4px; align-items: center; overflow-x: auto; overflow-y: hidden; flex: 1 1 auto; min-width: 0; scrollbar-width: thin; }
657
- .cc-tab { padding: 4px 10px; font-size: 10px; border: 1px solid var(--border); border-bottom: none; border-radius: 6px 6px 0 0; background: var(--surface2); color: var(--muted); cursor: pointer; white-space: nowrap; max-width: 140px; display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; margin-bottom: -1px; position: relative; }
658
- .cc-tab-new { color: var(--muted); padding: 4px 8px; }
657
+ .cc-tab { padding: 4px 10px; font-size: 10px; border: 1px solid var(--border); border-bottom: none; border-radius: 6px 6px 0 0; background: var(--surface2); color: var(--muted); cursor: grab; white-space: nowrap; max-width: 140px; display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; margin-bottom: -1px; position: relative; }
658
+ .cc-tab:active { cursor: grabbing; }
659
+ .cc-tab.cc-tab-dragging { opacity: 0.5; cursor: grabbing; }
660
+ .cc-tab.cc-tab-drop-target { box-shadow: inset 2px 0 0 0 var(--blue); }
661
+ .cc-tab-new { color: var(--muted); padding: 4px 8px; cursor: pointer; }
659
662
  .cc-tab-text { overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; }
660
663
  .cc-tab.active { color: var(--text); background: var(--bg); border-color: var(--border); z-index: 1; font-weight: 600; }
661
664
  .cc-tab.working { border-color: var(--blue); }
package/engine/cli.js CHANGED
@@ -521,7 +521,14 @@ const commands = {
521
521
  const savedBranch = normalizeSessionBranch(sj?.branch);
522
522
  if (sj?.sessionId && (!expectedBranch || savedBranch === expectedBranch)) {
523
523
  sessionId = sj.sessionId;
524
- } else if (sj?.sessionId && expectedBranch) {
524
+ } else if (sj?.sessionId && expectedBranch && sj?.dispatchId === item.id) {
525
+ // Only warn when the saved session is for THIS dispatch but on the
526
+ // wrong branch — that's a true anomaly worth flagging. The common
527
+ // case — leftover session.json from a previous (now-completed)
528
+ // dispatch on a different branch — is expected and silent, since
529
+ // the engine writes session.json on completion of each dispatch
530
+ // and a fresh dispatch may run on a different branch before
531
+ // saveSession overwrites it (W-mpbn93ou000611b3).
525
532
  shared.log('warn', `Reattach: ignoring session for ${agentId} on branch ${savedBranch || 'unknown'}; expected ${expectedBranch}`);
526
533
  }
527
534
  } catch {}
@@ -529,9 +529,44 @@ function main() {
529
529
  clearTimeout(startupTimer);
530
530
  clearTimeout(initialSnapshotTimer);
531
531
  clearInterval(descTimer);
532
+
533
+ // Compute the exit code and write the [process-exit] sentinel FIRST,
534
+ // before the descendant snapshot/reap. The engine's orphan reaper uses
535
+ // the sentinel as the single signal that "the runtime exited cleanly
536
+ // with code N"; if we delay it behind `snapshotDescendants()` (which
537
+ // shells out to `Get-CimInstance` on Windows and can block 1-5+s),
538
+ // there's a window where the runtime PID we track is already dead but
539
+ // no sentinel exists yet. After an engine restart that path triggers
540
+ // `canReapDeadProcess` and the dispatch gets auto-retried as orphaned
541
+ // even though it completed normally. See W-mpbn93ou000611b3 / the
542
+ // 2026-05-18 ripley-explore regression.
543
+ //
544
+ // Prefer the 'exit' event's code/signal when present (Node's 'close'
545
+ // event can report code=0 on Windows when the OS-level exit was
546
+ // non-zero — see the long-form note above the exit handler).
547
+ const effectiveCode = (realExitFromEvent != null) ? realExitFromEvent : code;
548
+ const effectiveSignal = realSignalFromEvent || signal;
549
+ const exitCode = normalizeRuntimeExit(effectiveCode, effectiveSignal);
550
+ if (sentinelWritten) {
551
+ // Defense-in-depth: never write a duplicate sentinel. We observed pairs
552
+ // of [process-exit] code=0 lines in live-output.log across many failed
553
+ // runs, which suggests close has fired twice in some edge cases (e.g.,
554
+ // shim re-launch on Windows). One sentinel per spawn is the contract.
555
+ // Skip descendant reap on the duplicate close too — the first close
556
+ // already handled it (reaping the same PIDs again is a no-op at best,
557
+ // but skipping is faster and matches the prior early-return contract).
558
+ fs.appendFileSync(debugPath, `EXIT (duplicate close, skipping sentinel): code=${exitCode}${effectiveSignal ? ` signal=${effectiveSignal}` : ''}\n`);
559
+ process.exit(exitCode);
560
+ return;
561
+ }
562
+ sentinelWritten = true;
563
+ const sentinelResult = writeProcessExitSentinel({ exitCode, signal: effectiveSignal });
564
+
532
565
  // Final snapshot + reap, but only when the runtime actually spawned
533
566
  // children. Read-only / very short agents (exit before the 3s initial
534
- // snapshot fires) skip the wmic shell-out entirely.
567
+ // snapshot fires) skip the wmic shell-out entirely. Runs AFTER the
568
+ // sentinel write so a slow Get-CimInstance call can't gate completion
569
+ // detection — see the hoist note above.
535
570
  if (trackedDescendants.size || gotFirstOutput) {
536
571
  snapshotDescendants();
537
572
  if (trackedDescendants.size) {
@@ -580,21 +615,6 @@ function main() {
580
615
  try { fs.appendFileSync(debugPath, `DESCENDANTS reaped=${reaped}/${toKillPids.length} kept=${kept.length}\n`); } catch {}
581
616
  }
582
617
  }
583
- // Prefer the 'exit' event's code/signal when present — see note above.
584
- const effectiveCode = (realExitFromEvent != null) ? realExitFromEvent : code;
585
- const effectiveSignal = realSignalFromEvent || signal;
586
- const exitCode = normalizeRuntimeExit(effectiveCode, effectiveSignal);
587
- if (sentinelWritten) {
588
- // Defense-in-depth: never write a duplicate sentinel. We observed pairs
589
- // of [process-exit] code=0 lines in live-output.log across many failed
590
- // runs, which suggests close has fired twice in some edge cases (e.g.,
591
- // shim re-launch on Windows). One sentinel per spawn is the contract.
592
- fs.appendFileSync(debugPath, `EXIT (duplicate close, skipping sentinel): code=${exitCode}${effectiveSignal ? ` signal=${effectiveSignal}` : ''}\n`);
593
- process.exit(exitCode);
594
- return;
595
- }
596
- sentinelWritten = true;
597
- const sentinelResult = writeProcessExitSentinel({ exitCode, signal: effectiveSignal });
598
618
  fs.appendFileSync(debugPath, `EXIT: code=${exitCode}${effectiveSignal ? ` signal=${effectiveSignal}` : ''} (close=${code} exit=${realExitFromEvent})\nSTDERR: ${stderrBuf.slice(0, 500)}\n`);
599
619
  if (!sentinelResult.fileWritten) {
600
620
  fs.appendFileSync(debugPath, `EXIT SENTINEL: file write failed for ${process.env.MINIONS_LIVE_OUTPUT_PATH}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1972",
3
+ "version": "0.1.1974",
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"