@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 +3 -1
- package/dashboard/js/modal-qa.js +1 -0
- package/dashboard/js/modal.js +1 -0
- package/dashboard/js/render-kb.js +1 -5
- package/dashboard/js/render-prs.js +1 -6
- package/dashboard/js/utils.js +21 -0
- package/dashboard/layout.html +1 -0
- package/engine/routing.js +18 -8
- package/engine.js +27 -31
- package/package.json +1 -1
- package/routing.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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)
|
package/dashboard/js/modal-qa.js
CHANGED
package/dashboard/js/modal.js
CHANGED
|
@@ -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
|
|
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();
|
package/dashboard/js/utils.js
CHANGED
|
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
40
61
|
}
|
package/dashboard/layout.html
CHANGED
|
@@ -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
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
//
|
|
129
|
-
const
|
|
130
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if (
|
|
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
|
-
//
|
|
1695
|
-
|
|
1696
|
-
|
|
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
|
-
|
|
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 (
|
|
1742
|
-
if (
|
|
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
|
-
|
|
2034
|
-
|
|
2035
|
-
if (
|
|
2036
|
-
const
|
|
2037
|
-
if (
|
|
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}
|
|
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
|
-
|
|
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.
|
|
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_ |
|
|
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
|
|