@yemi33/minions 0.1.554 → 0.1.556

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,11 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.554 (2026-04-07)
3
+ ## 0.1.556 (2026-04-07)
4
4
 
5
5
  ### Features
6
6
  - add pin/unpin button to inbox and KB document modals
7
7
 
8
8
  ### Fixes
9
+ - defer dispatched status until spawnAgent succeeds + add _any_ routing token (#482)
10
+ - move pin button to modal header next to Copy button
9
11
  - fetch actual build error logs instead of just reason string (#489)
10
12
 
11
13
  ## 0.1.552 (2026-04-07)
@@ -5,6 +5,7 @@ let _modalFilePath = null; // file path for steering (null = read-only Q&A only)
5
5
 
6
6
  function showModalQa() {
7
7
  document.getElementById('modal-qa').style.display = '';
8
+ updateModalPinBtn();
8
9
  }
9
10
 
10
11
  function _qaNotifySidebar(filePath) {
@@ -41,6 +41,7 @@ function closeModal() {
41
41
  body.contentEditable = 'false';
42
42
  body.style.border = '';
43
43
  body.style.padding = '';
44
+ document.getElementById('modal-pin-btn').style.display = 'none';
44
45
  document.getElementById('modal-edit-btn').style.display = 'none';
45
46
  document.getElementById('modal-save-btn').style.display = 'none';
46
47
  document.getElementById('modal-cancel-edit-btn').style.display = 'none';
@@ -216,13 +216,9 @@ async function kbOpenItem(category, file) {
216
216
  try {
217
217
  const content = await fetch('/api/knowledge/' + category + '/' + encodeURIComponent(file)).then(r => r.text());
218
218
  const display = content.replace(/^---[\s\S]*?---\n*/m, '');
219
- var pk = kbPinKey(category, file);
220
- var pinned = isPinned(pk);
221
219
  document.getElementById('modal-title').textContent = file;
222
220
  const modalBody = document.getElementById('modal-body');
223
- modalBody.innerHTML =
224
- '<div style="margin-bottom:12px"><button class="pr-pager-btn pin-btn' + (pinned ? ' pinned' : '') + '" style="font-size:10px;padding:3px 10px" data-pin-key="' + escHtml(pk) + '" onclick="_togglePinAndRefresh(this.dataset.pinKey,\'kb\');kbOpenItem(\'' + escHtml(category) + '\',\'' + escHtml(file) + '\')">' + (pinned ? 'Unpin' : 'Pin to top') + '</button></div>' +
225
- renderMd(display);
221
+ modalBody.innerHTML = renderMd(display);
226
222
  _modalDocContext = { title: file, content: display, selection: '' };
227
223
  _modalFilePath = 'knowledge/' + category + '/' + file; showModalQa();
228
224
  // Clear notification badge when opening this document
@@ -91,14 +91,9 @@ function openAllPrs() {
91
91
  function openModal(i) {
92
92
  const item = inboxData[i];
93
93
  if (!item) return;
94
- var pk = inboxPinKey(item.name);
95
- var pinned = isPinned(pk);
96
94
  document.getElementById('modal-title').textContent = item.name;
97
95
  document.getElementById('modal-body').innerHTML =
98
- '<div style="margin-bottom:12px;display:flex;gap:8px;align-items:center">' +
99
- '<button class="pr-pager-btn pin-btn' + (pinned ? ' pinned' : '') + '" style="font-size:10px;padding:3px 10px" data-pin-key="' + escHtml(pk) + '" onclick="_togglePinAndRefresh(this.dataset.pinKey,\'inbox\');openModal(' + i + ')">' + (pinned ? 'Unpin' : 'Pin to top') + '</button>' +
100
- '<button class="pr-pager-btn" style="font-size:10px;padding:3px 10px" onclick="promoteToKB(\'' + escHtml(item.name) + '\')">Add to Knowledge Base</button>' +
101
- '</div>' +
96
+ '<div style="margin-bottom:12px"><button class="pr-pager-btn" style="font-size:10px;padding:3px 10px" onclick="promoteToKB(\'' + escHtml(item.name) + '\')">Add to Knowledge Base</button></div>' +
102
97
  '<div style="font-size:12px;line-height:1.7;color:var(--muted)">' + renderMd(item.content) + '</div>';
103
98
  _modalDocContext = { title: item.name, content: item.content, selection: '' };
104
99
  _modalFilePath = 'notes/inbox/' + item.name; showModalQa();
@@ -35,6 +35,27 @@ function _togglePinAndRefresh(key, source) {
35
35
  else if (source === 'kb') renderKnowledgeBase();
36
36
  }
37
37
 
38
+ function updateModalPinBtn() {
39
+ var btn = document.getElementById('modal-pin-btn');
40
+ if (!btn) return;
41
+ // Only show for inbox and KB docs (pinnable paths)
42
+ if (!_modalFilePath || (!_modalFilePath.startsWith('notes/inbox/') && !_modalFilePath.startsWith('knowledge/'))) {
43
+ btn.style.display = 'none'; return;
44
+ }
45
+ btn.style.display = '';
46
+ var pinned = isPinned(_modalFilePath);
47
+ btn.textContent = pinned ? 'Unpin' : 'Pin';
48
+ btn.classList.toggle('pinned', pinned);
49
+ }
50
+ function toggleModalPin() {
51
+ if (!_modalFilePath) return;
52
+ var pinned = togglePin(_modalFilePath);
53
+ showToast('cmd-toast', pinned ? 'Pinned to top' : 'Unpinned', true);
54
+ updateModalPinBtn();
55
+ if (_modalFilePath.startsWith('notes/inbox/')) renderInbox(inboxData);
56
+ else if (_modalFilePath.startsWith('knowledge/')) renderKnowledgeBase();
57
+ }
58
+
38
59
  function escHtml(s) {
39
60
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
40
61
  }
@@ -101,6 +101,7 @@
101
101
  <button class="modal-copy" id="modal-edit-btn" onclick="modalToggleEdit()" title="Edit" style="display:none"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61zM12.9 2.97L4.288 11.58l-.537 1.878 1.878-.537L14.242 4.31 12.9 2.97z"/></svg> Edit</button>
102
102
  <button class="modal-copy" id="modal-save-btn" onclick="modalSaveEdit()" title="Save" style="display:none;color:var(--green);border-color:var(--green)"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg> Save</button>
103
103
  <button class="modal-copy" id="modal-cancel-edit-btn" onclick="modalCancelEdit()" title="Cancel edit" style="display:none">Cancel</button>
104
+ <button class="modal-copy pin-btn" id="modal-pin-btn" onclick="toggleModalPin()" title="Pin to top" style="display:none">Pin</button>
104
105
  <button class="modal-copy" id="modal-copy-btn" onclick="copyModalContent()" title="Copy to clipboard"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25z"/></svg> Copy</button>
105
106
  <button class="modal-close" onclick="closeModal()">X</button>
106
107
  </div>
package/engine/routing.js CHANGED
@@ -121,16 +121,26 @@ function resolveAgent(workType, config, authorAgent = null) {
121
121
  return true;
122
122
  };
123
123
 
124
- // Check preferred and fallback first (routing table order)
125
- if (preferred && isAvailable(preferred)) { _claimedAgents.add(preferred); return preferred; }
126
- if (fallback && isAvailable(fallback)) { _claimedAgents.add(fallback); return fallback; }
124
+ // Helper: pick any idle agent sorted by error rate
125
+ const pickAnyIdle = (exclude = []) => {
126
+ const excludeSet = new Set(exclude.filter(Boolean));
127
+ const idle = Object.keys(agents)
128
+ .filter(id => !excludeSet.has(id) && isAvailable(id))
129
+ .sort((a, b) => getAgentErrorRate(a) - getAgentErrorRate(b));
130
+ if (idle[0]) { _claimedAgents.add(idle[0]); return idle[0]; }
131
+ return null;
132
+ };
127
133
 
128
- // Fall back to any idle agent, preferring lower error rates
129
- const idle = Object.keys(agents)
130
- .filter(id => id !== preferred && id !== fallback && isAvailable(id))
131
- .sort((a, b) => getAgentErrorRate(a) - getAgentErrorRate(b));
134
+ // Resolve _any_ token — pick any available agent (#480)
135
+ if (preferred === '_any_') { const pick = pickAnyIdle(); if (pick) return pick; }
136
+ else if (preferred && isAvailable(preferred)) { _claimedAgents.add(preferred); return preferred; }
132
137
 
133
- if (idle[0]) { _claimedAgents.add(idle[0]); return idle[0]; }
138
+ if (fallback === '_any_') { const pick = pickAnyIdle([preferred]); if (pick) return pick; }
139
+ else if (fallback && isAvailable(fallback)) { _claimedAgents.add(fallback); return fallback; }
140
+
141
+ // Fall back to any idle agent, preferring lower error rates
142
+ const anyIdle = pickAnyIdle([preferred, fallback]);
143
+ if (anyIdle) return anyIdle;
134
144
 
135
145
  // No idle configured agent — try temp agent if enabled
136
146
  if (config.engine?.allowTempAgents) {
package/engine.js CHANGED
@@ -1556,7 +1556,7 @@ function discoverFromWorkItems(config, project) {
1556
1556
  const items = safeJson(projectWorkItemsPath(project)) || [];
1557
1557
  const cooldownMs = (src.cooldownMinutes || 0) * 60 * 1000;
1558
1558
  const newWork = [];
1559
- const prdSyncQueue = [];
1559
+ // PRD sync for dispatched status deferred to spawnAgent success (#480)
1560
1560
  const skipped = { gated: 0, noAgent: 0 };
1561
1561
  let needsWrite = false;
1562
1562
  const selfHealKeys = new Set(); // Collect keys for batched self-heal (1 lock instead of N)
@@ -1603,10 +1603,11 @@ function discoverFromWorkItems(config, project) {
1603
1603
  needsWrite = true;
1604
1604
  }
1605
1605
  if (isAlreadyDispatched(key)) {
1606
- if (item.status === WI_STATUS.PENDING) { item.status = WI_STATUS.DISPATCHED; needsWrite = true; }
1607
- if (!item.dispatched_to) {
1608
- const existing = getDispatch().active?.find(d => d.meta?.dispatchKey === key);
1609
- if (existing?.agent) { item.dispatched_to = existing.agent; needsWrite = true; }
1606
+ // Only self-heal to DISPATCHED if actually in dispatch.active (agent spawned) (#480)
1607
+ const existingActive = getDispatch().active?.find(d => d.meta?.dispatchKey === key);
1608
+ if (existingActive) {
1609
+ if (item.status === WI_STATUS.PENDING) { item.status = WI_STATUS.DISPATCHED; needsWrite = true; }
1610
+ if (!item.dispatched_to && existingActive.agent) { item.dispatched_to = existingActive.agent; needsWrite = true; }
1610
1611
  }
1611
1612
  if (item._pendingReason !== 'already_dispatched') { item._pendingReason = 'already_dispatched'; needsWrite = true; }
1612
1613
  skipped.gated++; continue;
@@ -1691,12 +1692,11 @@ function discoverFromWorkItems(config, project) {
1691
1692
  continue;
1692
1693
  }
1693
1694
 
1694
- // Mark item as dispatched BEFORE adding to newWork (prevents race on next tick)
1695
- item.status = WI_STATUS.DISPATCHED;
1696
- item.dispatched_at = ts();
1697
- item.dispatched_to = agentId;
1695
+ // Don't mark dispatched here item stays pending until spawnAgent succeeds.
1696
+ // spawnAgent() atomically stamps dispatched_to/dispatched_at/status via locked write (#480).
1697
+ // isAlreadyDispatched(key) prevents re-discovery on subsequent ticks.
1698
1698
  delete item._pendingReason;
1699
- prdSyncQueue.push({ id: item.id, sourcePlan: item.sourcePlan });
1699
+ needsWrite = true;
1700
1700
 
1701
1701
  newWork.push({
1702
1702
  type: workType,
@@ -1738,12 +1738,9 @@ function discoverFromWorkItems(config, project) {
1738
1738
  }
1739
1739
  }
1740
1740
 
1741
- // Write back updated statuses (always, since we mark items dispatched before newWork check)
1742
- if (newWork.length > 0 || needsWrite) {
1741
+ // Write back updated statuses (pendingReason clears, checkpoint counts, decompose flags, etc.)
1742
+ if (needsWrite) {
1743
1743
  mutateWorkItems(projectWorkItemsPath(project), () => items);
1744
- if (newWork.length > 0) {
1745
- for (const s of prdSyncQueue) syncPrdItemStatus(s.id, 'dispatched', s.sourcePlan);
1746
- }
1747
1744
  }
1748
1745
 
1749
1746
  const skipTotal = skipped.gated + skipped.noAgent;
@@ -2030,13 +2027,14 @@ function discoverCentralWorkItems(config) {
2030
2027
  const key = `central-work-${item.id}`;
2031
2028
  // Self-heal: if already dispatched but work item is still pending, fix the status
2032
2029
  if (isAlreadyDispatched(key)) {
2033
- const m = {};
2034
- if (item.status === WI_STATUS.PENDING) { m.status = WI_STATUS.DISPATCHED; }
2035
- if (!item.dispatched_to) {
2036
- const existing = getDispatch().active?.find(d => d.meta?.dispatchKey === key);
2037
- if (existing?.agent) { m.dispatched_to = existing.agent; }
2030
+ // Only self-heal to DISPATCHED if actually in dispatch.active (agent spawned) (#480)
2031
+ const existingActive = getDispatch().active?.find(d => d.meta?.dispatchKey === key);
2032
+ if (existingActive) {
2033
+ const m = {};
2034
+ if (item.status === WI_STATUS.PENDING) { m.status = WI_STATUS.DISPATCHED; }
2035
+ if (!item.dispatched_to && existingActive.agent) { m.dispatched_to = existingActive.agent; }
2036
+ if (Object.keys(m).length > 0) mutations.set(item.id, m);
2038
2037
  }
2039
- if (Object.keys(m).length > 0) mutations.set(item.id, m);
2040
2038
  continue;
2041
2039
  }
2042
2040
  if (isOnCooldown(key, 0)) continue;
@@ -2109,15 +2107,13 @@ function discoverCentralWorkItems(config) {
2109
2107
  });
2110
2108
  }
2111
2109
 
2110
+ // Don't mark dispatched here — spawnAgent stamps status atomically (#480)
2112
2111
  mutations.set(item.id, {
2113
- status: WI_STATUS.DISPATCHED,
2114
- dispatched_at: ts(),
2115
- dispatched_to: idleAgents.map(a => a.id).join(', '),
2116
2112
  scope: 'fan-out',
2117
2113
  fanOutAgents: idleAgents.map(a => a.id),
2118
2114
  });
2119
2115
  setCooldown(key);
2120
- log('info', `Fan-out: ${item.id} dispatched to ${idleAgents.length} agents: ${idleAgents.map(a => a.name).join(', ')}`);
2116
+ log('info', `Fan-out: ${item.id} queued for ${idleAgents.length} agents: ${idleAgents.map(a => a.name).join(', ')}`);
2121
2117
 
2122
2118
  } else {
2123
2119
  // ─── Normal: single agent dispatch ──────────────────────────────
@@ -2215,12 +2211,7 @@ function discoverCentralWorkItems(config) {
2215
2211
  continue;
2216
2212
  }
2217
2213
 
2218
- const dispatchMutation = {
2219
- status: WI_STATUS.DISPATCHED,
2220
- dispatched_at: ts(),
2221
- dispatched_to: agentId,
2222
- };
2223
- mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, dispatchMutation));
2214
+ // Don't mark dispatched here — spawnAgent stamps status atomically (#480)
2224
2215
 
2225
2216
  newWork.push({
2226
2217
  type: workType,
@@ -2675,6 +2666,11 @@ async function tickInner() {
2675
2666
  }
2676
2667
  } else {
2677
2668
  dispatched.add(item.id);
2669
+ // Sync PRD item status after successful spawn (#480)
2670
+ if (item.meta?.item?.sourcePlan) {
2671
+ try { syncPrdItemStatus(item.meta.item.id, WI_STATUS.DISPATCHED, item.meta.item.sourcePlan); }
2672
+ catch (e) { log('warn', `prd sync after spawn: ${e.message}`); }
2673
+ }
2678
2674
  }
2679
2675
  }
2680
2676
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.554",
3
+ "version": "0.1.556",
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"
package/routing.md CHANGED
@@ -10,7 +10,7 @@ How the engine decides who handles what. Parsed by engine.js — keep the table
10
10
  | implement | dallas | ralph |
11
11
  | implement:large | rebecca | dallas |
12
12
  | review | ripley | lambert |
13
- | fix | _author_ | dallas |
13
+ | fix | _author_ | _any_ |
14
14
  | plan | ripley | rebecca |
15
15
  | plan-to-prd | lambert | rebecca |
16
16
  | explore | ripley | rebecca |
@@ -22,6 +22,7 @@ How the engine decides who handles what. Parsed by engine.js — keep the table
22
22
 
23
23
  Notes:
24
24
  - `_author_` means route to the PR author
25
+ - `_any_` means route to any available idle agent (lowest error rate first)
25
26
  - `implement:large` is for items with `estimated_complexity: "large"`
26
27
  - Engine falls back to any idle agent if both preferred and fallback are busy
27
28