@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 +50 -0
- package/engine/discover-project-skills.js +6 -1
- package/engine/shared.js +17 -0
- package/engine/worktree-gc.js +2 -0
- package/package.json +1 -1
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
|
-
|
|
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,
|
package/engine/worktree-gc.js
CHANGED
|
@@ -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.
|
|
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"
|