@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 CHANGED
@@ -1,13 +1,16 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.420 (2026-04-06)
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 (cached for 4 hours) ──────────────────────────────────
172
+ // ── npm update check ────────────────────────────────────────────────────────
173
173
  let _npmVersionCache = null;
174
174
  let _npmVersionCacheTs = 0;
175
- const NPM_CHECK_INTERVAL = 4 * 60 * 60 * 1000; // 4 hours
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) < NPM_CHECK_INTERVAL) return _npmVersionCache;
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(() => {}), NPM_CHECK_INTERVAL);
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
- req.on('data', chunk => { body += chunk; if (body.length > 1e6) reject(new Error('Too large')); });
796
- req.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { reject(e); } });
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
- if (body.name !== undefined) target.name = body.name;
1326
- if (body.description !== undefined) target.description = body.description;
1327
- if (body.priority !== undefined) target.priority = body.priority;
1328
- if (body.estimated_complexity !== undefined) target.estimated_complexity = body.estimated_complexity;
1329
- if (body.status !== undefined) target.status = body.status;
1330
- item = target;
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 === 'paused' && w._pausedBy === 'prd-pause') {
1933
- w.status = 'pending';
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 === 'dispatched') {
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 !== 'paused') reset++;
2021
- w.status = 'paused';
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 f of fs.readdirSync(prdDir)) {
2678
- if (!f.endsWith('.json')) continue;
2679
- const prd = safeJson(path.join(prdDir, f));
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, f), prd);
2687
- pausedPrd = f;
2688
- // Pause work items (reuse pause logic inline)
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
- const items = safeJson(wiPath);
2698
- if (!items) continue;
2699
- let changed = false;
2700
- for (const w of items) {
2701
- if (w.sourcePlan !== f) continue;
2702
- if (w.completedAt || DONE_STATUSES.has(w.status)) continue;
2703
- if (w.status === 'dispatched') {
2704
- const activeEntry = (dispatch.active || []).find(d => d.meta?.item?.id === w.id || d.meta?.dispatchKey?.includes(w.id));
2705
- if (activeEntry) {
2706
- const statusPath = path.join(MINIONS_DIR, 'agents', activeEntry.agent, 'status.json');
2707
- try {
2708
- const agentStatus = JSON.parse(safeRead(statusPath) || '{}');
2709
- if (agentStatus.pid) {
2710
- try {
2711
- const safePid = shared.validatePid(agentStatus.pid);
2712
- if (process.platform === 'win32') {
2713
- require('child_process').execFileSync('taskkill', ['/PID', String(safePid), '/F', '/T'], { stdio: 'pipe', timeout: 5000, windowsHide: true });
2714
- } else {
2715
- process.kill(safePid, 'SIGTERM');
2716
- }
2717
- } catch { /* process may be dead or invalid PID */ }
2718
- }
2719
- agentStatus.status = 'idle';
2720
- delete agentStatus.currentTask;
2721
- delete agentStatus.dispatched;
2722
- safeWrite(statusPath, agentStatus);
2723
- } catch { /* agent reset */ }
2724
- killedAgents.add(activeEntry.agent);
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
- w.status = 'pending';
2728
- delete w.dispatched_at;
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.420",
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"