@yemi33/minions 0.1.1612 → 0.1.1614
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 +7 -1
- package/dashboard/js/command-center.js +5 -0
- package/dashboard/js/utils.js +2 -2
- package/dashboard.js +83 -8
- package/engine/queries.js +9 -1
- package/engine/routing.js +18 -1
- package/engine.js +7 -4
- package/minions.js +1 -0
- package/package.json +1 -1
- package/prompts/cc-system.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1614 (2026-04-29)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- recover ===ACTIONS=== JSON from fences and trailing prose (#1834) (#1837)
|
|
7
|
+
|
|
8
|
+
## 0.1.1613 (2026-04-28)
|
|
4
9
|
|
|
5
10
|
### Features
|
|
11
|
+
- fix npm test failures (#1846)
|
|
6
12
|
- hard-pin agent assignment when CC names a specific agent
|
|
7
13
|
- auto-detect available CLI runtimes and pin engine.defaultCli
|
|
8
14
|
- match runtime tags to actual logos (pixel-crab Claude, mascot Copilot)
|
|
@@ -623,6 +623,11 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
623
623
|
if (evt.actions && evt.actions.length > 0) {
|
|
624
624
|
_tagServerExecuted(evt.actions, evt.actionResults);
|
|
625
625
|
for (var ai = 0; ai < evt.actions.length; ai++) { await ccExecuteAction(evt.actions[ai], activeTabId); }
|
|
626
|
+
} else if (evt.actionParseError) {
|
|
627
|
+
// Issue #1834: server saw ===ACTIONS=== but couldn't parse the JSON.
|
|
628
|
+
// Surface as an inline warning so the user knows actions were dropped
|
|
629
|
+
// (was previously silent — appeared as "actions failed" with no signal).
|
|
630
|
+
addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--red);background:var(--surface2);border-radius:6px;margin:4px 0">⚠️ Actions block emitted but JSON could not be parsed — no actions were executed. Resend or rephrase. (' + escHtml(String(evt.actionParseError).slice(0, 200)) + ')</div>', false, activeTabId);
|
|
626
631
|
}
|
|
627
632
|
} else if (evt.type === 'error') {
|
|
628
633
|
terminalEventSeen = true;
|
package/dashboard/js/utils.js
CHANGED
|
@@ -183,8 +183,8 @@ function copyLlmText(btn) {
|
|
|
183
183
|
const clone = container.cloneNode(true);
|
|
184
184
|
clone.querySelectorAll('.llm-copy-btn').forEach(b => b.remove());
|
|
185
185
|
navigator.clipboard.writeText(clone.textContent.trim());
|
|
186
|
-
btn.
|
|
187
|
-
setTimeout(() => { btn.
|
|
186
|
+
btn.textContent = '\u2713';
|
|
187
|
+
setTimeout(() => { btn.textContent = '\u2398'; }, 1500);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
/**
|
package/dashboard.js
CHANGED
|
@@ -758,16 +758,68 @@ function findCCActionsDelimiter(text) {
|
|
|
758
758
|
return match.index + match[0].indexOf('===ACTIONS===');
|
|
759
759
|
}
|
|
760
760
|
|
|
761
|
+
// Issue #1834: non-Claude runtimes (Copilot/GPT) routinely wrap the action JSON
|
|
762
|
+
// in ```json fences or append trailing prose ("Let me know if that helps!").
|
|
763
|
+
// JSON.parse on the raw segment fails silently → actions dropped, user sees
|
|
764
|
+
// inert text. This extractor pulls out the balanced JSON value (array or
|
|
765
|
+
// object) regardless of fences, leading whitespace, or trailing junk so the
|
|
766
|
+
// downstream parse can succeed. Returns null if no plausible JSON value is
|
|
767
|
+
// present (caller surfaces the failure via _actionParseError).
|
|
768
|
+
function _extractActionsJson(segment) {
|
|
769
|
+
if (!segment) return null;
|
|
770
|
+
let body = segment.trim();
|
|
771
|
+
// Strip ```json / ``` fences (open + close). The model sometimes only emits
|
|
772
|
+
// an opening fence (truncation), so handle both halves independently.
|
|
773
|
+
body = body.replace(/^```[a-zA-Z0-9_-]*\s*\r?\n?/, '').replace(/\r?\n?```\s*$/, '').trim();
|
|
774
|
+
if (!body) return null;
|
|
775
|
+
const first = body.indexOf('[');
|
|
776
|
+
const firstObj = body.indexOf('{');
|
|
777
|
+
let start = -1;
|
|
778
|
+
let openCh = '';
|
|
779
|
+
let closeCh = '';
|
|
780
|
+
if (first >= 0 && (firstObj < 0 || first <= firstObj)) {
|
|
781
|
+
start = first; openCh = '['; closeCh = ']';
|
|
782
|
+
} else if (firstObj >= 0) {
|
|
783
|
+
start = firstObj; openCh = '{'; closeCh = '}';
|
|
784
|
+
}
|
|
785
|
+
if (start < 0) return null;
|
|
786
|
+
let depth = 0;
|
|
787
|
+
let inString = false;
|
|
788
|
+
let escape = false;
|
|
789
|
+
for (let i = start; i < body.length; i++) {
|
|
790
|
+
const ch = body[i];
|
|
791
|
+
if (escape) { escape = false; continue; }
|
|
792
|
+
if (ch === '\\') { escape = true; continue; }
|
|
793
|
+
if (ch === '"') { inString = !inString; continue; }
|
|
794
|
+
if (inString) continue;
|
|
795
|
+
if (ch === openCh) depth++;
|
|
796
|
+
else if (ch === closeCh) {
|
|
797
|
+
depth--;
|
|
798
|
+
if (depth === 0) return body.slice(start, i + 1);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
|
|
761
804
|
function parseCCActions(text) {
|
|
762
805
|
let actions = [];
|
|
763
806
|
let displayText = text;
|
|
807
|
+
let parseError = null;
|
|
764
808
|
const delimIdx = findCCActionsDelimiter(text);
|
|
765
809
|
if (delimIdx >= 0) {
|
|
766
810
|
displayText = text.slice(0, delimIdx).trim();
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
811
|
+
const segment = text.slice(delimIdx + '===ACTIONS==='.length);
|
|
812
|
+
const jsonStr = _extractActionsJson(segment);
|
|
813
|
+
if (jsonStr) {
|
|
814
|
+
try {
|
|
815
|
+
const parsed = JSON.parse(jsonStr);
|
|
816
|
+
actions = Array.isArray(parsed) ? parsed : [parsed];
|
|
817
|
+
} catch (e) {
|
|
818
|
+
parseError = e.message || 'invalid JSON';
|
|
819
|
+
}
|
|
820
|
+
} else if (segment.trim()) {
|
|
821
|
+
parseError = 'no JSON value found after ===ACTIONS=== delimiter';
|
|
822
|
+
}
|
|
771
823
|
}
|
|
772
824
|
if (actions.length === 0) {
|
|
773
825
|
const actionRegex = /`{3,}\s*action\s*\r?\n([\s\S]*?)`{3,}/g;
|
|
@@ -775,9 +827,24 @@ function parseCCActions(text) {
|
|
|
775
827
|
while ((match = actionRegex.exec(displayText)) !== null) {
|
|
776
828
|
try { actions.push(JSON.parse(match[1].trim())); } catch {}
|
|
777
829
|
}
|
|
778
|
-
if (actions.length > 0)
|
|
830
|
+
if (actions.length > 0) {
|
|
831
|
+
displayText = displayText.replace(/`{3,}\s*action\s*\r?\n[\s\S]*?`{3,}\n?/g, '').trim();
|
|
832
|
+
parseError = null; // legacy fallback recovered actions
|
|
833
|
+
}
|
|
779
834
|
}
|
|
780
|
-
|
|
835
|
+
const result = { text: displayText, actions };
|
|
836
|
+
if (parseError && actions.length === 0) {
|
|
837
|
+
result._actionParseError = parseError;
|
|
838
|
+
// Visibility for the engine log — silent failure here previously masked issue #1834.
|
|
839
|
+
try {
|
|
840
|
+
const snippet = (text.slice(delimIdx + '===ACTIONS==='.length).trim() || '').slice(0, 200);
|
|
841
|
+
console.warn(`[CC] action JSON parse failed (${parseError}); raw segment: ${snippet}`);
|
|
842
|
+
if (typeof shared !== 'undefined' && shared && typeof shared.log === 'function') {
|
|
843
|
+
shared.log('warn', `CC action JSON parse failed: ${parseError} — segment: ${snippet}`);
|
|
844
|
+
}
|
|
845
|
+
} catch { /* logging is best-effort */ }
|
|
846
|
+
}
|
|
847
|
+
return result;
|
|
781
848
|
}
|
|
782
849
|
|
|
783
850
|
// ── /loop → create-watch safety net ──────────────────────────────────────────
|
|
@@ -4382,7 +4449,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4382
4449
|
if (parsed.actions.length > 0) {
|
|
4383
4450
|
parsed.actionResults = await executeCCActions(parsed.actions);
|
|
4384
4451
|
}
|
|
4385
|
-
|
|
4452
|
+
// Issue #1834: rename _actionParseError → actionParseError (public field)
|
|
4453
|
+
// so the client can surface a warning when the model emitted ===ACTIONS===
|
|
4454
|
+
// but the JSON couldn't be recovered.
|
|
4455
|
+
const { _actionParseError, ...parsedReply } = parsed;
|
|
4456
|
+
const reply = { ...parsedReply, sessionId: ccSession.sessionId, newSession: !wasResume };
|
|
4457
|
+
if (_actionParseError) reply.actionParseError = _actionParseError;
|
|
4386
4458
|
if (sessionReset) reply.sessionReset = true;
|
|
4387
4459
|
return jsonReply(res, 200, reply);
|
|
4388
4460
|
} finally {
|
|
@@ -4639,7 +4711,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4639
4711
|
}
|
|
4640
4712
|
|
|
4641
4713
|
// Send final result with actions — execute server-side first
|
|
4642
|
-
const { text: displayText, actions } = parseCCActions(result.text);
|
|
4714
|
+
const { text: displayText, actions, _actionParseError } = parseCCActions(result.text);
|
|
4643
4715
|
// Safety net: detect /loop invocation and convert to create-watch
|
|
4644
4716
|
const _loopWatch = _detectLoopInvocation(displayText, actions, toolUses);
|
|
4645
4717
|
if (_loopWatch) {
|
|
@@ -4652,6 +4724,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4652
4724
|
actionResults = await executeCCActions(actions);
|
|
4653
4725
|
}
|
|
4654
4726
|
const donePayload = { type: 'done', text: displayText, actions, actionResults, sessionId: responseSessionId, newSession: !wasResume };
|
|
4727
|
+
// Issue #1834: surface action JSON parse failures so the UI can warn
|
|
4728
|
+
// instead of silently dropping. Client renders this as a small notice.
|
|
4729
|
+
if (_actionParseError) donePayload.actionParseError = _actionParseError;
|
|
4655
4730
|
if (sessionReset) donePayload.sessionReset = true;
|
|
4656
4731
|
liveState.donePayload = donePayload;
|
|
4657
4732
|
if (liveState.writer) liveState.writer(donePayload);
|
package/engine/queries.js
CHANGED
|
@@ -11,7 +11,7 @@ const shared = require('./shared');
|
|
|
11
11
|
|
|
12
12
|
const { safeRead, safeReadDir, safeJson, safeWrite, getProjects, mutateJsonFileLocked,
|
|
13
13
|
projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES,
|
|
14
|
-
WI_STATUS, DONE_STATUSES, PRD_ITEM_STATUS, PR_STATUS, ENGINE_DEFAULTS } = shared;
|
|
14
|
+
WI_STATUS, DONE_STATUSES, PRD_ITEM_STATUS, PR_STATUS, ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS } = shared;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Read the first `bytes` and last `bytes` of a file efficiently using byte offsets.
|
|
@@ -196,6 +196,14 @@ function getEngineLog() {
|
|
|
196
196
|
function getMetrics() {
|
|
197
197
|
const metrics = safeJson(path.join(ENGINE_DIR, 'metrics.json')) || {};
|
|
198
198
|
|
|
199
|
+
for (const [agentId, m] of Object.entries(metrics)) {
|
|
200
|
+
if (agentId.startsWith('_')) continue;
|
|
201
|
+
metrics[agentId] = {
|
|
202
|
+
...DEFAULT_AGENT_METRICS,
|
|
203
|
+
...(m && typeof m === 'object' && !Array.isArray(m) ? m : {}),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
199
207
|
// Enrich agent PR counts from pull-requests.json (source of truth)
|
|
200
208
|
const allPrs = getPullRequests();
|
|
201
209
|
const prCountByAgent = {};
|
package/engine/routing.js
CHANGED
|
@@ -116,7 +116,15 @@ function setTempBudget(n) {
|
|
|
116
116
|
}
|
|
117
117
|
function getTempBudget() { return _tempBudget; }
|
|
118
118
|
|
|
119
|
-
function
|
|
119
|
+
function normalizeAgentHints(agentHints, authorAgent = null) {
|
|
120
|
+
const raw = Array.isArray(agentHints) ? agentHints : (agentHints ? [agentHints] : []);
|
|
121
|
+
return raw
|
|
122
|
+
.map(id => id === '_author_' ? authorAgent : id)
|
|
123
|
+
.map(id => typeof id === 'string' ? id.trim().toLowerCase() : '')
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveAgent(workType, config, authorAgent = null, agentHints = null) {
|
|
120
128
|
const routes = getRoutingTableCached();
|
|
121
129
|
const route = routes[workType] || routes['implement'];
|
|
122
130
|
const agents = config.agents || {};
|
|
@@ -145,6 +153,14 @@ function resolveAgent(workType, config, authorAgent = null) {
|
|
|
145
153
|
return null;
|
|
146
154
|
};
|
|
147
155
|
|
|
156
|
+
const hintedAgents = normalizeAgentHints(agentHints, authorAgent);
|
|
157
|
+
if (hintedAgents.length > 0) {
|
|
158
|
+
for (const id of hintedAgents) {
|
|
159
|
+
if (isAvailable(id)) { _claimedAgents.add(id); return id; }
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
148
164
|
// Resolve _any_ token — pick any available agent (#480)
|
|
149
165
|
if (preferred === '_any_') { const pick = pickAnyIdle(); if (pick) return pick; }
|
|
150
166
|
else if (preferred && isAvailable(preferred)) { _claimedAgents.add(preferred); return preferred; }
|
|
@@ -192,4 +208,5 @@ module.exports = {
|
|
|
192
208
|
resolveAgent,
|
|
193
209
|
setTempBudget,
|
|
194
210
|
getTempBudget,
|
|
211
|
+
normalizeAgentHints,
|
|
195
212
|
};
|
package/engine.js
CHANGED
|
@@ -2441,7 +2441,8 @@ function discoverFromWorkItems(config, project) {
|
|
|
2441
2441
|
item._decomposing = true;
|
|
2442
2442
|
needsWrite = true;
|
|
2443
2443
|
}
|
|
2444
|
-
const
|
|
2444
|
+
const agentHints = item.preferred_agent || item.agents || null;
|
|
2445
|
+
const agentId = item.agent || resolveAgent(workType, config, null, agentHints);
|
|
2445
2446
|
if (!agentId) {
|
|
2446
2447
|
// Check if reason is budget
|
|
2447
2448
|
const cfgAgents = config.agents || {};
|
|
@@ -2960,7 +2961,8 @@ function discoverCentralWorkItems(config) {
|
|
|
2960
2961
|
|
|
2961
2962
|
} else {
|
|
2962
2963
|
// ─── Normal: single agent dispatch ──────────────────────────────
|
|
2963
|
-
const
|
|
2964
|
+
const agentHints = item.preferred_agent || item.agents || null;
|
|
2965
|
+
const agentId = item.agent || resolveAgent(workType, config, null, agentHints);
|
|
2964
2966
|
if (!agentId) continue;
|
|
2965
2967
|
|
|
2966
2968
|
const agentName = config.agents[agentId]?.name || agentId;
|
|
@@ -3602,7 +3604,8 @@ async function tickInner() {
|
|
|
3602
3604
|
// be of type string. Received undefined` and re-queues — every tick. Try to
|
|
3603
3605
|
// resolve a fallback via routing; if none is available, skip this tick.
|
|
3604
3606
|
if (!item.agent || typeof item.agent !== 'string') {
|
|
3605
|
-
const
|
|
3607
|
+
const agentHints = item.meta?.item?.preferred_agent || item.meta?.item?.agents || null;
|
|
3608
|
+
const fallback = resolveAgent(item.type || WORK_TYPE.FIX, config, null, agentHints);
|
|
3606
3609
|
if (!fallback) {
|
|
3607
3610
|
log('warn', `Pending dispatch ${item.id} has no agent and routing returned no fallback — skipping`);
|
|
3608
3611
|
continue;
|
|
@@ -3660,7 +3663,7 @@ async function tickInner() {
|
|
|
3660
3663
|
// Agent busy reassignment: if item has been waiting on a busy agent past the threshold,
|
|
3661
3664
|
// try to find an alternative agent via routing. Skip explicitly assigned items.
|
|
3662
3665
|
const reassignMs = config.engine?.agentBusyReassignMs ?? ENGINE_DEFAULTS.agentBusyReassignMs;
|
|
3663
|
-
const isExplicitReassign = !!item.meta?.item?.agent;
|
|
3666
|
+
const isExplicitReassign = !!(item.meta?.item?.agent || item.meta?.item?.preferred_agent || item.meta?.item?.agents?.length);
|
|
3664
3667
|
if (!isExplicitReassign && reassignMs > 0 && item._agentBusySince) {
|
|
3665
3668
|
const busySinceMs = new Date(item._agentBusySince).getTime();
|
|
3666
3669
|
if (Date.now() - busySinceMs > reassignMs) {
|
package/minions.js
CHANGED
|
@@ -465,6 +465,7 @@ async function initMinions({ skipScan = false, scanRoot, scanDepth } = {}) {
|
|
|
465
465
|
// Merge defaults — fills in new fields from upgrades while preserving user customizations
|
|
466
466
|
if (!config.engine) config.engine = {};
|
|
467
467
|
for (const [k, v] of Object.entries(ENGINE_DEFAULTS)) {
|
|
468
|
+
if (k === 'defaultCli') continue;
|
|
468
469
|
if (config.engine[k] === undefined) config.engine[k] = v;
|
|
469
470
|
}
|
|
470
471
|
if (!config.claude) config.claude = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1614",
|
|
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/prompts/cc-system.md
CHANGED
|
@@ -52,6 +52,8 @@ When in doubt, delegate. You are the dispatcher, not the worker. Agents have iso
|
|
|
52
52
|
## Actions
|
|
53
53
|
Append actions at the END of your response. Write your response first, then `===ACTIONS===` on its own line, then a JSON array. No text after the JSON. Omit entirely if no actions needed.
|
|
54
54
|
|
|
55
|
+
**CRITICAL — emit RAW JSON only.** Do NOT wrap the JSON array in ```json fences, ``` fences, or any other markdown. Do NOT add commentary or "Let me know if that helps" lines after the JSON. The JSON array must start with `[` on the line immediately after `===ACTIONS===` and end with `]` as the very last character of the response. Anything else (fences, prose, trailing commas) breaks server-side action parsing and your actions will be silently dropped.
|
|
56
|
+
|
|
55
57
|
Example:
|
|
56
58
|
I'll dispatch dallas to fix that bug.
|
|
57
59
|
|