@yemi33/minions 0.1.561 → 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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.563 (2026-04-08)
4
+
5
+ ### Fixes
6
+ - worktree lifecycle — fail on dep merge, fallback rm, cleanup on plan delete
7
+ - auto-reset CC session on resume failure, add error logging
8
+
3
9
  ## 0.1.561 (2026-04-08)
4
10
 
5
11
  ### Fixes
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
- return `### Agents
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 (all endpoints)
679
- ${_getApiRoutesSummary()}
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
- const parts = skipStatePreamble ? [] : [`## Current Minions State (${new Date().toISOString().slice(0, 16)})\n\n${buildCCStatePreamble()}`];
800
- if (extraContext) parts.push(extraContext);
801
- parts.push(message);
802
- const prompt = parts.join('\n\n---\n\n');
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 (skip for single-turn/no-tool calls nothing to resume)
817
+ // Attempt 1: resume existing session skip preamble (session already has context)
807
818
  if (sessionId && maxTurns > 1) {
808
- result = await llm.callLLM(prompt, '', {
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
- result = await llm.callLLM(prompt, CC_STATIC_SYSTEM_PROMPT, {
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(prompt, CC_STATIC_SYSTEM_PROMPT, {
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 }); }
@@ -3368,9 +3394,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3368
3394
  if (result.code !== 0 || !result.text) {
3369
3395
  const debugInfo = result.code !== 0 ? `(exit code ${result.code})` : '(empty response)';
3370
3396
  const stderrTail = (result.stderr || '').trim().split('\n').filter(Boolean).slice(-3).join(' | ');
3371
- const retryHint = ccSession.sessionId
3372
- ? 'Your session is still active just send your message again to retry.'
3373
- : 'Try clicking **New Session** and sending your message again.';
3397
+ console.error(`[CC-stream] Failed: code=${result.code}, stderr=${(result.stderr || '').slice(0, 500)}, stdout_tail=${(result.raw || '').slice(-500)}`);
3398
+ // If resuming a session failed, auto-reset so next attempt starts fresh
3399
+ let retryHint;
3400
+ if (wasResume && result.code !== 0) {
3401
+ ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
3402
+ safeWrite(path.join(ENGINE_DIR, 'cc-session.json'), ccSession);
3403
+ retryHint = 'Session was reset — send your message again to start fresh.';
3404
+ } else {
3405
+ retryHint = ccSession.sessionId
3406
+ ? 'Your session is still active — just send your message again to retry.'
3407
+ : 'Try clicking **New Session** and sending your message again.';
3408
+ }
3374
3409
  res.write('data: ' + JSON.stringify({ type: 'done', text: `I had trouble processing that ${debugInfo}. ${stderrTail ? 'Detail: ' + stderrTail : ''}\n\n${retryHint}`, actions: [], sessionId: ccSession.sessionId }) + '\n\n');
3375
3410
  res.end();
3376
3411
  return;
package/engine/cleanup.js CHANGED
@@ -250,12 +250,11 @@ function runCleanup(config, verbose = false) {
250
250
  }
251
251
  }
252
252
 
253
- try {
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
- } catch (e) {
258
- if (verbose) console.log(` Failed to remove worktree ${entry.wtPath}: ${e.message}`);
256
+ } else {
257
+ if (verbose) console.log(` Failed to remove worktree ${entry.wtPath}`);
259
258
  }
260
259
  }
261
260
  }
@@ -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.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
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 doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
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 && doneItems.find(w => w.id === linkedId) && pr.branch) {
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
- try {
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', `Archive: cleaned ${cleanedWt} worktree(s)`);
363
- } catch (err) { log('warn', `Worktree cleanup: ${err.message}`); }
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
- try {
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
- return env;
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.561",
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"