@yemi33/minions 0.1.420 → 0.1.422
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 +71 -62
- package/engine/shared.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.422 (2026-04-06)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- configurable version check interval, default 1 hour
|
|
7
|
+
- Dashboard robustness — raw string fixes, plan steering clarity, safeWrite race fix
|
|
6
8
|
- Low-priority cleanup: dedupe regex, consolidate streaming parse, add path validation alignment
|
|
7
9
|
- Fix 6 medium bugs: dispatch pruning, null guards, skill regex, meeting advancement, CLI PID check, pipeline retry
|
|
8
10
|
- Convert remaining lifecycle.js safeWrite calls to mutateJsonFileLocked
|
|
9
11
|
|
|
10
12
|
### Fixes
|
|
13
|
+
- CC retry now drains queued messages after success
|
|
11
14
|
- address review feedback — pipeline.js socket leak and magic numbers
|
|
12
15
|
|
|
13
16
|
## 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
|
@@ -169,15 +169,17 @@ function getVerifyGuides() {
|
|
|
169
169
|
function getArchivedPrds() { return []; }
|
|
170
170
|
function getEngineState() { return queries.getControl(); }
|
|
171
171
|
|
|
172
|
-
// ── npm update check
|
|
172
|
+
// ── npm update check ────────────────────────────────────────────────────────
|
|
173
173
|
let _npmVersionCache = null;
|
|
174
174
|
let _npmVersionCacheTs = 0;
|
|
175
|
-
|
|
175
|
+
function _getVersionCheckInterval() {
|
|
176
|
+
try { return queries.getConfig()?.engine?.versionCheckInterval || shared.ENGINE_DEFAULTS.versionCheckInterval; } catch { return shared.ENGINE_DEFAULTS.versionCheckInterval; }
|
|
177
|
+
}
|
|
176
178
|
const PKG_NAME = '@yemi33/minions';
|
|
177
179
|
|
|
178
180
|
async function checkNpmVersion() {
|
|
179
181
|
const now = Date.now();
|
|
180
|
-
if (_npmVersionCache && (now - _npmVersionCacheTs) <
|
|
182
|
+
if (_npmVersionCache && (now - _npmVersionCacheTs) < _getVersionCheckInterval()) return _npmVersionCache;
|
|
181
183
|
try {
|
|
182
184
|
// Use npm view — respects user's .npmrc proxy/registry config
|
|
183
185
|
// Must use shell:true on Windows because npm is a .cmd batch script
|
|
@@ -210,7 +212,7 @@ function _compareVersions(a, b) {
|
|
|
210
212
|
|
|
211
213
|
// Kick off first npm check on startup, then re-check every 4 hours
|
|
212
214
|
checkNpmVersion().catch(() => {});
|
|
213
|
-
setInterval(() => checkNpmVersion().catch(() => {}),
|
|
215
|
+
setInterval(() => checkNpmVersion().catch(() => {}), _getVersionCheckInterval());
|
|
214
216
|
|
|
215
217
|
// Cache disk version + git commit (only changes on deploy/pull, not per-request)
|
|
216
218
|
let _diskVersionCache = null;
|
|
@@ -792,8 +794,13 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
792
794
|
function readBody(req) {
|
|
793
795
|
return new Promise((resolve, reject) => {
|
|
794
796
|
let body = '';
|
|
795
|
-
|
|
796
|
-
|
|
797
|
+
const timeout = setTimeout(() => {
|
|
798
|
+
req.destroy();
|
|
799
|
+
reject(new Error('Request body timeout after 30s'));
|
|
800
|
+
}, 30000);
|
|
801
|
+
req.on('data', chunk => { body += chunk; if (body.length > 1e6) { clearTimeout(timeout); reject(new Error('Too large')); } });
|
|
802
|
+
req.on('end', () => { clearTimeout(timeout); try { resolve(JSON.parse(body)); } catch(e) { reject(e); } });
|
|
803
|
+
req.on('error', (e) => { clearTimeout(timeout); reject(e); });
|
|
797
804
|
});
|
|
798
805
|
}
|
|
799
806
|
|
|
@@ -1321,17 +1328,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
1321
1328
|
let item;
|
|
1322
1329
|
mutateJsonFileLocked(planPath, (plan) => {
|
|
1323
1330
|
const target = (plan.missing_features || []).find(f => f.id === body.itemId);
|
|
1324
|
-
if (target)
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
}
|
|
1331
|
+
if (!target) return plan; // TOCTOU: item deleted between pre-check and lock acquisition
|
|
1332
|
+
if (body.name !== undefined) target.name = body.name;
|
|
1333
|
+
if (body.description !== undefined) target.description = body.description;
|
|
1334
|
+
if (body.priority !== undefined) target.priority = body.priority;
|
|
1335
|
+
if (body.estimated_complexity !== undefined) target.estimated_complexity = body.estimated_complexity;
|
|
1336
|
+
if (body.status !== undefined) target.status = body.status;
|
|
1337
|
+
item = target;
|
|
1332
1338
|
return plan;
|
|
1333
1339
|
}, { defaultValue: preCheck });
|
|
1334
1340
|
|
|
1341
|
+
// If item was deleted between pre-check and lock, return 404
|
|
1342
|
+
if (!item) return jsonReply(res, 404, { error: 'item not found in plan (deleted concurrently)' });
|
|
1343
|
+
|
|
1335
1344
|
// Feature 3: Sync edits to materialized work item if still pending
|
|
1336
1345
|
let workItemSynced = false;
|
|
1337
1346
|
const wiSyncPaths = [path.join(MINIONS_DIR, 'work-items.json')];
|
|
@@ -1929,8 +1938,8 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1929
1938
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
1930
1939
|
if (!Array.isArray(items)) return items;
|
|
1931
1940
|
for (const w of items) {
|
|
1932
|
-
if (w.sourcePlan === body.file && w.status ===
|
|
1933
|
-
w.status =
|
|
1941
|
+
if (w.sourcePlan === body.file && w.status === WI_STATUS.PAUSED && w._pausedBy === 'prd-pause') {
|
|
1942
|
+
w.status = WI_STATUS.PENDING;
|
|
1934
1943
|
delete w._pausedBy;
|
|
1935
1944
|
w._resumedAt = new Date().toISOString();
|
|
1936
1945
|
resumedItemIds.push(w.id);
|
|
@@ -1991,7 +2000,7 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
1991
2000
|
// Keep completed items as-is, reset everything else to pending.
|
|
1992
2001
|
if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
|
|
1993
2002
|
|
|
1994
|
-
if (w.status ===
|
|
2003
|
+
if (w.status === WI_STATUS.DISPATCHED) {
|
|
1995
2004
|
// Kill the agent working on this item, if any.
|
|
1996
2005
|
const activeEntry = (dispatch.active || []).find(d => d.meta?.item?.id === w.id || d.meta?.dispatchKey?.includes(w.id));
|
|
1997
2006
|
if (activeEntry) {
|
|
@@ -2017,8 +2026,8 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2017
2026
|
}
|
|
2018
2027
|
}
|
|
2019
2028
|
|
|
2020
|
-
if (w.status !==
|
|
2021
|
-
w.status =
|
|
2029
|
+
if (w.status !== WI_STATUS.PAUSED) reset++;
|
|
2030
|
+
w.status = WI_STATUS.PAUSED;
|
|
2022
2031
|
w._pausedBy = 'prd-pause';
|
|
2023
2032
|
delete w._resumedAt;
|
|
2024
2033
|
delete w.dispatched_at;
|
|
@@ -2674,18 +2683,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2674
2683
|
try {
|
|
2675
2684
|
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
2676
2685
|
if (fs.existsSync(prdDir)) {
|
|
2677
|
-
for (const
|
|
2678
|
-
if (!
|
|
2679
|
-
const prd = safeJson(path.join(prdDir,
|
|
2686
|
+
for (const prdFile of fs.readdirSync(prdDir)) {
|
|
2687
|
+
if (!prdFile.endsWith('.json')) continue;
|
|
2688
|
+
const prd = safeJson(path.join(prdDir, prdFile));
|
|
2680
2689
|
if (!prd || prd.source_plan !== planFile) continue;
|
|
2681
2690
|
if (prd.status === 'paused' || prd.status === 'rejected') continue;
|
|
2682
2691
|
// Found an active PRD linked to this plan — pause it
|
|
2683
2692
|
prd.status = 'paused';
|
|
2684
2693
|
prd.pausedAt = new Date().toISOString();
|
|
2685
2694
|
prd.pausedBy = 'plan-steering';
|
|
2686
|
-
safeWrite(path.join(prdDir,
|
|
2687
|
-
pausedPrd =
|
|
2688
|
-
// Pause work items (
|
|
2695
|
+
safeWrite(path.join(prdDir, prdFile), prd);
|
|
2696
|
+
pausedPrd = prdFile;
|
|
2697
|
+
// Pause work items linked to this PRD (sourcePlan = PRD filename)
|
|
2689
2698
|
const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
|
|
2690
2699
|
for (const proj of PROJECTS) wiPaths.push(shared.projectWorkItemsPath(proj));
|
|
2691
2700
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
@@ -2694,45 +2703,45 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2694
2703
|
const resetItemIds = new Set();
|
|
2695
2704
|
for (const wiPath of wiPaths) {
|
|
2696
2705
|
try {
|
|
2697
|
-
|
|
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
|
-
|
|
2706
|
+
mutateJsonFileLocked(wiPath, (items) => {
|
|
2707
|
+
if (!Array.isArray(items)) return items;
|
|
2708
|
+
for (const w of items) {
|
|
2709
|
+
if (w.sourcePlan !== prdFile) continue;
|
|
2710
|
+
if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
|
|
2711
|
+
if (w.status === WI_STATUS.DISPATCHED) {
|
|
2712
|
+
const activeEntry = (dispatch.active || []).find(d => d.meta?.item?.id === w.id || d.meta?.dispatchKey?.includes(w.id));
|
|
2713
|
+
if (activeEntry) {
|
|
2714
|
+
const statusPath = path.join(MINIONS_DIR, 'agents', activeEntry.agent, 'status.json');
|
|
2715
|
+
try {
|
|
2716
|
+
const agentStatus = JSON.parse(safeRead(statusPath) || '{}');
|
|
2717
|
+
if (agentStatus.pid) {
|
|
2718
|
+
try {
|
|
2719
|
+
const safePid = shared.validatePid(agentStatus.pid);
|
|
2720
|
+
if (process.platform === 'win32') {
|
|
2721
|
+
require('child_process').execFileSync('taskkill', ['/PID', String(safePid), '/F', '/T'], { stdio: 'pipe', timeout: 5000, windowsHide: true });
|
|
2722
|
+
} else {
|
|
2723
|
+
process.kill(safePid, 'SIGTERM');
|
|
2724
|
+
}
|
|
2725
|
+
} catch { /* process may be dead or invalid PID */ }
|
|
2726
|
+
}
|
|
2727
|
+
agentStatus.status = 'idle';
|
|
2728
|
+
delete agentStatus.currentTask;
|
|
2729
|
+
delete agentStatus.dispatched;
|
|
2730
|
+
safeWrite(statusPath, agentStatus);
|
|
2731
|
+
} catch { /* agent reset */ }
|
|
2732
|
+
killedAgents.add(activeEntry.agent);
|
|
2733
|
+
}
|
|
2725
2734
|
}
|
|
2735
|
+
w.status = WI_STATUS.PAUSED;
|
|
2736
|
+
w._pausedBy = 'plan-steering';
|
|
2737
|
+
delete w.dispatched_at;
|
|
2738
|
+
delete w.dispatched_to;
|
|
2739
|
+
delete w.failReason;
|
|
2740
|
+
delete w.failedAt;
|
|
2741
|
+
if (w.id) resetItemIds.add(w.id);
|
|
2726
2742
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
delete w.dispatched_to;
|
|
2730
|
-
delete w.failReason;
|
|
2731
|
-
delete w.failedAt;
|
|
2732
|
-
changed = true;
|
|
2733
|
-
if (w.id) resetItemIds.add(w.id);
|
|
2734
|
-
}
|
|
2735
|
-
if (changed) safeWrite(wiPath, items);
|
|
2743
|
+
return items;
|
|
2744
|
+
}, { defaultValue: [] });
|
|
2736
2745
|
} catch { /* reset work items */ }
|
|
2737
2746
|
}
|
|
2738
2747
|
if (resetItemIds.size > 0 || killedAgents.size > 0) {
|
package/engine/shared.js
CHANGED
|
@@ -440,6 +440,7 @@ const ENGINE_DEFAULTS = {
|
|
|
440
440
|
maxRetries: 3, // max dispatch retries before marking work item as failed
|
|
441
441
|
pipelineApiRetries: 2, // max attempts for pipeline API calls
|
|
442
442
|
pipelineApiRetryDelay: 2000, // ms delay between pipeline API retries
|
|
443
|
+
versionCheckInterval: 3600000, // 1 hour — how often to check npm for updates (ms)
|
|
443
444
|
};
|
|
444
445
|
|
|
445
446
|
// ─── Status & Type Constants ─────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.422",
|
|
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"
|