@yemi33/minions 0.1.2424 → 0.1.2426
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/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +7 -5
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/index.html +9 -2
- package/docs/internal-install.md +212 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +1 -1
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/onboarding.md +26 -0
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/01-install-and-connect.md +23 -0
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- package/skills/check-self-authored-review-comment/SKILL.md +2 -2
package/engine/shared.js
CHANGED
|
@@ -920,9 +920,6 @@ function safeJsonNoRestore(p, defaultValue = null) {
|
|
|
920
920
|
* avoid temp file name collisions.
|
|
921
921
|
*/
|
|
922
922
|
let _tmpCounter = 0;
|
|
923
|
-
const SAFE_WRITE_RENAME_ATTEMPTS = 20;
|
|
924
|
-
const SAFE_WRITE_RENAME_RETRY_BASE_MS = 50;
|
|
925
|
-
const SAFE_WRITE_RENAME_RETRY_MAX_MS = 250;
|
|
926
923
|
|
|
927
924
|
function safeWrite(p, data) {
|
|
928
925
|
const dir = path.dirname(p);
|
|
@@ -932,16 +929,13 @@ function safeWrite(p, data) {
|
|
|
932
929
|
try {
|
|
933
930
|
fs.writeFileSync(tmp, content);
|
|
934
931
|
// Atomic rename — retry on Windows EPERM (file locking)
|
|
935
|
-
for (let attempt = 0; attempt <
|
|
932
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
936
933
|
try {
|
|
937
934
|
fs.renameSync(tmp, p);
|
|
938
935
|
return;
|
|
939
936
|
} catch (e) {
|
|
940
|
-
if (e.code === 'EPERM' && attempt <
|
|
941
|
-
const delay =
|
|
942
|
-
SAFE_WRITE_RENAME_RETRY_BASE_MS * (attempt + 1),
|
|
943
|
-
SAFE_WRITE_RENAME_RETRY_MAX_MS
|
|
944
|
-
);
|
|
937
|
+
if (e.code === 'EPERM' && attempt < 4) {
|
|
938
|
+
const delay = 50 * (attempt + 1); // 50, 100, 150, 200ms
|
|
945
939
|
sleepMs(delay);
|
|
946
940
|
continue;
|
|
947
941
|
}
|
|
@@ -952,7 +946,7 @@ function safeWrite(p, data) {
|
|
|
952
946
|
}
|
|
953
947
|
// All rename attempts exhausted without throw — should not happen, but clean up
|
|
954
948
|
try { fs.unlinkSync(tmp); } catch { /* cleanup */ }
|
|
955
|
-
throw new Error(`[safeWrite] All
|
|
949
|
+
throw new Error(`[safeWrite] All 5 rename attempts failed for ${p}`);
|
|
956
950
|
} catch (err) {
|
|
957
951
|
// Clean up tmp if it still exists, then re-throw — never silently swallow
|
|
958
952
|
try { fs.unlinkSync(tmp); } catch { /* cleanup */ }
|
|
@@ -1412,104 +1406,6 @@ function pruneCrashDiagnosticsReports(config) {
|
|
|
1412
1406
|
return removed;
|
|
1413
1407
|
}
|
|
1414
1408
|
|
|
1415
|
-
const FILE_LOCK_RELEASE_ATTEMPTS = 5;
|
|
1416
|
-
const FILE_LOCK_RELEASE_RETRY_DELAY_MS = 25;
|
|
1417
|
-
const FILE_LOCK_RETRY_MAX_DELAY_MS = 250;
|
|
1418
|
-
|
|
1419
|
-
function _releaseFileLock(lockPath, fd) {
|
|
1420
|
-
try { fs.closeSync(fd); } catch { /* continue to path cleanup */ }
|
|
1421
|
-
|
|
1422
|
-
const attempts = process.platform === 'win32' ? FILE_LOCK_RELEASE_ATTEMPTS : 1;
|
|
1423
|
-
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1424
|
-
try {
|
|
1425
|
-
fs.unlinkSync(lockPath);
|
|
1426
|
-
return true;
|
|
1427
|
-
} catch (err) {
|
|
1428
|
-
if (err && err.code === 'ENOENT') return true;
|
|
1429
|
-
const retryable = process.platform === 'win32'
|
|
1430
|
-
&& err && (err.code === 'EPERM' || err.code === 'EBUSY' || err.code === 'EACCES');
|
|
1431
|
-
if (!retryable || attempt === attempts - 1) {
|
|
1432
|
-
console.warn(`[withFileLock] Failed to release ${lockPath}: ${err?.message || err}`);
|
|
1433
|
-
return false;
|
|
1434
|
-
}
|
|
1435
|
-
sleepMs(FILE_LOCK_RELEASE_RETRY_DELAY_MS * (attempt + 1));
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
return false;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
function _isTransientFileLockPathError(err) {
|
|
1442
|
-
return !!err && (err.code === 'ENOENT'
|
|
1443
|
-
|| (process.platform === 'win32' && err.code === 'EPERM'));
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
function _readFileLockState(lockPath) {
|
|
1447
|
-
try {
|
|
1448
|
-
const stat = fs.statSync(lockPath);
|
|
1449
|
-
const raw = fs.readFileSync(lockPath, 'utf8');
|
|
1450
|
-
let holderPid = null;
|
|
1451
|
-
try {
|
|
1452
|
-
const parsed = JSON.parse(raw);
|
|
1453
|
-
if (parsed && Number.isFinite(parsed.pid) && parsed.pid > 0) holderPid = parsed.pid;
|
|
1454
|
-
} catch { /* legacy/empty/corrupt lock */ }
|
|
1455
|
-
return { raw, holderPid, mtimeAge: Date.now() - stat.mtimeMs };
|
|
1456
|
-
} catch (err) {
|
|
1457
|
-
if (_isTransientFileLockPathError(err)) return null;
|
|
1458
|
-
throw err;
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
function _readFileLockIdentity(lockPath) {
|
|
1463
|
-
try {
|
|
1464
|
-
return fs.readFileSync(lockPath, 'utf8');
|
|
1465
|
-
} catch (err) {
|
|
1466
|
-
if (_isTransientFileLockPathError(err)) return null;
|
|
1467
|
-
throw err;
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
function _tryReapFileLock(lockPath) {
|
|
1472
|
-
const claimPath = `${lockPath}.reap`;
|
|
1473
|
-
let claimFd;
|
|
1474
|
-
try {
|
|
1475
|
-
claimFd = fs.openSync(claimPath, 'wx');
|
|
1476
|
-
try {
|
|
1477
|
-
fs.writeSync(claimFd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
1478
|
-
} catch { /* claim existence provides serialization */ }
|
|
1479
|
-
} catch (err) {
|
|
1480
|
-
const contended = err && (err.code === 'EEXIST'
|
|
1481
|
-
|| (process.platform === 'win32' && err.code === 'EPERM'));
|
|
1482
|
-
if (contended) return false;
|
|
1483
|
-
throw err;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
try {
|
|
1487
|
-
const observed = _readFileLockState(lockPath);
|
|
1488
|
-
if (!observed) return false;
|
|
1489
|
-
const shouldReap = observed.holderPid !== null
|
|
1490
|
-
? !processUtils.isPidAlive(observed.holderPid) || observed.mtimeAge > LOCK_STALE_MS * 5
|
|
1491
|
-
: observed.mtimeAge > LOCK_STALE_MS;
|
|
1492
|
-
if (!shouldReap) return false;
|
|
1493
|
-
|
|
1494
|
-
// A holder can release and a successor can acquire between the PID probe
|
|
1495
|
-
// and deletion. The reap claim excludes competing reapers; this identity
|
|
1496
|
-
// check prevents the winner from deleting that successor's lock.
|
|
1497
|
-
const currentRaw = _readFileLockIdentity(lockPath);
|
|
1498
|
-
if (currentRaw === null) return false;
|
|
1499
|
-
if (currentRaw !== observed.raw) return false;
|
|
1500
|
-
|
|
1501
|
-
try {
|
|
1502
|
-
fs.unlinkSync(lockPath);
|
|
1503
|
-
return true;
|
|
1504
|
-
} catch (err) {
|
|
1505
|
-
if (_isTransientFileLockPathError(err)) return false;
|
|
1506
|
-
throw err;
|
|
1507
|
-
}
|
|
1508
|
-
} finally {
|
|
1509
|
-
_releaseFileLock(claimPath, claimFd);
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
1409
|
function withFileLock(lockPath, fn, {
|
|
1514
1410
|
timeoutMs = 5000,
|
|
1515
1411
|
retryDelayMs = 25,
|
|
@@ -1524,13 +1420,9 @@ function withFileLock(lockPath, fn, {
|
|
|
1524
1420
|
const backoff = retryBackoffMs * Math.pow(2, attempt - 1);
|
|
1525
1421
|
sleepMs(backoff);
|
|
1526
1422
|
}
|
|
1423
|
+
const start = Date.now();
|
|
1527
1424
|
let fd = null;
|
|
1528
|
-
|
|
1529
|
-
const waitBudgetMs = Math.max(0, Number(timeoutMs) || 0);
|
|
1530
|
-
let waitedMs = 0;
|
|
1531
|
-
let contentionCount = 0;
|
|
1532
|
-
let lastOwnerIdentity;
|
|
1533
|
-
while (true) {
|
|
1425
|
+
while (Date.now() - start < timeoutMs) {
|
|
1534
1426
|
try {
|
|
1535
1427
|
const dir = path.dirname(lockPath);
|
|
1536
1428
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
@@ -1540,11 +1432,7 @@ function withFileLock(lockPath, fn, {
|
|
|
1540
1432
|
// the lock's existence (not its contents) provides mutual exclusion, so
|
|
1541
1433
|
// a write failure here must NOT abort acquisition.
|
|
1542
1434
|
try {
|
|
1543
|
-
fs.writeSync(fd, JSON.stringify({
|
|
1544
|
-
pid: process.pid,
|
|
1545
|
-
ts: Date.now(),
|
|
1546
|
-
owner: crypto.randomBytes(8).toString('hex'),
|
|
1547
|
-
}));
|
|
1435
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
1548
1436
|
} catch { /* payload is advisory; lock semantics unaffected */ }
|
|
1549
1437
|
break;
|
|
1550
1438
|
} catch (err) {
|
|
@@ -1559,27 +1447,47 @@ function withFileLock(lockPath, fn, {
|
|
|
1559
1447
|
const isLockRaceErr = err.code === 'EEXIST' ||
|
|
1560
1448
|
(process.platform === 'win32' && err.code === 'EPERM');
|
|
1561
1449
|
if (!isLockRaceErr) throw err;
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1450
|
+
// PID-owned locks are safe to reap as soon as their owner is dead.
|
|
1451
|
+
// Alive owners retain the 5× stale window; legacy/partial lockfiles
|
|
1452
|
+
// without a readable PID retain the mtime-only stale window.
|
|
1453
|
+
try {
|
|
1454
|
+
const stat = fs.statSync(lockPath);
|
|
1455
|
+
const mtimeAge = Date.now() - stat.mtimeMs;
|
|
1456
|
+
let holderPid = null;
|
|
1457
|
+
try {
|
|
1458
|
+
const raw = fs.readFileSync(lockPath, 'utf8');
|
|
1459
|
+
const parsed = JSON.parse(raw);
|
|
1460
|
+
if (parsed && Number.isFinite(parsed.pid) && parsed.pid > 0) {
|
|
1461
|
+
holderPid = parsed.pid;
|
|
1462
|
+
}
|
|
1463
|
+
} catch { /* legacy/empty/corrupt lock → mtime-only fallback */ }
|
|
1464
|
+
|
|
1465
|
+
const shouldReap = holderPid !== null
|
|
1466
|
+
? !processUtils.isPidAlive(holderPid) || mtimeAge > LOCK_STALE_MS * 5
|
|
1467
|
+
: mtimeAge > LOCK_STALE_MS;
|
|
1468
|
+
|
|
1469
|
+
if (shouldReap) {
|
|
1470
|
+
try {
|
|
1471
|
+
fs.unlinkSync(lockPath);
|
|
1472
|
+
} catch (unlinkErr) {
|
|
1473
|
+
// ENOENT: another process deleted the lock between stat and unlink — safe to retry.
|
|
1474
|
+
// EPERM on Windows: file is in 'pending delete' state from a concurrent
|
|
1475
|
+
// reaper — the OS will finalize the delete shortly; retry will succeed.
|
|
1476
|
+
const tolerable = unlinkErr.code === 'ENOENT' ||
|
|
1477
|
+
(process.platform === 'win32' && unlinkErr.code === 'EPERM');
|
|
1478
|
+
if (!tolerable) throw unlinkErr;
|
|
1479
|
+
}
|
|
1480
|
+
continue; // lock just removed — retry immediately
|
|
1567
1481
|
}
|
|
1568
|
-
|
|
1482
|
+
} catch (staleErr) {
|
|
1483
|
+
// ENOENT from statSync: lock file disappeared between EEXIST and stat — retry will succeed.
|
|
1484
|
+
// EPERM on Windows: file is in 'pending delete' state — statSync itself
|
|
1485
|
+
// can fail; fall through to the normal retry sleep (W-mpd6lm1g000x195e).
|
|
1486
|
+
const tolerable = staleErr.code === 'ENOENT' ||
|
|
1487
|
+
(process.platform === 'win32' && staleErr.code === 'EPERM');
|
|
1488
|
+
if (!tolerable) throw staleErr;
|
|
1569
1489
|
}
|
|
1570
|
-
|
|
1571
|
-
// Count active retry sleeps rather than wall time. Host suspension must
|
|
1572
|
-
// not consume the whole budget without another acquisition attempt.
|
|
1573
|
-
if (waitedMs >= waitBudgetMs) break;
|
|
1574
|
-
const backoffMs = Math.min(
|
|
1575
|
-
pollDelayMs * Math.pow(2, Math.min(contentionCount, 4)),
|
|
1576
|
-
FILE_LOCK_RETRY_MAX_DELAY_MS
|
|
1577
|
-
);
|
|
1578
|
-
const jitterMs = contentionCount === 0 ? 0 : Math.floor(Math.random() * pollDelayMs);
|
|
1579
|
-
const delayMs = Math.min(waitBudgetMs - waitedMs, backoffMs + jitterMs);
|
|
1580
|
-
sleepMs(delayMs);
|
|
1581
|
-
waitedMs += delayMs;
|
|
1582
|
-
contentionCount += 1;
|
|
1490
|
+
sleepMs(retryDelayMs);
|
|
1583
1491
|
}
|
|
1584
1492
|
}
|
|
1585
1493
|
if (fd === null) {
|
|
@@ -1595,52 +1503,143 @@ function withFileLock(lockPath, fn, {
|
|
|
1595
1503
|
// completes, silently breaking mutual exclusion. Clean up our own fd /
|
|
1596
1504
|
// lock first, then throw so the caller cannot ignore the failure.
|
|
1597
1505
|
if (result && typeof result.then === 'function') {
|
|
1598
|
-
|
|
1506
|
+
try { fs.closeSync(fd); } catch { /* cleanup */ }
|
|
1507
|
+
try { fs.unlinkSync(lockPath); } catch { /* cleanup */ }
|
|
1599
1508
|
fd = null; // suppress double-cleanup in `finally`
|
|
1600
1509
|
throw new Error('withFileLock: fn must be synchronous; got Promise. Use synchronous operations only.');
|
|
1601
1510
|
}
|
|
1602
1511
|
return result;
|
|
1603
1512
|
} finally {
|
|
1604
1513
|
if (fd !== null) {
|
|
1605
|
-
|
|
1514
|
+
try { fs.closeSync(fd); } catch { /* cleanup */ }
|
|
1515
|
+
try { fs.unlinkSync(lockPath); } catch { /* cleanup */ }
|
|
1606
1516
|
}
|
|
1607
1517
|
}
|
|
1608
1518
|
}
|
|
1609
1519
|
throw lastErr;
|
|
1610
1520
|
}
|
|
1611
1521
|
|
|
1522
|
+
const DELETE_JSON_FILE = Symbol('minions.deleteJsonFile');
|
|
1523
|
+
|
|
1612
1524
|
function mutateJsonFileLocked(filePath, mutateFn, {
|
|
1613
1525
|
defaultValue = {},
|
|
1614
1526
|
lockRetries,
|
|
1615
1527
|
lockRetryBackoffMs,
|
|
1616
1528
|
skipWriteIfUnchanged = false,
|
|
1529
|
+
restoreOnRead = true,
|
|
1530
|
+
writeDefaultOnInvalid = true,
|
|
1531
|
+
exclusiveClaim = false,
|
|
1617
1532
|
onWrote = null,
|
|
1618
1533
|
} = {}) {
|
|
1619
1534
|
const lockPath = `${filePath}.lock`;
|
|
1620
1535
|
const retries = lockRetries ?? ENGINE_DEFAULTS.lockRetries;
|
|
1621
1536
|
const retryBackoffMs = lockRetryBackoffMs ?? ENGINE_DEFAULTS.lockRetryBackoffMs;
|
|
1622
1537
|
return withFileLock(lockPath, () => {
|
|
1623
|
-
|
|
1624
|
-
let
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
try { if (fileExists) fs.copyFileSync(filePath, backupPath); } catch { /* backup is best-effort */ }
|
|
1634
|
-
safeWrite(filePath, finalData);
|
|
1635
|
-
// Side-effect hook fired only when an actual write happened. Callers
|
|
1636
|
-
// use this to emit cache-invalidation signals (events table row) so
|
|
1637
|
-
// skip-write-unchanged paths (dedup, idempotent no-op POSTs) don't
|
|
1638
|
-
// produce spurious cache busts.
|
|
1639
|
-
if (typeof onWrote === 'function') {
|
|
1640
|
-
try { onWrote(finalData); } catch { /* hook errors must not break the write */ }
|
|
1538
|
+
let fileExists = fs.existsSync(filePath);
|
|
1539
|
+
let claimPath = null;
|
|
1540
|
+
if (exclusiveClaim && fileExists) {
|
|
1541
|
+
claimPath = `${filePath}.claim-${process.pid}-${uid()}`;
|
|
1542
|
+
try {
|
|
1543
|
+
fs.renameSync(filePath, claimPath);
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
1546
|
+
fileExists = false;
|
|
1547
|
+
claimPath = null;
|
|
1641
1548
|
}
|
|
1642
1549
|
}
|
|
1643
|
-
|
|
1550
|
+
const readPath = claimPath || filePath;
|
|
1551
|
+
const cleanupClaim = () => {
|
|
1552
|
+
if (!claimPath) return;
|
|
1553
|
+
for (const candidate of [claimPath, claimPath + '.backup']) {
|
|
1554
|
+
try { fs.unlinkSync(candidate); } catch (error) {
|
|
1555
|
+
if (error && error.code !== 'ENOENT') throw error;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
const restoreClaim = () => {
|
|
1560
|
+
if (!claimPath || !fs.existsSync(claimPath)) return;
|
|
1561
|
+
try {
|
|
1562
|
+
fs.copyFileSync(claimPath, filePath, fs.constants.COPYFILE_EXCL);
|
|
1563
|
+
cleanupClaim();
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
if (error && error.code === 'EEXIST') {
|
|
1566
|
+
cleanupClaim();
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
throw error;
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1573
|
+
try {
|
|
1574
|
+
let data = restoreOnRead ? safeJson(readPath) : safeJsonNoRestore(readPath);
|
|
1575
|
+
const parsedInvalid = fileExists && data === null;
|
|
1576
|
+
if (data === null || typeof data !== 'object') {
|
|
1577
|
+
data = Array.isArray(defaultValue) ? [...defaultValue] : { ...defaultValue };
|
|
1578
|
+
}
|
|
1579
|
+
const beforeSerialized = skipWriteIfUnchanged ? JSON.stringify(data) : null;
|
|
1580
|
+
const next = mutateFn(data, { fileExists, readPath });
|
|
1581
|
+
if (next === DELETE_JSON_FILE) {
|
|
1582
|
+
if (claimPath) {
|
|
1583
|
+
if (fs.existsSync(filePath)) {
|
|
1584
|
+
const error = new Error(`JSON file was replaced during mutation: ${filePath}`);
|
|
1585
|
+
error.code = 'JSON_FILE_REPLACED';
|
|
1586
|
+
throw error;
|
|
1587
|
+
}
|
|
1588
|
+
cleanupClaim();
|
|
1589
|
+
} else {
|
|
1590
|
+
for (const candidate of [filePath, filePath + '.backup']) {
|
|
1591
|
+
try { fs.unlinkSync(candidate); } catch (error) {
|
|
1592
|
+
if (error && error.code !== 'ENOENT') throw error;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
const finalData = next === undefined ? data : next;
|
|
1599
|
+
const shouldWrite = !skipWriteIfUnchanged
|
|
1600
|
+
|| (parsedInvalid && writeDefaultOnInvalid)
|
|
1601
|
+
|| JSON.stringify(finalData) !== beforeSerialized;
|
|
1602
|
+
if (shouldWrite) {
|
|
1603
|
+
if (claimPath) {
|
|
1604
|
+
safeWrite(claimPath, finalData);
|
|
1605
|
+
if (fs.existsSync(filePath)) {
|
|
1606
|
+
const error = new Error(`JSON file was replaced during mutation: ${filePath}`);
|
|
1607
|
+
error.code = 'JSON_FILE_REPLACED';
|
|
1608
|
+
throw error;
|
|
1609
|
+
}
|
|
1610
|
+
try {
|
|
1611
|
+
fs.copyFileSync(claimPath, filePath, fs.constants.COPYFILE_EXCL);
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
if (error && error.code === 'EEXIST') {
|
|
1614
|
+
error.code = 'JSON_FILE_REPLACED';
|
|
1615
|
+
}
|
|
1616
|
+
throw error;
|
|
1617
|
+
}
|
|
1618
|
+
cleanupClaim();
|
|
1619
|
+
} else {
|
|
1620
|
+
const backupPath = filePath + '.backup';
|
|
1621
|
+
try { if (fileExists) fs.copyFileSync(filePath, backupPath); } catch { /* backup is best-effort */ }
|
|
1622
|
+
safeWrite(filePath, finalData);
|
|
1623
|
+
}
|
|
1624
|
+
// Side-effect hook fired only when an actual write happened. Callers
|
|
1625
|
+
// use this to emit cache-invalidation signals (events table row) so
|
|
1626
|
+
// skip-write-unchanged paths (dedup, idempotent no-op POSTs) don't
|
|
1627
|
+
// produce spurious cache busts.
|
|
1628
|
+
if (typeof onWrote === 'function') {
|
|
1629
|
+
try { onWrote(finalData); } catch { /* hook errors must not break the write */ }
|
|
1630
|
+
}
|
|
1631
|
+
} else if (claimPath) {
|
|
1632
|
+
restoreClaim();
|
|
1633
|
+
}
|
|
1634
|
+
return finalData;
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
if (claimPath && fs.existsSync(claimPath)) {
|
|
1637
|
+
try { restoreClaim(); } catch (restoreError) {
|
|
1638
|
+
error.restoreError = restoreError;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
throw error;
|
|
1642
|
+
}
|
|
1644
1643
|
}, { retries, retryBackoffMs });
|
|
1645
1644
|
}
|
|
1646
1645
|
|
|
@@ -2468,6 +2467,73 @@ function parseStreamJsonOutput(raw, runtimeName, opts) {
|
|
|
2468
2467
|
return resolveRuntime(runtimeName).parseOutput(raw, opts || {});
|
|
2469
2468
|
}
|
|
2470
2469
|
|
|
2470
|
+
// ── Stale resumed-session detection (W-ms0vk9nv000z985b) ────────────────────
|
|
2471
|
+
//
|
|
2472
|
+
// A `--resume`d CLI can die instantly, before it writes a single stdout byte,
|
|
2473
|
+
// when its persisted session is no longer usable. Two distinct signals, both
|
|
2474
|
+
// runtime-agnostic in shape (each adapter's CLI phrases them slightly
|
|
2475
|
+
// differently, so we match on the stable substrings actually emitted):
|
|
2476
|
+
//
|
|
2477
|
+
// 1. The backing conversation was purged from the runtime's session store.
|
|
2478
|
+
// 2. The directory the session was created in is gone — the engine removed
|
|
2479
|
+
// or quarantined that worktree since the previous dispatch. Copilot
|
|
2480
|
+
// reports this as `resume-auto-cd: ignoring persisted cwd <path> ...
|
|
2481
|
+
// missing or not a directory` on stderr and then exits 1 with EMPTY
|
|
2482
|
+
// stdout, so the failure otherwise classifies as a meaningless
|
|
2483
|
+
// EMPTY_OUTPUT.
|
|
2484
|
+
//
|
|
2485
|
+
// Callers use this to (a) purge the cached session so the retry starts fresh
|
|
2486
|
+
// and (b) classify the failure as RUNTIME_SESSION_STALE instead of
|
|
2487
|
+
// EMPTY_OUTPUT. Prevention lives in each adapter's getResumeSessionId (it
|
|
2488
|
+
// refuses to hand back a session whose recorded cwd no longer exists); this
|
|
2489
|
+
// helper is the defense-in-depth for the race where the directory disappears
|
|
2490
|
+
// between resolution and spawn.
|
|
2491
|
+
const _STALE_RESUME_SESSION_PATTERNS = [
|
|
2492
|
+
/no conversation found/i,
|
|
2493
|
+
/ignoring persisted cwd/i,
|
|
2494
|
+
/session (?:not found|no longer exists|has expired)/i,
|
|
2495
|
+
];
|
|
2496
|
+
|
|
2497
|
+
function isStaleResumeSessionSignal(text) {
|
|
2498
|
+
const value = text == null ? '' : String(text);
|
|
2499
|
+
if (!value) return false;
|
|
2500
|
+
return _STALE_RESUME_SESSION_PATTERNS.some(re => re.test(value));
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* PREVENTION side of the same bug (W-ms0vk9nv000z985b): decide whether a
|
|
2505
|
+
* persisted session may be resumed given the directory it was created in.
|
|
2506
|
+
*
|
|
2507
|
+
* Runtime CLIs anchor a resumable session to the working directory it was
|
|
2508
|
+
* started in. Minions gives most dispatches an engine-owned worktree that is
|
|
2509
|
+
* removed, recycled, or quarantined once the dispatch ends — so by the time
|
|
2510
|
+
* the same agent is dispatched again, the session's directory can be gone or
|
|
2511
|
+
* a different path entirely. Handing that session id to `--resume` makes the
|
|
2512
|
+
* CLI bail before it writes any stdout, which surfaced as an opaque
|
|
2513
|
+
* `empty-output` failure.
|
|
2514
|
+
*
|
|
2515
|
+
* Returns a human-readable reason to REFUSE the resume, or null when the
|
|
2516
|
+
* session is safe to resume. Sessions saved before this field existed carry no
|
|
2517
|
+
* `cwd` and are treated as compatible — same opt-in convention as the
|
|
2518
|
+
* `runtime` field, so deploying this cannot invalidate live sessions.
|
|
2519
|
+
*
|
|
2520
|
+
* Shared by every runtime adapter: the constraint is a property of how the
|
|
2521
|
+
* engine manages worktrees, not of any one CLI.
|
|
2522
|
+
*/
|
|
2523
|
+
function staleResumeSessionCwdReason(sessionCwd, currentCwd) {
|
|
2524
|
+
if (!sessionCwd || typeof sessionCwd !== 'string') return null;
|
|
2525
|
+
let exists = false;
|
|
2526
|
+
try { exists = fs.existsSync(sessionCwd); } catch { exists = false; }
|
|
2527
|
+
if (!exists) return `session working directory no longer exists (${sessionCwd})`;
|
|
2528
|
+
if (!currentCwd || typeof currentCwd !== 'string') return null;
|
|
2529
|
+
const recorded = _normalizeWorktreePath(sessionCwd);
|
|
2530
|
+
const current = _normalizeWorktreePath(currentCwd);
|
|
2531
|
+
if (recorded && current && recorded !== current) {
|
|
2532
|
+
return `session was created in ${sessionCwd} but this dispatch runs in ${currentCwd}`;
|
|
2533
|
+
}
|
|
2534
|
+
return null;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2471
2537
|
// ── Knowledge Base ──────────────────────────────────────────────────────────
|
|
2472
2538
|
|
|
2473
2539
|
const KB_CATEGORIES = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
|
|
@@ -2502,6 +2568,7 @@ const KB_READABLE_CATEGORIES = Object.freeze([
|
|
|
2502
2568
|
'consolidated', 'consolidation', 'consolidations',
|
|
2503
2569
|
'team-memory', 'general', 'patterns',
|
|
2504
2570
|
]);
|
|
2571
|
+
/**
|
|
2505
2572
|
/**
|
|
2506
2573
|
* Detect engine-authored system alerts (dirty-tree refusals, worktree-skip-live
|
|
2507
2574
|
* guard notices, blocked/conflicted live-checkout dispatches, managed-spawn
|
|
@@ -2926,6 +2993,7 @@ const ENGINE_DEFAULTS = {
|
|
|
2926
2993
|
worktreeCreateTimeout: 300000, // 5min for git worktree add on large Windows repos
|
|
2927
2994
|
worktreeCreateRetries: 1, // retry once on transient timeout/lock races
|
|
2928
2995
|
worktreeRoot: '../worktrees',
|
|
2996
|
+
worktreeMinFreeSpaceBytes: 10 * 1024 * 1024 * 1024, // fail-closed before checkout below 10 GiB; 0 disables
|
|
2929
2997
|
worktreeCountCacheTtl: 30000, // 30s — TTL for cached _countWorktrees() result in dashboard
|
|
2930
2998
|
// W-mq1habhf: timeout (ms) for the `git status --porcelain` probe inside
|
|
2931
2999
|
// engine.js assertCleanSharedWorktree. The default 10s is fine for ordinary
|
|
@@ -2991,7 +3059,7 @@ const ENGINE_DEFAULTS = {
|
|
|
2991
3059
|
// to before this knob existed. Flip via Dashboard → Settings → Auto-fix
|
|
2992
3060
|
// & Review Loop, or set `engine.autoFixPaused: true` in config.json.
|
|
2993
3061
|
autoFixPaused: false, // hard-stop kill-switch / master override (see comment above)
|
|
2994
|
-
autoConsolidateMemory:
|
|
3062
|
+
autoConsolidateMemory: true, // default-on: periodically spawn engine/kb-sweep-runner.js from the tick loop (4h cadence). Explicit false disables it; inbox→notes consolidation is independent.
|
|
2995
3063
|
// W-mrktrokd — batteries-included default schedules. When true (default),
|
|
2996
3064
|
// adding a project auto-creates its per-project git-fetch-freshness schedule
|
|
2997
3065
|
// (`keep-branch-fresh-git-fetch-origin-<slug>`, cron `0 */12 *`, explore) and
|
|
@@ -3302,6 +3370,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3302
3370
|
preDispatchEvalMaxRejections: 5,
|
|
3303
3371
|
completionNonceRequired: false, // P-d2a8f6c1 (agent trust boundary F8): when true, a missing `nonce` field in the completion JSON hard-fails the dispatch with failure_class:'completion-nonce-mismatch'. Default false for one release so older agents/runtime caches that haven't picked up the prompt change degrade with a warning instead of breaking. Mismatched nonces hard-fail regardless of this flag. See docs/completion-reports.md → "Trust boundary".
|
|
3304
3372
|
autoApplyReviewVote: false, // Master gate for platform vote mutations; local verdicts remain informational when false.
|
|
3373
|
+
createPrsAsDraft: false, // W-mrtdy7xv — when true, the playbook PR-creation guidance (engine/playbook.js#getPrCreateInstructions) tells agents to open normal Minions-created PRs as DRAFT (GitHub `gh pr create --draft`; ADO `az repos pr create --draft true`). Default false = behave exactly as today. Per-project-overridable via `project.createPrsAsDraft` (explicit boolean), resolved through `shared.resolveCreatePrsAsDraft(project, engine)` — a project can opt IN or OUT independent of this fleet-wide default. Aggregate/E2E verify PRs already open as draft via verify.md regardless of this flag; the engine-opened shared-branch aggregate PR is outside the playbook seam and unaffected.
|
|
3305
3374
|
|
|
3306
3375
|
// ── Runtime fleet (P-3b8e5f1d) ──────────────────────────────────────────────
|
|
3307
3376
|
// Single source of truth for which CLI runtime + model every spawn uses.
|
|
@@ -3326,10 +3395,46 @@ const ENGINE_DEFAULTS = {
|
|
|
3326
3395
|
// Engine-scoped per the CLAUDE.md rule against `runtime.name === ...` branches at call sites — the runtime check
|
|
3327
3396
|
// lives inside the helper (engine/spawn-agent.js#preApproveWorkspaceMcps) which no-ops for non-Claude runtimes.
|
|
3328
3397
|
claudePreApproveWorkspaceMcps: true,
|
|
3398
|
+
// W-mrdon0pe000l045a — propagate repo-authored CLAUDE.md instructions to
|
|
3399
|
+
// runtimes that do NOT auto-discover CLAUDE.md natively (Copilot, Codex).
|
|
3400
|
+
// When TRUE (default), the engine bounded-discovers the nearest applicable
|
|
3401
|
+
// CLAUDE.md files for a dispatch and injects them as a "Project instructions
|
|
3402
|
+
// (CLAUDE.md)" context layer (engine/playbook.js → engine/claude-md-context.js).
|
|
3403
|
+
// Claude is always skipped — its own CLI already reads CLAUDE.md, so injecting
|
|
3404
|
+
// it again would double the instructions. CLAUDE.md carries no split-brain
|
|
3405
|
+
// risk for Copilot/Codex because those runtimes never read it themselves, so
|
|
3406
|
+
// this is orthogonal to the native AGENTS.md / repo-instruction discovery that
|
|
3407
|
+
// each runtime's CLI now owns (#817). Per-project override via
|
|
3408
|
+
// `project.propagateClaudeMdForNonClaudeRuntimes`; resolved through
|
|
3409
|
+
// `shared.resolvePropagateClaudeMdForNonClaudeRuntimes(project, engine)`.
|
|
3410
|
+
propagateClaudeMdForNonClaudeRuntimes: true,
|
|
3411
|
+
// W-mrtdmtwq000kf391 — surface the repo HARNESS assets in effect for a
|
|
3412
|
+
// dispatch (runtime + project) in the Minions-authored PR body. When TRUE
|
|
3413
|
+
// (default), engine/playbook.js injects a PR-body appendix instructing the
|
|
3414
|
+
// agent to append a fenced "## Repo harnesses used" section listing the
|
|
3415
|
+
// dispatch's runtime, injected CLAUDE.md files, AGENTS.md discovery, MCP
|
|
3416
|
+
// servers, and skills. Pure reporting via the read-only harness-diagnostics
|
|
3417
|
+
// source (engine/harness-context.js → engine/preflight._runtimeHarnessRows +
|
|
3418
|
+
// engine/claude-md-context.js); it never propagates or attests any asset.
|
|
3419
|
+
includeHarnessSectionInPrBody: true,
|
|
3329
3420
|
copilotStreamMode: 'on', // Copilot --stream <on|off>: 'on' streams assistant.message_delta events live; 'off' batches them
|
|
3330
3421
|
copilotReasoningSummaries: false, // Copilot --enable-reasoning-summaries (Anthropic-family models only)
|
|
3331
|
-
ccUseWorkerPool: true, //
|
|
3332
|
-
|
|
3422
|
+
ccUseWorkerPool: true, // Route Copilot CC/doc-chat through a persistent `copilot --acp` worker per tab by default. Structurally Copilot-only: resolveCcUseWorkerPool forces false for runtimes without this transport.
|
|
3423
|
+
// P-9b3d5f61 (Pool copilot agent dispatches through a shared ACP worker pool):
|
|
3424
|
+
// when true AND the resolved runtime declares `capabilities.acpWorkerPool`,
|
|
3425
|
+
// engine.js spawnAgent routes per-agent dispatches through
|
|
3426
|
+
// engine/agent-worker-pool.js (persistent `copilot --acp` workers) instead of
|
|
3427
|
+
// cold-spawning a fresh CLI per dispatch. Opt-in while fleet-pool lifecycle
|
|
3428
|
+
// hardening remains newer than the established cold-spawn path. Structurally
|
|
3429
|
+
// Copilot-only, same as
|
|
3430
|
+
// ccUseWorkerPool: resolveAgentUseWorkerPool hard-returns false whenever the
|
|
3431
|
+
// resolved runtime's capability flag isn't true, regardless of this override.
|
|
3432
|
+
agentUseWorkerPool: false,
|
|
3433
|
+
// P-9b3d5f61 — max concurrent ACP workers the per-agent pool
|
|
3434
|
+
// (engine/agent-worker-pool.js) will keep warm. `undefined` = fall back to
|
|
3435
|
+
// the fleet's `maxConcurrent` dispatch cap via resolveAgentAcpPoolSize, so
|
|
3436
|
+
// the pool never queues more than the engine would have dispatched anyway.
|
|
3437
|
+
agentAcpPoolSize: undefined,
|
|
3333
3438
|
maxBudgetUsd: undefined, // fleet USD ceiling for --max-budget-usd (per-agent override: agents.<id>.maxBudgetUsd). Honors 0 via ?? so a literal cap of $0 works
|
|
3334
3439
|
disableModelDiscovery: false, // skip runtime.listModels() REST calls fleet-wide (settings UI falls back to free-text)
|
|
3335
3440
|
// W-mpmwxkrw000872ec — dashboard global font-size scale. Drives the
|
|
@@ -3376,27 +3481,49 @@ const ENGINE_DEFAULTS = {
|
|
|
3376
3481
|
// the kill); holder details are appended to the inbox note either way.
|
|
3377
3482
|
autoReapOrphanWorktreeHolders: false,
|
|
3378
3483
|
// W-mqvejug6000eeb20 — fleet-wide fallback for live-checkout auto-reset.
|
|
3379
|
-
// When ON, a dirty
|
|
3380
|
-
// `git
|
|
3381
|
-
//
|
|
3382
|
-
// this. Default
|
|
3383
|
-
//
|
|
3384
|
-
//
|
|
3385
|
-
//
|
|
3386
|
-
// recoverable via `git stash pop` — see `resolveLiveCheckoutAutoStash` /
|
|
3387
|
-
// `applyLiveCheckoutAutoStash` in engine/live-checkout.js), which the
|
|
3388
|
-
// stash-before-reset ordering (W-mrawgw4q000a6bff) now always attempts FIRST
|
|
3389
|
-
// when both flags are enabled for a dispatch. Auto-reset remains available as
|
|
3390
|
-
// an explicit opt-in (per-project `project.liveCheckoutAutoReset: true` or
|
|
3391
|
-
// fleet `engine.liveCheckoutAutoReset: true`) for cases that specifically
|
|
3392
|
-
// need the branch fast-forwarded/discarded to `origin/<mainRef>`, not just
|
|
3393
|
-
// cleaned. Resolution precedence lives in `resolveLiveCheckoutAutoReset`
|
|
3394
|
-
// (UNCHANGED by this flip). Fires ONLY on confirmed dirty-tree detection,
|
|
3395
|
-
// never on mid-operation / blob-fetch / tooling failures.
|
|
3484
|
+
// When ON, a dirty live-checkout tree is cleaned with `git reset --hard HEAD`
|
|
3485
|
+
// + `git clean -fd` before dispatch instead of failing LIVE_CHECKOUT_DIRTY.
|
|
3486
|
+
// The current branch and committed history are preserved. Per-project
|
|
3487
|
+
// `project.liveCheckoutAutoReset` overrides this. Default false;
|
|
3488
|
+
// `liveCheckoutAutoStash` gets first chance when both policies are enabled.
|
|
3489
|
+
// Fires only on confirmed dirt, never on mid-operation / detached-HEAD /
|
|
3490
|
+
// tooling failures.
|
|
3396
3491
|
liveCheckoutAutoReset: false,
|
|
3492
|
+
// W-mr3lunnq000o9f41 — opt-in companion to the live-checkout stale-base guard.
|
|
3493
|
+
// When ON, a live-checkout dispatch that would fail LIVE_CHECKOUT_WRONG_BASE
|
|
3494
|
+
// (HEAD on an unrelated topic branch AND no local `refs/heads/<mainBranch>` to
|
|
3495
|
+
// fork from) is auto-recovered by materializing a local base branch from the
|
|
3496
|
+
// `refs/remotes/origin/<mainBranch>` remote-tracking ref (a LOCAL ref — NO
|
|
3497
|
+
// `git fetch`, so issue #226 is preserved) and forking cleanly off it.
|
|
3498
|
+
// origin/<mainBranch> IS the correct base, so there is NO scope-contamination
|
|
3499
|
+
// risk. Per-project `project.liveCheckoutAutoBaseRepair` overrides this.
|
|
3500
|
+
// Resolution precedence lives in `resolveLiveCheckoutAutoBaseRepair`. Default
|
|
3501
|
+
// OFF because the recovery's `git checkout <mainBranch>` may hydrate blobs on a
|
|
3502
|
+
// partial clone (the one op issue #226 flagged as hang-prone headless); when it
|
|
3503
|
+
// does hang it degrades to exactly the pre-existing fail-closed refusal.
|
|
3504
|
+
liveCheckoutAutoBaseRepair: false,
|
|
3505
|
+
// W-mrdlcud2000e630e — opt-in companion to the live-checkout stale-base guard.
|
|
3506
|
+
// When ON, a live-checkout (or hybrid liveValidation) dispatch that is about to
|
|
3507
|
+
// fork a NEW branch off the local `mainBranch` fast-forwards that local base to
|
|
3508
|
+
// `origin/<mainBranch>` FIRST, but ONLY in the strictly-safe case: the tree is
|
|
3509
|
+
// clean (the dirty-tree guard already covers this), HEAD is on `mainBranch`, the
|
|
3510
|
+
// local base has ZERO commits ahead of `origin/<mainBranch>` (i.e. NOT the
|
|
3511
|
+
// stale-base-with-unpushed-commits case — that refusal is unchanged), and the
|
|
3512
|
+
// local base IS behind `origin/<mainBranch>`. It runs `git fetch origin
|
|
3513
|
+
// <mainBranch>` then a `--ff-only` update — the ONE sanctioned live-mode fetch
|
|
3514
|
+
// (issue #226 otherwise forbids fetching live checkouts, to preserve operator
|
|
3515
|
+
// control over history rewrites); a fast-forward can never rewrite history or
|
|
3516
|
+
// lose local commits (there are none ahead), and it NEVER touches a dirty tree
|
|
3517
|
+
// or a mid-operation state. On fetch/network/auth failure it FAILS OPEN to the
|
|
3518
|
+
// pre-existing (un-freshened) stale-base check. Per-project
|
|
3519
|
+
// `project.liveCheckoutAutoFreshen` overrides this. Resolution precedence lives
|
|
3520
|
+
// in `resolveLiveCheckoutAutoFreshen`. Default OFF (conservative — a human opts
|
|
3521
|
+
// individual projects in). Only affects projects running in live/in-place
|
|
3522
|
+
// (or hybrid liveValidation) checkout mode.
|
|
3523
|
+
liveCheckoutAutoFreshen: false,
|
|
3397
3524
|
orphanHolderScanTimeoutMs: 5000, // 5s ceiling for the cross-platform holder scan (PowerShell / /proc walk / lsof)
|
|
3398
3525
|
ccMaxTurns: 50, // max tool-use turns per CC/doc-chat call before CLI stops (per response, not per session)
|
|
3399
|
-
ccWorkerIdleTimeoutMs: 30 * 60 * 1000, // W-mr0qs0vw: idle-reaper window for the persistent `copilot --acp` worker pool (engine/cc-worker-pool.js). After this much inactivity with no in-flight turn the warm ACP process is killed; the next message cold-spawns a fresh session with NO memory of prior turns (CC shows a "context cleared after inactivity" notice). Tradeoff: shorter = less idle memory/process footprint, longer = more context durability across gaps between messages. Wired into the pool via ccWorkerPool.setIdleTimeoutMs() on every reloadConfig(); clamped to [60000, 28800000] (1min–8h) in the settings POST handler.
|
|
3526
|
+
ccWorkerIdleTimeoutMs: 30 * 60 * 1000, // W-mr0qs0vw: idle-reaper window for the persistent `copilot --acp` worker pool (engine/cc-worker-pool.js). After this much inactivity with no in-flight turn the warm ACP process is killed; the next message cold-spawns a fresh session with NO memory of prior turns (CC shows a "context cleared after inactivity" notice). Tradeoff: shorter = less idle memory/process footprint, longer = more context durability across gaps between messages. Wired into the pool via ccWorkerPool.setIdleTimeoutMs() on every reloadConfig(); clamped to [60000, 28800000] (1min–8h) in the settings POST handler. W-mr1qbj90000d30bd: `0` is an explicit "never idle-reap" sentinel that bypasses the clamp — cc-worker-pool.js maps it to an Infinity window so only an explicit closeTab kills the warm worker; Settings page exposes this as the "Never idle-reap CC workers" toggle.
|
|
3400
3527
|
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.
|
|
3401
3528
|
docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity (LRU; sessions are non-expiring otherwise)
|
|
3402
3529
|
ccLiveStreamMaxAgeMs: 30 * 60 * 1000, // hard cap reconnect buffers if abort/cleanup stalls
|
|
@@ -3698,10 +3825,7 @@ function resolveCcUseWorkerPool(engine) {
|
|
|
3698
3825
|
* resolveCcUseWorkerPool's CC-runtime guard, W-mphlriic00095f69).
|
|
3699
3826
|
* 2. `engine.agentUseWorkerPool` explicit true/false — operator override
|
|
3700
3827
|
* wins *within* an ACP-capable runtime.
|
|
3701
|
-
* 3. `ENGINE_DEFAULTS.agentUseWorkerPool`
|
|
3702
|
-
* Deliberately different from CC's default-true: the fleet dispatch path
|
|
3703
|
-
* has a much larger MCP-auth-popup / isolation surface than the single
|
|
3704
|
-
* CC singleton, so this stays off until proven safe.
|
|
3828
|
+
* 3. `ENGINE_DEFAULTS.agentUseWorkerPool` — default OFF.
|
|
3705
3829
|
*/
|
|
3706
3830
|
function resolveAgentUseWorkerPool(engine, runtime) {
|
|
3707
3831
|
if (!runtime || !runtime.capabilities || runtime.capabilities.acpWorkerPool !== true) return false;
|
|
@@ -3839,6 +3963,24 @@ function resolvePropagateClaudeMdForNonClaudeRuntimes(project, engine) {
|
|
|
3839
3963
|
return true;
|
|
3840
3964
|
}
|
|
3841
3965
|
|
|
3966
|
+
// W-mrxv648b — resolve the effective "create PRs as draft" flag for a dispatch.
|
|
3967
|
+
// Resolution order: per-project `project.createPrsAsDraft` (explicit boolean)
|
|
3968
|
+
// wins → else `engine.createPrsAsDraft` (explicit boolean) → else
|
|
3969
|
+
// ENGINE_DEFAULTS.createPrsAsDraft (false). Only an explicit boolean counts as
|
|
3970
|
+
// "set" at either level — undefined/null/absent falls through to the next tier
|
|
3971
|
+
// so operators can enable draft PRs fleet-wide while opting a specific project
|
|
3972
|
+
// OUT (or vice-versa). Pure, no I/O. Consumed by
|
|
3973
|
+
// engine/playbook.js#getPrCreateInstructions.
|
|
3974
|
+
function resolveCreatePrsAsDraft(project, engine) {
|
|
3975
|
+
if (project && typeof project === 'object' && typeof project.createPrsAsDraft === 'boolean') {
|
|
3976
|
+
return project.createPrsAsDraft;
|
|
3977
|
+
}
|
|
3978
|
+
if (engine && typeof engine === 'object' && typeof engine.createPrsAsDraft === 'boolean') {
|
|
3979
|
+
return engine.createPrsAsDraft;
|
|
3980
|
+
}
|
|
3981
|
+
return ENGINE_DEFAULTS.createPrsAsDraft;
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3842
3984
|
// ─── Legacy ccModel → defaultModel Migration ─────────────────────────────────
|
|
3843
3985
|
//
|
|
3844
3986
|
// Pre-P-3b8e5f1d, `engine.ccModel` was the single fleet-wide model knob (it
|
|
@@ -4705,6 +4847,9 @@ const FAILURE_CLASS = {
|
|
|
4705
4847
|
OUT_OF_CONTEXT: 'out-of-context', // Context window exhausted (token limit, context length)
|
|
4706
4848
|
MAX_TURNS: 'max-turns', // Claude CLI error_max_turns — work in progress, retryable
|
|
4707
4849
|
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
|
|
4850
|
+
COMPLETION_REPORT_MISSING: 'completion-report-missing', // W-mrju48xn000a5b84: mutating agent exited without a valid required JSON completion report; process exit alone is not proof of successful completion
|
|
4851
|
+
RUNTIME_TRUNCATED_EXIT: 'runtime-truncated-exit', // W-ms0vk9nv000z985b: the runtime process exited 0 in the MIDDLE of a turn — it produced neither a terminal result (no final assistant text from the adapter's own parseOutput) nor the required completion report. Observed when Copilot exits 0 right after starting a long-running tool call (e.g. `npm test`): the last event is a tool start with no matching completion. This is a truncated runtime turn, NOT an agent that finished and forgot to write its report, so it gets its own explicitly-diagnosed class instead of the generic COMPLETION_REPORT_MISSING. Retryable — a fresh attempt normally runs to completion. The completion-report requirement itself is unchanged: the dispatch still fails.
|
|
4852
|
+
RUNTIME_SESSION_STALE: 'runtime-session-stale', // W-ms0vk9nv000z985b: a `--resume`d runtime died because its persisted session is no longer usable — the CLI reported the backing conversation is gone, or its persisted working directory no longer exists (Copilot's `resume-auto-cd: ignoring persisted cwd ... missing or not a directory`, emitted when the worktree the session was created in has since been removed). Without this class the run surfaced as an opaque EMPTY_OUTPUT because the CLI exits 1 with no stdout at all. Retryable — the engine clears the cached session so the retry starts a fresh one.
|
|
4708
4853
|
WORKTREE_PREFLIGHT: 'worktree-preflight', // Pre-spawn worktree validation rejected (nested-in-project, drive-root collapse) — never retryable
|
|
4709
4854
|
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.
|
|
4710
4855
|
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.
|
|
@@ -4735,8 +4880,92 @@ const FAILURE_CLASS = {
|
|
|
4735
4880
|
UNKNOWN: 'unknown', // Unclassified failure
|
|
4736
4881
|
};
|
|
4737
4882
|
|
|
4883
|
+
function resolveTerminalDispatchResult({
|
|
4884
|
+
code,
|
|
4885
|
+
agentReportedFailure = false,
|
|
4886
|
+
autoRecovered = false,
|
|
4887
|
+
benignNoop = false,
|
|
4888
|
+
trustedCompletionSuccess = false,
|
|
4889
|
+
hardContractFail = false,
|
|
4890
|
+
nonceFail = false,
|
|
4891
|
+
completionReportFail = false,
|
|
4892
|
+
keepProcessesAcceptanceFail = false,
|
|
4893
|
+
managedSpawnAcceptanceFail = false,
|
|
4894
|
+
managedSpawnHealthcheckFail = false,
|
|
4895
|
+
} = {}) {
|
|
4896
|
+
if (
|
|
4897
|
+
hardContractFail
|
|
4898
|
+
|| nonceFail
|
|
4899
|
+
|| completionReportFail
|
|
4900
|
+
|| keepProcessesAcceptanceFail
|
|
4901
|
+
|| managedSpawnAcceptanceFail
|
|
4902
|
+
|| managedSpawnHealthcheckFail
|
|
4903
|
+
) {
|
|
4904
|
+
return DISPATCH_RESULT.ERROR;
|
|
4905
|
+
}
|
|
4906
|
+
// A nonce-verified terminal-success completion report (trustedCompletionSuccess)
|
|
4907
|
+
// must win over a kill-induced nonzero exit so a startup-timer / steering /
|
|
4908
|
+
// watchdog kill that races a valid success report cannot persist a
|
|
4909
|
+
// contradictory `workerState:failed` alongside `structuredCompletion.status:
|
|
4910
|
+
// success` (incident ralph-fix-mrz8ka5g0015124a). It is gated by the same hard-
|
|
4911
|
+
// failure early-returns above, so a forged/failed/contract-violating report can
|
|
4912
|
+
// never ride this path to success.
|
|
4913
|
+
return ((code === 0 && !agentReportedFailure) || autoRecovered || benignNoop || trustedCompletionSuccess)
|
|
4914
|
+
? DISPATCH_RESULT.SUCCESS
|
|
4915
|
+
: DISPATCH_RESULT.ERROR;
|
|
4916
|
+
}
|
|
4917
|
+
|
|
4918
|
+
function resolveTerminalWorkerState({ code, effectiveResult, errorReason } = {}) {
|
|
4919
|
+
if (effectiveResult === DISPATCH_RESULT.SUCCESS) {
|
|
4920
|
+
return {
|
|
4921
|
+
status: AGENT_STATUS.FINISHED,
|
|
4922
|
+
detail: code === 0
|
|
4923
|
+
? 'Agent completion validated'
|
|
4924
|
+
: `Agent work recovered after process exit code ${code}`,
|
|
4925
|
+
};
|
|
4926
|
+
}
|
|
4927
|
+
return {
|
|
4928
|
+
status: AGENT_STATUS.FAILED,
|
|
4929
|
+
detail: errorReason || (code === 0
|
|
4930
|
+
? 'Agent process exited but completion validation failed'
|
|
4931
|
+
: `Agent exited with code ${code}`),
|
|
4932
|
+
};
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4935
|
+
const _dispatchFinalizationOwners = new Map();
|
|
4936
|
+
const _dispatchFinalizationWaiters = new Map();
|
|
4937
|
+
|
|
4938
|
+
function claimDispatchFinalization(dispatchId, owner) {
|
|
4939
|
+
if (!dispatchId || !owner || _dispatchFinalizationOwners.has(dispatchId)) return false;
|
|
4940
|
+
_dispatchFinalizationOwners.set(dispatchId, owner);
|
|
4941
|
+
return true;
|
|
4942
|
+
}
|
|
4943
|
+
|
|
4944
|
+
function releaseDispatchFinalization(dispatchId, owner) {
|
|
4945
|
+
if (_dispatchFinalizationOwners.get(dispatchId) !== owner) return false;
|
|
4946
|
+
_dispatchFinalizationOwners.delete(dispatchId);
|
|
4947
|
+
const waiters = _dispatchFinalizationWaiters.get(dispatchId);
|
|
4948
|
+
if (waiters) {
|
|
4949
|
+
_dispatchFinalizationWaiters.delete(dispatchId);
|
|
4950
|
+
for (const resolve of waiters) resolve();
|
|
4951
|
+
}
|
|
4952
|
+
return true;
|
|
4953
|
+
}
|
|
4954
|
+
|
|
4955
|
+
function waitForDispatchFinalization(dispatchId) {
|
|
4956
|
+
if (!_dispatchFinalizationOwners.has(dispatchId)) return Promise.resolve();
|
|
4957
|
+
return new Promise(resolve => {
|
|
4958
|
+
let waiters = _dispatchFinalizationWaiters.get(dispatchId);
|
|
4959
|
+
if (!waiters) {
|
|
4960
|
+
waiters = new Set();
|
|
4961
|
+
_dispatchFinalizationWaiters.set(dispatchId, waiters);
|
|
4962
|
+
}
|
|
4963
|
+
waiters.add(resolve);
|
|
4964
|
+
});
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4738
4967
|
// Structured completion protocol — fields agents must produce in ```completion blocks
|
|
4739
|
-
const COMPLETION_FIELDS = ['status', 'summary', 'files_changed', 'tests', 'pr', 'not_changed', 'failure_class', 'retryable', 'needs_rerun', 'verdict', 'artifacts', 'nonce', 'securityFlags'];
|
|
4968
|
+
const COMPLETION_FIELDS = ['status', 'summary', 'files_changed', 'tests', 'pr', 'not_changed', 'failure_class', 'retryable', 'needs_rerun', 'verdict', 'artifacts', 'nonce', 'securityFlags', 'reviewLearnings', 'reviewLearningApplications'];
|
|
4740
4969
|
|
|
4741
4970
|
const DEFAULT_AGENT_METRICS = {
|
|
4742
4971
|
tasksCompleted: 0, tasksErrored: 0,
|
|
@@ -4778,13 +5007,14 @@ function _bumpPruneDefaultClaudeStrip() {
|
|
|
4778
5007
|
} catch {}
|
|
4779
5008
|
}
|
|
4780
5009
|
|
|
4781
|
-
function pruneDefaultClaudeConfig(config) {
|
|
5010
|
+
function pruneDefaultClaudeConfig(config, options = {}) {
|
|
5011
|
+
const trackStrip = options.trackStrip !== false;
|
|
4782
5012
|
if (!config || typeof config !== 'object') return false;
|
|
4783
5013
|
const claude = config.claude;
|
|
4784
5014
|
if (claude === undefined || claude === null) return false;
|
|
4785
5015
|
if (typeof claude !== 'object' || Array.isArray(claude)) {
|
|
4786
5016
|
delete config.claude;
|
|
4787
|
-
_bumpPruneDefaultClaudeStrip();
|
|
5017
|
+
if (trackStrip) _bumpPruneDefaultClaudeStrip();
|
|
4788
5018
|
return true;
|
|
4789
5019
|
}
|
|
4790
5020
|
|
|
@@ -4811,7 +5041,7 @@ function pruneDefaultClaudeConfig(config) {
|
|
|
4811
5041
|
delete config.claude;
|
|
4812
5042
|
changed = true;
|
|
4813
5043
|
}
|
|
4814
|
-
if (changed) _bumpPruneDefaultClaudeStrip();
|
|
5044
|
+
if (changed && trackStrip) _bumpPruneDefaultClaudeStrip();
|
|
4815
5045
|
return changed;
|
|
4816
5046
|
}
|
|
4817
5047
|
|
|
@@ -5046,6 +5276,54 @@ function formatUnknownProjectError(projectName, projects = []) {
|
|
|
5046
5276
|
return `Project "${projectName}" not found. Known projects: ${known}`;
|
|
5047
5277
|
}
|
|
5048
5278
|
|
|
5279
|
+
function _parseProjectRepositoryIdentity(value) {
|
|
5280
|
+
const raw = String(value || '').trim();
|
|
5281
|
+
if (!raw) return null;
|
|
5282
|
+
|
|
5283
|
+
const canonicalScope = _normalizeScopeString(raw);
|
|
5284
|
+
if (canonicalScope) return { kind: 'scope', value: canonicalScope };
|
|
5285
|
+
|
|
5286
|
+
const remoteScope = _parseRemoteUrlToScope(raw);
|
|
5287
|
+
if (remoteScope) return { kind: 'scope', value: remoteScope };
|
|
5288
|
+
|
|
5289
|
+
if (path.isAbsolute(raw) || raw.startsWith('.') || raw.includes('\\') || raw.includes(':')) {
|
|
5290
|
+
return null;
|
|
5291
|
+
}
|
|
5292
|
+
const parts = raw
|
|
5293
|
+
.replace(/\.git$/i, '')
|
|
5294
|
+
.replace(/^\/+|\/+$/g, '')
|
|
5295
|
+
.split('/')
|
|
5296
|
+
.map(normalizePrScopeSegment)
|
|
5297
|
+
.filter(Boolean);
|
|
5298
|
+
if (parts.length === 1) return { kind: 'basename', value: parts[0] };
|
|
5299
|
+
if (parts.length === 2 || parts.length === 3) return { kind: 'slug', value: parts.join('/') };
|
|
5300
|
+
return null;
|
|
5301
|
+
}
|
|
5302
|
+
|
|
5303
|
+
function _findProjectsByRepositoryIdentity(value, projects) {
|
|
5304
|
+
const identity = _parseProjectRepositoryIdentity(value);
|
|
5305
|
+
if (!identity) return [];
|
|
5306
|
+
|
|
5307
|
+
const matches = [];
|
|
5308
|
+
for (const project of projects || []) {
|
|
5309
|
+
if (!project) continue;
|
|
5310
|
+
const scopes = getProjectAllPrScopes(project);
|
|
5311
|
+
const matched = scopes.some(scope => {
|
|
5312
|
+
if (identity.kind === 'scope') return scope === identity.value;
|
|
5313
|
+
const slug = scope.slice(scope.indexOf(':') + 1);
|
|
5314
|
+
if (identity.kind === 'slug') return slug === identity.value;
|
|
5315
|
+
return slug.split('/').pop() === identity.value;
|
|
5316
|
+
});
|
|
5317
|
+
if (matched && !matches.includes(project)) matches.push(project);
|
|
5318
|
+
}
|
|
5319
|
+
return matches;
|
|
5320
|
+
}
|
|
5321
|
+
|
|
5322
|
+
function _formatAmbiguousProjectRepositoryError(value, projects) {
|
|
5323
|
+
const names = [...new Set(projects.map(project => project?.name).filter(Boolean))];
|
|
5324
|
+
return `Repository identity "${value}" is ambiguous; it matches multiple configured projects: ${names.join(', ')}. Use an exact configured project name.`;
|
|
5325
|
+
}
|
|
5326
|
+
|
|
5049
5327
|
function findProjectByName(projects, projectName) {
|
|
5050
5328
|
const name = String(projectName || '').trim().toLowerCase();
|
|
5051
5329
|
if (!name) return null;
|
|
@@ -5066,9 +5344,12 @@ function resolveConfiguredProject(projectName, projectsOrConfig, options = {}) {
|
|
|
5066
5344
|
const value = String(raw || '').trim();
|
|
5067
5345
|
if (value) {
|
|
5068
5346
|
const project = findProjectByNameOrPath(projects, value);
|
|
5069
|
-
return project
|
|
5070
|
-
|
|
5071
|
-
|
|
5347
|
+
if (project) return { project, explicit: true, value };
|
|
5348
|
+
const repositoryMatches = _findProjectsByRepositoryIdentity(value, projects);
|
|
5349
|
+
const error = repositoryMatches.length > 1
|
|
5350
|
+
? _formatAmbiguousProjectRepositoryError(value, repositoryMatches)
|
|
5351
|
+
: formatUnknownProjectError(value, projects);
|
|
5352
|
+
return { project: null, explicit: true, value, error };
|
|
5072
5353
|
}
|
|
5073
5354
|
if (options.defaultWhenSingle && projects.length === 1) {
|
|
5074
5355
|
return { project: projects[0], explicit: false, value: '' };
|
|
@@ -5152,6 +5433,25 @@ function resolveProjectSource(source, projectsOrConfig, options = {}) {
|
|
|
5152
5433
|
}
|
|
5153
5434
|
}
|
|
5154
5435
|
|
|
5436
|
+
// Repository identities are fallback aliases; exact configured names and
|
|
5437
|
+
// paths above remain authoritative.
|
|
5438
|
+
const repositoryMatches = _findProjectsByRepositoryIdentity(value, projects);
|
|
5439
|
+
if (repositoryMatches.length === 1) {
|
|
5440
|
+
return _projectSourceDescriptor(repositoryMatches[0], value, explicit, minionsDir);
|
|
5441
|
+
}
|
|
5442
|
+
if (repositoryMatches.length > 1) {
|
|
5443
|
+
return {
|
|
5444
|
+
project: null,
|
|
5445
|
+
explicit: true,
|
|
5446
|
+
value,
|
|
5447
|
+
sourceName: '',
|
|
5448
|
+
isCentral: false,
|
|
5449
|
+
scope: null,
|
|
5450
|
+
stateDir: null,
|
|
5451
|
+
error: _formatAmbiguousProjectRepositoryError(value, repositoryMatches),
|
|
5452
|
+
};
|
|
5453
|
+
}
|
|
5454
|
+
|
|
5155
5455
|
return {
|
|
5156
5456
|
project: null,
|
|
5157
5457
|
explicit: true,
|
|
@@ -6216,6 +6516,7 @@ function buildSecurityHeaders() {
|
|
|
6216
6516
|
// keys, and shell invocations — never bypass them for "internal" callers.
|
|
6217
6517
|
|
|
6218
6518
|
const PROJECT_NAME_RE = /^[a-zA-Z0-9_\-]{1,64}$/;
|
|
6519
|
+
const CC_TURN_ID_MAX = 80;
|
|
6219
6520
|
|
|
6220
6521
|
function _httpError(status, message, extra) {
|
|
6221
6522
|
const err = new Error(message);
|
|
@@ -6271,6 +6572,14 @@ function validateProjectPath(pathStr, options = {}) {
|
|
|
6271
6572
|
if (!fs.existsSync(resolved)) {
|
|
6272
6573
|
throw _httpError(400, `Invalid project path: directory does not exist: ${resolved}`);
|
|
6273
6574
|
}
|
|
6575
|
+
let stat;
|
|
6576
|
+
try { stat = fs.statSync(resolved); }
|
|
6577
|
+
catch (e) {
|
|
6578
|
+
throw _httpError(400, `Invalid project path: cannot inspect directory: ${resolved} (${e.message})`);
|
|
6579
|
+
}
|
|
6580
|
+
if (!stat.isDirectory()) {
|
|
6581
|
+
throw _httpError(400, `Invalid project path: path is not a directory: ${resolved}`);
|
|
6582
|
+
}
|
|
6274
6583
|
const gitMarker = path.join(resolved, '.git');
|
|
6275
6584
|
if (fs.existsSync(gitMarker)) return resolved; // .git dir OR worktree .git file
|
|
6276
6585
|
|
|
@@ -6900,20 +7209,66 @@ function getProjectAllPrScopes(project) {
|
|
|
6900
7209
|
const _gitRemoteScopeCache = new Map();
|
|
6901
7210
|
const GIT_REMOTE_SCOPE_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
6902
7211
|
|
|
7212
|
+
function _remotePartsToScope(host, values) {
|
|
7213
|
+
const parts = values.map((value, index) => {
|
|
7214
|
+
let decoded = String(value || '');
|
|
7215
|
+
try { decoded = decodeURIComponent(decoded); } catch { /* retain malformed input for a non-match */ }
|
|
7216
|
+
if (index === values.length - 1) decoded = decoded.replace(/\.git$/i, '');
|
|
7217
|
+
return normalizePrScopeSegment(decoded);
|
|
7218
|
+
});
|
|
7219
|
+
if (parts.some(part => !part)) return null;
|
|
7220
|
+
return `${host}:${parts.join('/')}`;
|
|
7221
|
+
}
|
|
7222
|
+
|
|
6903
7223
|
// Best-effort parse of a git remote fetch URL into a canonical scope string.
|
|
6904
7224
|
// Handles the URL shapes git itself produces for `git remote -v` output:
|
|
6905
7225
|
// SSH (`git@host:owner/repo.git`), HTTPS (`https://host/owner/repo.git`),
|
|
6906
|
-
// and
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
let
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
7226
|
+
// and ADO HTTPS/SSH clone URLs. Returns null when unrecognized.
|
|
7227
|
+
function _parseRemoteUrlToScope(remoteUrl) {
|
|
7228
|
+
const raw = String(remoteUrl || '').trim();
|
|
7229
|
+
if (!raw) return null;
|
|
7230
|
+
|
|
7231
|
+
let normalized = raw;
|
|
7232
|
+
const scpMatch = raw.match(/^(?:([^@\s/:]+)@)?(github\.com|ssh\.dev\.azure\.com):(.+)$/i);
|
|
7233
|
+
if (scpMatch) {
|
|
7234
|
+
const user = scpMatch[1] ? `${scpMatch[1]}@` : '';
|
|
7235
|
+
normalized = `ssh://${user}${scpMatch[2]}/${scpMatch[3]}`;
|
|
7236
|
+
} else if (/^(?:github\.com|dev\.azure\.com|[^/\s]+\.visualstudio\.com)\//i.test(raw)) {
|
|
7237
|
+
normalized = `https://${raw}`;
|
|
7238
|
+
}
|
|
7239
|
+
|
|
7240
|
+
let parsed;
|
|
7241
|
+
try {
|
|
7242
|
+
parsed = new URL(normalized);
|
|
7243
|
+
} catch {
|
|
7244
|
+
return null;
|
|
7245
|
+
}
|
|
7246
|
+
const protocol = parsed.protocol.toLowerCase();
|
|
7247
|
+
if (!['http:', 'https:', 'ssh:', 'git:', 'git+ssh:'].includes(protocol)) return null;
|
|
7248
|
+
|
|
7249
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
7250
|
+
const parts = parsed.pathname.split('/').filter(Boolean);
|
|
7251
|
+
if (hostname === 'github.com' && parts.length === 2) {
|
|
7252
|
+
return _remotePartsToScope('github', parts);
|
|
7253
|
+
}
|
|
7254
|
+
if (hostname === 'dev.azure.com' &&
|
|
7255
|
+
parts.length === 4 &&
|
|
7256
|
+
String(parts[2]).toLowerCase() === '_git') {
|
|
7257
|
+
return _remotePartsToScope('ado', [parts[0], parts[1], parts[3]]);
|
|
7258
|
+
}
|
|
7259
|
+
if (hostname.endsWith('.visualstudio.com')) {
|
|
7260
|
+
const org = hostname.slice(0, -'.visualstudio.com'.length);
|
|
7261
|
+
const offset = String(parts[0] || '').toLowerCase() === 'defaultcollection' ? 1 : 0;
|
|
7262
|
+
if (org && parts.length === offset + 3 && String(parts[offset + 1]).toLowerCase() === '_git') {
|
|
7263
|
+
return _remotePartsToScope('ado', [org, parts[offset], parts[offset + 2]]);
|
|
7264
|
+
}
|
|
7265
|
+
}
|
|
7266
|
+
if (hostname === 'ssh.dev.azure.com' &&
|
|
7267
|
+
['ssh:', 'git+ssh:'].includes(protocol) &&
|
|
7268
|
+
parts.length === 4 &&
|
|
7269
|
+
String(parts[0]).toLowerCase() === 'v3') {
|
|
7270
|
+
return _remotePartsToScope('ado', parts.slice(1));
|
|
7271
|
+
}
|
|
6917
7272
|
return null;
|
|
6918
7273
|
}
|
|
6919
7274
|
|
|
@@ -8050,18 +8405,22 @@ function prFixEvidenceFingerprint(pr, cause = PR_FIX_CAUSE.UNKNOWN) {
|
|
|
8050
8405
|
|
|
8051
8406
|
function prReviewFindingIdentity(pr) {
|
|
8052
8407
|
const review = pr?.minionsReview || {};
|
|
8408
|
+
const persisted = String(review.findingIdentity || '').trim().toLowerCase();
|
|
8409
|
+
if (/^review-[0-9a-f]{16}$/.test(persisted)) return persisted;
|
|
8053
8410
|
const threads = Array.isArray(review.threads)
|
|
8054
8411
|
? review.threads.map(value => String(value)).filter(Boolean).sort()
|
|
8055
8412
|
: [];
|
|
8056
8413
|
const evidence = {
|
|
8057
8414
|
pr: pr?.id || pr?.url || '',
|
|
8058
8415
|
sourceItem: review.sourceItem || '',
|
|
8059
|
-
dispatchId: review.dispatchId || '',
|
|
8060
8416
|
reviewer: review.reviewer || '',
|
|
8061
|
-
|
|
8417
|
+
reviewedHeadSha: pr?.reviewedHeadSha || '',
|
|
8062
8418
|
threads,
|
|
8063
8419
|
note: review.note || pr?.reviewNote || '',
|
|
8064
8420
|
};
|
|
8421
|
+
if (!evidence.sourceItem && !evidence.reviewedHeadSha) {
|
|
8422
|
+
evidence.dispatchId = review.dispatchId || '';
|
|
8423
|
+
}
|
|
8065
8424
|
return `review-${crypto.createHash('sha1').update(JSON.stringify(evidence)).digest('hex').slice(0, 16)}`;
|
|
8066
8425
|
}
|
|
8067
8426
|
|
|
@@ -8165,6 +8524,7 @@ function isWorktreePathLive(worktreePath, opts = {}) {
|
|
|
8165
8524
|
const target = _normalizeWorktreePath(worktreePath);
|
|
8166
8525
|
if (!target) return false;
|
|
8167
8526
|
const excludeDispatchId = opts.excludeDispatchId ? String(opts.excludeDispatchId) : null;
|
|
8527
|
+
const excludeSpawnLeaseId = opts.excludeSpawnLeaseId ? String(opts.excludeSpawnLeaseId) : null;
|
|
8168
8528
|
let db = opts.db || null;
|
|
8169
8529
|
if (!db) {
|
|
8170
8530
|
try { db = require('./db').getDb(); }
|
|
@@ -8182,7 +8542,8 @@ function isWorktreePathLive(worktreePath, opts = {}) {
|
|
|
8182
8542
|
rows = db.prepare(`
|
|
8183
8543
|
SELECT id,
|
|
8184
8544
|
json_extract(data, '$.worktreePath') AS top_wt,
|
|
8185
|
-
json_extract(data, '$.meta.worktreePath') AS meta_wt
|
|
8545
|
+
json_extract(data, '$.meta.worktreePath') AS meta_wt,
|
|
8546
|
+
json_extract(data, '$._spawnLeaseId') AS spawn_lease_id
|
|
8186
8547
|
FROM dispatches
|
|
8187
8548
|
WHERE status IN ('pending', 'active')
|
|
8188
8549
|
`).all();
|
|
@@ -8191,7 +8552,9 @@ function isWorktreePathLive(worktreePath, opts = {}) {
|
|
|
8191
8552
|
return true;
|
|
8192
8553
|
}
|
|
8193
8554
|
for (const row of rows || []) {
|
|
8194
|
-
if (excludeDispatchId && String(row.id) === excludeDispatchId)
|
|
8555
|
+
if (excludeDispatchId && String(row.id) === excludeDispatchId) {
|
|
8556
|
+
if (!excludeSpawnLeaseId || String(row.spawn_lease_id || '') === excludeSpawnLeaseId) continue;
|
|
8557
|
+
}
|
|
8195
8558
|
if (row.top_wt && _normalizeWorktreePath(row.top_wt) === target) return true;
|
|
8196
8559
|
if (row.meta_wt && _normalizeWorktreePath(row.meta_wt) === target) return true;
|
|
8197
8560
|
}
|
|
@@ -8214,6 +8577,7 @@ function getWorktreeBlockingDispatchInfo(worktreePath, opts = {}) {
|
|
|
8214
8577
|
const target = _normalizeWorktreePath(worktreePath);
|
|
8215
8578
|
if (!target) return null;
|
|
8216
8579
|
const excludeDispatchId = opts.excludeDispatchId ? String(opts.excludeDispatchId) : null;
|
|
8580
|
+
const excludeSpawnLeaseId = opts.excludeSpawnLeaseId ? String(opts.excludeSpawnLeaseId) : null;
|
|
8217
8581
|
let db = opts.db || null;
|
|
8218
8582
|
if (!db) {
|
|
8219
8583
|
try { db = require('./db').getDb(); }
|
|
@@ -8223,12 +8587,15 @@ function getWorktreeBlockingDispatchInfo(worktreePath, opts = {}) {
|
|
|
8223
8587
|
const rows = db.prepare(`
|
|
8224
8588
|
SELECT id, status, updated_at,
|
|
8225
8589
|
json_extract(data, '$.worktreePath') AS top_wt,
|
|
8226
|
-
json_extract(data, '$.meta.worktreePath') AS meta_wt
|
|
8590
|
+
json_extract(data, '$.meta.worktreePath') AS meta_wt,
|
|
8591
|
+
json_extract(data, '$._spawnLeaseId') AS spawn_lease_id
|
|
8227
8592
|
FROM dispatches
|
|
8228
8593
|
WHERE status IN ('pending', 'active')
|
|
8229
8594
|
`).all();
|
|
8230
8595
|
for (const row of rows || []) {
|
|
8231
|
-
if (excludeDispatchId && String(row.id) === excludeDispatchId)
|
|
8596
|
+
if (excludeDispatchId && String(row.id) === excludeDispatchId) {
|
|
8597
|
+
if (!excludeSpawnLeaseId || String(row.spawn_lease_id || '') === excludeSpawnLeaseId) continue;
|
|
8598
|
+
}
|
|
8232
8599
|
const topMatch = row.top_wt && _normalizeWorktreePath(row.top_wt) === target;
|
|
8233
8600
|
const metaMatch = row.meta_wt && _normalizeWorktreePath(row.meta_wt) === target;
|
|
8234
8601
|
if (topMatch || metaMatch) {
|
|
@@ -8319,6 +8686,10 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot, opts = {}) {
|
|
|
8319
8686
|
log('warn', `removeWorktree: refusing to remove ${wtPath} — not under ${worktreeRoot}`);
|
|
8320
8687
|
return false;
|
|
8321
8688
|
}
|
|
8689
|
+
if (hasProtectedQuarantineMarker(resolved)) {
|
|
8690
|
+
log('warn', `removeWorktree: refusing to remove ${wtPath} — protected quarantine contains work without complete recovery refs`);
|
|
8691
|
+
return false;
|
|
8692
|
+
}
|
|
8322
8693
|
// ── Data-loss hardening (W-mqecdoot) — NEVER recursively delete a real git
|
|
8323
8694
|
// main repository. ──────────────────────────────────────────────────────
|
|
8324
8695
|
// The containment check above only proves `resolved` is *under* the
|
|
@@ -8378,6 +8749,7 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot, opts = {}) {
|
|
|
8378
8749
|
_writeWorktreeSkipLiveInboxNote(wtPath, 'shared.removeWorktree', blockingInfo);
|
|
8379
8750
|
return false;
|
|
8380
8751
|
}
|
|
8752
|
+
const hadOwnerMarker = hasWorktreeOwnerMarker(resolved);
|
|
8381
8753
|
_pruneRemoveWorktreeFailures();
|
|
8382
8754
|
// Skip paths that failed 3+ times — retry after 1 hour cooldown
|
|
8383
8755
|
const prior = _removeWorktreeFailures.get(resolved);
|
|
@@ -8433,6 +8805,7 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot, opts = {}) {
|
|
|
8433
8805
|
log('warn', `removeWorktree: rd /s /q fallback failed for ${wtPath}: ${rdErr.message}`);
|
|
8434
8806
|
}
|
|
8435
8807
|
}
|
|
8808
|
+
_restoreWorktreeOwnerMarkerAfterFailedRemoval(resolved, hadOwnerMarker);
|
|
8436
8809
|
const fail = _removeWorktreeFailures.get(resolved) || { count: 0, lastAttempt: 0 };
|
|
8437
8810
|
fail.count++;
|
|
8438
8811
|
fail.lastAttempt = Date.now();
|
|
@@ -8456,6 +8829,9 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot, opts = {}) {
|
|
|
8456
8829
|
// ownership before removal; a worktree with NO marker is treated as foreign
|
|
8457
8830
|
// and left untouched (fail-open: leak a dir rather than nuke someone's work).
|
|
8458
8831
|
const WORKTREE_OWNER_MARKER = '.minions-worktree';
|
|
8832
|
+
const WORKTREE_RETRY_PATH_MARKER = '.retry-';
|
|
8833
|
+
const WORKTREE_QUARANTINE_PROTECTION_MARKER = '.minions-quarantine-protected';
|
|
8834
|
+
const WORKTREE_QUARANTINE_PROTECTED_PATH_TOKEN = '-quarantine-protected-';
|
|
8459
8835
|
|
|
8460
8836
|
// Best-effort stamp dropped right after a successful `git worktree add`. Never
|
|
8461
8837
|
// throws — a missing marker only costs us the ability to auto-GC that dir.
|
|
@@ -8489,6 +8865,36 @@ function hasWorktreeOwnerMarker(worktreePath) {
|
|
|
8489
8865
|
}
|
|
8490
8866
|
}
|
|
8491
8867
|
|
|
8868
|
+
function hasProtectedQuarantineMarker(worktreePath) {
|
|
8869
|
+
if (!worktreePath) return false;
|
|
8870
|
+
try {
|
|
8871
|
+
let current = path.resolve(worktreePath);
|
|
8872
|
+
while (true) {
|
|
8873
|
+
if (path.basename(current).includes(WORKTREE_QUARANTINE_PROTECTED_PATH_TOKEN)
|
|
8874
|
+
|| fs.existsSync(path.join(current, WORKTREE_QUARANTINE_PROTECTION_MARKER))) {
|
|
8875
|
+
return true;
|
|
8876
|
+
}
|
|
8877
|
+
const parent = path.dirname(current);
|
|
8878
|
+
if (parent === current) return false;
|
|
8879
|
+
current = parent;
|
|
8880
|
+
}
|
|
8881
|
+
} catch {
|
|
8882
|
+
return true;
|
|
8883
|
+
}
|
|
8884
|
+
}
|
|
8885
|
+
|
|
8886
|
+
function _restoreWorktreeOwnerMarkerAfterFailedRemoval(worktreePath, hadOwnerMarker) {
|
|
8887
|
+
if (!hadOwnerMarker || !worktreePath) return false;
|
|
8888
|
+
let exists = false;
|
|
8889
|
+
try { exists = fs.existsSync(worktreePath); } catch { return false; }
|
|
8890
|
+
if (!exists) return false;
|
|
8891
|
+
try {
|
|
8892
|
+
const gitStat = fs.lstatSync(path.join(path.resolve(worktreePath), '.git'));
|
|
8893
|
+
if (gitStat && gitStat.isDirectory()) return false;
|
|
8894
|
+
} catch { /* a missing .git is expected for a partially removed worktree */ }
|
|
8895
|
+
return writeWorktreeOwnerMarker(worktreePath, { restoredAfterFailedRemoval: true });
|
|
8896
|
+
}
|
|
8897
|
+
|
|
8492
8898
|
// True when a `git status --porcelain` line refers ONLY to the engine's own
|
|
8493
8899
|
// worktree-ownership marker (.minions-worktree) at the worktree root (#284).
|
|
8494
8900
|
// The marker is gitignored in the repo's current tree, but a reused worktree
|
|
@@ -8720,6 +9126,77 @@ function createBackoffTracker({ baseMs = 30000, maxMs = 10 * 60 * 1000 } = {}) {
|
|
|
8720
9126
|
return { recordFailure, recordSuccess, isBackingOff, getState, _reset, _setForTest };
|
|
8721
9127
|
}
|
|
8722
9128
|
|
|
9129
|
+
// ── npm package identity (W-ms1j8m54000g4a4a) ───────────────────────────────
|
|
9130
|
+
// Minions is published to more than one npm distribution channel: the public
|
|
9131
|
+
// `@yemi33/minions` and internal builds renamed by tools/prepare-package.js
|
|
9132
|
+
// (`INTERNAL_PACKAGE_NAME`, default `@yemishin_microsoft/minions`). Anything
|
|
9133
|
+
// that shells out to `npm view` / `npm install -g` / `npm uninstall -g` must
|
|
9134
|
+
// target the package that is ACTUALLY installed — hardcoding the public name
|
|
9135
|
+
// makes an internal install resolve versions against the wrong channel and
|
|
9136
|
+
// then install a SECOND package that claims the same `minions` bin, which npm
|
|
9137
|
+
// rejects with EEXIST.
|
|
9138
|
+
//
|
|
9139
|
+
// The identity is read from the installed package manifest rather than a
|
|
9140
|
+
// build-time constant, so a single codebase behaves correctly on every channel.
|
|
9141
|
+
const PUBLIC_PACKAGE_NAME = '@yemi33/minions';
|
|
9142
|
+
|
|
9143
|
+
// npm's package-name rules, restricted to the subset we need: lowercase,
|
|
9144
|
+
// url-safe, optional `@scope/` prefix, ≤214 chars. Also the security gate for
|
|
9145
|
+
// the manifest-derived name — the result is interpolated into a shell command,
|
|
9146
|
+
// so anything outside this grammar (spaces, `;`, `&&`, path traversal) is
|
|
9147
|
+
// rejected rather than escaped.
|
|
9148
|
+
const NPM_PACKAGE_NAME_RE = /^(?:@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*$/;
|
|
9149
|
+
// Exact versions only (never a floating tag) — `npm view` output is echoed
|
|
9150
|
+
// straight back into `npm install -g <pkg>@<version>`.
|
|
9151
|
+
const NPM_VERSION_RE = /^[0-9][0-9a-zA-Z.+-]*$/;
|
|
9152
|
+
|
|
9153
|
+
function isValidNpmPackageName(name) {
|
|
9154
|
+
if (typeof name !== 'string' || !name || name.length > 214) return false;
|
|
9155
|
+
return NPM_PACKAGE_NAME_RE.test(name);
|
|
9156
|
+
}
|
|
9157
|
+
|
|
9158
|
+
function isValidNpmVersion(version) {
|
|
9159
|
+
if (typeof version !== 'string' || !version || version.length > 256) return false;
|
|
9160
|
+
return NPM_VERSION_RE.test(version);
|
|
9161
|
+
}
|
|
9162
|
+
|
|
9163
|
+
// Resolve the active distribution's package name from `<pkgRoot>/package.json`.
|
|
9164
|
+
// Read fresh from disk (not require()) so an in-place npm update is observed
|
|
9165
|
+
// within the same process. Falls back to the public package whenever the
|
|
9166
|
+
// manifest is missing, unreadable, or carries a name we would not be willing to
|
|
9167
|
+
// put on a command line.
|
|
9168
|
+
function resolvePackageName(pkgRoot) {
|
|
9169
|
+
if (typeof pkgRoot !== 'string' || !pkgRoot) return PUBLIC_PACKAGE_NAME;
|
|
9170
|
+
try {
|
|
9171
|
+
const name = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8')).name;
|
|
9172
|
+
if (isValidNpmPackageName(name)) return name;
|
|
9173
|
+
} catch {}
|
|
9174
|
+
return PUBLIC_PACKAGE_NAME;
|
|
9175
|
+
}
|
|
9176
|
+
|
|
9177
|
+
function assertNpmPackageName(pkgName) {
|
|
9178
|
+
if (!isValidNpmPackageName(pkgName)) {
|
|
9179
|
+
throw new Error(`Refusing to run npm against an invalid package name: ${JSON.stringify(pkgName)}`);
|
|
9180
|
+
}
|
|
9181
|
+
return pkgName;
|
|
9182
|
+
}
|
|
9183
|
+
|
|
9184
|
+
function buildNpmViewVersionCommand(pkgName) {
|
|
9185
|
+
return `npm view ${assertNpmPackageName(pkgName)} version`;
|
|
9186
|
+
}
|
|
9187
|
+
|
|
9188
|
+
function buildNpmGlobalInstallCommand(pkgName, version) {
|
|
9189
|
+
assertNpmPackageName(pkgName);
|
|
9190
|
+
if (!isValidNpmVersion(version)) {
|
|
9191
|
+
throw new Error(`Refusing to install an invalid npm version: ${JSON.stringify(version)}`);
|
|
9192
|
+
}
|
|
9193
|
+
return `npm install -g ${pkgName}@${version}`;
|
|
9194
|
+
}
|
|
9195
|
+
|
|
9196
|
+
function buildNpmGlobalUninstallCommand(pkgName) {
|
|
9197
|
+
return `npm uninstall -g ${assertNpmPackageName(pkgName)}`;
|
|
9198
|
+
}
|
|
9199
|
+
|
|
8723
9200
|
module.exports = {
|
|
8724
9201
|
MINIONS_DIR,
|
|
8725
9202
|
ENGINE_DIR,
|
|
@@ -8752,6 +9229,14 @@ module.exports = {
|
|
|
8752
9229
|
// Node / node:sqlite version gate (issue #244)
|
|
8753
9230
|
NODE_SQLITE_MIN_VERSION,
|
|
8754
9231
|
compareDottedVersions,
|
|
9232
|
+
// npm distribution identity (W-ms1j8m54000g4a4a)
|
|
9233
|
+
PUBLIC_PACKAGE_NAME,
|
|
9234
|
+
isValidNpmPackageName,
|
|
9235
|
+
isValidNpmVersion,
|
|
9236
|
+
resolvePackageName,
|
|
9237
|
+
buildNpmViewVersionCommand,
|
|
9238
|
+
buildNpmGlobalInstallCommand,
|
|
9239
|
+
buildNpmGlobalUninstallCommand,
|
|
8755
9240
|
nodeSupportsBuiltinSqlite,
|
|
8756
9241
|
nodeSqliteRemediationLine,
|
|
8757
9242
|
log,
|
|
@@ -8761,6 +9246,7 @@ module.exports = {
|
|
|
8761
9246
|
safeJson, safeJsonObj, safeJsonArr, safeJsonNoRestore,
|
|
8762
9247
|
safeWrite,
|
|
8763
9248
|
mutateTextFileLocked,
|
|
9249
|
+
DELETE_JSON_FILE,
|
|
8764
9250
|
safeUnlink,
|
|
8765
9251
|
createDispatchTmpDir,
|
|
8766
9252
|
removeDispatchTmpDir,
|
|
@@ -8825,6 +9311,8 @@ module.exports = {
|
|
|
8825
9311
|
cleanChildEnv,
|
|
8826
9312
|
gitEnv,
|
|
8827
9313
|
parseStreamJsonOutput,
|
|
9314
|
+
isStaleResumeSessionSignal, // W-ms0vk9nv000z985b — stale `--resume` session/cwd detection
|
|
9315
|
+
staleResumeSessionCwdReason, // W-ms0vk9nv000z985b — refuse resuming a session anchored to a dead/other worktree
|
|
8828
9316
|
KB_CATEGORIES,
|
|
8829
9317
|
KB_READABLE_CATEGORIES,
|
|
8830
9318
|
classifyInboxItem,
|
|
@@ -8836,6 +9324,7 @@ module.exports = {
|
|
|
8836
9324
|
resolvePollFlag, // P-c4d8e1a3 — granular per-poller flag resolution
|
|
8837
9325
|
resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentUseWorkerPool, resolveAgentAcpPoolSize, resolveAgentModel, resolveCcModel,
|
|
8838
9326
|
resolveAgentAllowedTools, resolveAgentMaxBudget, resolveAgentBareMode, resolvePropagateClaudeMdForNonClaudeRuntimes,
|
|
9327
|
+
resolveCreatePrsAsDraft,
|
|
8839
9328
|
applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
|
|
8840
9329
|
runtimeConfigWarnings,
|
|
8841
9330
|
projectWorkSourceWarnings,
|
|
@@ -8846,6 +9335,8 @@ module.exports = {
|
|
|
8846
9335
|
WATCH_STALLED_DEFAULT_TICKS, WATCH_STUCK_STAGE_DEFAULT_TICKS,
|
|
8847
9336
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
8848
9337
|
FAILURE_CLASS, COMPLETION_FIELDS,
|
|
9338
|
+
resolveTerminalDispatchResult, resolveTerminalWorkerState,
|
|
9339
|
+
claimDispatchFinalization, releaseDispatchFinalization, waitForDispatchFinalization,
|
|
8849
9340
|
DEFAULT_AGENT_METRICS,
|
|
8850
9341
|
DEFAULT_AGENTS,
|
|
8851
9342
|
DEFAULT_CLAUDE,
|
|
@@ -8961,6 +9452,7 @@ module.exports = {
|
|
|
8961
9452
|
HAS_DANGEROUS_KEY_MAX_DEPTH,
|
|
8962
9453
|
HAS_DANGEROUS_KEY_MAX_NODES,
|
|
8963
9454
|
validateProjectName,
|
|
9455
|
+
CC_TURN_ID_MAX,
|
|
8964
9456
|
validateProjectPath,
|
|
8965
9457
|
CHECKOUT_MODES,
|
|
8966
9458
|
validateCheckoutMode,
|
|
@@ -8993,8 +9485,13 @@ module.exports = {
|
|
|
8993
9485
|
isWorktreePathLive,
|
|
8994
9486
|
getWorktreeBlockingDispatchInfo,
|
|
8995
9487
|
WORKTREE_OWNER_MARKER,
|
|
9488
|
+
WORKTREE_RETRY_PATH_MARKER,
|
|
9489
|
+
WORKTREE_QUARANTINE_PROTECTION_MARKER,
|
|
9490
|
+
WORKTREE_QUARANTINE_PROTECTED_PATH_TOKEN,
|
|
8996
9491
|
writeWorktreeOwnerMarker,
|
|
8997
9492
|
hasWorktreeOwnerMarker,
|
|
9493
|
+
hasProtectedQuarantineMarker,
|
|
9494
|
+
_restoreWorktreeOwnerMarkerAfterFailedRemoval, // exported for testing
|
|
8998
9495
|
isWorktreeOwnerMarkerStatusLine,
|
|
8999
9496
|
_normalizeWorktreePath, // exported for testing
|
|
9000
9497
|
_writeWorktreeSkipLiveInboxNote, // exported for testing
|