@yemi33/minions 0.1.2102 → 0.1.2103
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/engine/dispatch.js +4 -0
- package/engine/github.js +15 -2
- package/engine/lifecycle.js +47 -2
- package/engine/shared.js +2 -0
- package/engine.js +316 -40
- package/package.json +1 -1
package/engine/dispatch.js
CHANGED
|
@@ -434,6 +434,8 @@ function isRetryableFailureReason(reason = '', failureClass = '') {
|
|
|
434
434
|
FAILURE_CLASS.PERMISSION_BLOCKED,
|
|
435
435
|
FAILURE_CLASS.AUTH, // W-mpcuc8i80003a7b3 — git/network credential failure; mechanical retry won't fix missing az / GCM creds
|
|
436
436
|
FAILURE_CLASS.WORKTREE_PREFLIGHT, // pre-spawn worktree validation — recompute will produce the same failure
|
|
437
|
+
FAILURE_CLASS.WORKTREE_DIRTY, // #2996: reused worktree was dirty and could not be auto-healed — non-retryable for this dispatch attempt; the engine quarantined the worktree so the next discovery cycle creates a fresh one
|
|
438
|
+
FAILURE_CLASS.WORKTREE_DIVERGENT, // #2996: reused worktree's local branch had unpushed commits — engine quarantined + backed up the local ref; non-retryable for this dispatch (next discovery creates fresh worktree on origin/<branch>)
|
|
437
439
|
FAILURE_CLASS.INVALID_KEEP_PROCESSES_WORKDIR, // W-mp6k7ywi000fa33c — keep-pids cwd is not a real git worktree; re-running won't fix the structural issue
|
|
438
440
|
FAILURE_CLASS.INVALID_KEEP_PROCESSES_SCHEMA, // W-mp7i902u000l991f — keep-pids.json failed shape validation; re-running with the same wrong file won't fix it
|
|
439
441
|
FAILURE_CLASS.INVALID_MANAGED_SPAWN, // W-mpbhxg3b000u8411 — managed-spawn.json failed validation; re-running with the same wrong file won't fix it
|
|
@@ -797,6 +799,8 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
|
|
|
797
799
|
[FAILURE_CLASS.PERMISSION_BLOCKED]: 'permission or auth failure',
|
|
798
800
|
[FAILURE_CLASS.AUTH]: 'ADO/git authentication failed (missing or expired credentials)',
|
|
799
801
|
[FAILURE_CLASS.WORKTREE_PREFLIGHT]: 'worktree preflight rejected (nested in project root or rootDir collapsed to drive root)',
|
|
802
|
+
[FAILURE_CLASS.WORKTREE_DIRTY]: 'reused worktree had uncommitted edits and could not be auto-healed (#2996) — engine quarantined the dir so the next dispatch creates a fresh worktree',
|
|
803
|
+
[FAILURE_CLASS.WORKTREE_DIVERGENT]: 'reused worktree had unpushed local commits ahead of origin (#2996) — engine backed up the local ref and quarantined the dir so the next dispatch starts from origin/<branch>',
|
|
800
804
|
[FAILURE_CLASS.INVALID_KEEP_PROCESSES_WORKDIR]: 'keep_processes cwd is not a real git worktree (rerun in a `git worktree add` directory)',
|
|
801
805
|
[FAILURE_CLASS.INVALID_KEEP_PROCESSES_SCHEMA]: 'keep-pids.json failed shape validation (wrong keys/types/values — see inbox alert for the canonical shape)',
|
|
802
806
|
[FAILURE_CLASS.INVALID_MANAGED_SPAWN]: 'managed-spawn.json failed validation (bad schema, workdir, or allowlist — see inbox alert)',
|
package/engine/github.js
CHANGED
|
@@ -1127,7 +1127,14 @@ async function pollPrHumanComments(config) {
|
|
|
1127
1127
|
author: c.user?.login || 'Human',
|
|
1128
1128
|
content: c.body || '',
|
|
1129
1129
|
date,
|
|
1130
|
-
_isAgent: false
|
|
1130
|
+
_isAgent: false,
|
|
1131
|
+
// Issue #2994: namespace GH comment ids by API source ("issue" from
|
|
1132
|
+
// `/issues/N/comments` vs "review" from `/pulls/N/comments`). Each
|
|
1133
|
+
// endpoint maintains its own id space, so two comments — one issue,
|
|
1134
|
+
// one review — can collide on the same numeric id. Mirror the ADO
|
|
1135
|
+
// poller's thread-aware key shape so the engine.js human-feedback
|
|
1136
|
+
// same-head guard can dedup via a single comparable key.
|
|
1137
|
+
_type: c._type,
|
|
1131
1138
|
};
|
|
1132
1139
|
allCommentEntries.push(entry);
|
|
1133
1140
|
|
|
@@ -1186,7 +1193,13 @@ async function pollPrHumanComments(config) {
|
|
|
1186
1193
|
pr.humanFeedback = {
|
|
1187
1194
|
lastProcessedCommentDate: latestDate,
|
|
1188
1195
|
lastProcessedCommentId: String(newComments[newComments.length - 1].commentId),
|
|
1189
|
-
|
|
1196
|
+
// Issue #2994: namespaced key — `${type}:${commentId}` — for the
|
|
1197
|
+
// engine.js human-feedback same-head guard. Mirrors ADO's
|
|
1198
|
+
// `${threadId}:${commentId}` shape so the guard compares a single key
|
|
1199
|
+
// form. Falls back to the bare id when `_type` is absent (defensive;
|
|
1200
|
+
// every entry above sets it, but the pollPrHumanComments path in
|
|
1201
|
+
// engine/ado.js writes the legacy format too).
|
|
1202
|
+
lastProcessedCommentKey: `${newComments[newComments.length - 1]._type || ''}:${newComments[newComments.length - 1].commentId}`,
|
|
1190
1203
|
pendingFix: true,
|
|
1191
1204
|
feedbackContent,
|
|
1192
1205
|
editsSeen,
|
package/engine/lifecycle.js
CHANGED
|
@@ -2004,8 +2004,39 @@ function recordPrNoOpFixAttempt(target, cause, source, dispatchItem, branchChang
|
|
|
2004
2004
|
// noops so the symmetric same-head guard at engine.js:~2847 can short-circuit
|
|
2005
2005
|
// when both the head SHA AND the lastProcessedCommentId match the recorded
|
|
2006
2006
|
// dispatch. Other causes have no comment-id concept, so the field is omitted.
|
|
2007
|
+
//
|
|
2008
|
+
// Issue #2994: also capture `lastProcessedCommentKey` (the thread-aware
|
|
2009
|
+
// form on ADO: `${threadId}:${commentId}`, type-namespaced on GH:
|
|
2010
|
+
// `${type}:${commentId}`). ADO comment ids are per-thread (not globally
|
|
2011
|
+
// unique), so a bare-id compare suppressed legitimate dispatches when two
|
|
2012
|
+
// different discussions both began at comment id:1 on the same head SHA
|
|
2013
|
+
// (live repro: office-bohemia/pullrequest/5284819, discussion 66508411
|
|
2014
|
+
// suppressed because discussion 66487840 noop'd at comment id:1 on the
|
|
2015
|
+
// same head a425ab05). PREFER the dispatch snapshot
|
|
2016
|
+
// (`dispatchItem.meta.pr.humanFeedback`) over live `target.humanFeedback`:
|
|
2017
|
+
// the poller can advance live state to a NEWER comment between dispatch
|
|
2018
|
+
// and lifecycle, and we MUST persist the comment that actually triggered
|
|
2019
|
+
// this dispatch — not the next one queued behind it. Fall back to live
|
|
2020
|
+
// target when the dispatch builder didn't snapshot humanFeedback (older
|
|
2021
|
+
// callers, manual re-dispatch paths).
|
|
2007
2022
|
...(cause === shared.PR_FIX_CAUSE.HUMAN_FEEDBACK
|
|
2008
|
-
?
|
|
2023
|
+
? (() => {
|
|
2024
|
+
const dispatchedFeedback = dispatchItem?.meta?.pr?.humanFeedback;
|
|
2025
|
+
const liveFeedback = target.humanFeedback;
|
|
2026
|
+
const commentId = String(
|
|
2027
|
+
(dispatchedFeedback && dispatchedFeedback.lastProcessedCommentId)
|
|
2028
|
+
|| (liveFeedback && liveFeedback.lastProcessedCommentId)
|
|
2029
|
+
|| ''
|
|
2030
|
+
);
|
|
2031
|
+
const commentKey = String(
|
|
2032
|
+
(dispatchedFeedback && dispatchedFeedback.lastProcessedCommentKey)
|
|
2033
|
+
|| (liveFeedback && liveFeedback.lastProcessedCommentKey)
|
|
2034
|
+
|| ''
|
|
2035
|
+
);
|
|
2036
|
+
const out = { lastProcessedCommentId: commentId };
|
|
2037
|
+
if (commentKey) out.lastProcessedCommentKey = commentKey;
|
|
2038
|
+
return out;
|
|
2039
|
+
})()
|
|
2009
2040
|
: {}),
|
|
2010
2041
|
};
|
|
2011
2042
|
target.lastDispatchedAt = now;
|
|
@@ -2361,10 +2392,24 @@ function updatePrAfterFixError(pr, project, source, options = {}) {
|
|
|
2361
2392
|
// engine.js human-feedback guard's commentId match still works once
|
|
2362
2393
|
// indeterminate flips back off (which it doesn't here, but state shape
|
|
2363
2394
|
// stays consistent across paths).
|
|
2395
|
+
//
|
|
2396
|
+
// Issue #2994: also carry/refresh `lastProcessedCommentKey` (thread-aware
|
|
2397
|
+
// form) so the same-head guard can compare the per-thread key rather than
|
|
2398
|
+
// the per-thread-collidable bare id. Preference order matches the noop
|
|
2399
|
+
// path: prior record's key → dispatch snapshot → live target.
|
|
2364
2400
|
if (cause === shared.PR_FIX_CAUSE.HUMAN_FEEDBACK) {
|
|
2401
|
+
const dispatchedFeedback = dispatchItem?.meta?.pr?.humanFeedback;
|
|
2402
|
+
const liveFeedback = target.humanFeedback;
|
|
2365
2403
|
const commentId = prior?.lastProcessedCommentId
|
|
2366
|
-
|| String(
|
|
2404
|
+
|| String((dispatchedFeedback && dispatchedFeedback.lastProcessedCommentId)
|
|
2405
|
+
|| (liveFeedback && liveFeedback.lastProcessedCommentId)
|
|
2406
|
+
|| '');
|
|
2367
2407
|
if (commentId) next.lastProcessedCommentId = commentId;
|
|
2408
|
+
const commentKey = prior?.lastProcessedCommentKey
|
|
2409
|
+
|| String((dispatchedFeedback && dispatchedFeedback.lastProcessedCommentKey)
|
|
2410
|
+
|| (liveFeedback && liveFeedback.lastProcessedCommentKey)
|
|
2411
|
+
|| '');
|
|
2412
|
+
if (commentKey) next.lastProcessedCommentKey = commentKey;
|
|
2368
2413
|
}
|
|
2369
2414
|
target._lastDispatchByCause[cause] = next;
|
|
2370
2415
|
result = { cause, indeterminate: true, errorClass };
|
package/engine/shared.js
CHANGED
|
@@ -3079,6 +3079,8 @@ const FAILURE_CLASS = {
|
|
|
3079
3079
|
MAX_TURNS: 'max-turns', // Claude CLI error_max_turns — work in progress, retryable
|
|
3080
3080
|
COMPLETION_NONCE_MISMATCH: 'completion-nonce-mismatch', // P-d2a8f6c1: completion JSON nonce did not match the per-spawn value injected via MINIONS_COMPLETION_NONCE — treat as forged/untrusted; ignore PR/noop/status fields from the report
|
|
3081
3081
|
WORKTREE_PREFLIGHT: 'worktree-preflight', // Pre-spawn worktree validation rejected (nested-in-project, drive-root collapse) — never retryable
|
|
3082
|
+
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.
|
|
3083
|
+
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.
|
|
3082
3084
|
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
|
|
3083
3085
|
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
|
|
3084
3086
|
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
|
@@ -846,26 +846,49 @@ async function pruneStaleWorktreeForBranch(rootDir, branchName, gitOpts) {
|
|
|
846
846
|
}
|
|
847
847
|
|
|
848
848
|
// ─── assertCleanSharedWorktree (#2439) ──────────────────────────────────────
|
|
849
|
-
// Engine-side preflight that prevents shared-branch
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
// work items
|
|
849
|
+
// Engine-side preflight that prevents shared-branch (and PR-targeted reused —
|
|
850
|
+
// see opts.quarantineOnUnsafe, issue #2996) dispatches from spawning into a
|
|
851
|
+
// dirty worktree. Without this, the playbook's "git status + bail out" guard
|
|
852
|
+
// fires AFTER dispatch — converting an orchestration hygiene problem into a
|
|
853
|
+
// fake implementation failure that also cascades to dependent work items, and
|
|
854
|
+
// (for PR-targeted reused worktrees) keeps redispatching agents into the same
|
|
855
|
+
// contaminated state.
|
|
854
856
|
//
|
|
855
857
|
// Behavior:
|
|
856
|
-
// 1. Run `git status --porcelain`
|
|
857
|
-
//
|
|
858
|
-
//
|
|
858
|
+
// 1. Run `git status --porcelain` + `git rev-list --left-right --count
|
|
859
|
+
// refs/remotes/origin/<branch>...HEAD` in the target worktree.
|
|
860
|
+
// 2. If clean AND not divergent → { clean: true, healed: false }.
|
|
861
|
+
// (Pure shared-branch callers pass quarantineOnUnsafe=false and DO NOT
|
|
862
|
+
// treat ahead-of-origin as dirty — that path's existing semantics are
|
|
863
|
+
// preserved. PR-targeted reused worktrees pass quarantineOnUnsafe=true
|
|
864
|
+
// and DO treat ahead-of-origin as unsafe because the underlying bug
|
|
865
|
+
// (#2996) was a 157-commit-ahead local branch.)
|
|
866
|
+
// 3. If unsafe, decide:
|
|
859
867
|
// - Block (preserve dirty tree for inspection) when ANOTHER active
|
|
860
|
-
// dispatch claims the same branch (ownership ambiguity
|
|
868
|
+
// dispatch claims the same branch (ownership ambiguity — NEVER
|
|
869
|
+
// quarantine, even when quarantineOnUnsafe=true).
|
|
861
870
|
// - Block when local commits exist that aren't on origin/<branch>
|
|
862
|
-
// (would lose unpushed work).
|
|
863
|
-
//
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
871
|
+
// (would lose unpushed work). When quarantineOnUnsafe=true,
|
|
872
|
+
// QUARANTINE instead: rename worktree to <path>-quarantine-<ts>,
|
|
873
|
+
// prune git's stale metadata, back up the local branch HEAD to
|
|
874
|
+
// refs/minions/quarantine/<sanitized>/<ts>, reset
|
|
875
|
+
// refs/heads/<branch> → origin/<branch>, and write a notes/inbox/
|
|
876
|
+
// engine-worktree-quarantine note with the diagnostics so a human
|
|
877
|
+
// can recover any unpushed work.
|
|
878
|
+
// - Otherwise (filesystem-only dirty) reset --hard + clean -fd, then
|
|
879
|
+
// re-verify.
|
|
880
|
+
// 4. Return { clean, healed, reason, dirtyFiles, ahead, behind,
|
|
881
|
+
// quarantined, quarantinedPath, backupRef } so the caller can fail
|
|
882
|
+
// fast with a first-class DIRTY_WORKTREE / WORKTREE_DIRTY /
|
|
883
|
+
// WORKTREE_DIVERGENT reason and retry semantics.
|
|
884
|
+
async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, dispatchId, gitOpts = {}, opts = {}) {
|
|
885
|
+
const quarantineOnUnsafe = !!opts.quarantineOnUnsafe;
|
|
886
|
+
const result = {
|
|
887
|
+
clean: false, healed: false, reason: null, dirtyFiles: [],
|
|
888
|
+
ahead: 0, behind: 0,
|
|
889
|
+
quarantined: false, quarantinedPath: null, backupRef: null,
|
|
890
|
+
};
|
|
891
|
+
// 1. Status probe (filesystem)
|
|
869
892
|
let statusOut = '';
|
|
870
893
|
try {
|
|
871
894
|
const r = await execAsync('git status --porcelain', { ...gitOpts, cwd: worktreePath, timeout: 10000 });
|
|
@@ -875,13 +898,43 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
875
898
|
result.error = e.message;
|
|
876
899
|
return result;
|
|
877
900
|
}
|
|
878
|
-
if (
|
|
901
|
+
if (statusOut) {
|
|
902
|
+
result.dirtyFiles = statusOut.split('\n').map(l => l.trim()).filter(Boolean);
|
|
903
|
+
}
|
|
904
|
+
const filesystemDirty = result.dirtyFiles.length > 0;
|
|
905
|
+
|
|
906
|
+
// 2. Ahead/behind vs refs/remotes/origin/<branch>. Computed BEFORE the
|
|
907
|
+
// early clean return so a divergent-but-clean worktree (no uncommitted
|
|
908
|
+
// edits, but local branch is N commits ahead of origin) is caught when
|
|
909
|
+
// the caller opted into quarantineOnUnsafe (issue #2996).
|
|
910
|
+
let upstreamKnown = false;
|
|
911
|
+
try {
|
|
912
|
+
const r = await execAsync(
|
|
913
|
+
`git rev-list --left-right --count refs/remotes/origin/${branchName}...HEAD`,
|
|
914
|
+
{ ...gitOpts, cwd: worktreePath, timeout: 10000 },
|
|
915
|
+
);
|
|
916
|
+
const parts = String(r || '').trim().split(/\s+/);
|
|
917
|
+
const behind = parseInt(parts[0], 10);
|
|
918
|
+
const ahead = parseInt(parts[1], 10);
|
|
919
|
+
if (Number.isFinite(ahead) && Number.isFinite(behind)) {
|
|
920
|
+
result.ahead = ahead;
|
|
921
|
+
result.behind = behind;
|
|
922
|
+
upstreamKnown = true;
|
|
923
|
+
}
|
|
924
|
+
} catch (e) {
|
|
925
|
+
// origin/<branch> may not exist yet (never pushed). Mark unknown.
|
|
926
|
+
upstreamKnown = false;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const considerUnsafe = filesystemDirty || (quarantineOnUnsafe && (result.ahead > 0 || !upstreamKnown));
|
|
930
|
+
if (!considerUnsafe) {
|
|
879
931
|
result.clean = true;
|
|
880
932
|
return result;
|
|
881
933
|
}
|
|
882
|
-
result.dirtyFiles = statusOut.split('\n').map(l => l.trim()).filter(Boolean);
|
|
883
934
|
|
|
884
|
-
//
|
|
935
|
+
// 3. Ownership check — another live dispatch on the same branch?
|
|
936
|
+
// NEVER quarantine in this case — the other dispatch may be using the
|
|
937
|
+
// worktree right now. Always preserve.
|
|
885
938
|
const sanitized = sanitizeBranch(branchName);
|
|
886
939
|
let otherActive = false;
|
|
887
940
|
try {
|
|
@@ -902,24 +955,38 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
902
955
|
return result;
|
|
903
956
|
}
|
|
904
957
|
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
try {
|
|
911
|
-
const r = await execAsync('git log @{u}..HEAD --oneline', { ...gitOpts, cwd: worktreePath, timeout: 10000 });
|
|
912
|
-
hasUnpushed = (r || '').toString().trim().length > 0;
|
|
913
|
-
} catch (e) {
|
|
914
|
-
// No upstream configured (e.g., branch never pushed) — be conservative.
|
|
915
|
-
hasUnpushed = true;
|
|
916
|
-
}
|
|
958
|
+
// 4. Unpushed-commit check — refuse to reset when local work would be lost.
|
|
959
|
+
// Use the ahead count we already computed (cheaper + correct vs.
|
|
960
|
+
// `git log @{u}..HEAD` which needs an upstream config and silently
|
|
961
|
+
// returns empty when missing).
|
|
962
|
+
const hasUnpushed = upstreamKnown ? result.ahead > 0 : true;
|
|
917
963
|
if (hasUnpushed) {
|
|
918
|
-
result.reason = 'has-unpushed-commits';
|
|
964
|
+
result.reason = upstreamKnown ? 'has-unpushed-commits' : 'no-upstream';
|
|
965
|
+
if (quarantineOnUnsafe) {
|
|
966
|
+
try {
|
|
967
|
+
const q = await _quarantineDirtyWorktree(
|
|
968
|
+
rootDir, worktreePath, branchName, gitOpts,
|
|
969
|
+
{
|
|
970
|
+
dispatchId,
|
|
971
|
+
reason: result.reason,
|
|
972
|
+
ahead: result.ahead,
|
|
973
|
+
behind: result.behind,
|
|
974
|
+
dirtyFiles: result.dirtyFiles,
|
|
975
|
+
},
|
|
976
|
+
);
|
|
977
|
+
result.quarantined = true;
|
|
978
|
+
result.quarantinedPath = q.quarantinedPath;
|
|
979
|
+
result.backupRef = q.backupRef;
|
|
980
|
+
} catch (qErr) {
|
|
981
|
+
result.quarantineError = qErr.message;
|
|
982
|
+
log('error', `assertCleanSharedWorktree: quarantine failed for ${worktreePath}: ${qErr.message}`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
919
985
|
return result;
|
|
920
986
|
}
|
|
921
987
|
|
|
922
|
-
//
|
|
988
|
+
// 5. Safe to self-heal (filesystem-dirty only, no unpushed commits, no
|
|
989
|
+
// other active dispatch): reset + clean.
|
|
923
990
|
try {
|
|
924
991
|
await execAsync('git reset --hard HEAD', { ...gitOpts, cwd: worktreePath, timeout: 30000 });
|
|
925
992
|
await execAsync('git clean -fd', { ...gitOpts, cwd: worktreePath, timeout: 30000 });
|
|
@@ -929,7 +996,7 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
929
996
|
return result;
|
|
930
997
|
}
|
|
931
998
|
|
|
932
|
-
//
|
|
999
|
+
// 6. Re-verify
|
|
933
1000
|
try {
|
|
934
1001
|
const r2 = await execAsync('git status --porcelain', { ...gitOpts, cwd: worktreePath, timeout: 10000 });
|
|
935
1002
|
const after = (r2 || '').toString().trim();
|
|
@@ -944,12 +1011,122 @@ async function assertCleanSharedWorktree(rootDir, worktreePath, branchName, disp
|
|
|
944
1011
|
return result;
|
|
945
1012
|
}
|
|
946
1013
|
|
|
947
|
-
log('info', `assertCleanSharedWorktree: auto-healed dirty
|
|
1014
|
+
log('info', `assertCleanSharedWorktree: auto-healed dirty worktree ${worktreePath} on ${branchName} (${result.dirtyFiles.length} dirty files reset)`);
|
|
948
1015
|
result.clean = true;
|
|
949
1016
|
result.healed = true;
|
|
950
1017
|
return result;
|
|
951
1018
|
}
|
|
952
1019
|
|
|
1020
|
+
// Quarantine a dirty/divergent reused worktree (#2996). Renames the worktree
|
|
1021
|
+
// directory to <path>-quarantine-<ts>, runs `git worktree prune` so git no
|
|
1022
|
+
// longer treats the missing path as an active checkout, backs up the local
|
|
1023
|
+
// branch HEAD to refs/minions/quarantine/<sanitized>/<ts>, and resets
|
|
1024
|
+
// refs/heads/<branch> → refs/remotes/origin/<branch> so the next dispatch
|
|
1025
|
+
// gets a fresh worktree on the origin tip. Writes a notes/inbox/
|
|
1026
|
+
// engine-worktree-quarantine note naming the dirty files + ahead/behind
|
|
1027
|
+
// counts so a human can recover any unpushed work.
|
|
1028
|
+
//
|
|
1029
|
+
// Caller MUST have already confirmed no other active dispatch holds the
|
|
1030
|
+
// branch (assertCleanSharedWorktree returns 'other-dispatch-active' before
|
|
1031
|
+
// invoking this). Renaming an active worktree out from under a running
|
|
1032
|
+
// agent would be catastrophic.
|
|
1033
|
+
async function _quarantineDirtyWorktree(rootDir, worktreePath, branchName, gitOpts, diag = {}) {
|
|
1034
|
+
const ts = Date.now();
|
|
1035
|
+
const quarantinedPath = `${worktreePath}-quarantine-${ts}`;
|
|
1036
|
+
|
|
1037
|
+
// Capture HEAD sha BEFORE renaming so we can back up the local branch ref.
|
|
1038
|
+
let headSha = '';
|
|
1039
|
+
try {
|
|
1040
|
+
const r = await execAsync('git rev-parse HEAD', { ...gitOpts, cwd: worktreePath, timeout: 10000 });
|
|
1041
|
+
headSha = (r || '').toString().trim();
|
|
1042
|
+
} catch (e) {
|
|
1043
|
+
log('warn', `_quarantineDirtyWorktree: rev-parse HEAD failed for ${worktreePath}: ${e.message} — backup ref will be skipped`);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Rename the worktree dir. Once this succeeds the worktree is functionally
|
|
1047
|
+
// quarantined; subsequent failures only affect ref bookkeeping.
|
|
1048
|
+
fs.renameSync(worktreePath, quarantinedPath);
|
|
1049
|
+
|
|
1050
|
+
// Prune git's stale worktree metadata so the next `git worktree add` for
|
|
1051
|
+
// the same branch isn't blocked by "branch is already used by worktree".
|
|
1052
|
+
try {
|
|
1053
|
+
await shared.shellSafeGit(['worktree', 'prune'], { ...gitOpts, cwd: rootDir, timeout: 15000 });
|
|
1054
|
+
} catch (e) {
|
|
1055
|
+
log('warn', `_quarantineDirtyWorktree: worktree prune after rename: ${e.message}`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Backup the local branch HEAD to refs/minions/quarantine/<sanitized>/<ts>
|
|
1059
|
+
// and reset refs/heads/<branch> → origin/<branch> so the next checkout
|
|
1060
|
+
// starts on a clean origin tip rather than recreating the divergent state.
|
|
1061
|
+
// Both ref ops are best-effort: if the local ref machinery is in a weird
|
|
1062
|
+
// state we still want the worktree quarantine to stick.
|
|
1063
|
+
const sanitizedRefSegment = sanitizeBranch(branchName).replace(/\//g, '-');
|
|
1064
|
+
const backupRef = `refs/minions/quarantine/${sanitizedRefSegment}/${ts}`;
|
|
1065
|
+
let backupRefCreated = false;
|
|
1066
|
+
if (headSha) {
|
|
1067
|
+
try {
|
|
1068
|
+
await shared.shellSafeGit(['update-ref', backupRef, headSha], { ...gitOpts, cwd: rootDir, timeout: 10000 });
|
|
1069
|
+
backupRefCreated = true;
|
|
1070
|
+
} catch (e) {
|
|
1071
|
+
log('warn', `_quarantineDirtyWorktree: backup-ref ${backupRef} failed: ${e.message}`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
try {
|
|
1075
|
+
await shared.shellSafeGit(['fetch', 'origin', branchName], { ...gitOpts, cwd: rootDir, timeout: 30000 });
|
|
1076
|
+
} catch (e) {
|
|
1077
|
+
log('warn', `_quarantineDirtyWorktree: fetch origin/${branchName} after rename: ${e.message}`);
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
await shared.shellSafeGit(
|
|
1081
|
+
['update-ref', `refs/heads/${branchName}`, `refs/remotes/origin/${branchName}`],
|
|
1082
|
+
{ ...gitOpts, cwd: rootDir, timeout: 10000 },
|
|
1083
|
+
);
|
|
1084
|
+
log('info', `_quarantineDirtyWorktree: reset refs/heads/${branchName} → refs/remotes/origin/${branchName}`);
|
|
1085
|
+
} catch (e) {
|
|
1086
|
+
log('warn', `_quarantineDirtyWorktree: reset refs/heads/${branchName} → origin/${branchName} failed: ${e.message}`);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Notes/inbox diagnostic so a human (and future agents) can see why the
|
|
1090
|
+
// worktree was moved and how to recover any unpushed work.
|
|
1091
|
+
const dirtyFiles = Array.isArray(diag.dirtyFiles) ? diag.dirtyFiles : [];
|
|
1092
|
+
const dirtyPreview = dirtyFiles.slice(0, 50).map(f => `- ${f}`).join('\n');
|
|
1093
|
+
const dirtyTrailer = dirtyFiles.length > 50 ? `\n- ...and ${dirtyFiles.length - 50} more` : '';
|
|
1094
|
+
const inboxBody = [
|
|
1095
|
+
'# Engine quarantined a dirty reused worktree (#2996)',
|
|
1096
|
+
'',
|
|
1097
|
+
`- Dispatch: ${diag.dispatchId || '<unknown>'}`,
|
|
1098
|
+
`- Branch: ${branchName}`,
|
|
1099
|
+
`- Original worktree: ${worktreePath}`,
|
|
1100
|
+
`- Quarantined to: ${quarantinedPath}`,
|
|
1101
|
+
`- Reason: ${diag.reason || 'unsafe'}`,
|
|
1102
|
+
`- Local commits ahead of origin: ${diag.ahead || 0}`,
|
|
1103
|
+
`- Local commits behind origin: ${diag.behind || 0}`,
|
|
1104
|
+
`- Dirty files (${dirtyFiles.length}):`,
|
|
1105
|
+
dirtyPreview || '- (none reported)',
|
|
1106
|
+
dirtyTrailer,
|
|
1107
|
+
'',
|
|
1108
|
+
'## Recovery',
|
|
1109
|
+
'',
|
|
1110
|
+
backupRefCreated
|
|
1111
|
+
? `The local branch HEAD was backed up to \`${backupRef}\` (${headSha.slice(0, 12)}) and \`refs/heads/${branchName}\` was reset to \`refs/remotes/origin/${branchName}\`.`
|
|
1112
|
+
: `Backup ref was NOT created (HEAD sha unavailable). The local branch ref was still reset to \`refs/remotes/origin/${branchName}\` if possible. Inspect the quarantined directory directly to recover unpushed work.`,
|
|
1113
|
+
'',
|
|
1114
|
+
`1. Inspect the quarantined diff: \`git -C "${rootDir}" diff refs/remotes/origin/${branchName}${backupRefCreated ? ` ${backupRef}` : ''} -- .\``,
|
|
1115
|
+
`2. Cherry-pick the commits you want from \`${backupRefCreated ? backupRef : `<inspect ${quarantinedPath}>`}\` onto a fresh ${branchName} worktree.`,
|
|
1116
|
+
`3. Inspect uncommitted edits inside the quarantined dir directly: \`${quarantinedPath}\`.`,
|
|
1117
|
+
`4. Delete the quarantined dir when done: \`Remove-Item -Recurse -Force "${quarantinedPath}"\` (Windows) or \`rm -rf "${quarantinedPath}"\` (POSIX).`,
|
|
1118
|
+
backupRefCreated ? `5. Delete the backup ref when done: \`git -C "${rootDir}" update-ref -d ${backupRef}\`.` : '',
|
|
1119
|
+
].filter(Boolean).join('\n');
|
|
1120
|
+
try {
|
|
1121
|
+
shared.writeToInbox('engine', `worktree-quarantine-${diag.dispatchId || sanitizedRefSegment}`, inboxBody);
|
|
1122
|
+
} catch (e) {
|
|
1123
|
+
log('warn', `_quarantineDirtyWorktree: writeToInbox failed: ${e.message}`);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
log('warn', `Quarantined dirty worktree ${worktreePath} → ${quarantinedPath} (branch ${branchName}, ${diag.ahead || 0} ahead, ${diag.behind || 0} behind, ${dirtyFiles.length} dirty files)`);
|
|
1127
|
+
return { quarantinedPath, backupRef: backupRefCreated ? backupRef : null };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
953
1130
|
async function recoverPartialWorktree(rootDir, worktreePath, branchName, gitOpts) {
|
|
954
1131
|
if (!branchName) return false;
|
|
955
1132
|
const existingWt = await findExistingWorktree(rootDir, branchName);
|
|
@@ -1214,6 +1391,11 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1214
1391
|
|
|
1215
1392
|
if (branchName) {
|
|
1216
1393
|
updateAgentStatus(id, AGENT_STATUS.WORKTREE_SETUP, `Setting up worktree for branch ${branchName}`);
|
|
1394
|
+
// W-mpwy3mp5 (#2996): track whether we ended up reusing an existing
|
|
1395
|
+
// worktree (rather than creating a fresh one) so the post-setup dirty/
|
|
1396
|
+
// divergent gate knows to quarantine when a contaminated reused tree
|
|
1397
|
+
// would otherwise loop fix-dispatch agents forever.
|
|
1398
|
+
let worktreeReused = false;
|
|
1217
1399
|
const wtDirName = shared.buildWorktreeDirName({
|
|
1218
1400
|
dispatchId: id,
|
|
1219
1401
|
projectName: project.name || 'default',
|
|
@@ -1244,6 +1426,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1244
1426
|
throw assertErr;
|
|
1245
1427
|
}
|
|
1246
1428
|
worktreePath = existingWt;
|
|
1429
|
+
worktreeReused = true;
|
|
1247
1430
|
log('info', `Reusing existing worktree for ${branchName}: ${existingWt}`);
|
|
1248
1431
|
// Probe origin first — locally-created branches that were never pushed
|
|
1249
1432
|
// (orphan/timeout retry before first push) would otherwise emit a
|
|
@@ -1363,6 +1546,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1363
1546
|
}
|
|
1364
1547
|
log('info', `Shared branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
1365
1548
|
worktreePath = existingWtPath;
|
|
1549
|
+
worktreeReused = true;
|
|
1366
1550
|
} else {
|
|
1367
1551
|
// Branch is registered but path is missing on disk (#2454).
|
|
1368
1552
|
// git keeps the entry — sometimes locked: initializing — and
|
|
@@ -1437,6 +1621,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1437
1621
|
}
|
|
1438
1622
|
log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
1439
1623
|
worktreePath = existingWtPath;
|
|
1624
|
+
worktreeReused = true;
|
|
1440
1625
|
} else {
|
|
1441
1626
|
const pruned = await pruneStaleWorktreeForBranch(rootDir, branchName, _gitOpts);
|
|
1442
1627
|
if (pruned > 0) {
|
|
@@ -1524,6 +1709,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1524
1709
|
}
|
|
1525
1710
|
log('info', `Branch ${branchName} already checked out at ${existingWtPath} — reusing`);
|
|
1526
1711
|
worktreePath = existingWtPath;
|
|
1712
|
+
worktreeReused = true;
|
|
1527
1713
|
} else if (existingWtPath && !fs.existsSync(existingWtPath)) {
|
|
1528
1714
|
log('warn', `Branch ${branchName} tracked in missing dir ${existingWtPath} — pruning and recreating`);
|
|
1529
1715
|
try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
@@ -1592,6 +1778,70 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1592
1778
|
}
|
|
1593
1779
|
}
|
|
1594
1780
|
|
|
1781
|
+
// PR-targeted reused-worktree preflight (W-mpwy3mp5, #2996): when the
|
|
1782
|
+
// engine ADOPTS an existing worktree for a non-shared-branch dispatch
|
|
1783
|
+
// (typically a fix/build-fix or review on a PR branch), the worktree may
|
|
1784
|
+
// be in any state — uncommitted edits from the previous agent, local
|
|
1785
|
+
// commits never pushed, or even a manually-created tree (e.g.
|
|
1786
|
+
// `copilot-pr<N>` next to the user's home dir). Without this gate, the
|
|
1787
|
+
// playbook's "git status + bail out" guard fires AFTER dispatch and we
|
|
1788
|
+
// burn an agent run on a no-op; worse, dispatch.js re-discovers the same
|
|
1789
|
+
// PR-targeted item and loops the same dirty tree until a human notices.
|
|
1790
|
+
//
|
|
1791
|
+
// assertCleanSharedWorktree({ quarantineOnUnsafe: true }) self-heals
|
|
1792
|
+
// filesystem-only dirt, but for has-unpushed-commits (the actual #2996
|
|
1793
|
+
// case) it QUARANTINES the worktree — rename → prune → reset
|
|
1794
|
+
// refs/heads/<branch> to origin/<branch> + back up the divergent HEAD
|
|
1795
|
+
// under refs/minions/quarantine/<branch>/<ts> — so the next dispatch
|
|
1796
|
+
// starts fresh. We mark the failure non-retryable (WORKTREE_DIVERGENT /
|
|
1797
|
+
// WORKTREE_DIRTY) which routes through dispatch.js's neverRetry set;
|
|
1798
|
+
// the 15-minute cooldown.js dedupe window then naturally gates the
|
|
1799
|
+
// fresh dispatch.
|
|
1800
|
+
//
|
|
1801
|
+
// Why no `!meta?.useExistingBranch` guard: discoverFromWorkItems sets
|
|
1802
|
+
// `useExistingBranch: true` for EVERY isPrTargeted WI (engine.js:5898),
|
|
1803
|
+
// which is the exact category named in issue #2996 — fix/review/test
|
|
1804
|
+
// dispatches against a PR branch. The previous revision of this gate
|
|
1805
|
+
// included `&& !meta?.useExistingBranch` which excluded the entire bug
|
|
1806
|
+
// class. The `branchStrategy !== 'shared-branch'` check above is
|
|
1807
|
+
// sufficient to protect the existing shared-branch retryable
|
|
1808
|
+
// DIRTY_WORKTREE block (shared-branch with featureBranch is the OTHER
|
|
1809
|
+
// useExistingBranch:true case, and it's already filtered here).
|
|
1810
|
+
if (
|
|
1811
|
+
worktreeReused
|
|
1812
|
+
&& worktreePath
|
|
1813
|
+
&& fs.existsSync(worktreePath)
|
|
1814
|
+
&& branchName
|
|
1815
|
+
&& meta?.branchStrategy !== 'shared-branch'
|
|
1816
|
+
) {
|
|
1817
|
+
_phaseT.dirtyReusedCheckStart = Date.now();
|
|
1818
|
+
const cleanResult = await assertCleanSharedWorktree(
|
|
1819
|
+
rootDir, worktreePath, branchName, id, _gitOpts,
|
|
1820
|
+
{ quarantineOnUnsafe: true },
|
|
1821
|
+
);
|
|
1822
|
+
_phaseT.dirtyReusedCheckEnd = Date.now();
|
|
1823
|
+
if (!cleanResult.clean) {
|
|
1824
|
+
const previewFiles = (cleanResult.dirtyFiles || []).slice(0, 5).join(', ');
|
|
1825
|
+
const isDivergent = (cleanResult.ahead || 0) > 0 || cleanResult.reason === 'no-upstream';
|
|
1826
|
+
const failureClassValue = isDivergent ? FAILURE_CLASS.WORKTREE_DIVERGENT : FAILURE_CLASS.WORKTREE_DIRTY;
|
|
1827
|
+
const failureClassName = isDivergent ? 'WORKTREE_DIVERGENT' : 'WORKTREE_DIRTY';
|
|
1828
|
+
const reasonMsg = cleanResult.quarantined
|
|
1829
|
+
? `${failureClassName}: reused worktree at ${worktreePath} was dirty/divergent (${cleanResult.reason}; ${cleanResult.ahead || 0} ahead, ${cleanResult.behind || 0} behind, ${cleanResult.dirtyFiles?.length || 0} dirty file(s)${previewFiles ? ': ' + previewFiles : ''}) — quarantined to ${cleanResult.quarantinedPath}. Next dispatch will start fresh.`
|
|
1830
|
+
: `${failureClassName}: reused worktree at ${worktreePath} is dirty/divergent (${cleanResult.reason}; ${cleanResult.ahead || 0} ahead, ${cleanResult.behind || 0} behind, ${cleanResult.dirtyFiles?.length || 0} file(s)${previewFiles ? ': ' + previewFiles : ''}). Quarantine ${cleanResult.quarantineError ? 'errored: ' + cleanResult.quarantineError : 'was not attempted (' + cleanResult.reason + ').'}`;
|
|
1831
|
+
log('error', reasonMsg);
|
|
1832
|
+
_cleanupPromptFiles();
|
|
1833
|
+
completeDispatch(
|
|
1834
|
+
id,
|
|
1835
|
+
DISPATCH_RESULT.ERROR,
|
|
1836
|
+
reasonMsg.slice(0, 500),
|
|
1837
|
+
`Engine preflight refused to dispatch into a dirty/divergent reused worktree (#2996). Reason: ${cleanResult.reason}.${cleanResult.quarantined ? ` Worktree quarantined to ${cleanResult.quarantinedPath}; backup ref ${cleanResult.backupRef || '(skipped)'}. See notes/inbox/ for recovery instructions.` : ''}`,
|
|
1838
|
+
{ agentRetryable: false, failureClass: failureClassValue },
|
|
1839
|
+
);
|
|
1840
|
+
cleanupTempAgent(agentId);
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1595
1845
|
// Merge dependency PR branches into worktree (applies to both reused and new worktrees)
|
|
1596
1846
|
if (worktreePath && fs.existsSync(worktreePath)) {
|
|
1597
1847
|
cwd = worktreePath;
|
|
@@ -4609,6 +4859,33 @@ async function discoverFromPrs(config, project) {
|
|
|
4609
4859
|
const currentHeadSha = String(pr.headSha || pr._adoSourceCommit || pr._adoHeadCommit || '').trim();
|
|
4610
4860
|
const lastHumanDispatch = pr._lastDispatchByCause?.[shared.PR_FIX_CAUSE.HUMAN_FEEDBACK];
|
|
4611
4861
|
const currentCommentId = String(pr.humanFeedback?.lastProcessedCommentId || '');
|
|
4862
|
+
// Issue #2994: the bare commentId is per-thread on ADO (each discussion's
|
|
4863
|
+
// first reply is id=1), so two threads' opening comments collide on the
|
|
4864
|
+
// SAME head SHA and false-suppress legitimate new-thread dispatches.
|
|
4865
|
+
// Live repro: ADO PR `office-bohemia#5284819`, discussion 66487840
|
|
4866
|
+
// noop'd at comment id:1 on head a425ab05 → discussion 66508411 (a
|
|
4867
|
+
// NEW thread, also at comment id:1, same head) suppressed for a full
|
|
4868
|
+
// tick cycle until the operator inspected the dispatch log.
|
|
4869
|
+
//
|
|
4870
|
+
// Fix: compare the thread-aware key (`${threadId}:${commentId}` on ADO,
|
|
4871
|
+
// `${type}:${commentId}` on GH for issue-vs-review namespacing) when
|
|
4872
|
+
// BOTH sides have it; fall back to the legacy bare-id compare when
|
|
4873
|
+
// BOTH lack it (covers PRs whose last dispatch record predates this
|
|
4874
|
+
// fix). MIXED case = release: dispatch record predates the fix while
|
|
4875
|
+
// the current poller has the key → don't risk a cross-thread false
|
|
4876
|
+
// positive; let one extra dispatch through to refresh the record with
|
|
4877
|
+
// the key on its next noop.
|
|
4878
|
+
const currentCommentKey = String(pr.humanFeedback?.lastProcessedCommentKey || '');
|
|
4879
|
+
const recordedCommentId = String(lastHumanDispatch?.lastProcessedCommentId || '');
|
|
4880
|
+
const recordedCommentKey = String(lastHumanDispatch?.lastProcessedCommentKey || '');
|
|
4881
|
+
let commentMatch = false;
|
|
4882
|
+
if (currentCommentKey && recordedCommentKey) {
|
|
4883
|
+
commentMatch = currentCommentKey === recordedCommentKey;
|
|
4884
|
+
} else if (!currentCommentKey && !recordedCommentKey) {
|
|
4885
|
+
commentMatch = !!(currentCommentId && recordedCommentId && currentCommentId === recordedCommentId);
|
|
4886
|
+
}
|
|
4887
|
+
// Else: mixed (one side has the key, other doesn't) → release the
|
|
4888
|
+
// suppression so the next dispatch refreshes the record format.
|
|
4612
4889
|
// Issue #2632: this same-head/same-comment guard MUST be cause-local. A
|
|
4613
4890
|
// previous `continue` here aborted the whole PR iteration and starved
|
|
4614
4891
|
// the build-failure / re-review / conflict-fix evaluation blocks below
|
|
@@ -4641,11 +4918,10 @@ async function discoverFromPrs(config, project) {
|
|
|
4641
4918
|
&& lastHumanDispatch.headSha
|
|
4642
4919
|
&& currentHeadSha
|
|
4643
4920
|
&& lastHumanDispatch.headSha === currentHeadSha
|
|
4644
|
-
&&
|
|
4645
|
-
&& currentCommentId
|
|
4646
|
-
&& lastHumanDispatch.lastProcessedCommentId === currentCommentId);
|
|
4921
|
+
&& commentMatch);
|
|
4647
4922
|
if (skipHumanFeedback) {
|
|
4648
|
-
|
|
4923
|
+
const commentLabel = currentCommentKey || currentCommentId;
|
|
4924
|
+
log('info', `Skipping human-feedback fix for ${pr.id}: last human-feedback dispatch was noop on the same head ${currentHeadSha.slice(0, 8)} and same comment ${commentLabel.slice(0, 48)} (${(lastHumanDispatch.reason || '').slice(0, 120)})`);
|
|
4649
4925
|
}
|
|
4650
4926
|
if (!skipHumanFeedback) {
|
|
4651
4927
|
const key = humanFixKey;
|
|
@@ -7432,7 +7708,7 @@ module.exports = {
|
|
|
7432
7708
|
areDependenciesMet, // exported for testing (P-bf04-decompose-zero-children)
|
|
7433
7709
|
parseConflictFiles, pruneAncestorDeps, preflightMergeSimulation, // exported for testing
|
|
7434
7710
|
buildDepConflictFixItem, deriveConflictFixKey, // exported for testing (W-mpcwojgr000a0244)
|
|
7435
|
-
isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, // exported for testing
|
|
7711
|
+
isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
|
|
7436
7712
|
pruneStaleWorktreeForBranch, // exported for testing
|
|
7437
7713
|
findExistingWorktree, // exported for testing
|
|
7438
7714
|
probeBranchOnRemote, // exported for testing (W-mphnm6a1000281b8)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2103",
|
|
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"
|