@yemi33/minions 0.1.2253 → 0.1.2255

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/engine/cleanup.js CHANGED
@@ -425,6 +425,39 @@ function _killProcessInWorktree(dir, activeProcesses, activeIds) {
425
425
  } catch {} // tmp dir may not exist
426
426
  }
427
427
 
428
+ // Agent scratch (`.agent-temp` under the worktree root) holds throwaway git
429
+ // repos written by dispatched agents + the test-suite-run-by-agents. Nothing
430
+ // owns its lifecycle, so it accumulates unboundedly (observed: 26k+ dirs). The
431
+ // worktree sweeps now SKIP it (shared.isWorktreeRootInfraEntry), so the pile no
432
+ // longer stalls the tick loop — but it still needs bounded hygiene. This reaps
433
+ // entries whose mtime is older than the TTL via plain fs deletion (NOT
434
+ // removeWorktree — these are standalone repos, not worktrees). Bounded per pass
435
+ // so the reaper itself can never stall the tick on a huge backlog; it drains
436
+ // across cleanup cycles.
437
+ const AGENT_SCRATCH_TTL_MS = 6 * 60 * 60 * 1000; // 6h — comfortably > agentTimeout (5h), so only dead-dispatch scratch is reaped
438
+ const AGENT_SCRATCH_MAX_SCAN_PER_CLEANUP = 2000;
439
+
440
+ function reapAgentScratch(worktreeRoot) {
441
+ const scratchDir = path.join(worktreeRoot, shared.WORKTREE_SCRATCH_DIR_NAME);
442
+ let names;
443
+ try { names = fs.readdirSync(scratchDir); } catch { return 0; } // no scratch dir → nothing to do
444
+ const cutoff = Date.now() - AGENT_SCRATCH_TTL_MS;
445
+ let reaped = 0;
446
+ const limit = Math.min(names.length, AGENT_SCRATCH_MAX_SCAN_PER_CLEANUP);
447
+ for (let i = 0; i < limit; i++) {
448
+ const full = path.join(scratchDir, names[i]);
449
+ let stat;
450
+ try { stat = fs.statSync(full); } catch { continue; }
451
+ if (stat.mtimeMs >= cutoff) continue; // recent — may belong to a live dispatch
452
+ try { fs.rmSync(full, { recursive: true, force: true }); reaped++; }
453
+ catch { /* locked / in use — leave it for a later pass */ }
454
+ }
455
+ if (reaped > 0) {
456
+ log('info', `Reaped ${reaped} stale agent-scratch entries (>${Math.round(AGENT_SCRATCH_TTL_MS / 3600000)}h) from ${scratchDir}`);
457
+ }
458
+ return reaped;
459
+ }
460
+
428
461
  // ─── Cleanup Orchestrator ────────────────────────────────────────────────────
429
462
 
430
463
  async function runCleanup(config, verbose = false) {
@@ -685,6 +718,8 @@ async function runCleanup(config, verbose = false) {
685
718
 
686
719
  // 3. Clean git worktrees for merged/abandoned PRs
687
720
  const _attemptedWorktreePaths = new Set(); // dedup across projects sharing a worktreeRoot
721
+ const _reapedScratchRoots = new Set(); // reap each worktree root's .agent-temp once per cleanup
722
+ cleaned.agentScratch = 0;
688
723
  for (const project of projects) {
689
724
  const root = project.localPath ? path.resolve(project.localPath) : null;
690
725
  if (!root || !fs.existsSync(root)) continue;
@@ -698,6 +733,14 @@ async function runCleanup(config, verbose = false) {
698
733
 
699
734
  for (const worktreeRoot of worktreeRoots) {
700
735
 
736
+ // Bounded TTL reaper for the agent-scratch dir under this worktree root
737
+ // (once per unique root). Plain fs deletion — these are standalone repos,
738
+ // not worktrees, so removeWorktree (correctly) refuses them.
739
+ if (!_reapedScratchRoots.has(worktreeRoot)) {
740
+ _reapedScratchRoots.add(worktreeRoot);
741
+ try { cleaned.agentScratch += reapAgentScratch(worktreeRoot); } catch (_e) { /* best-effort */ }
742
+ }
743
+
701
744
  // Get PRs for this project
702
745
  const prs = safeJson(projectPrPath(project)) || [];
703
746
  const mergedBranches = new Set();
@@ -714,6 +757,12 @@ async function runCleanup(config, verbose = false) {
714
757
  const allDirs = [];
715
758
  const topDirs = fs.readdirSync(worktreeRoot);
716
759
  for (const dir of topDirs) {
760
+ // Skip infra/scratch dirs (`.agent-temp`, `.git`, …). They are never
761
+ // managed worktrees, and recursing into them (the else branch below)
762
+ // would readdir + stat their entire contents — `.agent-temp` routinely
763
+ // holds thousands of throwaway test/agent git repos, and walking that
764
+ // pile every cleanup cycle stalls the engine tick loop ("engine stale").
765
+ if (shared.isWorktreeRootInfraEntry(dir)) continue;
717
766
  const dirPath = path.join(worktreeRoot, dir);
718
767
  try { if (!fs.statSync(dirPath).isDirectory()) continue; } catch { continue; }
719
768
  // Check if this is a git worktree (has .git file) or a parent directory
@@ -1672,4 +1721,5 @@ module.exports = {
1672
1721
  getWorktreeBranch, // exported for lifecycle cleanup
1673
1722
  cleanupMergedPrLocalBranch, // exported for lifecycle cleanup and testing
1674
1723
  collectPhantomBranchesForProject, // P-e0b4f7a5 — exported for testing
1724
+ reapAgentScratch, // exported for testing
1675
1725
  };
@@ -354,7 +354,12 @@ function discoverProjectSkills(args) {
354
354
  seenKey.add(key);
355
355
  all.push(entry);
356
356
  }
357
- if (_now() > deadline) break;
357
+ // No deadline break here — skills and commands for the same area are
358
+ // scanned as an atomic unit. The deadline guards *starting* a new area
359
+ // (line above, `if (_now() > deadline) break`) but must not split a
360
+ // half-scanned area: doing so drops commands while keeping skills from
361
+ // the same area, causing test/prod divergence under load (PR-82 blind spot
362
+ // regression, CI failure yemi33#28135287117).
358
363
  for (const entry of _discoverCommandsAt(areaBase, projectPath, opts, deadline, `area:${area}`)) {
359
364
  const key = `command:${entry.name}`;
360
365
  if (seenKey.has(key)) continue;
package/engine/shared.js CHANGED
@@ -5553,6 +5553,21 @@ function buildWorktreeDirName({
5553
5553
  return `${projectSlug}-${branchSlug}-${suffix}`;
5554
5554
  }
5555
5555
 
5556
+ // The worktree root (`<localPath>/../worktrees`) is a SHARED namespace: it holds
5557
+ // managed git worktrees AND infra/scratch dirs — most importantly `.agent-temp`,
5558
+ // the agent scratch base (`resolveAgentTempBaseDir`) where dispatched agents and
5559
+ // the test-suite-run-by-agents write thousands of throwaway git repos. Worktree
5560
+ // names from `buildWorktreeDirName` are ALWAYS `W-…` / `<project>-<branch>-<hash>`
5561
+ // and never dot-prefixed, so any dot-prefixed entry in the worktree root is infra
5562
+ // or scratch, never a managed worktree. The worktree sweeps MUST skip these — a
5563
+ // single `.agent-temp` accumulating thousands of entries otherwise makes the
5564
+ // sweep (which recurses one level into non-worktree dirs) readdir + stat the
5565
+ // whole pile every cleanup cycle, stalling the engine tick loop (engine "stale").
5566
+ const WORKTREE_SCRATCH_DIR_NAME = '.agent-temp';
5567
+ function isWorktreeRootInfraEntry(name) {
5568
+ return typeof name === 'string' && name.length > 0 && name.charCodeAt(0) === 46; // '.'
5569
+ }
5570
+
5556
5571
  /**
5557
5572
  * True when `childPath` is strictly nested within `parentPath` (descendant,
5558
5573
  * NOT the same path). Cross-platform via `path.relative`; resilient to mixed
@@ -8904,6 +8919,8 @@ module.exports = {
8904
8919
  deriveWorkItemBranchName,
8905
8920
  safeSlugComponent,
8906
8921
  buildWorktreeDirName, // exported for testing
8922
+ isWorktreeRootInfraEntry,
8923
+ WORKTREE_SCRATCH_DIR_NAME,
8907
8924
  isPathInside,
8908
8925
  isPathInsideOrEqual,
8909
8926
  parseWorktreePorcelain,
@@ -819,6 +819,8 @@ function pruneOrphanWorktrees(opts) {
819
819
  if (!ent || (typeof ent.isDirectory === 'function' && !ent.isDirectory())) continue;
820
820
  const name = ent.name || ent;
821
821
  if (typeof name !== 'string' || name.length === 0) continue;
822
+ // Skip infra/scratch dirs (`.agent-temp`, `.git`, …) — never worktrees.
823
+ if (shared.isWorktreeRootInfraEntry(name)) continue;
822
824
  projStats.scanned++;
823
825
  result.scanned++;
824
826
  const wtPath = path.join(wtParent, name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2253",
3
+ "version": "0.1.2255",
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"