@yemi33/minions 0.1.2138 → 0.1.2139

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.
@@ -758,6 +758,7 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
758
758
  [FAILURE_CLASS.EMPTY_OUTPUT]: 'agent produced no output \u2014 likely crashed on startup',
759
759
  [FAILURE_CLASS.BUILD_FAILURE]: 'build/test/lint failure in output',
760
760
  [FAILURE_CLASS.MERGE_CONFLICT]: 'merge conflict',
761
+ [FAILURE_CLASS.DEPENDENCY_MERGE_SETUP]: 'dependency merge setup failed',
761
762
  [FAILURE_CLASS.MAX_TURNS]: 'reached max turn limit',
762
763
  [FAILURE_CLASS.TIMEOUT]: 'timed out waiting for agent',
763
764
  [FAILURE_CLASS.SPAWN_ERROR]: 'agent process failed to start',
package/engine/shared.js CHANGED
@@ -3519,6 +3519,7 @@ const FAILURE_CLASS = {
3519
3519
  WORKTREE_PREFLIGHT: 'worktree-preflight', // Pre-spawn worktree validation rejected (nested-in-project, drive-root collapse) — never retryable
3520
3520
  WORKTREE_DIRTY: 'worktree-dirty', // #2996: reused worktree had uncommitted edits and the engine could not auto-heal (or already quarantined). Non-retryable for this dispatch — next discovery cycle creates a fresh worktree.
3521
3521
  WORKTREE_DIVERGENT: 'worktree-divergent', // #2996: reused worktree's local branch was N commits ahead of origin (unsafe to reset, may contain unpushed agent work). Engine quarantined the worktree + backed up the local ref; non-retryable for this dispatch.
3522
+ DEPENDENCY_MERGE_SETUP: 'dependency-merge-setup', // Dependency pre-merge plumbing (stash/status/reset) failed before a real file conflict was verified. Retryable so a fresh worktree can recover.
3522
3523
  INVALID_KEEP_PROCESSES_WORKDIR: 'invalid-keep-processes-workdir', // W-mp6k7ywi000fa33c: keep-pids.json declared a cwd that is not a real git worktree (likely a selective copy of the repo) — never retryable; agent must rerun in a real worktree
3523
3524
  INVALID_KEEP_PROCESSES_SCHEMA: 'invalid-keep-processes-schema', // W-mp7i902u000l991f: keep-pids.json failed validation for a reason other than workdir (pids-missing, ttl-too-long, expires_at-missing, pids-too-many, port-invalid, etc.) — agent wrote the wrong shape; never retryable until they fix the file
3524
3525
  INVALID_MANAGED_SPAWN: 'invalid-managed-spawn', // P-7a3b1c92: agents/<id>/managed-spawn.json failed validator (bad schema, broken workdir, executable/env not on allowlist, healthcheck shape wrong). Engine refuses to spawn any spec — agent must fix file; never retryable as-is.
package/engine.js CHANGED
@@ -208,6 +208,51 @@ function parseConflictFiles(mergeOutput) {
208
208
  return [...new Set(files)]; // dedupe
209
209
  }
210
210
 
211
+ function gitOutputToString(result) {
212
+ if (result == null) return '';
213
+ if (Buffer.isBuffer(result)) return result.toString();
214
+ if (typeof result === 'string') return result;
215
+ if (result.stdout != null) return gitOutputToString(result.stdout);
216
+ return String(result);
217
+ }
218
+
219
+ function gitErrorOutput(err) {
220
+ return [
221
+ err?.message || '',
222
+ gitOutputToString(err?.stdout),
223
+ gitOutputToString(err?.stderr),
224
+ ].filter(Boolean).join('\n');
225
+ }
226
+
227
+ async function listUnmergedFiles(worktreePath, gitOpts = {}) {
228
+ try {
229
+ const out = await shared.shellSafeGit(['diff', '--name-only', '--diff-filter=U'], { ...gitOpts, cwd: worktreePath });
230
+ return gitOutputToString(out).split(/\r?\n/).map(s => s.trim()).filter(Boolean);
231
+ } catch (e) {
232
+ log('warn', `Failed to inspect unmerged files in ${worktreePath}: ${e.message}`);
233
+ return [];
234
+ }
235
+ }
236
+
237
+ function classifyDepMergeFailureOutput(errOutput, unmergedFiles = []) {
238
+ const output = String(errOutput || '');
239
+ const files = Array.isArray(unmergedFiles) ? unmergedFiles.filter(Boolean) : [];
240
+ const conflictFiles = files.length > 0 ? files : parseConflictFiles(output);
241
+ const hasConflictSignal = files.length > 0
242
+ || /\bCONFLICT \(/.test(output)
243
+ || /Automatic merge failed/i.test(output)
244
+ || /fix conflicts/i.test(output)
245
+ || /you have unmerged paths/i.test(output);
246
+ const stashPlumbingFailed = /(?:fatal:\s*)?stash failed/i.test(output)
247
+ || /cannot save.*stash/i.test(output)
248
+ || /could not.*stash/i.test(output);
249
+ return {
250
+ isConflict: hasConflictSignal,
251
+ isPlumbingFailure: stashPlumbingFailed && !hasConflictSignal,
252
+ conflictFiles: hasConflictSignal ? conflictFiles : [],
253
+ };
254
+ }
255
+
211
256
  // Build the work item used by the dep-merge-failure auto-queue path
212
257
  // (W-mpcwojgr000a0244). Routes the conflict-fix through the shared-branch
213
258
  // dispatch path (`branchStrategy: 'shared-branch'` + `featureBranch:
@@ -2029,6 +2074,8 @@ async function spawnAgent(dispatchItem, config) {
2029
2074
  let depMergeFailed = false;
2030
2075
  let depConflictBranch = null; // track which dep branch caused the conflict
2031
2076
  let depConflictFiles = []; // conflicting file names parsed from git output
2077
+ let depMergeSetupFailed = false;
2078
+ let depMergeSetupReason = '';
2032
2079
  // W-mpcuc8i80003a7b3 — track whether ANY git op in the dep phase
2033
2080
  // failed with an ADO auth signature. If so, we escalate as
2034
2081
  // FAILURE_CLASS.AUTH (non-retryable + dedup'd inbox alert) instead
@@ -2186,14 +2233,18 @@ async function spawnAgent(dispatchItem, config) {
2186
2233
  let stashed = false;
2187
2234
  if (!depMergeFailed && !skipDepMerge && prunedDeps.length > 0) {
2188
2235
  try {
2189
- const statusOut = (await shared.shellSafeGit(['status', '--porcelain'], { ..._gitOpts, cwd: worktreePath })).stdout.toString().trim();
2236
+ const statusOut = gitOutputToString(await shared.shellSafeGit(['status', '--porcelain'], { ..._gitOpts, cwd: worktreePath })).trim();
2190
2237
  if (statusOut) {
2191
2238
  await shared.shellSafeGit(['stash', 'push', '--include-untracked', '-m', 'engine: stash before dep re-merge'], { ..._gitOpts, cwd: worktreePath });
2192
2239
  stashed = true;
2193
2240
  log('info', `Stashed uncommitted changes in ${branchName} before dep merge`);
2194
2241
  }
2195
2242
  } catch (stashErr) {
2196
- log('warn', `Failed to stash changes in ${branchName} before dep merge: ${stashErr.message}`);
2243
+ const stashMsg = gitErrorOutput(stashErr);
2244
+ depMergeFailed = true;
2245
+ depMergeSetupFailed = true;
2246
+ depMergeSetupReason = `Dependency merge setup failed while stashing local changes in ${branchName}: ${stashMsg || stashErr.message}`;
2247
+ log('warn', depMergeSetupReason);
2197
2248
  }
2198
2249
  }
2199
2250
  if (!depMergeFailed && !skipDepMerge) {
@@ -2222,41 +2273,49 @@ async function spawnAgent(dispatchItem, config) {
2222
2273
  }
2223
2274
  log('info', `Successfully re-merged all ${prunedDeps.length} dep branches after reset for ${branchName}`);
2224
2275
  } catch (resetErr) {
2225
- const errOutput = (resetErr.message || '') + '\n' + (resetErr.stdout?.toString?.() || '') + '\n' + (resetErr.stderr?.toString?.() || '');
2276
+ const errOutput = gitErrorOutput(resetErr);
2226
2277
  log('warn', `Failed to reset and re-merge deps for ${branchName}: ${resetErr.message}`);
2227
2278
  if (adoGitAuth.isAdoAuthFailure(resetErr)) _depAuthFailed = true;
2228
2279
  try { await shared.shellSafeGit(['merge', '--abort'], { ..._gitOpts, cwd: worktreePath }); } catch (_) { /* no merge in progress */ }
2280
+ const unmergedFiles = await listUnmergedFiles(worktreePath, _gitOpts);
2281
+ const depFailure = classifyDepMergeFailureOutput(errOutput, unmergedFiles);
2282
+ if (depFailure.isPlumbingFailure) {
2283
+ depMergeSetupFailed = true;
2284
+ depMergeSetupReason = `Dependency merge setup failed in engine git plumbing for ${branchName}: ${errOutput.split('\n').find(Boolean) || resetErr.message}`;
2285
+ }
2229
2286
  // Post-mortem: incremental simulation to identify which dep caused the conflict (#958)
2230
2287
  // Uses same chained merge-tree approach as pre-flight to catch inter-dep conflicts
2231
2288
  const pmMainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
2232
- try {
2233
- const sim = await preflightMergeSimulation(prunedDeps, pmMainRef, _gitOpts, rootDir);
2234
- if (!sim.ok) {
2235
- depConflictBranch = sim.conflictBranch;
2236
- depConflictFiles = sim.conflictFiles;
2237
- _isInterDepConflict = sim.isInterDep;
2238
- _preflightConflictPrev = sim.prevBranch;
2289
+ if (!depMergeSetupFailed || depFailure.isConflict) {
2290
+ try {
2291
+ const sim = await preflightMergeSimulation(prunedDeps, pmMainRef, _gitOpts, rootDir);
2292
+ if (!sim.ok) {
2293
+ depConflictBranch = sim.conflictBranch;
2294
+ depConflictFiles = sim.conflictFiles;
2295
+ _isInterDepConflict = sim.isInterDep;
2296
+ _preflightConflictPrev = sim.prevBranch;
2297
+ }
2298
+ } catch (_simErr) {
2299
+ // Fallback: old per-branch isolation check via 3-arg git merge-tree
2300
+ for (const { branch: reBranch2 } of prunedDeps) {
2301
+ try {
2302
+ const mainRef2 = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
2303
+ const mergeBase = gitOutputToString(await shared.shellSafeGit(['merge-base', `origin/${mainRef2}`, `origin/${reBranch2}`], { ..._gitOpts, cwd: rootDir })).trim();
2304
+ const treeResult = await shared.shellSafeGit(['merge-tree', mergeBase, `origin/${mainRef2}`, `origin/${reBranch2}`], { ..._gitOpts, cwd: rootDir });
2305
+ const treeOutput = gitOutputToString(treeResult);
2306
+ if (treeOutput.includes('<<<<<<<') || treeOutput.includes('changed in both')) {
2307
+ depConflictBranch = reBranch2;
2308
+ depConflictFiles = parseConflictFiles(treeOutput);
2309
+ break;
2310
+ }
2311
+ } catch (_e) { /* merge-tree may fail — continue checking other branches */ }
2312
+ }
2239
2313
  }
2240
- } catch (_simErr) {
2241
- // Fallback: old per-branch isolation check via 3-arg git merge-tree
2242
- for (const { branch: reBranch2 } of prunedDeps) {
2243
- try {
2244
- const mainRef2 = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
2245
- const mergeBase = (await shared.shellSafeGit(['merge-base', `origin/${mainRef2}`, `origin/${reBranch2}`], { ..._gitOpts, cwd: rootDir })).stdout.toString().trim();
2246
- const treeResult = await shared.shellSafeGit(['merge-tree', mergeBase, `origin/${mainRef2}`, `origin/${reBranch2}`], { ..._gitOpts, cwd: rootDir });
2247
- const treeOutput = treeResult.stdout?.toString?.() || '';
2248
- if (treeOutput.includes('<<<<<<<') || treeOutput.includes('changed in both')) {
2249
- depConflictBranch = reBranch2;
2250
- depConflictFiles = parseConflictFiles(treeOutput);
2251
- break;
2252
- }
2253
- } catch (_e) { /* merge-tree may fail — continue checking other branches */ }
2314
+ // Fallback: use verified unmerged files or conflict output if merge-tree didn't identify them.
2315
+ if (!depConflictBranch) {
2316
+ depConflictFiles = depFailure.conflictFiles;
2254
2317
  }
2255
2318
  }
2256
- // Fallback: parse conflict files from the error output if merge-tree didn't identify them
2257
- if (!depConflictBranch) {
2258
- depConflictFiles = parseConflictFiles(errOutput);
2259
- }
2260
2319
  depMergeFailed = true;
2261
2320
  }
2262
2321
  break;
@@ -2327,6 +2386,18 @@ async function spawnAgent(dispatchItem, config) {
2327
2386
  cleanupTempAgent(agentId);
2328
2387
  return;
2329
2388
  }
2389
+ if (depMergeSetupFailed) {
2390
+ const setupReason = (depMergeSetupReason || `Dependency merge setup failed for ${branchName}`).slice(0, 800);
2391
+ completeDispatch(
2392
+ id,
2393
+ DISPATCH_RESULT.ERROR,
2394
+ setupReason,
2395
+ 'Engine dependency pre-merge plumbing failed before any real unmerged files were verified. Retrying in a fresh worktree should recover if the dependency branches still merge cleanly.',
2396
+ { failureClass: FAILURE_CLASS.DEPENDENCY_MERGE_SETUP, agentRetryable: true },
2397
+ );
2398
+ cleanupTempAgent(agentId);
2399
+ return;
2400
+ }
2330
2401
  // Build actionable failReason identifying the conflicting branch and files (#958)
2331
2402
  const mainBranch = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
2332
2403
  let failReason = 'Dependency merge failed';
@@ -8082,6 +8153,7 @@ module.exports = {
8082
8153
  reconcileItemsWithPrs, detectDependencyCycles,
8083
8154
  areDependenciesMet, // exported for testing (P-bf04-decompose-zero-children)
8084
8155
  parseConflictFiles, pruneAncestorDeps, preflightMergeSimulation, // exported for testing
8156
+ gitOutputToString, gitErrorOutput, classifyDepMergeFailureOutput, listUnmergedFiles, // exported for testing
8085
8157
  buildDepConflictFixItem, deriveConflictFixKey, // exported for testing (W-mpcwojgr000a0244)
8086
8158
  isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
8087
8159
  pruneStaleWorktreeForBranch, // exported for testing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2138",
3
+ "version": "0.1.2139",
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"