@yemi33/minions 0.1.322 → 0.1.324

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,6 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.322 (2026-04-03)
3
+ ## 0.1.324 (2026-04-03)
4
+
5
+ ### Fixes
6
+ - 7 bugs from engine audit — runtime crashes, race conditions, stale state
7
+ - scan skips NugetCache, OneDrive, .vs, packages + validates .git/HEAD
4
8
 
5
9
  ### Other
6
10
  - refactor: final magic string replacements in engine, lifecycle, cleanup
package/dashboard.js CHANGED
@@ -878,18 +878,22 @@ const server = http.createServer(async (req, res) => {
878
878
  }
879
879
  if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
880
880
 
881
- const items = JSON.parse(safeRead(wiPath) || '[]');
882
- const item = items.find(i => i.id === id);
883
- if (!item) return jsonReply(res, 404, { error: 'item not found' });
884
-
885
- item.status = 'pending';
886
- item._retryCount = 0; // Reset retry counter on manual retry
887
- delete item.dispatched_at;
888
- delete item.dispatched_to;
889
- delete item.failReason;
890
- delete item.failedAt;
891
- delete item.fanOutAgents;
892
- safeWrite(wiPath, items);
881
+ let found = false;
882
+ mutateJsonFileLocked(wiPath, (items) => {
883
+ if (!Array.isArray(items)) items = [];
884
+ const item = items.find(i => i.id === id);
885
+ if (!item) return items;
886
+ found = true;
887
+ item.status = 'pending';
888
+ item._retryCount = 0; // Reset retry counter on manual retry
889
+ delete item.dispatched_at;
890
+ delete item.dispatched_to;
891
+ delete item.failReason;
892
+ delete item.failedAt;
893
+ delete item.fanOutAgents;
894
+ return items;
895
+ });
896
+ if (!found) return jsonReply(res, 404, { error: 'item not found' });
893
897
 
894
898
  // Clear completed dispatch entries so the engine doesn't dedup this item
895
899
  const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
@@ -2897,7 +2901,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2897
2901
 
2898
2902
  // Find git repos recursively (same logic as minions.js findGitRepos)
2899
2903
  const skipDirs = new Set(['node_modules', '.git', '.hg', 'AppData', '$Recycle.Bin', 'Windows',
2900
- 'Program Files', 'Program Files (x86)', '.cache', '.npm', '.yarn', '.nuget', 'worktrees', '.minions', '.squad']);
2904
+ 'Program Files', 'Program Files (x86)', '.cache', '.npm', '.yarn', '.nuget', 'NugetCache',
2905
+ 'worktrees', '.minions', '.squad', '.vs', '.vscode', 'obj', 'bin', 'packages',
2906
+ 'OneDrive', 'OneDrive - Microsoft', '.copilot', 'marketplace-cache']);
2901
2907
  const repos = [];
2902
2908
  function walk(dir, depth) {
2903
2909
  if (depth > maxDepth) return;
@@ -2905,7 +2911,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
2905
2911
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
2906
2912
  for (const e of entries) {
2907
2913
  if (!e.isDirectory()) continue;
2908
- if (e.name === '.git') { repos.push(dir); return; }
2914
+ if (e.name === '.git') {
2915
+ // Validate it's a real repo — must have HEAD file
2916
+ try { if (fs.existsSync(path.join(dir, '.git', 'HEAD'))) repos.push(dir); } catch {}
2917
+ return;
2918
+ }
2909
2919
  if (e.name.startsWith('.') || skipDirs.has(e.name)) continue;
2910
2920
  walk(path.join(dir, e.name), depth + 1);
2911
2921
  }
@@ -108,9 +108,11 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
108
108
  ? path.join(MINIONS_DIR, 'work-items.json')
109
109
  : item.meta.project?.name ? projectWorkItemsPath({ name: item.meta.project.name, localPath: item.meta.project.localPath }) : null;
110
110
  if (wiPath) {
111
- const items = safeJson(wiPath) || [];
112
- const wi = items.find(i => i.id === item.meta.item.id);
113
- if (wi) retries = wi._retryCount || 0;
111
+ const items = safeJson(wiPath);
112
+ if (items && Array.isArray(items)) {
113
+ const wi = items.find(i => i.id === item.meta.item.id);
114
+ if (wi) retries = wi._retryCount || 0;
115
+ }
114
116
  }
115
117
  } catch (e) { log('warn', 'read retry count: ' + e.message); }
116
118
  const maxRetries = ENGINE_DEFAULTS.maxRetries;
@@ -809,7 +809,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
809
809
  if (item && item.status !== 'done') {
810
810
  log('info', `Post-merge: marking work item ${mergedItemId} as done (was ${item.status}) for ${pr.id}`);
811
811
  item.status = 'done';
812
- item.completedAt = e.ts();
812
+ item.completedAt = ts();
813
813
  item._mergedVia = pr.id;
814
814
  shared.safeWrite(wiPath, items);
815
815
  break;
@@ -1221,7 +1221,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1221
1221
  return d.includes(branchSlug) && fs.statSync(path.join(worktreeRoot, d)).isDirectory();
1222
1222
  });
1223
1223
  // Only remove if no other active dispatch uses this branch
1224
- const dispatch = e.getDispatch();
1224
+ const dispatch = getDispatch();
1225
1225
  const otherActive = ((dispatch.active || []).concat(dispatch.pending || [])).some(d =>
1226
1226
  d.id !== dispatchItem.id && d.meta?.branch && shared.sanitizeBranch && shared.sanitizeBranch(d.meta.branch) === branchSlug
1227
1227
  );
@@ -1229,7 +1229,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1229
1229
  for (const dir of dirs) {
1230
1230
  const wtPath = path.join(worktreeRoot, dir);
1231
1231
  try {
1232
- shared.exec(`git worktree remove "${wtPath}" --force`, { cwd: rootDir, stdio: 'pipe', timeout: 15000, windowsHide: true });
1232
+ execSilent(`git worktree remove "${wtPath}" --force`, { cwd: rootDir, timeout: 15000 });
1233
1233
  log('info', `Post-completion: removed worktree ${dir}`);
1234
1234
  } catch (err) {
1235
1235
  log('warn', `Post-completion: failed to remove worktree ${dir}: ${err.message}`);
@@ -1269,10 +1269,10 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1269
1269
  wi._retryCount = retries + 1;
1270
1270
  delete wi.dispatched_at;
1271
1271
  delete wi.dispatched_to;
1272
- e.log('info', `Auto-retry ${retries + 1}/${maxR} for ${meta.item.id} (no PR created)`);
1272
+ log('info', `Auto-retry ${retries + 1}/${maxR} for ${meta.item.id} (no PR created)`);
1273
1273
  } else {
1274
1274
  wi.status = WI_STATUS.FAILED;
1275
- e.log('warn', `${meta.item.id} failed after ${maxR} retries — no PR created`);
1275
+ log('warn', `${meta.item.id} failed after ${maxR} retries — no PR created`);
1276
1276
  }
1277
1277
  shared.safeWrite(wiPath, items);
1278
1278
  }
package/engine.js CHANGED
@@ -591,6 +591,11 @@ function spawnAgent(dispatchItem, config) {
591
591
  // Re-attach to existing tracking
592
592
  activeProcesses.set(id, { proc: resumeProc, agentId, startedAt: procInfo.startedAt, sessionId: steerSessionId });
593
593
 
594
+ // Reset output buffers so post-completion parsing only sees the resumed session
595
+ stdout = '';
596
+ stderr = '';
597
+ lastOutputAt = Date.now();
598
+
594
599
  // Re-wire stdout/stderr handlers (same as original)
595
600
  resumeProc.stdout.on('data', (data) => {
596
601
  const chunk = data.toString();
package/minions.js CHANGED
@@ -257,7 +257,9 @@ function findGitRepos(rootDir, maxDepth = 3) {
257
257
  // Skip common non-project dirs
258
258
  const base = path.basename(dir);
259
259
  if (['node_modules', '.git', '.hg', 'AppData', '$Recycle.Bin', 'Windows', 'Program Files',
260
- 'Program Files (x86)', '.cache', '.npm', '.yarn', '.nuget', 'worktrees', '.minions'].includes(base)) return;
260
+ 'Program Files (x86)', '.cache', '.npm', '.yarn', '.nuget', 'NugetCache',
261
+ 'worktrees', '.minions', '.squad', '.vs', '.vscode', 'obj', 'bin', 'packages',
262
+ 'OneDrive', 'OneDrive - Microsoft', '.copilot', 'marketplace-cache'].includes(base)) return;
261
263
  // Skip minions home directory itself
262
264
  if (path.resolve(dir) === path.resolve(MINIONS_HOME)) return;
263
265
 
@@ -271,6 +273,8 @@ function findGitRepos(rootDir, maxDepth = 3) {
271
273
  if (content.trimStart().startsWith('gitdir:')) return; // worktree, skip
272
274
  } catch {}
273
275
  }
276
+ // Validate it's a real repo — must have HEAD file
277
+ if (stat.isDirectory() && !fs.existsSync(path.join(gitDir, 'HEAD'))) return;
274
278
  repos.push(dir);
275
279
  return; // Don't recurse into git repos (they may have nested submodules)
276
280
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.322",
3
+ "version": "0.1.324",
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"