@yemi33/minions 0.1.420 → 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 CHANGED
@@ -1,13 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.420 (2026-04-06)
3
+ ## 0.1.421 (2026-04-06)
4
4
 
5
5
  ### Features
6
+ - Dashboard robustness — raw string fixes, plan steering clarity, safeWrite race fix
6
7
  - Low-priority cleanup: dedupe regex, consolidate streaming parse, add path validation alignment
7
8
  - Fix 6 medium bugs: dispatch pruning, null guards, skill regex, meeting advancement, CLI PID check, pipeline retry
8
9
  - Convert remaining lifecycle.js safeWrite calls to mutateJsonFileLocked
9
10
 
10
11
  ### Fixes
12
+ - CC retry now drains queued messages after success
11
13
  - address review feedback — pipeline.js socket leak and magic numbers
12
14
 
13
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
- 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); } });
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
- 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
- }
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 === 'paused' && w._pausedBy === 'prd-pause') {
1933
- w.status = 'pending';
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 === 'dispatched') {
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 !== 'paused') reset++;
2021
- w.status = 'paused';
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;
@@ -2674,18 +2681,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2674
2681
  try {
2675
2682
  const prdDir = path.join(MINIONS_DIR, 'prd');
2676
2683
  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));
2684
+ for (const prdFile of fs.readdirSync(prdDir)) {
2685
+ if (!prdFile.endsWith('.json')) continue;
2686
+ const prd = safeJson(path.join(prdDir, prdFile));
2680
2687
  if (!prd || prd.source_plan !== planFile) continue;
2681
2688
  if (prd.status === 'paused' || prd.status === 'rejected') continue;
2682
2689
  // Found an active PRD linked to this plan — pause it
2683
2690
  prd.status = 'paused';
2684
2691
  prd.pausedAt = new Date().toISOString();
2685
2692
  prd.pausedBy = 'plan-steering';
2686
- safeWrite(path.join(prdDir, f), prd);
2687
- pausedPrd = f;
2688
- // Pause work items (reuse pause logic inline)
2693
+ safeWrite(path.join(prdDir, prdFile), prd);
2694
+ pausedPrd = prdFile;
2695
+ // Pause work items linked to this PRD (sourcePlan = PRD filename)
2689
2696
  const wiPaths = [path.join(MINIONS_DIR, 'work-items.json')];
2690
2697
  for (const proj of PROJECTS) wiPaths.push(shared.projectWorkItemsPath(proj));
2691
2698
  const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
@@ -2694,45 +2701,45 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2694
2701
  const resetItemIds = new Set();
2695
2702
  for (const wiPath of wiPaths) {
2696
2703
  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);
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
+ }
2725
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);
2726
2740
  }
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);
2741
+ return items;
2742
+ }, { defaultValue: [] });
2736
2743
  } catch { /* reset work items */ }
2737
2744
  }
2738
2745
  if (resetItemIds.size > 0 || killedAgents.size > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.420",
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"