@yemi33/minions 0.1.2147 → 0.1.2149
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/bin/minions.js +8 -0
- package/dashboard/js/settings.js +9 -0
- package/dashboard/slim/body.html +2 -1
- package/dashboard/slim/js/command-send.js +3 -2
- package/dashboard/slim/js/history.js +9 -2
- package/dashboard/slim/styles.css +88 -10
- package/dashboard.js +5 -0
- package/docs/README.md +1 -0
- package/docs/completion-reports.md +44 -12
- package/docs/project-skills.md +133 -0
- package/engine/ado.js +19 -0
- package/engine/discover-project-skills.js +490 -0
- package/engine/discover-review-skills.js +15 -269
- package/engine/dispatch.js +2 -0
- package/engine/github.js +14 -0
- package/engine/lifecycle.js +17 -0
- package/engine/playbook-intents.js +76 -0
- package/engine/playbook.js +59 -20
- package/engine/shared.js +288 -0
- package/engine/worktree-gc.js +241 -21
- package/engine.js +294 -32
- package/package.json +1 -1
- package/playbooks/fix.md +4 -0
- package/playbooks/implement-shared.md +4 -0
- package/playbooks/implement.md +4 -0
- package/playbooks/plan-to-prd.md +4 -0
- package/playbooks/plan.md +4 -0
- package/playbooks/review.md +3 -3
- package/bin/minions.js.rej +0 -16
- package/dashboard/slim/body.html.rej +0 -11
- package/dashboard/slim/js/command-send.js.rej +0 -12
- package/dashboard/slim/js/history.js.rej +0 -26
- package/dashboard/slim/styles.css.rej +0 -124
- package/docs/README.md.rej +0 -9
- package/docs/onboarding.md.rej +0 -10
package/engine/shared.js
CHANGED
|
@@ -2352,6 +2352,30 @@ const ENGINE_DEFAULTS = {
|
|
|
2352
2352
|
autoConsolidateMemory: false, // opt-in: periodically spawn engine/kb-sweep-runner.js from the tick loop (4h cadence). Inbox→notes consolidation already runs every tick via consolidateInbox; this flag only controls the KB sweep.
|
|
2353
2353
|
prNoOpFixPauseAttempts: 2, // pause one PR automation cause after repeated no-op fixes for unchanged evidence
|
|
2354
2354
|
quarantineAutoRecoveryMax: 2, // #2996 follow-up: cap on auto-flipping WORKTREE_DIRTY/WORKTREE_DIVERGENT failures back to pending (the quarantine is self-healing so the next dispatch starts clean; the cap prevents infinite loops if quarantine itself keeps failing).
|
|
2355
|
+
// W-mq5n1zx5 — Layer 1a/2b: harden the quarantine rename path against
|
|
2356
|
+
// Windows EBUSY/EPERM races where a lingering `git.exe` descendant (leaked
|
|
2357
|
+
// by a status-probe timeout) still holds packfile handles when we try to
|
|
2358
|
+
// rename the worktree dir out of the way. With these on, the engine
|
|
2359
|
+
// retries the rename with jittered backoff (worst case ≈ baseMs * 2^N + N
|
|
2360
|
+
// jitter), then — if everything still fails — asks git to tear down its
|
|
2361
|
+
// own worktree record via `git worktree remove --force` so the next
|
|
2362
|
+
// dispatch isn't blocked by a half-renamed tree. Each knob is overridable
|
|
2363
|
+
// via `config.engine.<name>`.
|
|
2364
|
+
quarantineRenameRetryAttempts: 6,
|
|
2365
|
+
quarantineRenameRetryBaseMs: 250,
|
|
2366
|
+
quarantineForceRemoveFallback: true,
|
|
2367
|
+
// W-mq5n1zx5 — Layer 1b/2a: pre-empt the EBUSY race itself. (1b)
|
|
2368
|
+
// `--no-optional-locks` tells git to skip the .git/index.lock acquire
|
|
2369
|
+
// around the untracked-cache refresh in status; typical probe duration
|
|
2370
|
+
// under AV drops from 6–12s to <500ms, removing the timeout that
|
|
2371
|
+
// creates the leaked child in the first place. (2a) On Windows only,
|
|
2372
|
+
// before attempting the rename we walk live git.exe processes whose
|
|
2373
|
+
// command line points at the worktree path and Stop-Process them, so
|
|
2374
|
+
// a still-alive descendant from a previous status probe doesn't pin
|
|
2375
|
+
// packfile handles. Both gates are belt-and-suspenders — most
|
|
2376
|
+
// incidents are closed by (1b) alone — but both are cheap and idempotent.
|
|
2377
|
+
statusProbeUseNoOptionalLocks: true,
|
|
2378
|
+
statusProbeKillDescendantsWin32: true,
|
|
2355
2379
|
completionReportRetentionDays: 90, // retain completion report sidecars beyond capped dispatch history
|
|
2356
2380
|
completionReportMaxFiles: 5000, // hard cap for completion report sidecars during cleanup
|
|
2357
2381
|
// P-bfa2c-cors-wildcard: extra Origins permitted to receive an
|
|
@@ -2538,6 +2562,17 @@ const ENGINE_DEFAULTS = {
|
|
|
2538
2562
|
worktreeStuckThreshold: 10, // consecutive removal failures before escalation
|
|
2539
2563
|
worktreeStuckSuppressMs: 60 * 60 * 1000, // 60min — suppress per-tick warn after escalation
|
|
2540
2564
|
worktreeStuckSlowRetryMs: 30 * 60 * 1000, // 30min — slow-cadence retry window after escalation
|
|
2565
|
+
// W-mq6f2fe0000557fa — orphan-worktree GC: identify the OS process holding
|
|
2566
|
+
// a stuck worktree's cwd and, behind this opt-in flag, kill it. Default OFF
|
|
2567
|
+
// because killing a foreign process is destructive; flipping on rescues the
|
|
2568
|
+
// engine from the W-mq1k8z6o003acd89 / PID 16828 lock-spin pattern automatically.
|
|
2569
|
+
// Auto-reap only fires for processes whose cmdline matches `spawn-agent.js`
|
|
2570
|
+
// AND whose age exceeds `agentTimeout * 2` AND that have no live dispatch row
|
|
2571
|
+
// — i.e. unambiguous orphan agents the engine forgot about. The holder scan
|
|
2572
|
+
// itself ALWAYS runs on every orphan-sweep escalation (the flag only gates
|
|
2573
|
+
// the kill); holder details are appended to the inbox note either way.
|
|
2574
|
+
autoReapOrphanWorktreeHolders: false,
|
|
2575
|
+
orphanHolderScanTimeoutMs: 5000, // 5s ceiling for the cross-platform holder scan (PowerShell / /proc walk / lsof)
|
|
2541
2576
|
ccMaxTurns: 50, // max tool-use turns per CC/doc-chat call before CLI stops (per response, not per session)
|
|
2542
2577
|
ccTurnTimeoutMs: 300000, // W-mpmwxni2000c25c7-b/-d: 5min per-turn no-progress watchdog. The window resets on every liveness signal — token chunk, tool-call notification, tool-update — so an actively-streaming CC/doc-chat turn (long shell command, deep search, sub-agent loop) survives indefinitely up to the outer CC_CALL_TIMEOUT_MS (~1h) ceiling. Only true silence past this window with no progress fires the cancel: the in-flight LLM call is aborted and the handler surfaces `{code:'cc-turn-timeout', retryable:true}` via the typed error envelope so the UI can stop the spinner and offer Retry. Clamped to [10000, 3600000] in the settings POST handler. Independent of CC_CALL_TIMEOUT_MS. Non-streaming doc-chat is the lone wall-clock exception (no progress hooks); see _raceCcDocChatTimeout in dashboard.js for the dual factory/promise shape.
|
|
2543
2578
|
docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity (LRU; sessions are non-expiring otherwise)
|
|
@@ -3682,6 +3717,7 @@ const FAILURE_CLASS = {
|
|
|
3682
3717
|
WORKTREE_PREFLIGHT: 'worktree-preflight', // Pre-spawn worktree validation rejected (nested-in-project, drive-root collapse) — never retryable
|
|
3683
3718
|
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.
|
|
3684
3719
|
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.
|
|
3720
|
+
WORKTREE_QUARANTINE_ENV_BLOCKED: 'worktree-quarantine-env-blocked', // W-mq5n1zx5: quarantine rename was attempted but the OS refused to release the worktree dir (Windows EBUSY/EPERM/EACCES from a lingering git.exe descendant). Engine retried with jittered backoff and (when enabled) `git worktree remove --force`; if all paths failed we surface this dedicated class so the WI auto-recovery loop can re-queue WITHOUT bumping the per-agent retry counter (the failure is environmental, not the agent's fault).
|
|
3685
3721
|
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.
|
|
3686
3722
|
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
|
|
3687
3723
|
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
|
|
@@ -6239,6 +6275,189 @@ function listAllProcesses() {
|
|
|
6239
6275
|
return process.platform === 'win32' ? _winListProcesses() : _unixListProcesses();
|
|
6240
6276
|
}
|
|
6241
6277
|
|
|
6278
|
+
// W-mq6f2fe0000557fa — orphan-worktree GC: identify OS processes whose cwd
|
|
6279
|
+
// is at or inside `dir`. Cross-platform with a fail-open contract: on any
|
|
6280
|
+
// error or timeout we return [] (the caller continues with its existing
|
|
6281
|
+
// escalation path and an unenriched note).
|
|
6282
|
+
//
|
|
6283
|
+
// Return shape: [{ pid, cmdline, startedAt, ageMs }, ...]
|
|
6284
|
+
// - pid: numeric OS pid
|
|
6285
|
+
// - cmdline: full command line as a single string (best-effort; possibly truncated)
|
|
6286
|
+
// - startedAt: ms-since-epoch process start time (0 when unknown)
|
|
6287
|
+
// - ageMs: Date.now() - startedAt (0 when startedAt is 0)
|
|
6288
|
+
//
|
|
6289
|
+
// Platform notes:
|
|
6290
|
+
// - Windows: PowerShell `Get-CimInstance Win32_Process` filtered by the
|
|
6291
|
+
// worktree basename appearing in CommandLine. (Win32_Process does not
|
|
6292
|
+
// expose the working directory; the basename match is the most-reliable
|
|
6293
|
+
// heuristic — every spawn-agent invocation embeds the dispatch id, which
|
|
6294
|
+
// is the worktree basename, in its argv.)
|
|
6295
|
+
// - Linux: walk `/proc/*/cwd` and keep PIDs whose readlinkSync resolves
|
|
6296
|
+
// under `dir`. Cmdline read from `/proc/<pid>/cmdline` (NUL-separated).
|
|
6297
|
+
// Start time derived from stat() mtime of the cwd entry as a proxy.
|
|
6298
|
+
// - macOS: `lsof -F p -d cwd` filtered by directory, then `ps -p <pid>` for
|
|
6299
|
+
// cmdline + start time.
|
|
6300
|
+
function findProcessesWithCwdInside(dir, opts = {}) {
|
|
6301
|
+
if (!dir || typeof dir !== 'string') return [];
|
|
6302
|
+
let resolved;
|
|
6303
|
+
try { resolved = path.resolve(dir); } catch { return []; }
|
|
6304
|
+
if (!resolved) return [];
|
|
6305
|
+
const timeoutMs = Number(opts.timeoutMs) > 0
|
|
6306
|
+
? Number(opts.timeoutMs)
|
|
6307
|
+
: (ENGINE_DEFAULTS.orphanHolderScanTimeoutMs || 5000);
|
|
6308
|
+
const now = Date.now();
|
|
6309
|
+
|
|
6310
|
+
try {
|
|
6311
|
+
if (process.platform === 'win32') {
|
|
6312
|
+
return _findWindowsProcessesWithCwdInside(resolved, timeoutMs, now);
|
|
6313
|
+
}
|
|
6314
|
+
if (process.platform === 'linux') {
|
|
6315
|
+
return _findLinuxProcessesWithCwdInside(resolved, timeoutMs, now);
|
|
6316
|
+
}
|
|
6317
|
+
return _findMacProcessesWithCwdInside(resolved, timeoutMs, now);
|
|
6318
|
+
} catch { return []; }
|
|
6319
|
+
}
|
|
6320
|
+
|
|
6321
|
+
function _findWindowsProcessesWithCwdInside(resolved, timeoutMs, now) {
|
|
6322
|
+
// Win32_Process exposes CommandLine but not the cwd. Match by basename of
|
|
6323
|
+
// the worktree dir (the dispatch id like W-mq1k8z6o003acd89) appearing in
|
|
6324
|
+
// the cmdline — every spawn-agent prompt-file path embeds it.
|
|
6325
|
+
const basename = path.basename(resolved);
|
|
6326
|
+
if (!basename || basename.length < 3) return [];
|
|
6327
|
+
// PowerShell-quote: outer escape ' as ''
|
|
6328
|
+
const psBasename = basename.replace(/'/g, "''");
|
|
6329
|
+
const psResolved = resolved.replace(/'/g, "''");
|
|
6330
|
+
// Use -like with wildcards on BOTH basename and full path so a process
|
|
6331
|
+
// whose cmdline carries the worktree dir (but a different basename
|
|
6332
|
+
// anywhere in argv) also matches.
|
|
6333
|
+
const script = `Get-CimInstance Win32_Process | Where-Object { ($_.CommandLine -like '*${psBasename}*') -or ($_.CommandLine -like '*${psResolved}*') } | ForEach-Object { [PSCustomObject]@{ pid = $_.ProcessId; cmdline = $_.CommandLine; startedAt = if ($_.CreationDate) { [int64](([datetimeoffset]$_.CreationDate).ToUnixTimeMilliseconds()) } else { 0 } } } | ConvertTo-Json -Compress -Depth 2`;
|
|
6334
|
+
let raw;
|
|
6335
|
+
try {
|
|
6336
|
+
raw = _execSync(
|
|
6337
|
+
`powershell -NoProfile -NonInteractive -Command "${script}"`,
|
|
6338
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: timeoutMs, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }
|
|
6339
|
+
);
|
|
6340
|
+
} catch { return []; }
|
|
6341
|
+
if (!raw || !String(raw).trim()) return [];
|
|
6342
|
+
let parsed;
|
|
6343
|
+
try { parsed = JSON.parse(raw); }
|
|
6344
|
+
catch { return []; }
|
|
6345
|
+
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
|
6346
|
+
const out = [];
|
|
6347
|
+
for (const r of arr) {
|
|
6348
|
+
if (!r) continue;
|
|
6349
|
+
const pid = Number(r.pid);
|
|
6350
|
+
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
6351
|
+
const startedAt = Number(r.startedAt) || 0;
|
|
6352
|
+
out.push({
|
|
6353
|
+
pid,
|
|
6354
|
+
cmdline: r.cmdline ? String(r.cmdline) : '',
|
|
6355
|
+
startedAt,
|
|
6356
|
+
ageMs: startedAt > 0 ? Math.max(0, now - startedAt) : 0,
|
|
6357
|
+
});
|
|
6358
|
+
}
|
|
6359
|
+
return out;
|
|
6360
|
+
}
|
|
6361
|
+
|
|
6362
|
+
function _findLinuxProcessesWithCwdInside(resolved, _timeoutMs, now) {
|
|
6363
|
+
// /proc walk: scan numeric entries, readlink cwd, keep matches.
|
|
6364
|
+
let entries;
|
|
6365
|
+
try { entries = fs.readdirSync('/proc'); }
|
|
6366
|
+
catch { return []; }
|
|
6367
|
+
const prefix = resolved + path.sep;
|
|
6368
|
+
const out = [];
|
|
6369
|
+
for (const e of entries) {
|
|
6370
|
+
if (!/^\d+$/.test(e)) continue;
|
|
6371
|
+
const pid = Number(e);
|
|
6372
|
+
let cwd;
|
|
6373
|
+
try { cwd = fs.readlinkSync(`/proc/${pid}/cwd`); }
|
|
6374
|
+
catch { continue; }
|
|
6375
|
+
if (!cwd) continue;
|
|
6376
|
+
if (cwd !== resolved && !cwd.startsWith(prefix)) continue;
|
|
6377
|
+
let cmdline = '';
|
|
6378
|
+
try {
|
|
6379
|
+
const buf = fs.readFileSync(`/proc/${pid}/cmdline`);
|
|
6380
|
+
cmdline = buf.toString('utf8').replace(/\0/g, ' ').trim();
|
|
6381
|
+
} catch { /* cmdline read can fail on race */ }
|
|
6382
|
+
let startedAt = 0;
|
|
6383
|
+
try {
|
|
6384
|
+
const st = fs.statSync(`/proc/${pid}`);
|
|
6385
|
+
startedAt = st && st.ctimeMs ? Math.floor(st.ctimeMs) : 0;
|
|
6386
|
+
} catch { /* stat can fail on race */ }
|
|
6387
|
+
out.push({
|
|
6388
|
+
pid,
|
|
6389
|
+
cmdline,
|
|
6390
|
+
startedAt,
|
|
6391
|
+
ageMs: startedAt > 0 ? Math.max(0, now - startedAt) : 0,
|
|
6392
|
+
});
|
|
6393
|
+
}
|
|
6394
|
+
return out;
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6397
|
+
function _findMacProcessesWithCwdInside(resolved, timeoutMs, now) {
|
|
6398
|
+
// `lsof -a -d cwd -F pn` emits PID then cwd path. Filter for dir matches,
|
|
6399
|
+
// then run a single `ps` pass to enrich cmdline + start time.
|
|
6400
|
+
let raw;
|
|
6401
|
+
try {
|
|
6402
|
+
raw = _execSync('lsof -a -d cwd -F pn 2>/dev/null', {
|
|
6403
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024,
|
|
6404
|
+
});
|
|
6405
|
+
} catch { return []; }
|
|
6406
|
+
if (!raw) return [];
|
|
6407
|
+
const prefix = resolved + path.sep;
|
|
6408
|
+
const pids = new Set();
|
|
6409
|
+
let currentPid = null;
|
|
6410
|
+
for (const line of String(raw).split(/\r?\n/)) {
|
|
6411
|
+
if (!line) continue;
|
|
6412
|
+
const tag = line[0];
|
|
6413
|
+
const val = line.slice(1);
|
|
6414
|
+
if (tag === 'p') {
|
|
6415
|
+
const n = Number(val);
|
|
6416
|
+
currentPid = Number.isInteger(n) && n > 0 ? n : null;
|
|
6417
|
+
} else if (tag === 'n' && currentPid != null) {
|
|
6418
|
+
if (val === resolved || val.startsWith(prefix)) pids.add(currentPid);
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
6421
|
+
if (pids.size === 0) return [];
|
|
6422
|
+
const out = [];
|
|
6423
|
+
for (const pid of pids) {
|
|
6424
|
+
let line;
|
|
6425
|
+
try {
|
|
6426
|
+
line = String(_execSync(`ps -p ${pid} -o lstart=,command=`, {
|
|
6427
|
+
stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000, encoding: 'utf8',
|
|
6428
|
+
}) || '').trim();
|
|
6429
|
+
} catch { line = ''; }
|
|
6430
|
+
let startedAt = 0;
|
|
6431
|
+
let cmdline = '';
|
|
6432
|
+
if (line) {
|
|
6433
|
+
// ps lstart= emits "Mon Jun 9 07:04:33 2026" (24 chars), then cmdline.
|
|
6434
|
+
const lstart = line.slice(0, 24).trim();
|
|
6435
|
+
cmdline = line.slice(24).trim();
|
|
6436
|
+
const parsed = Date.parse(lstart);
|
|
6437
|
+
if (!Number.isNaN(parsed)) startedAt = parsed;
|
|
6438
|
+
}
|
|
6439
|
+
out.push({
|
|
6440
|
+
pid,
|
|
6441
|
+
cmdline,
|
|
6442
|
+
startedAt,
|
|
6443
|
+
ageMs: startedAt > 0 ? Math.max(0, now - startedAt) : 0,
|
|
6444
|
+
});
|
|
6445
|
+
}
|
|
6446
|
+
return out;
|
|
6447
|
+
}
|
|
6448
|
+
|
|
6449
|
+
// W-mq6f2fe0000557fa — clear the per-path failure cooldown entry so the
|
|
6450
|
+
// post-holder-reap retry can attempt removeWorktree even though the path
|
|
6451
|
+
// has already failed >= 3 times. Without this, the cooldown silently
|
|
6452
|
+
// suppresses the retry and the orphan dir is left stuck.
|
|
6453
|
+
function clearWorktreeFailureCache(wtPath) {
|
|
6454
|
+
if (!wtPath) return false;
|
|
6455
|
+
try {
|
|
6456
|
+
const resolved = path.resolve(wtPath);
|
|
6457
|
+
return _removeWorktreeFailures.delete(resolved);
|
|
6458
|
+
} catch { return false; }
|
|
6459
|
+
}
|
|
6460
|
+
|
|
6242
6461
|
// Cross-check a single PID's command line for a Minions agent invocation
|
|
6243
6462
|
// (`claude` or `copilot`, including the `node spawn-agent.js --runtime <name>`
|
|
6244
6463
|
// wrapper and `gh copilot` fallback). Used by orphan/recycled-PID safety:
|
|
@@ -6573,6 +6792,61 @@ function _prHeadSha(pr) {
|
|
|
6573
6792
|
return String(pr?.headRefOid || pr?.headSha || pr?._adoSourceCommit || pr?._adoHeadCommit || '').trim();
|
|
6574
6793
|
}
|
|
6575
6794
|
|
|
6795
|
+
// Target-branch base SHA, normalized across hosts. GitHub PRs carry `baseSha`
|
|
6796
|
+
// (engine/github.js:746-749); ADO PRs carry `_adoTargetCommit`
|
|
6797
|
+
// (engine/ado.js:1168-1172, inline in pollPrStatus).
|
|
6798
|
+
function _prBaseSha(pr) {
|
|
6799
|
+
return String(pr?.baseSha || pr?._adoTargetCommit || '').trim();
|
|
6800
|
+
}
|
|
6801
|
+
|
|
6802
|
+
// Target ref name, normalized across hosts. GitHub PRs carry `baseRefName`
|
|
6803
|
+
// (e.g. "master"); ADO PRs carry `targetRefName` (e.g. "refs/heads/main").
|
|
6804
|
+
function _prTargetRefName(pr) {
|
|
6805
|
+
return String(pr?.baseRefName || pr?.targetRefName || '').trim();
|
|
6806
|
+
}
|
|
6807
|
+
|
|
6808
|
+
// #3079 — Composite key the engine uses to gate merge-conflict re-dispatch.
|
|
6809
|
+
// The same-head guard at engine.js:5676 previously compared only source head
|
|
6810
|
+
// SHA, missing the case where a PR is retargeted (e.g. parent branch → main)
|
|
6811
|
+
// without the source moving. Including the base SHA and target ref name makes
|
|
6812
|
+
// the guard target-aware so retargets naturally release the pause.
|
|
6813
|
+
function prMergeConflictGuardKey(pr) {
|
|
6814
|
+
return `${_prHeadSha(pr)}|${_prBaseSha(pr)}|${_prTargetRefName(pr)}`;
|
|
6815
|
+
}
|
|
6816
|
+
|
|
6817
|
+
// #3079 — Helper invoked by the ADO + GitHub poller metadata-apply paths.
|
|
6818
|
+
// When the PR's target ref changes (user retargeted via gh pr edit --base or
|
|
6819
|
+
// ADO PR edit), clear the stale MERGE_CONFLICT records so the next tick can
|
|
6820
|
+
// re-evaluate the conflict against the new target. Other causes
|
|
6821
|
+
// (BUILD_FAILURE, REVIEW_FEEDBACK, HUMAN_FEEDBACK) are untouched — they are
|
|
6822
|
+
// not target-sensitive. First-poll seeding (no prior target ref recorded)
|
|
6823
|
+
// is a no-op so we don't strip state immediately after an upsert.
|
|
6824
|
+
//
|
|
6825
|
+
// Returns true when state was actually mutated (callers may use this to set
|
|
6826
|
+
// the surrounding "updated" flag and trigger a persist).
|
|
6827
|
+
function resetMergeConflictStateOnRetarget(pr, newTargetRef) {
|
|
6828
|
+
if (!pr) return false;
|
|
6829
|
+
const next = String(newTargetRef || '').trim();
|
|
6830
|
+
if (!next) return false;
|
|
6831
|
+
const prior = _prTargetRefName(pr);
|
|
6832
|
+
// First-poll seeding: no prior value → record new value via the caller,
|
|
6833
|
+
// do NOT clear merge-conflict state.
|
|
6834
|
+
if (!prior) return false;
|
|
6835
|
+
if (prior === next) return false;
|
|
6836
|
+
let changed = false;
|
|
6837
|
+
if (pr._lastDispatchByCause && pr._lastDispatchByCause[PR_FIX_CAUSE.MERGE_CONFLICT]) {
|
|
6838
|
+
delete pr._lastDispatchByCause[PR_FIX_CAUSE.MERGE_CONFLICT];
|
|
6839
|
+
if (Object.keys(pr._lastDispatchByCause).length === 0) delete pr._lastDispatchByCause;
|
|
6840
|
+
changed = true;
|
|
6841
|
+
}
|
|
6842
|
+
if (pr._noOpFixes && pr._noOpFixes[PR_FIX_CAUSE.MERGE_CONFLICT]) {
|
|
6843
|
+
delete pr._noOpFixes[PR_FIX_CAUSE.MERGE_CONFLICT];
|
|
6844
|
+
if (Object.keys(pr._noOpFixes).length === 0) delete pr._noOpFixes;
|
|
6845
|
+
changed = true;
|
|
6846
|
+
}
|
|
6847
|
+
return changed;
|
|
6848
|
+
}
|
|
6849
|
+
|
|
6576
6850
|
function prFixEvidenceFingerprint(pr, cause = PR_FIX_CAUSE.UNKNOWN) {
|
|
6577
6851
|
const review = pr?.minionsReview || {};
|
|
6578
6852
|
const feedback = pr?.humanFeedback || {};
|
|
@@ -6596,6 +6870,16 @@ function prFixEvidenceFingerprint(pr, cause = PR_FIX_CAUSE.UNKNOWN) {
|
|
|
6596
6870
|
evidence.mergeConflict = !!pr?._mergeConflict;
|
|
6597
6871
|
evidence.mergeStatus = pr?.mergeStatus || '';
|
|
6598
6872
|
evidence.mergeConflictDetail = pr?._mergeConflictDetail || '';
|
|
6873
|
+
// #3079 — same rationale as BUILD_FAILURE / REVIEW_FEEDBACK in #2979:
|
|
6874
|
+
// without head/base/target fields the fingerprint was sticky across a
|
|
6875
|
+
// rebase + force-push AND across a PR retarget, so existing
|
|
6876
|
+
// _noOpFixes[MERGE_CONFLICT] pauses never released. Adding head + base
|
|
6877
|
+
// SHA + target ref name gives MERGE_CONFLICT the same natural-unsticking
|
|
6878
|
+
// property the other auto-fix causes already have.
|
|
6879
|
+
evidence.headRefOid = _prHeadSha(pr);
|
|
6880
|
+
evidence.baseSha = _prBaseSha(pr);
|
|
6881
|
+
evidence.targetRefName = _prTargetRefName(pr);
|
|
6882
|
+
evidence.lastPushedAt = pr?.lastPushedAt || '';
|
|
6599
6883
|
} else {
|
|
6600
6884
|
evidence.reviewStatus = pr?.reviewStatus || '';
|
|
6601
6885
|
evidence.lastReviewedAt = pr?.lastReviewedAt || '';
|
|
@@ -7221,6 +7505,8 @@ module.exports = {
|
|
|
7221
7505
|
PR_FIX_CAUSE,
|
|
7222
7506
|
getPrFixAutomationCause,
|
|
7223
7507
|
prFixEvidenceFingerprint,
|
|
7508
|
+
prMergeConflictGuardKey,
|
|
7509
|
+
resetMergeConflictStateOnRetarget,
|
|
7224
7510
|
getPrNoOpFixRecord,
|
|
7225
7511
|
isPrNoOpFixCausePaused,
|
|
7226
7512
|
getPrPausedCauses,
|
|
@@ -7233,6 +7519,8 @@ module.exports = {
|
|
|
7233
7519
|
killByPidsImmediate,
|
|
7234
7520
|
isProcessCommandLineMatchingAgent,
|
|
7235
7521
|
listAllProcesses,
|
|
7522
|
+
findProcessesWithCwdInside,
|
|
7523
|
+
clearWorktreeFailureCache,
|
|
7236
7524
|
listProcessDescendants,
|
|
7237
7525
|
listProcessReachable,
|
|
7238
7526
|
removeWorktree,
|