@yemi33/minions 0.1.419 → 0.1.421
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 +4 -1
- package/dashboard/js/command-center.js +8 -2
- package/dashboard.js +67 -61
- package/engine/ado.js +1 -1
- package/engine/llm.js +2 -15
- package/engine/preflight.js +7 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.421 (2026-04-06)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- Dashboard robustness — raw string fixes, plan steering clarity, safeWrite race fix
|
|
7
|
+
- Low-priority cleanup: dedupe regex, consolidate streaming parse, add path validation alignment
|
|
6
8
|
- Fix 6 medium bugs: dispatch pruning, null guards, skill regex, meeting advancement, CLI PID check, pipeline retry
|
|
7
9
|
- Convert remaining lifecycle.js safeWrite calls to mutateJsonFileLocked
|
|
8
10
|
|
|
9
11
|
### Fixes
|
|
12
|
+
- CC retry now drains queued messages after success
|
|
10
13
|
- address review feedback — pipeline.js socket leak and magic numbers
|
|
11
14
|
|
|
12
15
|
## 0.1.417 (2026-04-06)
|
|
@@ -344,8 +344,14 @@ function ccRetryLast() {
|
|
|
344
344
|
const el = document.getElementById('cc-messages');
|
|
345
345
|
if (el?.lastElementChild) el.lastElementChild.remove();
|
|
346
346
|
_ccMessages = _ccMessages.slice(0, -1); // remove error from history
|
|
347
|
-
// Resend
|
|
348
|
-
_ccDoSend(text.trim())
|
|
347
|
+
// Resend, then drain queue
|
|
348
|
+
_ccDoSend(text.trim()).then(async () => {
|
|
349
|
+
while (_ccQueue.length > 0) {
|
|
350
|
+
const next = _ccQueue.shift();
|
|
351
|
+
_renderQueueIndicator();
|
|
352
|
+
await _ccDoSend(next);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
349
355
|
}
|
|
350
356
|
|
|
351
357
|
async function _ccFetch(url, body) {
|
package/dashboard.js
CHANGED
|
@@ -792,8 +792,13 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
792
792
|
function readBody(req) {
|
|
793
793
|
return new Promise((resolve, reject) => {
|
|
794
794
|
let body = '';
|
|
795
|
-
|
|
796
|
-
|
|
795
|
+
const timeout = setTimeout(() => {
|
|
796
|
+
req.destroy();
|
|
797
|
+
reject(new Error('Request body timeout after 30s'));
|
|
798
|
+
}, 30000);
|
|
799
|
+
req.on('data', chunk => { body += chunk; if (body.length > 1e6) { clearTimeout(timeout); reject(new Error('Too large')); } });
|
|
800
|
+
req.on('end', () => { clearTimeout(timeout); try { resolve(JSON.parse(body)); } catch(e) { reject(e); } });
|
|
801
|
+
req.on('error', (e) => { clearTimeout(timeout); reject(e); });
|
|
797
802
|
});
|
|
798
803
|
}
|
|
799
804
|
|
|
@@ -1321,17 +1326,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
1321
1326
|
let item;
|
|
1322
1327
|
mutateJsonFileLocked(planPath, (plan) => {
|
|
1323
1328
|
const target = (plan.missing_features || []).find(f => f.id === body.itemId);
|
|
1324
|
-
if (target)
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
}
|
|
1329
|
+
if (!target) return plan; // TOCTOU: item deleted between pre-check and lock acquisition
|
|
1330
|
+
if (body.name !== undefined) target.name = body.name;
|
|
1331
|
+
if (body.description !== undefined) target.description = body.description;
|
|
1332
|
+
if (body.priority !== undefined) target.priority = body.priority;
|
|
1333
|
+
if (body.estimated_complexity !== undefined) target.estimated_complexity = body.estimated_complexity;
|
|
1334
|
+
if (body.status !== undefined) target.status = body.status;
|
|
1335
|
+
item = target;
|
|
1332
1336
|
return plan;
|
|
1333
1337
|
}, { defaultValue: preCheck });
|
|
1334
1338
|
|
|
1339
|
+
// If item was deleted between pre-check and lock, return 404
|
|
1340
|
+
if (!item) return jsonReply(res, 404, { error: 'item not found in plan (deleted concurrently)' });
|
|
1341
|
+
|
|
1335
1342
|
// Feature 3: Sync edits to materialized work item if still pending
|
|
1336
1343
|
let workItemSynced = false;
|
|
1337
1344
|
const wiSyncPaths = [path.join(MINIONS_DIR, 'work-items.json')];
|
|
@@ -1929,8 +1936,8 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1929
1936
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
1930
1937
|
if (!Array.isArray(items)) return items;
|
|
1931
1938
|
for (const w of items) {
|
|
1932
|
-
if (w.sourcePlan === body.file && w.status ===
|
|
1933
|
-
w.status =
|
|
1939
|
+
if (w.sourcePlan === body.file && w.status === WI_STATUS.PAUSED && w._pausedBy === 'prd-pause') {
|
|
1940
|
+
w.status = WI_STATUS.PENDING;
|
|
1934
1941
|
delete w._pausedBy;
|
|
1935
1942
|
w._resumedAt = new Date().toISOString();
|
|
1936
1943
|
resumedItemIds.push(w.id);
|
|
@@ -1991,7 +1998,7 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1991
1998
|
// Keep completed items as-is, reset everything else to pending.
|
|
1992
1999
|
if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
|
|
1993
2000
|
|
|
1994
|
-
if (w.status ===
|
|
2001
|
+
if (w.status === WI_STATUS.DISPATCHED) {
|
|
1995
2002
|
// Kill the agent working on this item, if any.
|
|
1996
2003
|
const activeEntry = (dispatch.active || []).find(d => d.meta?.item?.id === w.id || d.meta?.dispatchKey?.includes(w.id));
|
|
1997
2004
|
if (activeEntry) {
|
|
@@ -2017,8 +2024,8 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2017
2024
|
}
|
|
2018
2025
|
}
|
|
2019
2026
|
|
|
2020
|
-
if (w.status !==
|
|
2021
|
-
w.status =
|
|
2027
|
+
if (w.status !== WI_STATUS.PAUSED) reset++;
|
|
2028
|
+
w.status = WI_STATUS.PAUSED;
|
|
2022
2029
|
w._pausedBy = 'prd-pause';
|
|
2023
2030
|
delete w._resumedAt;
|
|
2024
2031
|
delete w.dispatched_at;
|
|
@@ -2329,9 +2336,8 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2329
2336
|
try {
|
|
2330
2337
|
const body = await readBody(req);
|
|
2331
2338
|
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
}
|
|
2339
|
+
try { shared.sanitizePath(body.file, body.file.endsWith('.json') ? PRD_DIR : PLANS_DIR); }
|
|
2340
|
+
catch { return jsonReply(res, 400, { error: 'invalid filename' }); }
|
|
2335
2341
|
const isJson = body.file.endsWith('.json');
|
|
2336
2342
|
const targetDir = isJson ? PRD_DIR : PLANS_DIR;
|
|
2337
2343
|
const archivePath = path.join(targetDir, 'archive', body.file);
|
|
@@ -2675,18 +2681,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2675
2681
|
try {
|
|
2676
2682
|
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
2677
2683
|
if (fs.existsSync(prdDir)) {
|
|
2678
|
-
for (const
|
|
2679
|
-
if (!
|
|
2680
|
-
const prd = safeJson(path.join(prdDir,
|
|
2684
|
+
for (const prdFile of fs.readdirSync(prdDir)) {
|
|
2685
|
+
if (!prdFile.endsWith('.json')) continue;
|
|
2686
|
+
const prd = safeJson(path.join(prdDir, prdFile));
|
|
2681
2687
|
if (!prd || prd.source_plan !== planFile) continue;
|
|
2682
2688
|
if (prd.status === 'paused' || prd.status === 'rejected') continue;
|
|
2683
2689
|
// Found an active PRD linked to this plan — pause it
|
|
2684
2690
|
prd.status = 'paused';
|
|
2685
2691
|
prd.pausedAt = new Date().toISOString();
|
|
2686
2692
|
prd.pausedBy = 'plan-steering';
|
|
2687
|
-
safeWrite(path.join(prdDir,
|
|
2688
|
-
pausedPrd =
|
|
2689
|
-
// Pause work items (
|
|
2693
|
+
safeWrite(path.join(prdDir, prdFile), prd);
|
|
2694
|
+
pausedPrd = prdFile;
|
|
2695
|
+
// Pause work items linked to this PRD (sourcePlan = PRD filename)
|
|
2690
2696
|
const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
|
|
2691
2697
|
for (const proj of PROJECTS) wiPaths.push(shared.projectWorkItemsPath(proj));
|
|
2692
2698
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
@@ -2695,45 +2701,45 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2695
2701
|
const resetItemIds = new Set();
|
|
2696
2702
|
for (const wiPath of wiPaths) {
|
|
2697
2703
|
try {
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2704
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
2705
|
+
if (!Array.isArray(items)) return items;
|
|
2706
|
+
for (const w of items) {
|
|
2707
|
+
if (w.sourcePlan !== prdFile) continue;
|
|
2708
|
+
if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
|
|
2709
|
+
if (w.status === WI_STATUS.DISPATCHED) {
|
|
2710
|
+
const activeEntry = (dispatch.active || []).find(d => d.meta?.item?.id === w.id || d.meta?.dispatchKey?.includes(w.id));
|
|
2711
|
+
if (activeEntry) {
|
|
2712
|
+
const statusPath = path.join(MINIONS_DIR, 'agents', activeEntry.agent, 'status.json');
|
|
2713
|
+
try {
|
|
2714
|
+
const agentStatus = JSON.parse(safeRead(statusPath) || '{}');
|
|
2715
|
+
if (agentStatus.pid) {
|
|
2716
|
+
try {
|
|
2717
|
+
const safePid = shared.validatePid(agentStatus.pid);
|
|
2718
|
+
if (process.platform === 'win32') {
|
|
2719
|
+
require('child_process').execFileSync('taskkill', ['/PID', String(safePid), '/F', '/T'], { stdio: 'pipe', timeout: 5000, windowsHide: true });
|
|
2720
|
+
} else {
|
|
2721
|
+
process.kill(safePid, 'SIGTERM');
|
|
2722
|
+
}
|
|
2723
|
+
} catch { /* process may be dead or invalid PID */ }
|
|
2724
|
+
}
|
|
2725
|
+
agentStatus.status = 'idle';
|
|
2726
|
+
delete agentStatus.currentTask;
|
|
2727
|
+
delete agentStatus.dispatched;
|
|
2728
|
+
safeWrite(statusPath, agentStatus);
|
|
2729
|
+
} catch { /* agent reset */ }
|
|
2730
|
+
killedAgents.add(activeEntry.agent);
|
|
2731
|
+
}
|
|
2726
2732
|
}
|
|
2733
|
+
w.status = WI_STATUS.PAUSED;
|
|
2734
|
+
w._pausedBy = 'plan-steering';
|
|
2735
|
+
delete w.dispatched_at;
|
|
2736
|
+
delete w.dispatched_to;
|
|
2737
|
+
delete w.failReason;
|
|
2738
|
+
delete w.failedAt;
|
|
2739
|
+
if (w.id) resetItemIds.add(w.id);
|
|
2727
2740
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
delete w.dispatched_to;
|
|
2731
|
-
delete w.failReason;
|
|
2732
|
-
delete w.failedAt;
|
|
2733
|
-
changed = true;
|
|
2734
|
-
if (w.id) resetItemIds.add(w.id);
|
|
2735
|
-
}
|
|
2736
|
-
if (changed) safeWrite(wiPath, items);
|
|
2741
|
+
return items;
|
|
2742
|
+
}, { defaultValue: [] });
|
|
2737
2743
|
} catch { /* reset work items */ }
|
|
2738
2744
|
}
|
|
2739
2745
|
if (resetItemIds.size > 0 || killedAgents.size > 0) {
|
package/engine/ado.js
CHANGED
|
@@ -381,7 +381,7 @@ async function reconcilePrs(config) {
|
|
|
381
381
|
const title = adoPr.title || '';
|
|
382
382
|
// Extract item ID from branch name or PR title (e.g., feat(P-2cafdc2a): ...)
|
|
383
383
|
const branchMatch = branch.match(/(P-[a-z0-9]{6,})/i) || branch.match(/(W-[a-z0-9]{6,})/i) || branch.match(/(PL-[a-z0-9]{6,})/i);
|
|
384
|
-
const titleMatch = title.match(/\((P-[a-z0-9]{6,})\)/) || title.match(/\((W-[a-z0-9]{6,})\)/) || title.match(/\((
|
|
384
|
+
const titleMatch = title.match(/\((P-[a-z0-9]{6,})\)/) || title.match(/\((W-[a-z0-9]{6,})\)/) || title.match(/\((PL-[a-z0-9]{6,})\)/);
|
|
385
385
|
const linkedItemId = branchMatch?.[1] || titleMatch?.[1] || null;
|
|
386
386
|
const linkedItem = linkedItemId ? allItems.find(i => i.id === linkedItemId) : null;
|
|
387
387
|
const confirmedItemId = linkedItem ? linkedItemId : null;
|
package/engine/llm.js
CHANGED
|
@@ -160,25 +160,12 @@ function callLLMStreaming(promptText, sysPromptText, { timeout = 120000, label =
|
|
|
160
160
|
if (block.type === 'text' && block.text && block.text !== lastTextSent) {
|
|
161
161
|
lastTextSent = block.text;
|
|
162
162
|
onChunk(block.text);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} catch { /* incomplete JSON or non-JSON line */ }
|
|
167
|
-
}
|
|
168
|
-
// Also emit tool_use events so the frontend can show "Using tool: Read..."
|
|
169
|
-
for (const line of lines) {
|
|
170
|
-
const trimmed = line.trim();
|
|
171
|
-
if (!trimmed || !trimmed.startsWith('{')) continue;
|
|
172
|
-
try {
|
|
173
|
-
const obj = JSON.parse(trimmed);
|
|
174
|
-
if (obj.type === 'assistant' && obj.message?.content) {
|
|
175
|
-
for (const block of obj.message.content) {
|
|
176
|
-
if (block.type === 'tool_use' && block.name && onToolUse) {
|
|
163
|
+
} else if (block.type === 'tool_use' && block.name && onToolUse) {
|
|
177
164
|
onToolUse(block.name, block.input);
|
|
178
165
|
}
|
|
179
166
|
}
|
|
180
167
|
}
|
|
181
|
-
} catch {}
|
|
168
|
+
} catch { /* incomplete JSON or non-JSON line */ }
|
|
182
169
|
}
|
|
183
170
|
});
|
|
184
171
|
proc.stderr.on('data', d => { stderr += d.toString(); });
|
package/engine/preflight.js
CHANGED
|
@@ -27,7 +27,13 @@ function findClaudeBinary() {
|
|
|
27
27
|
path.join(path.dirname(process.execPath), '..', 'lib', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
28
28
|
// fnm / volta — sibling to the node binary
|
|
29
29
|
path.join(path.dirname(process.execPath), 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
30
|
-
].filter(
|
|
30
|
+
].filter(p => {
|
|
31
|
+
if (!p) {
|
|
32
|
+
if (process.env.MINIONS_DEBUG) console.log('[preflight] Dropped empty CLI search path entry');
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
31
37
|
for (const p of searchPaths) {
|
|
32
38
|
try { if (fs.existsSync(p)) return p; } catch {}
|
|
33
39
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.421",
|
|
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"
|