@yemi33/minions 0.1.562 → 0.1.563
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 +2 -1
- package/dashboard.js +38 -12
- package/engine/cleanup.js +3 -4
- package/engine/lifecycle.js +18 -17
- package/engine/shared.js +32 -1
- package/engine.js +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard.js
CHANGED
|
@@ -621,7 +621,13 @@ Many common English words have specific meanings in the Minions context. **When
|
|
|
621
621
|
// Hash the system prompt so we can detect changes and invalidate stale sessions
|
|
622
622
|
const _ccPromptHash = require('crypto').createHash('md5').update(CC_STATIC_SYSTEM_PROMPT).digest('hex').slice(0, 8);
|
|
623
623
|
|
|
624
|
+
let _preambleCache = null;
|
|
625
|
+
let _preambleCacheTs = 0;
|
|
626
|
+
const PREAMBLE_TTL = 10000; // 10s — same as status cache
|
|
627
|
+
|
|
624
628
|
function buildCCStatePreamble() {
|
|
629
|
+
const now = Date.now();
|
|
630
|
+
if (_preambleCache && now - _preambleCacheTs < PREAMBLE_TTL) return _preambleCache;
|
|
625
631
|
// Lightweight snapshot — just enough to orient. Use tools for details.
|
|
626
632
|
const agents = getAgents().map(a => `- ${a.name} (${a.id}): ${a.status}${a.currentTask ? ' — ' + a.currentTask.slice(0, 60) : ''}`).join('\n');
|
|
627
633
|
const projects = PROJECTS.map(p => `- ${p.name}: ${p.localPath}`).join('\n');
|
|
@@ -656,7 +662,7 @@ function buildCCStatePreamble() {
|
|
|
656
662
|
}
|
|
657
663
|
} catch {}
|
|
658
664
|
|
|
659
|
-
|
|
665
|
+
const result = `### Agents
|
|
660
666
|
${agents}
|
|
661
667
|
|
|
662
668
|
### Active Dispatch
|
|
@@ -675,10 +681,13 @@ ${schedSummary}
|
|
|
675
681
|
### Pipelines
|
|
676
682
|
${pipelineSummary}
|
|
677
683
|
|
|
678
|
-
### Dashboard API
|
|
679
|
-
|
|
684
|
+
### Dashboard API
|
|
685
|
+
Run \`curl http://localhost:7331/api/routes\` for full endpoint listing.
|
|
680
686
|
|
|
681
687
|
For details on any of the above, use your tools to read files under \`${MINIONS_DIR}\`.`;
|
|
688
|
+
_preambleCache = result;
|
|
689
|
+
_preambleCacheTs = now;
|
|
690
|
+
return result;
|
|
682
691
|
}
|
|
683
692
|
|
|
684
693
|
function parseCCActions(text) {
|
|
@@ -796,16 +805,18 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
796
805
|
const existing = resolveSession(store, sessionKey);
|
|
797
806
|
let sessionId = existing ? existing.sessionId : null;
|
|
798
807
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
808
|
+
function buildPrompt({ includePreamble = true } = {}) {
|
|
809
|
+
const parts = (!skipStatePreamble && includePreamble) ? [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`] : [];
|
|
810
|
+
if (extraContext) parts.push(extraContext);
|
|
811
|
+
parts.push(message);
|
|
812
|
+
return parts.join('\n\n---\n\n');
|
|
813
|
+
}
|
|
803
814
|
|
|
804
815
|
let result;
|
|
805
816
|
|
|
806
|
-
// Attempt 1: resume existing session
|
|
817
|
+
// Attempt 1: resume existing session — skip preamble (session already has context)
|
|
807
818
|
if (sessionId && maxTurns > 1) {
|
|
808
|
-
result = await llm.callLLM(
|
|
819
|
+
result = await llm.callLLM(buildPrompt({ includePreamble: false }), '', {
|
|
809
820
|
timeout, label, model, maxTurns, allowedTools, sessionId,
|
|
810
821
|
});
|
|
811
822
|
llm.trackEngineUsage(label, result.usage);
|
|
@@ -838,8 +849,9 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
838
849
|
}
|
|
839
850
|
}
|
|
840
851
|
|
|
841
|
-
// Attempt 2: fresh session
|
|
842
|
-
|
|
852
|
+
// Attempt 2: fresh session (include preamble for full context)
|
|
853
|
+
const freshPrompt = buildPrompt();
|
|
854
|
+
result = await llm.callLLM(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
|
|
843
855
|
timeout, label, model, maxTurns, allowedTools,
|
|
844
856
|
});
|
|
845
857
|
llm.trackEngineUsage(label, result.usage);
|
|
@@ -853,7 +865,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
|
|
|
853
865
|
if (maxTurns <= 1) return result;
|
|
854
866
|
console.log(`[${label}] Fresh call also failed (code=${result.code}, empty=${!result.text}), retrying once more...`);
|
|
855
867
|
await new Promise(r => setTimeout(r, 2000));
|
|
856
|
-
result = await llm.callLLM(
|
|
868
|
+
result = await llm.callLLM(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
|
|
857
869
|
timeout, label, model, maxTurns, allowedTools,
|
|
858
870
|
});
|
|
859
871
|
llm.trackEngineUsage(label, result.usage);
|
|
@@ -2427,6 +2439,13 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2427
2439
|
} catch (e) { console.error('plan-to-prd cleanup:', e.message); }
|
|
2428
2440
|
}
|
|
2429
2441
|
|
|
2442
|
+
// Clean up worktrees associated with this plan
|
|
2443
|
+
try {
|
|
2444
|
+
const plan = body.file.endsWith('.json') ? (safeJsonObj(path.join(PRD_DIR, 'archive', body.file)) || safeJsonObj(path.join(PRD_DIR, body.file)) || {}) : {};
|
|
2445
|
+
const { cleanupPlanWorktrees } = require('./engine/lifecycle');
|
|
2446
|
+
cleanupPlanWorktrees(body.file, plan, PROJECTS, getConfig());
|
|
2447
|
+
} catch (e) { console.error('plan worktree cleanup:', e.message); }
|
|
2448
|
+
|
|
2430
2449
|
invalidateStatusCache();
|
|
2431
2450
|
return jsonReply(res, 200, { ok: true, cleanedWorkItems: cleaned, cleanedDispatches: dispatchCleaned });
|
|
2432
2451
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
@@ -2467,6 +2486,13 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2467
2486
|
} catch { /* optional */ }
|
|
2468
2487
|
}
|
|
2469
2488
|
|
|
2489
|
+
// Clean up worktrees associated with this plan
|
|
2490
|
+
try {
|
|
2491
|
+
const plan = body.file.endsWith('.json') ? (safeJsonObj(archivePath) || {}) : {};
|
|
2492
|
+
const { cleanupPlanWorktrees } = require('./engine/lifecycle');
|
|
2493
|
+
cleanupPlanWorktrees(body.file, plan, PROJECTS, getConfig());
|
|
2494
|
+
} catch (e) { console.error('plan worktree cleanup:', e.message); }
|
|
2495
|
+
|
|
2470
2496
|
invalidateStatusCache();
|
|
2471
2497
|
return jsonReply(res, 200, { ok: true, archived: body.file, archivedSource });
|
|
2472
2498
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
package/engine/cleanup.js
CHANGED
|
@@ -250,12 +250,11 @@ function runCleanup(config, verbose = false) {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
exec(`git worktree remove "${entry.wtPath}" --force`, { cwd: root, stdio: 'pipe', timeout: 30000 });
|
|
253
|
+
if (shared.removeWorktree(entry.wtPath, root, worktreeRoot)) {
|
|
255
254
|
cleaned.worktrees++;
|
|
256
255
|
if (verbose) console.log(` Removed worktree: ${entry.wtPath}`);
|
|
257
|
-
}
|
|
258
|
-
if (verbose) console.log(` Failed to remove worktree ${entry.wtPath}
|
|
256
|
+
} else {
|
|
257
|
+
if (verbose) console.log(` Failed to remove worktree ${entry.wtPath}`);
|
|
259
258
|
}
|
|
260
259
|
}
|
|
261
260
|
}
|
package/engine/lifecycle.js
CHANGED
|
@@ -313,34 +313,40 @@ function archivePlan(planFile, plan, projects, config) {
|
|
|
313
313
|
} catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
|
|
314
314
|
|
|
315
315
|
// Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
|
|
316
|
+
cleanupPlanWorktrees(planFile, plan, projects, config);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Clean up worktrees associated with a plan's work items and PRs.
|
|
321
|
+
* Called from archivePlan() and also from plan delete/archive handlers.
|
|
322
|
+
*/
|
|
323
|
+
function cleanupPlanWorktrees(planFile, plan, projects, config) {
|
|
316
324
|
try {
|
|
317
325
|
const branchSlugs = new Set();
|
|
318
|
-
if (plan
|
|
326
|
+
if (plan?.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
|
|
319
327
|
|
|
320
|
-
// Collect work items for this plan
|
|
321
328
|
const allWorkItems = queries.getWorkItems(config);
|
|
322
329
|
const planItems = allWorkItems.filter(w => w.sourcePlan === planFile);
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
for (const w of doneItems) {
|
|
330
|
+
for (const w of planItems) {
|
|
326
331
|
if (w.branch) branchSlugs.add(shared.sanitizeBranch(w.branch).toLowerCase());
|
|
327
332
|
if (w.id) branchSlugs.add(w.id.toLowerCase());
|
|
328
333
|
}
|
|
329
334
|
|
|
330
|
-
// Collect PR branches
|
|
331
335
|
for (const p of projects) {
|
|
332
336
|
try {
|
|
333
337
|
const prs = safeJson(shared.projectPrPath(p)) || [];
|
|
334
338
|
const prLinks = getPrLinks();
|
|
335
339
|
for (const pr of prs) {
|
|
336
340
|
const linkedId = prLinks[pr.id];
|
|
337
|
-
if (linkedId &&
|
|
341
|
+
if (linkedId && planItems.find(w => w.id === linkedId) && pr.branch) {
|
|
338
342
|
branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
|
|
339
343
|
}
|
|
340
344
|
}
|
|
341
345
|
} catch { /* optional */ }
|
|
342
346
|
}
|
|
343
347
|
|
|
348
|
+
if (branchSlugs.size === 0) return;
|
|
349
|
+
|
|
344
350
|
let cleanedWt = 0;
|
|
345
351
|
for (const p of projects) {
|
|
346
352
|
const root = path.resolve(p.localPath);
|
|
@@ -352,15 +358,12 @@ function archivePlan(planFile, plan, projects, config) {
|
|
|
352
358
|
const matches = [...branchSlugs].some(slug => dirLower.includes(slug));
|
|
353
359
|
if (matches) {
|
|
354
360
|
const wtPath = path.join(wtRoot, dir);
|
|
355
|
-
|
|
356
|
-
execSilent(`git worktree remove "${wtPath}" --force`, { cwd: root, stdio: 'pipe', timeout: 15000 });
|
|
357
|
-
cleanedWt++;
|
|
358
|
-
} catch (err) { log('warn', `Failed to remove worktree ${dir}: ${err.message}`); }
|
|
361
|
+
if (shared.removeWorktree(wtPath, root, wtRoot)) cleanedWt++;
|
|
359
362
|
}
|
|
360
363
|
}
|
|
361
364
|
}
|
|
362
|
-
if (cleanedWt > 0) log('info', `
|
|
363
|
-
} catch (err) { log('warn', `
|
|
365
|
+
if (cleanedWt > 0) log('info', `Plan worktree cleanup: removed ${cleanedWt} worktree(s)`);
|
|
366
|
+
} catch (err) { log('warn', `Plan worktree cleanup: ${err.message}`); }
|
|
364
367
|
}
|
|
365
368
|
|
|
366
369
|
// ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
|
|
@@ -1245,11 +1248,8 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1245
1248
|
if (!otherActive) {
|
|
1246
1249
|
for (const dir of dirs) {
|
|
1247
1250
|
const wtPath = path.join(worktreeRoot, dir);
|
|
1248
|
-
|
|
1249
|
-
shared.exec(`git worktree remove "${wtPath}" --force`, { cwd: rootDir, stdio: 'pipe', timeout: 15000, windowsHide: true });
|
|
1251
|
+
if (shared.removeWorktree(wtPath, rootDir, worktreeRoot)) {
|
|
1250
1252
|
log('info', `Post-completion: removed worktree ${dir}`);
|
|
1251
|
-
} catch (err) {
|
|
1252
|
-
log('warn', `Post-completion: failed to remove worktree ${dir}: ${err.message}`);
|
|
1253
1253
|
}
|
|
1254
1254
|
}
|
|
1255
1255
|
}
|
|
@@ -1392,6 +1392,7 @@ function syncPrdFromPrs(config) {
|
|
|
1392
1392
|
module.exports = {
|
|
1393
1393
|
checkPlanCompletion,
|
|
1394
1394
|
archivePlan,
|
|
1395
|
+
cleanupPlanWorktrees,
|
|
1395
1396
|
updateWorkItemStatus,
|
|
1396
1397
|
syncPrdItemStatus,
|
|
1397
1398
|
syncPrsFromOutput,
|
package/engine/shared.js
CHANGED
|
@@ -394,14 +394,17 @@ function resolveMainBranch(rootDir, configuredBranch) {
|
|
|
394
394
|
|
|
395
395
|
// ── Environment ─────────────────────────────────────────────────────────────
|
|
396
396
|
|
|
397
|
+
let _cleanEnvCache = null;
|
|
397
398
|
function cleanChildEnv() {
|
|
399
|
+
if (_cleanEnvCache) return { ..._cleanEnvCache };
|
|
398
400
|
const env = { ...process.env };
|
|
399
401
|
delete env.CLAUDECODE;
|
|
400
402
|
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
401
403
|
for (const key of Object.keys(env)) {
|
|
402
404
|
if (key.startsWith('CLAUDE_CODE') || key.startsWith('CLAUDECODE_')) delete env[key];
|
|
403
405
|
}
|
|
404
|
-
|
|
406
|
+
_cleanEnvCache = env;
|
|
407
|
+
return { ..._cleanEnvCache };
|
|
405
408
|
}
|
|
406
409
|
|
|
407
410
|
// Environment for git commands — prevents credential manager from opening browser
|
|
@@ -799,6 +802,33 @@ function mutatePullRequests(filePath, mutator) {
|
|
|
799
802
|
}, { defaultValue: [] });
|
|
800
803
|
}
|
|
801
804
|
|
|
805
|
+
/**
|
|
806
|
+
* Remove a git worktree, falling back to fs.rmSync if git fails (e.g., locked on Windows).
|
|
807
|
+
* Only removes directories under worktreeRoot to prevent accidental deletion.
|
|
808
|
+
*/
|
|
809
|
+
function removeWorktree(wtPath, gitRoot, worktreeRoot) {
|
|
810
|
+
const resolved = path.resolve(wtPath);
|
|
811
|
+
const resolvedRoot = path.resolve(worktreeRoot);
|
|
812
|
+
if (!resolved.startsWith(resolvedRoot)) {
|
|
813
|
+
log('warn', `removeWorktree: refusing to remove ${wtPath} — not under ${worktreeRoot}`);
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
exec(`git worktree remove "${wtPath}" --force`, { cwd: gitRoot, stdio: 'pipe', timeout: 15000, windowsHide: true });
|
|
818
|
+
return true;
|
|
819
|
+
} catch (gitErr) {
|
|
820
|
+
try {
|
|
821
|
+
fs.rmSync(resolved, { recursive: true, force: true });
|
|
822
|
+
// Also prune the stale worktree entry from git metadata
|
|
823
|
+
try { exec('git worktree prune', { cwd: gitRoot, stdio: 'pipe', timeout: 10000, windowsHide: true }); } catch {}
|
|
824
|
+
return true;
|
|
825
|
+
} catch (rmErr) {
|
|
826
|
+
log('warn', `removeWorktree: both git and rm failed for ${wtPath}: ${rmErr.message}`);
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
802
832
|
module.exports = {
|
|
803
833
|
MINIONS_DIR,
|
|
804
834
|
PR_LINKS_PATH,
|
|
@@ -852,6 +882,7 @@ module.exports = {
|
|
|
852
882
|
sleepMs,
|
|
853
883
|
killGracefully,
|
|
854
884
|
killImmediate,
|
|
885
|
+
removeWorktree,
|
|
855
886
|
LOCK_STALE_MS,
|
|
856
887
|
flushLogs,
|
|
857
888
|
_logBuffer, // exported for testing
|
package/engine.js
CHANGED
|
@@ -412,10 +412,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
412
412
|
if (depIds.length > 0) {
|
|
413
413
|
try {
|
|
414
414
|
const depBranches = resolveDependencyBranches(depIds, meta?.item?.sourcePlan, project, config);
|
|
415
|
+
let depMergeFailed = false;
|
|
415
416
|
for (const { branch: depBranch, prId } of depBranches) {
|
|
416
417
|
// Skip refs already known to be missing this tick (avoids repeated 30s ETIMEDOUT)
|
|
417
418
|
if (_failedRefCache.has(depBranch)) {
|
|
418
419
|
log('warn', `Skipping dependency ${depBranch} — already failed to fetch this tick`);
|
|
420
|
+
depMergeFailed = true;
|
|
419
421
|
continue;
|
|
420
422
|
}
|
|
421
423
|
try {
|
|
@@ -425,8 +427,13 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
425
427
|
} catch (mergeErr) {
|
|
426
428
|
_failedRefCache.add(depBranch);
|
|
427
429
|
log('warn', `Failed to merge dependency ${depBranch} into ${branchName}: ${mergeErr.message}`);
|
|
430
|
+
depMergeFailed = true;
|
|
428
431
|
}
|
|
429
432
|
}
|
|
433
|
+
if (depMergeFailed) {
|
|
434
|
+
completeDispatch(id, DISPATCH_RESULT.ERROR, `Dependency merge failed — will retry next tick`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
430
437
|
} catch (e) {
|
|
431
438
|
log('warn', `Could not resolve dependency branches for ${branchName}: ${e.message}`);
|
|
432
439
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.563",
|
|
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"
|