@yemi33/minions 0.1.869 → 0.1.871

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,6 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.869 (2026-04-11)
3
+ ## 0.1.871 (2026-04-11)
4
4
 
5
5
  ### Features
6
6
  - ticking runtime counter on working agent cards
@@ -9,6 +9,8 @@
9
9
  - optimistic running state on pipeline Run Now click
10
10
 
11
11
  ### Fixes
12
+ - orphan recovery misclassifies mid-run agents as failed on engine restart
13
+ - dep merge failures identify conflicting branch and auto-queue fix (closes #814) (#882)
12
14
  - always show doc-chat expand bar when thread is collapsed
13
15
  - poll for old process exit before steering resume (replaces fixed delay)
14
16
  - add 3s delay before steering resume to let old process tree exit
package/engine/cli.js CHANGED
@@ -228,15 +228,20 @@ const commands = {
228
228
  const agentId = item.agent;
229
229
  const outputPath = path.join(MINIONS_DIR, 'agents', agentId, 'live-output.log');
230
230
  try {
231
- const stat = fs.statSync(outputPath);
232
231
  const output = fs.readFileSync(outputPath, 'utf8');
233
232
 
234
- // Check for completion markers in output
235
- const hasResult = output.includes('"type":"result"') || output.includes('"type": "result"');
236
- const hasError = output.includes('"is_error":true') || output.includes('"is_error": true');
237
- if (!hasResult && !hasError) continue;
233
+ // Only process if the session actually emitted a result line — no result means the
234
+ // session was still running when the engine died and should be requeued, not failed.
235
+ // Tool-level is_error:true (e.g. a Read on a missing file) must not be confused with
236
+ // a session-level error, so we scope the is_error check to the result line only.
237
+ const resultIdx = output.search(/"type"\s*:\s*"result"/);
238
+ if (resultIdx === -1) continue;
238
239
 
239
- let isSuccess = hasResult && !hasError;
240
+ const resultLineEnd = output.indexOf('\n', resultIdx);
241
+ const resultLine = output.slice(resultIdx, resultLineEnd === -1 ? output.length : resultLineEnd);
242
+ const hasError = resultLine.includes('"is_error":true') || resultLine.includes('"is_error": true');
243
+
244
+ let isSuccess = !hasError;
240
245
 
241
246
  // Extract PRs from output first — if PRs were created, the agent succeeded
242
247
  // regardless of intermediate error lines in the log
package/engine.js CHANGED
@@ -25,7 +25,8 @@ const fs = require('fs');
25
25
  const path = require('path');
26
26
  const shared = require('./engine/shared');
27
27
  const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS: DEFAULTS,
28
- WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, DISPATCH_RESULT, AGENT_STATUS } = shared;
28
+ WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, DISPATCH_RESULT, AGENT_STATUS,
29
+ FAILURE_CLASS } = shared;
29
30
  const queries = require('./engine/queries');
30
31
 
31
32
  // ─── Paths ──────────────────────────────────────────────────────────────────
@@ -149,6 +150,22 @@ let engineRestartGraceUntil = 0; // timestamp — suppress orphan detection unti
149
150
  // Per-tick cache of refs that failed to fetch — avoids repeating 30s ETIMEDOUT for same missing ref
150
151
  // Cleared at the start of each tick cycle (see tickInner)
151
152
  const _failedRefCache = new Set();
153
+
154
+ // Parse conflicting file names from git merge error output (stderr/stdout combined)
155
+ function parseConflictFiles(mergeOutput) {
156
+ const files = [];
157
+ // Match "CONFLICT (content): Merge conflict in <file>" lines
158
+ const conflictRe = /CONFLICT \([^)]*\): .*?(?:in|for) (.+)/g;
159
+ let m;
160
+ while ((m = conflictRe.exec(mergeOutput)) !== null) files.push(m[1].trim());
161
+ // Also match "Auto-merging <file>" followed by conflict (less specific, fallback)
162
+ if (files.length === 0) {
163
+ const autoMergeRe = /Auto-merging (.+)/g;
164
+ while ((m = autoMergeRe.exec(mergeOutput)) !== null) files.push(m[1].trim());
165
+ }
166
+ return [...new Set(files)]; // dedupe
167
+ }
168
+
152
169
  const _FAST_WORK_TYPES = new Set([WORK_TYPE.EXPLORE, WORK_TYPE.ASK, WORK_TYPE.REVIEW]);
153
170
  const _MAX_TURNS_BY_TYPE = {
154
171
  [WORK_TYPE.EXPLORE]: 30, [WORK_TYPE.ASK]: 20, [WORK_TYPE.REVIEW]: 30,
@@ -451,6 +468,8 @@ async function spawnAgent(dispatchItem, config) {
451
468
  try {
452
469
  const depBranches = resolveDependencyBranches(depIds, meta?.item?.sourcePlan, project, config);
453
470
  let depMergeFailed = false;
471
+ let depConflictBranch = null; // track which dep branch caused the conflict
472
+ let depConflictFiles = []; // conflicting file names parsed from git output
454
473
  // Fetch all dependency branches in parallel (git fetches are independent)
455
474
  const fetchable = depBranches.filter(d => !_failedRefCache.has(d.branch));
456
475
  const unfetchable = depBranches.filter(d => _failedRefCache.has(d.branch));
@@ -525,8 +544,28 @@ async function spawnAgent(dispatchItem, config) {
525
544
  }
526
545
  log('info', `Successfully re-merged all ${fetched.length} dep branches after reset for ${branchName}`);
527
546
  } catch (resetErr) {
547
+ const errOutput = (resetErr.message || '') + '\n' + (resetErr.stdout?.toString?.() || '') + '\n' + (resetErr.stderr?.toString?.() || '');
528
548
  log('warn', `Failed to reset and re-merge deps for ${branchName}: ${resetErr.message}`);
529
549
  try { await execAsync(`git merge --abort`, { ..._gitOpts, cwd: worktreePath }); } catch (_) { /* no merge in progress */ }
550
+ // Identify which dep branch caused the conflict during re-merge
551
+ // The last branch attempted in the re-merge loop is the one that failed
552
+ for (const { branch: reBranch2 } of fetched) {
553
+ try {
554
+ const mainRef2 = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
555
+ const mergeBase = (await execAsync(`git merge-base "origin/${mainRef2}" "origin/${reBranch2}"`, { ..._gitOpts, cwd: rootDir })).stdout.toString().trim();
556
+ const treeResult = await execAsync(`git merge-tree "${mergeBase}" "origin/${mainRef2}" "origin/${reBranch2}"`, { ..._gitOpts, cwd: rootDir });
557
+ const treeOutput = treeResult.stdout?.toString?.() || '';
558
+ if (treeOutput.includes('<<<<<<<') || treeOutput.includes('changed in both')) {
559
+ depConflictBranch = reBranch2;
560
+ depConflictFiles = parseConflictFiles(treeOutput);
561
+ break;
562
+ }
563
+ } catch (_e) { /* merge-tree may fail — continue checking other branches */ }
564
+ }
565
+ // Fallback: parse conflict files from the error output if merge-tree didn't identify them
566
+ if (!depConflictBranch) {
567
+ depConflictFiles = parseConflictFiles(errOutput);
568
+ }
530
569
  depMergeFailed = true;
531
570
  }
532
571
  break;
@@ -535,7 +574,47 @@ async function spawnAgent(dispatchItem, config) {
535
574
  }
536
575
  if (depMergeFailed) {
537
576
  _cleanupPromptFiles();
538
- completeDispatch(id, DISPATCH_RESULT.ERROR, `Dependency merge failed will retry next tick`);
577
+ // Build actionable failReason identifying the conflicting branch and files
578
+ let failReason = 'Dependency merge failed';
579
+ if (depConflictBranch) {
580
+ failReason = `Dependency merge failed: ${depConflictBranch} conflicts with ${sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch))}`;
581
+ if (depConflictFiles.length > 0) failReason += ` in ${depConflictFiles.slice(0, 5).join(', ')}`;
582
+ failReason += ' — dep branch needs updating';
583
+ }
584
+ completeDispatch(id, DISPATCH_RESULT.ERROR, failReason, '', { failureClass: FAILURE_CLASS.MERGE_CONFLICT });
585
+
586
+ // Auto-queue conflict-fix work item when a specific dep branch is identified
587
+ if (depConflictBranch && meta?.item?.id && project) {
588
+ try {
589
+ const wiPath = project.name
590
+ ? projectWorkItemsPath(project)
591
+ : path.join(MINIONS_DIR, 'work-items.json');
592
+ const conflictFixId = `conflict-fix-${depConflictBranch.replace(/[^a-zA-Z0-9-]/g, '-')}`;
593
+ const filesDesc = depConflictFiles.length > 0
594
+ ? `\n\nConflicting files:\n${depConflictFiles.map(f => '- ' + f).join('\n')}`
595
+ : '';
596
+ const mainBranch = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
597
+ mutateWorkItems(wiPath, items => {
598
+ // Don't create duplicate conflict-fix items
599
+ const existing = items.find(i => i.id === conflictFixId && i.status !== WI_STATUS.DONE && i.status !== WI_STATUS.FAILED && i.status !== WI_STATUS.CANCELLED);
600
+ if (existing) return;
601
+ items.push({
602
+ id: conflictFixId,
603
+ title: `Fix merge conflict: ${depConflictBranch} conflicts with ${mainBranch}`,
604
+ type: WORK_TYPE.FIX,
605
+ priority: 'high',
606
+ status: WI_STATUS.PENDING,
607
+ description: `Branch \`${depConflictBranch}\` conflicts with \`${mainBranch}\`. Merge ${mainBranch} into the branch and resolve conflicts, then push.${filesDesc}\n\nBlocked downstream item: \`${meta.item.id}\` — ${meta.item.title || ''}`,
608
+ created: ts(),
609
+ createdBy: 'engine:dep-conflict-fix',
610
+ _branch: depConflictBranch,
611
+ _blockedItem: meta.item.id,
612
+ project: project.name || null,
613
+ });
614
+ log('info', `Auto-queued conflict-fix work item ${conflictFixId} for ${depConflictBranch} (blocked: ${meta.item.id})`);
615
+ });
616
+ } catch (e) { log('warn', `Failed to auto-queue conflict-fix: ${e.message}`); }
617
+ }
539
618
  return;
540
619
  }
541
620
  } catch (e) {
@@ -3088,6 +3167,7 @@ module.exports = {
3088
3167
 
3089
3168
  // Shared helpers (used by lifecycle.js and tests)
3090
3169
  reconcileItemsWithPrs, detectDependencyCycles,
3170
+ parseConflictFiles, // exported for testing
3091
3171
 
3092
3172
  // Playbooks
3093
3173
  renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS, buildWorkItemDispatchVars,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.869",
3
+ "version": "0.1.871",
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"