@yemi33/minions 0.1.2137 → 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.
- package/dashboard.js +10 -0
- package/engine/consolidation.js +16 -1
- package/engine/dispatch.js +1 -0
- package/engine/shared.js +83 -0
- package/engine/watches.js +22 -5
- package/engine.js +100 -28
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -7470,6 +7470,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7470
7470
|
}
|
|
7471
7471
|
safeWrite(notesPath, notes);
|
|
7472
7472
|
|
|
7473
|
+
// W-mq1j85cj00055a8f — rewrite WI references that pointed at the
|
|
7474
|
+
// now-archived inbox note to the persisted destination (notes.md).
|
|
7475
|
+
try { shared.rewriteInboxRefsAcrossProjects(name, 'notes.md'); }
|
|
7476
|
+
catch (e) { console.error('inbox-ref rewrite (persist):', e.message); }
|
|
7477
|
+
|
|
7473
7478
|
// Move to archive
|
|
7474
7479
|
const archiveDir = path.join(MINIONS_DIR, 'notes', 'archive');
|
|
7475
7480
|
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
|
|
@@ -7509,6 +7514,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7509
7514
|
safeWrite(kbFile, kbContent);
|
|
7510
7515
|
queries.invalidateKnowledgeBaseCache();
|
|
7511
7516
|
|
|
7517
|
+
// W-mq1j85cj00055a8f — rewrite WI references that pointed at the
|
|
7518
|
+
// now-archived inbox note to the KB destination.
|
|
7519
|
+
try { shared.rewriteInboxRefsAcrossProjects(name, `knowledge/${category}/${name}`); }
|
|
7520
|
+
catch (e) { console.error('inbox-ref rewrite (promote-kb):', e.message); }
|
|
7521
|
+
|
|
7512
7522
|
// Move inbox item to archive
|
|
7513
7523
|
const archiveDir = path.join(MINIONS_DIR, 'notes', 'archive');
|
|
7514
7524
|
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
|
package/engine/consolidation.js
CHANGED
|
@@ -1155,7 +1155,22 @@ function archiveInboxFiles(files) {
|
|
|
1155
1155
|
|
|
1156
1156
|
if (!fs.existsSync(ARCHIVE_DIR)) fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
1157
1157
|
for (const f of files) {
|
|
1158
|
-
try {
|
|
1158
|
+
try {
|
|
1159
|
+
// Resolve the final destination path BEFORE rename so the WI-ref
|
|
1160
|
+
// rewrite can point at the actual on-disk location (uniquePath may
|
|
1161
|
+
// suffix `-2`, `-3` … if a same-day collision exists).
|
|
1162
|
+
const dest = shared.uniquePath(path.join(ARCHIVE_DIR, `${dateStamp()}-${f}`));
|
|
1163
|
+
// W-mq1j85cj00055a8f — rewrite WI references that pointed at the
|
|
1164
|
+
// now-archived inbox note to the archive destination. Computed
|
|
1165
|
+
// relative to MINIONS_DIR so it matches the dashboard's relative-URL
|
|
1166
|
+
// render convention. Wrapped so a rewrite failure can't break the
|
|
1167
|
+
// archive step itself (which is the load-bearing operation here).
|
|
1168
|
+
try {
|
|
1169
|
+
const rel = path.relative(shared.MINIONS_DIR, dest).replace(/\\/g, '/');
|
|
1170
|
+
shared.rewriteInboxRefsAcrossProjects(f, rel);
|
|
1171
|
+
} catch (e) { log('warn', `Inbox-ref rewrite (${f}): ${e.message}`); }
|
|
1172
|
+
fs.renameSync(path.join(INBOX_DIR, f), dest);
|
|
1173
|
+
} catch (err) { log('warn', `Inbox archive: ${err.message}`); }
|
|
1159
1174
|
}
|
|
1160
1175
|
}
|
|
1161
1176
|
|
package/engine/dispatch.js
CHANGED
|
@@ -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.
|
|
@@ -5090,6 +5091,87 @@ function extractStructuredWorkItemPrRef(item) {
|
|
|
5090
5091
|
return null;
|
|
5091
5092
|
}
|
|
5092
5093
|
|
|
5094
|
+
// W-mq1j85cj00055a8f — when an inbox note (notes/inbox/<name>) leaves the
|
|
5095
|
+
// inbox (persisted to notes.md, promoted to knowledge/, or auto-archived by
|
|
5096
|
+
// consolidation), any work-item reference pointing at the old inbox path
|
|
5097
|
+
// turns into a broken link. Rewrite those references across every project's
|
|
5098
|
+
// work-items.json + the central one to the new canonical location.
|
|
5099
|
+
//
|
|
5100
|
+
// Match semantics (intentionally narrow per the WI scope):
|
|
5101
|
+
// - Scope is strictly item.references[]; description prose is NOT scanned.
|
|
5102
|
+
// - String entries: 'notes/inbox/<inboxName>' (anchored on start or '/').
|
|
5103
|
+
// - Object entries: { url | path | href }. Other keys (label, kind, …) are
|
|
5104
|
+
// preserved.
|
|
5105
|
+
// - Trailing `?query` or `#fragment` is tolerated; substring overlaps in
|
|
5106
|
+
// unrelated path segments (e.g. .../notes/inbox/foobar for foo.md) are
|
|
5107
|
+
// NOT rewritten.
|
|
5108
|
+
// - Archived work-items files are skipped — only live work-items.json paths
|
|
5109
|
+
// surfaced by getProjects() + the central path are touched.
|
|
5110
|
+
//
|
|
5111
|
+
// Idempotent: re-running with the same inboxName after the rewrite is a
|
|
5112
|
+
// no-op (the references already read newLocation, which won't match the
|
|
5113
|
+
// notes/inbox/<inboxName> regex).
|
|
5114
|
+
//
|
|
5115
|
+
// Returns the count of references rewritten across all files (for logging).
|
|
5116
|
+
// Each file's mutate is wrapped in try/catch so one corrupt project can't
|
|
5117
|
+
// block the others.
|
|
5118
|
+
//
|
|
5119
|
+
// `opts._mutate` is an undocumented test seam — production callers always
|
|
5120
|
+
// take the default (the in-file `mutateWorkItems` closure reference). Tests
|
|
5121
|
+
// inject a wrapper to exercise the per-file try/catch boundary without
|
|
5122
|
+
// having to manufacture a real SQL-store failure.
|
|
5123
|
+
function rewriteInboxRefsAcrossProjects(inboxName, newLocation, opts = {}) {
|
|
5124
|
+
if (!inboxName || typeof newLocation !== 'string' || !newLocation) return 0;
|
|
5125
|
+
const baseName = String(inboxName).replace(/^.*[/\\]/, '').trim();
|
|
5126
|
+
if (!baseName) return 0;
|
|
5127
|
+
const escaped = baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
5128
|
+
const inboxRefRe = new RegExp(`(?:^|/)notes/inbox/${escaped}(?=$|[?#])`);
|
|
5129
|
+
const _mutate = (opts && typeof opts._mutate === 'function') ? opts._mutate : mutateWorkItems;
|
|
5130
|
+
|
|
5131
|
+
let rewritten = 0;
|
|
5132
|
+
const config = safeJson(path.join(MINIONS_DIR, 'config.json')) || {};
|
|
5133
|
+
const candidates = [];
|
|
5134
|
+
try {
|
|
5135
|
+
for (const project of getProjects(config)) {
|
|
5136
|
+
try { candidates.push(projectWorkItemsPath(project)); } catch { /* skip bad project entry */ }
|
|
5137
|
+
}
|
|
5138
|
+
} catch { /* getProjects failed — fall back to central only */ }
|
|
5139
|
+
candidates.push(centralWorkItemsPath());
|
|
5140
|
+
|
|
5141
|
+
for (const wiPath of candidates) {
|
|
5142
|
+
try {
|
|
5143
|
+
_mutate(wiPath, items => {
|
|
5144
|
+
if (!Array.isArray(items)) return items;
|
|
5145
|
+
for (const item of items) {
|
|
5146
|
+
if (!item || !Array.isArray(item.references)) continue;
|
|
5147
|
+
for (let i = 0; i < item.references.length; i++) {
|
|
5148
|
+
const ref = item.references[i];
|
|
5149
|
+
if (typeof ref === 'string') {
|
|
5150
|
+
if (inboxRefRe.test(ref)) {
|
|
5151
|
+
item.references[i] = newLocation;
|
|
5152
|
+
rewritten++;
|
|
5153
|
+
}
|
|
5154
|
+
} else if (ref && typeof ref === 'object') {
|
|
5155
|
+
for (const key of ['url', 'path', 'href']) {
|
|
5156
|
+
const v = ref[key];
|
|
5157
|
+
if (typeof v === 'string' && inboxRefRe.test(v)) {
|
|
5158
|
+
ref[key] = newLocation;
|
|
5159
|
+
rewritten++;
|
|
5160
|
+
break;
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
return items;
|
|
5167
|
+
});
|
|
5168
|
+
} catch (e) {
|
|
5169
|
+
try { console.warn(`rewriteInboxRefsAcrossProjects(${wiPath}): ${e.message}`); } catch { /* logging best-effort */ }
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
return rewritten;
|
|
5173
|
+
}
|
|
5174
|
+
|
|
5093
5175
|
function extractWorkItemPrRef(item) {
|
|
5094
5176
|
if (!item || typeof item !== 'object') return null;
|
|
5095
5177
|
const fromStructured = extractStructuredWorkItemPrRef(item);
|
|
@@ -6500,6 +6582,7 @@ module.exports = {
|
|
|
6500
6582
|
extractPrRefFromText,
|
|
6501
6583
|
extractWorkItemPrRef,
|
|
6502
6584
|
extractStructuredWorkItemPrRef,
|
|
6585
|
+
rewriteInboxRefsAcrossProjects,
|
|
6503
6586
|
getProjectPrScope,
|
|
6504
6587
|
getPrNumber,
|
|
6505
6588
|
getPrDisplayId,
|
package/engine/watches.js
CHANGED
|
@@ -320,7 +320,10 @@ function evaluateWatch(watch, state) {
|
|
|
320
320
|
if (!tt.conditions.includes(condition)) return { triggered: false, message: `Unknown condition: ${condition}` };
|
|
321
321
|
|
|
322
322
|
const entity = tt.fetchEntity(target, state || {});
|
|
323
|
-
if (!entity)
|
|
323
|
+
if (!entity) {
|
|
324
|
+
const targetStr = typeof target === 'string' ? target : JSON.stringify(target);
|
|
325
|
+
return { triggered: false, message: `${tt.label} ${targetStr} not found` };
|
|
326
|
+
}
|
|
324
327
|
|
|
325
328
|
const prevState = watch._lastState || {};
|
|
326
329
|
let primary;
|
|
@@ -645,21 +648,35 @@ async function _runActionTask(task) {
|
|
|
645
648
|
/**
|
|
646
649
|
* Internal: capture state snapshot for a watch target.
|
|
647
650
|
* Dispatches to the registered target type's captureState.
|
|
651
|
+
*
|
|
652
|
+
* W-mq1n83pw000844b8 — Preserve prior state on transient fetchEntity null
|
|
653
|
+
* (and on captureState exceptions / unknown target types). The old behavior
|
|
654
|
+
* wiped to {} whenever the entity could not be resolved, which then made
|
|
655
|
+
* line 431 re-initialize via captureState on the next tick, losing
|
|
656
|
+
* type-specific dedup keys (gh-author-prs `numbers`, work-item
|
|
657
|
+
* `_unchangedTicks`, pipeline `_stuckStageTicks`). The result was watches
|
|
658
|
+
* re-firing for already-seen entities after every engine restart while a
|
|
659
|
+
* plugin's background fetch cache warmed up. Preserving prevState is
|
|
660
|
+
* harmless for types where fetchEntity-null means "entity deleted" because
|
|
661
|
+
* evaluate already gates on entity being non-null, and is the bug-free
|
|
662
|
+
* behavior for plugins with transient nulls (cache miss, network hiccup).
|
|
648
663
|
*/
|
|
649
664
|
function _captureState(watch, state) {
|
|
665
|
+
const prevState = watch._lastState || {};
|
|
650
666
|
const tt = TARGET_TYPES[watch.targetType];
|
|
651
|
-
if (!tt) return
|
|
667
|
+
if (!tt) return prevState;
|
|
652
668
|
const entity = tt.fetchEntity(watch.target, state || {});
|
|
653
|
-
if (!entity) return
|
|
669
|
+
if (!entity) return prevState;
|
|
654
670
|
try {
|
|
655
671
|
// P-w5b8d2c9 — Phase 2.2: pass prevState so captureState can carry
|
|
656
672
|
// forward unchanged-tick counters (e.g. _unchangedTicks for work-item
|
|
657
673
|
// stalled, _stuckStageTicks for pipeline stuck-in-stage). Existing
|
|
658
674
|
// captureState fns that take only 1 arg ignore this — backward-compat.
|
|
659
|
-
|
|
675
|
+
const out = tt.captureState(entity, prevState);
|
|
676
|
+
return (out && typeof out === 'object') ? out : prevState;
|
|
660
677
|
} catch (err) {
|
|
661
678
|
log('warn', `_captureState ${watch.targetType}: ${err.message}`);
|
|
662
|
-
return
|
|
679
|
+
return prevState;
|
|
663
680
|
}
|
|
664
681
|
}
|
|
665
682
|
|
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 })).
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
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
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
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.
|
|
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"
|