@yemi33/minions 0.1.2383 → 0.1.2385
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/js/refresh.js +2 -1
- package/dashboard/js/settings.js +1 -1
- package/dashboard.js +133 -46
- package/docs/keep-processes.md +7 -2
- package/docs/live-checkout-mode.md +7 -7
- package/docs/managed-spawn.md +14 -1
- package/docs/runtime-adapters.md +7 -4
- package/docs/worktree-lifecycle.md +22 -0
- package/engine/acp-transport.js +216 -29
- package/engine/agent-worker-pool.js +268 -51
- package/engine/cc-worker-pool.js +8 -1
- package/engine/cleanup.js +15 -11
- package/engine/cli.js +47 -27
- package/engine/create-pr-worktree.js +57 -79
- package/engine/keep-process-sweep.js +131 -9
- package/engine/lifecycle.js +1 -1
- package/engine/live-checkout.js +4 -2
- package/engine/managed-spawn.js +112 -5
- package/engine/pooled-agent-process.js +486 -21
- package/engine/pr-action.js +6 -8
- package/engine/process-utils.js +154 -18
- package/engine/runtimes/copilot.js +22 -4
- package/engine/shared.js +254 -61
- package/engine/spawn-agent.js +6 -3
- package/engine/timeout.js +14 -10
- package/engine/worktree-gc.js +55 -46
- package/engine.js +237 -134
- package/package.json +1 -1
- package/playbooks/shared-rules.md +2 -2
- package/prompts/cc-system.md +11 -11
package/engine.js
CHANGED
|
@@ -445,6 +445,25 @@ function _buildAgentSpawnFlags(runtime, opts = {}) {
|
|
|
445
445
|
return buildSpawnFlags(runtime, opts);
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
function _syncAgentWorkerPoolConfig(engineConfig = {}) {
|
|
449
|
+
const enabled = (engineConfig.agentUseWorkerPool ?? ENGINE_DEFAULTS.agentUseWorkerPool) === true;
|
|
450
|
+
agentWorkerPool.setEnabled(enabled);
|
|
451
|
+
if (enabled) agentWorkerPool.setPoolSize(shared.resolveAgentAcpPoolSize(engineConfig));
|
|
452
|
+
return enabled;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function _resolveAgentPoolProcessCwd(project, engineConfig) {
|
|
456
|
+
const scratchRoot = shared.resolveAgentTempBaseDir(project, engineConfig, MINIONS_DIR)
|
|
457
|
+
|| path.join(os.tmpdir(), 'minions-agent-temp');
|
|
458
|
+
const processCwd = path.join(
|
|
459
|
+
scratchRoot,
|
|
460
|
+
'acp-workers',
|
|
461
|
+
shared.safeSlugComponent(project?.name || project?.repoName || 'project')
|
|
462
|
+
);
|
|
463
|
+
fs.mkdirSync(processCwd, { recursive: true });
|
|
464
|
+
return processCwd;
|
|
465
|
+
}
|
|
466
|
+
|
|
448
467
|
function _runtimeLogger() {
|
|
449
468
|
return {
|
|
450
469
|
info: (msg) => log('info', msg),
|
|
@@ -995,15 +1014,6 @@ async function _probeBranchLocally(rootDir, branchName, gitOpts) {
|
|
|
995
1014
|
}
|
|
996
1015
|
}
|
|
997
1016
|
|
|
998
|
-
// W-mr3tayu4 — thin wrapper around shared.removeStaleIndexLock. The age-gated
|
|
999
|
-
// (>300000ms) stale-lock removal now lives in engine/shared.js so both the
|
|
1000
|
-
// worktree-add retry paths here and engine/live-checkout.js#prepareLiveCheckout
|
|
1001
|
-
// share one implementation (no circular import). Kept as a named local because
|
|
1002
|
-
// three internal call sites and the test harness reference `removeStaleIndexLock`.
|
|
1003
|
-
function removeStaleIndexLock(rootDir) {
|
|
1004
|
-
return shared.removeStaleIndexLock(rootDir);
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
1017
|
async function runWorktreeAdd(rootDir, worktreePath, addArgs, gitOpts, worktreeCreateRetries) {
|
|
1008
1018
|
// P-a7c4d2e8 (F3): argv-form `git worktree add` — `addArgs` is an array of
|
|
1009
1019
|
// additional arguments (typically a branch name, optionally preceded by
|
|
@@ -1018,7 +1028,6 @@ async function runWorktreeAdd(rootDir, worktreePath, addArgs, gitOpts, worktreeC
|
|
|
1018
1028
|
try {
|
|
1019
1029
|
if (attempt > 0) {
|
|
1020
1030
|
try { await shared.shellSafeGit(['worktree', 'prune'], { ...gitOpts, cwd: rootDir, timeout: 15000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
1021
|
-
removeStaleIndexLock(rootDir);
|
|
1022
1031
|
log('warn', `Retrying git worktree add (attempt ${attempt + 1}/${retries + 1}) for ${path.basename(worktreePath)}`);
|
|
1023
1032
|
}
|
|
1024
1033
|
await shared.shellSafeGit(['worktree', 'add', worktreePath, ...addArgs], { ...gitOpts, cwd: rootDir });
|
|
@@ -2388,8 +2397,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2388
2397
|
updateAgentStatus(id, AGENT_STATUS.FAILED, err.message);
|
|
2389
2398
|
throw err;
|
|
2390
2399
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2400
|
+
// Persisted dispatch metadata is an identity hint only. Never merge stale
|
|
2401
|
+
// checkout configuration back into the current configured project.
|
|
2402
|
+
const project = projectResolution.project ? { ...projectResolution.project } : {};
|
|
2403
|
+
const configuredProjects = getProjects(config);
|
|
2404
|
+
const assertSafeWorktreePath = (candidate) =>
|
|
2405
|
+
shared.assertWorktreeOutsideProjects(candidate, configuredProjects);
|
|
2393
2406
|
|
|
2394
2407
|
// ── Workspace manifest repo gate (W-mq07avbk000m5543) ────────────────────
|
|
2395
2408
|
// Dispatch-time enforcement of `agents.<id>.workspace_manifest.allowed_repos`.
|
|
@@ -2423,19 +2436,14 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2423
2436
|
|
|
2424
2437
|
// W-mp73x32w000l143d: decouple agent cwd from worktree placement.
|
|
2425
2438
|
// resolveSpawnPaths returns:
|
|
2426
|
-
// - read-only types:
|
|
2427
|
-
//
|
|
2439
|
+
// - project-bound read-only types: detached worktree placement
|
|
2440
|
+
// - project-less read-only types: { cwd: MINIONS_DIR, worktreeRootDir: null }
|
|
2428
2441
|
// - code-mutating types: { cwd: null, worktreeRootDir: <project root> }
|
|
2429
2442
|
// (caller defaults cwd to worktreeRootDir; drive-root collapse throws
|
|
2430
2443
|
// WORKTREE_ROOTDIR_COLLAPSED_TO_DRIVE_ROOT — same fail-fast behavior as
|
|
2431
2444
|
// the legacy resolveProjectRootDir call this replaced).
|
|
2432
|
-
//
|
|
2433
|
-
//
|
|
2434
|
-
// on read-only stages are a no-op label; the stage doesn't commit anything, so
|
|
2435
|
-
// the worktree had no functional purpose and was only there to absorb a drive-
|
|
2436
|
-
// root preflight that fired against MINIONS_DIR's parent. Read-only pipeline
|
|
2437
|
-
// stages now short-circuit alongside any other read-only WI (see the gate at
|
|
2438
|
-
// `if (branchName && READ_ONLY_ROOT_TASK_TYPES.has(type))` below).
|
|
2445
|
+
// Pipeline branch labels remain irrelevant to read-only tasks, so they are
|
|
2446
|
+
// discarded before detached placement.
|
|
2439
2447
|
const _preBranchName = meta?.branch ? sanitizeBranch(meta.branch) : null;
|
|
2440
2448
|
|
|
2441
2449
|
// ── Per-WI meta.workdir override (P-714ef144) ─────────────────────────
|
|
@@ -2482,9 +2490,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2482
2490
|
}
|
|
2483
2491
|
const validatedWorkdir = _wdValidation.value; // null when unset / empty
|
|
2484
2492
|
|
|
2485
|
-
let cwd, worktreeRootDir, liveMode = false;
|
|
2493
|
+
let cwd, worktreeRootDir, liveMode = false, readOnlyWorktree = false;
|
|
2486
2494
|
try {
|
|
2487
|
-
({ cwd, worktreeRootDir, liveMode = false
|
|
2495
|
+
({ cwd, worktreeRootDir, liveMode = false, readOnlyWorktree = false } =
|
|
2496
|
+
shared.resolveSpawnPaths(project, type, MINIONS_DIR, { workdir: validatedWorkdir }));
|
|
2488
2497
|
} catch (rootErr) {
|
|
2489
2498
|
if (rootErr?.code === 'WORKTREE_ROOTDIR_COLLAPSED_TO_DRIVE_ROOT' || rootErr?.code === 'WORKTREE_ROOTDIR_MISSING_BASE') {
|
|
2490
2499
|
log('error', `spawnAgent: project rootDir resolution failed for ${id}: ${rootErr.message}`);
|
|
@@ -2533,6 +2542,29 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2533
2542
|
}
|
|
2534
2543
|
throw rootErr;
|
|
2535
2544
|
}
|
|
2545
|
+
const checkoutModeAtDispatch = liveMode
|
|
2546
|
+
? shared.CHECKOUT_MODES.LIVE
|
|
2547
|
+
: shared.CHECKOUT_MODES.WORKTREE;
|
|
2548
|
+
const resolvedProjectMeta = project?.name || project?.localPath
|
|
2549
|
+
? { name: project.name, localPath: project.localPath }
|
|
2550
|
+
: null;
|
|
2551
|
+
dispatchItem.checkoutMode = checkoutModeAtDispatch;
|
|
2552
|
+
if (resolvedProjectMeta && dispatchItem.meta) {
|
|
2553
|
+
dispatchItem.meta.project = resolvedProjectMeta;
|
|
2554
|
+
}
|
|
2555
|
+
try {
|
|
2556
|
+
mutateDispatch((dispatch) => {
|
|
2557
|
+
for (const queue of ['pending', 'active']) {
|
|
2558
|
+
const found = (dispatch?.[queue] || []).find(d => d && d.id === id);
|
|
2559
|
+
if (!found) continue;
|
|
2560
|
+
found.checkoutMode = checkoutModeAtDispatch;
|
|
2561
|
+
if (resolvedProjectMeta && found.meta) found.meta.project = resolvedProjectMeta;
|
|
2562
|
+
}
|
|
2563
|
+
return dispatch;
|
|
2564
|
+
});
|
|
2565
|
+
} catch (e) {
|
|
2566
|
+
log('warn', `spawnAgent: failed to persist checkout context for ${id}: ${e.message}`);
|
|
2567
|
+
}
|
|
2536
2568
|
// Legacy local alias: downstream git ops (worktree add, prune, fetch) and
|
|
2537
2569
|
// the `cwd === rootDir` safety warn at line ~1387 reference `rootDir`. For
|
|
2538
2570
|
// read-only rootless tasks (no worktree, no branch) this is null — the
|
|
@@ -2752,17 +2784,11 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2752
2784
|
_phaseT.afterPrompt = Date.now();
|
|
2753
2785
|
|
|
2754
2786
|
if (branchName && READ_ONLY_ROOT_TASK_TYPES.has(type)) {
|
|
2755
|
-
//
|
|
2756
|
-
//
|
|
2757
|
-
//
|
|
2758
|
-
|
|
2759
|
-
// to be exempt — they don't need to be (W-mp8ho6w500034a58): read-only stages
|
|
2760
|
-
// never commit, so a pipeline-branch label is meaningless for them and the
|
|
2761
|
-
// forced worktree only existed to feed the drive-root preflight that this
|
|
2762
|
-
// short-circuit now correctly avoids.
|
|
2763
|
-
log('info', `${type}: read-only task with branch ${branchName} — skipping worktree, running in cwd ${cwd}`);
|
|
2787
|
+
// Read-only dispatches never own a branch. Project-bound worktree-mode
|
|
2788
|
+
// dispatches still get a detached worktree below; project-less/live tasks
|
|
2789
|
+
// keep their resolved cwd.
|
|
2790
|
+
log('info', `${type}: ignoring branch label ${branchName} for read-only dispatch`);
|
|
2764
2791
|
branchName = null;
|
|
2765
|
-
worktreePath = null;
|
|
2766
2792
|
}
|
|
2767
2793
|
|
|
2768
2794
|
// ── Live-checkout mode handling (P-a3f9b204) ─────────────────────────────
|
|
@@ -3521,6 +3547,55 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3521
3547
|
}
|
|
3522
3548
|
}
|
|
3523
3549
|
|
|
3550
|
+
const needsDetachedProjectWorktree = !liveMode && !!rootDir && !!project?.localPath && !branchName;
|
|
3551
|
+
if (needsDetachedProjectWorktree) {
|
|
3552
|
+
const detachedKind = readOnlyWorktree ? 'read-only' : 'branchless';
|
|
3553
|
+
updateAgentStatus(id, AGENT_STATUS.WORKTREE_SETUP, `Setting up detached ${detachedKind} worktree for ${project.name || 'project'}`);
|
|
3554
|
+
const wtDirName = shared.buildWorktreeDirName({
|
|
3555
|
+
dispatchId: id,
|
|
3556
|
+
projectName: project.name || 'default',
|
|
3557
|
+
branchName: detachedKind,
|
|
3558
|
+
});
|
|
3559
|
+
const worktreeRoot = path.resolve(rootDir, engineConfig.worktreeRoot || '../worktrees');
|
|
3560
|
+
worktreePath = path.resolve(worktreeRoot, wtDirName);
|
|
3561
|
+
try {
|
|
3562
|
+
assertSafeWorktreePath(worktreePath);
|
|
3563
|
+
if (fs.existsSync(worktreePath)) {
|
|
3564
|
+
shared.removeWorktree(worktreePath, rootDir, worktreeRoot, { excludeDispatchId: id });
|
|
3565
|
+
}
|
|
3566
|
+
const mainRef = sanitizeBranch(shared.resolveMainBranch(rootDir, project.mainBranch));
|
|
3567
|
+
await runWorktreeAdd(
|
|
3568
|
+
rootDir,
|
|
3569
|
+
worktreePath,
|
|
3570
|
+
['--detach', mainRef],
|
|
3571
|
+
_worktreeGitOpts,
|
|
3572
|
+
worktreeCreateRetries,
|
|
3573
|
+
);
|
|
3574
|
+
cwd = worktreePath;
|
|
3575
|
+
log('info', `${type}: running in detached ${detachedKind} worktree ${worktreePath}; operator checkout ${project.localPath} is protected`);
|
|
3576
|
+
} catch (readOnlyWtErr) {
|
|
3577
|
+
try {
|
|
3578
|
+
if (worktreePath && fs.existsSync(worktreePath)) {
|
|
3579
|
+
shared.removeWorktree(worktreePath, rootDir, worktreeRoot, { excludeDispatchId: id });
|
|
3580
|
+
}
|
|
3581
|
+
} catch (cleanupErr) {
|
|
3582
|
+
log('warn', `read-only worktree cleanup failed for ${id}: ${cleanupErr.message}`);
|
|
3583
|
+
}
|
|
3584
|
+
const summary = `Detached ${detachedKind} worktree setup failed: ${readOnlyWtErr.message}`;
|
|
3585
|
+
log('error', `spawnAgent: ${summary}`);
|
|
3586
|
+
_cleanupPromptFiles();
|
|
3587
|
+
completeDispatch(
|
|
3588
|
+
id,
|
|
3589
|
+
DISPATCH_RESULT.ERROR,
|
|
3590
|
+
summary.slice(0, 800),
|
|
3591
|
+
'The operator checkout was not used. Fix the worktree setup failure and retry.',
|
|
3592
|
+
{ failureClass: FAILURE_CLASS.WORKTREE_PREFLIGHT, agentRetryable: true },
|
|
3593
|
+
);
|
|
3594
|
+
cleanupTempAgent(agentId);
|
|
3595
|
+
return null;
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3524
3599
|
if (branchName && !liveMode) {
|
|
3525
3600
|
updateAgentStatus(id, AGENT_STATUS.WORKTREE_SETUP, `Setting up worktree for branch ${branchName}`);
|
|
3526
3601
|
// W-mpwy3mp5 (#2996): track whether we ended up reusing an existing
|
|
@@ -3538,7 +3613,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3538
3613
|
// nested worktrees cause glob/grep to match both copies (mirror writes).
|
|
3539
3614
|
// WORKTREE_NESTED_IN_PROJECT is non-retryable: the recompute on the next
|
|
3540
3615
|
// tick will produce the same path. Fail fast (W-mp62taw2000ubcc3).
|
|
3541
|
-
try {
|
|
3616
|
+
try { assertSafeWorktreePath(worktreePath); }
|
|
3542
3617
|
catch (assertErr) {
|
|
3543
3618
|
if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
|
|
3544
3619
|
throw assertErr;
|
|
@@ -3552,7 +3627,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3552
3627
|
// Same guard for reuse — a previously-created bad worktree must not
|
|
3553
3628
|
// be silently reused either; the cleanup sweep flags these so the
|
|
3554
3629
|
// operator can remove them.
|
|
3555
|
-
try {
|
|
3630
|
+
try { assertSafeWorktreePath(existingWt); }
|
|
3556
3631
|
catch (assertErr) {
|
|
3557
3632
|
if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
|
|
3558
3633
|
throw assertErr;
|
|
@@ -3604,7 +3679,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3604
3679
|
if (!_branchOnRemote) {
|
|
3605
3680
|
const borrowed = worktreePool.tryBorrow(_poolProject, id);
|
|
3606
3681
|
if (borrowed && borrowed.path && fs.existsSync(borrowed.path)) {
|
|
3607
|
-
try {
|
|
3682
|
+
try { assertSafeWorktreePath(borrowed.path); }
|
|
3608
3683
|
catch (assertErr) {
|
|
3609
3684
|
// Always evict before deciding what to do — leaves no BORROWED
|
|
3610
3685
|
// orphan tied to a dispatch that won't complete normally.
|
|
@@ -3666,9 +3741,6 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3666
3741
|
const isSharedBranch = meta?.branchStrategy === 'shared-branch' || meta?.useExistingBranch;
|
|
3667
3742
|
// Prune stale worktree entries before creating (handles leftover entries from crashed runs)
|
|
3668
3743
|
try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
|
|
3669
|
-
// Remove stale index.lock before creating worktree (Windows crashes can leave this behind)
|
|
3670
|
-
removeStaleIndexLock(rootDir);
|
|
3671
|
-
|
|
3672
3744
|
if (isSharedBranch) {
|
|
3673
3745
|
log('info', `Creating worktree for shared branch: ${worktreePath} on ${branchName}`);
|
|
3674
3746
|
await _fetchWithTransientRetry(['origin', branchName], { ..._gitOpts, cwd: rootDir }, `origin/${branchName} (shared-branch pre-create)`);
|
|
@@ -3678,7 +3750,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3678
3750
|
if (eShared.message?.includes('already used by worktree') || eShared.message?.includes('already checked out')) {
|
|
3679
3751
|
const existingWtPath = await findExistingWorktree(rootDir, branchName);
|
|
3680
3752
|
if (existingWtPath && fs.existsSync(existingWtPath)) {
|
|
3681
|
-
try {
|
|
3753
|
+
try { assertSafeWorktreePath(existingWtPath); }
|
|
3682
3754
|
catch (assertErr) {
|
|
3683
3755
|
if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
|
|
3684
3756
|
throw assertErr;
|
|
@@ -3763,7 +3835,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3763
3835
|
log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
|
|
3764
3836
|
throw eRemote;
|
|
3765
3837
|
}
|
|
3766
|
-
try {
|
|
3838
|
+
try { assertSafeWorktreePath(existingWtPath); }
|
|
3767
3839
|
catch (assertErr) {
|
|
3768
3840
|
if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
|
|
3769
3841
|
throw assertErr;
|
|
@@ -3829,10 +3901,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3829
3901
|
const branchExists = e1.message?.includes('already exists');
|
|
3830
3902
|
log('warn', `Worktree -b failed for ${branchName}: ${e1.message?.split('\n')[0]}`);
|
|
3831
3903
|
if (!branchExists) {
|
|
3832
|
-
// Transient error (lock, timeout) — prune
|
|
3904
|
+
// Transient error (lock, timeout) — prune and retry -b once more.
|
|
3905
|
+
// Never delete the operator checkout's .git/index.lock.
|
|
3833
3906
|
log('info', `Retrying -b create after prune for ${branchName}`);
|
|
3834
3907
|
try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 15000 }); } catch { /* optional */ }
|
|
3835
|
-
removeStaleIndexLock(rootDir);
|
|
3836
3908
|
// Clean up partial worktree directory from the failed -b
|
|
3837
3909
|
// attempt. This husk pre-dates `git worktree add`, so git
|
|
3838
3910
|
// does not yet own it and shared.removeWorktree's
|
|
@@ -3894,7 +3966,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3894
3966
|
log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
|
|
3895
3967
|
throw e2;
|
|
3896
3968
|
}
|
|
3897
|
-
try {
|
|
3969
|
+
try { assertSafeWorktreePath(existingWtPath); }
|
|
3898
3970
|
catch (assertErr) {
|
|
3899
3971
|
if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
|
|
3900
3972
|
throw assertErr;
|
|
@@ -4604,6 +4676,21 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4604
4676
|
}
|
|
4605
4677
|
}
|
|
4606
4678
|
|
|
4679
|
+
if (worktreePath) {
|
|
4680
|
+
dispatchItem.worktreePath = worktreePath;
|
|
4681
|
+
try {
|
|
4682
|
+
mutateDispatch((dispatch) => {
|
|
4683
|
+
for (const queue of ['pending', 'active']) {
|
|
4684
|
+
const found = (dispatch?.[queue] || []).find(d => d && d.id === id);
|
|
4685
|
+
if (found) found.worktreePath = worktreePath;
|
|
4686
|
+
}
|
|
4687
|
+
return dispatch;
|
|
4688
|
+
});
|
|
4689
|
+
} catch (e) {
|
|
4690
|
+
log('warn', `spawnAgent: failed to persist pre-spawn worktree path for ${id}: ${e.message}`);
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4607
4694
|
updateAgentStatus(id, AGENT_STATUS.READY, 'Worktree ready, preparing to spawn process');
|
|
4608
4695
|
|
|
4609
4696
|
if (worktreePath && meta?.source === 'work-item' && meta?.item?.branchStrategy === 'shared-branch') {
|
|
@@ -4721,13 +4808,14 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4721
4808
|
const resolvedMaxBudget = shared.resolveAgentMaxBudget(agentConfig, engineConfig);
|
|
4722
4809
|
const resolvedBare = shared.resolveAgentBareMode(agentConfig, engineConfig);
|
|
4723
4810
|
// Pool through the persistent ACP worker transport when the selected adapter
|
|
4724
|
-
// opts into that capability and config allows it.
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4811
|
+
// opts into that capability and config allows it. keep_processes requires
|
|
4812
|
+
// the cold wrapper so descendant ownership remains enforceable.
|
|
4813
|
+
_syncAgentWorkerPoolConfig(engineConfig);
|
|
4814
|
+
const keepProcessesRequested = !!meta?.item?.meta?.keep_processes || !!dispatchItem.meta?.keep_processes;
|
|
4815
|
+
const agentPoolRequested = shared.resolveAgentUseWorkerPool(engineConfig, runtime);
|
|
4816
|
+
const useAgentPool = agentPoolRequested && !keepProcessesRequested;
|
|
4817
|
+
if (keepProcessesRequested && agentPoolRequested) {
|
|
4818
|
+
log('info', `spawnAgent: disabling ACP worker pool for keep_processes dispatch ${id} so descendants can be reaped safely`);
|
|
4731
4819
|
}
|
|
4732
4820
|
|
|
4733
4821
|
const prevFailureClass = meta?.item?._lastFailureClass || null;
|
|
@@ -4792,6 +4880,18 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4792
4880
|
|
|
4793
4881
|
log('info', `Spawning agent: ${agentId} (${id}) in ${cwd}`);
|
|
4794
4882
|
log('info', `Task type: ${type} | Branch: ${branchName || 'none'}`);
|
|
4883
|
+
dispatchItem.executionCwd = cwd;
|
|
4884
|
+
try {
|
|
4885
|
+
mutateDispatch((dispatch) => {
|
|
4886
|
+
for (const queue of ['pending', 'active']) {
|
|
4887
|
+
const found = (dispatch?.[queue] || []).find(d => d && d.id === id);
|
|
4888
|
+
if (found) found.executionCwd = cwd;
|
|
4889
|
+
}
|
|
4890
|
+
return dispatch;
|
|
4891
|
+
});
|
|
4892
|
+
} catch (e) {
|
|
4893
|
+
log('warn', `spawnAgent: failed to persist execution cwd for ${id}: ${e.message}`);
|
|
4894
|
+
}
|
|
4795
4895
|
|
|
4796
4896
|
// Agent status is derived from dispatch state.
|
|
4797
4897
|
|
|
@@ -4808,6 +4908,11 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4808
4908
|
|| dispatchItem.meta?.keep_processes_skip_workdir_check) {
|
|
4809
4909
|
childEnv.MINIONS_KEEP_PROCESSES_SKIP_WORKDIR_CHECK = '1';
|
|
4810
4910
|
}
|
|
4911
|
+
const keepProcessesAllowedCwdRoot = worktreePath
|
|
4912
|
+
|| (liveMode && project?.localPath ? path.resolve(String(project.localPath)) : cwd);
|
|
4913
|
+
if (keepProcessesAllowedCwdRoot) {
|
|
4914
|
+
childEnv.MINIONS_KEEP_PROCESSES_ALLOWED_CWD_ROOT = keepProcessesAllowedCwdRoot;
|
|
4915
|
+
}
|
|
4811
4916
|
|
|
4812
4917
|
// W-mpg54mi2000n7b7e — suppress Git's interactive credential prompts and
|
|
4813
4918
|
// Git Credential Manager's GUI dialog for every agent spawn. Without these,
|
|
@@ -4918,9 +5023,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4918
5023
|
}
|
|
4919
5024
|
|
|
4920
5025
|
_phaseT.spawnCallStart = Date.now();
|
|
4921
|
-
// P-c5a1f3b8 (Item D — live-mode push/PR-create cwd guard):
|
|
4922
|
-
// worktreePath
|
|
4923
|
-
//
|
|
5026
|
+
// P-c5a1f3b8 (Item D — live-mode push/PR-create cwd guard): only live mode
|
|
5027
|
+
// leaves worktreePath null with cwd=project.localPath. Worktree-mode
|
|
5028
|
+
// project dispatches, including read-only types, have an isolated path. The
|
|
4924
5029
|
// agent's `git push -u origin <branch>` and PR-create therefore run in-place
|
|
4925
5030
|
// against the operator's checkout. This cwd MUST NEVER be null for a mutating
|
|
4926
5031
|
// live dispatch — shared.resolveSpawnPaths returns cwd === project.localPath
|
|
@@ -4959,6 +5064,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4959
5064
|
'MINIONS_LIVE_OUTPUT_PATH',
|
|
4960
5065
|
'MINIONS_STEERING_ACK_DIR',
|
|
4961
5066
|
'MINIONS_AGENT_CWD',
|
|
5067
|
+
'MINIONS_KEEP_PROCESSES_ALLOWED_CWD_ROOT',
|
|
4962
5068
|
];
|
|
4963
5069
|
const pooledProcessEnv = { ...childEnv };
|
|
4964
5070
|
const pooledSessionContext = {};
|
|
@@ -4971,16 +5077,27 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
4971
5077
|
runtime,
|
|
4972
5078
|
dispatchId: id,
|
|
4973
5079
|
cwd,
|
|
4974
|
-
|
|
4975
|
-
|
|
5080
|
+
processCwd: _resolveAgentPoolProcessCwd(project, engineConfig),
|
|
5081
|
+
model: runtimeOptions.model,
|
|
5082
|
+
effort: runtimeOptions.effort,
|
|
4976
5083
|
// Mirrors the CC worker pool's convention (dashboard.js) — a single
|
|
4977
5084
|
// flat top-level config array, not per-agent-resolved.
|
|
4978
5085
|
mcpServers: (engineConfig && engineConfig.mcpServers) || [],
|
|
4979
5086
|
hermeticDirs: addDirs,
|
|
4980
5087
|
processEnv: pooledProcessEnv,
|
|
5088
|
+
workerOptions: runtimeOptions,
|
|
5089
|
+
sessionId: cachedSessionId,
|
|
4981
5090
|
sessionContext: pooledSessionContext,
|
|
5091
|
+
agentId,
|
|
5092
|
+
reapDescendants: true,
|
|
5093
|
+
keepProcessesSkipWorkdirCheck: !!(
|
|
5094
|
+
meta?.item?.meta?.keep_processes_skip_workdir_check
|
|
5095
|
+
|| dispatchItem.meta?.keep_processes_skip_workdir_check
|
|
5096
|
+
),
|
|
4982
5097
|
systemPromptText: systemPrompt,
|
|
4983
|
-
|
|
5098
|
+
// The prompt file may have gained dirty-file or cross-repo dependency
|
|
5099
|
+
// sections after `fullTaskPrompt` was first built.
|
|
5100
|
+
promptText: fs.readFileSync(promptPath, 'utf8'),
|
|
4984
5101
|
});
|
|
4985
5102
|
// The child_process PID file is normally written asynchronously by
|
|
4986
5103
|
// spawn-agent.js itself (engine/spawn-agent.js:606) once it starts.
|
|
@@ -5336,6 +5453,24 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5336
5453
|
// Re-expose the engine-resolved cwd on steering resume (matches the
|
|
5337
5454
|
// initial spawn path) so $MINIONS_AGENT_CWD stays valid across resumes.
|
|
5338
5455
|
childEnv.MINIONS_AGENT_CWD = cwd;
|
|
5456
|
+
if (keepProcessesAllowedCwdRoot) {
|
|
5457
|
+
childEnv.MINIONS_KEEP_PROCESSES_ALLOWED_CWD_ROOT = keepProcessesAllowedCwdRoot;
|
|
5458
|
+
}
|
|
5459
|
+
try {
|
|
5460
|
+
mutateDispatch((dp) => {
|
|
5461
|
+
const active = dp.active.find((entry) => entry.id === id);
|
|
5462
|
+
if (active) delete active.pooled;
|
|
5463
|
+
return dp;
|
|
5464
|
+
});
|
|
5465
|
+
delete dispatchItem.pooled;
|
|
5466
|
+
} catch (e) {
|
|
5467
|
+
log('warn', `Steering: failed to persist cold-resume mode for ${agentId}: ${e.message}`);
|
|
5468
|
+
try { fs.unlinkSync(steerPromptPath); } catch { /* cleanup */ }
|
|
5469
|
+
activeProcesses.delete(id);
|
|
5470
|
+
completeDispatch(id, DISPATCH_RESULT.ERROR, `Steering state update failed: ${e.message}`);
|
|
5471
|
+
cleanupTempAgent(agentId);
|
|
5472
|
+
return;
|
|
5473
|
+
}
|
|
5339
5474
|
let resumeProc;
|
|
5340
5475
|
try {
|
|
5341
5476
|
// detached so the resumed steering session also survives engine death (matches initial spawn)
|
|
@@ -5354,6 +5489,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5354
5489
|
cleanupTempAgent(agentId);
|
|
5355
5490
|
return;
|
|
5356
5491
|
}
|
|
5492
|
+
const writeResumePid = () => {
|
|
5493
|
+
if (!resumeProc.pid) return;
|
|
5494
|
+
try { fs.writeFileSync(pidFilePath, String(resumeProc.pid)); } catch { /* best-effort */ }
|
|
5495
|
+
};
|
|
5496
|
+
if (resumeProc.pid) writeResumePid();
|
|
5497
|
+
else if (typeof resumeProc.once === 'function') resumeProc.once('spawn', writeResumePid);
|
|
5357
5498
|
|
|
5358
5499
|
// Re-attach to existing tracking — do NOT carry _steeringAt forward (#1052).
|
|
5359
5500
|
// The kill watcher in timeout.js fires 30s after _steeringAt is set. If we carry it
|
|
@@ -5576,8 +5717,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5576
5717
|
let evalResult = schemaResult;
|
|
5577
5718
|
let isWorkdirRejection = false;
|
|
5578
5719
|
const workdirGateOn = !_kpSkipWorkdir && ENGINE_DEFAULTS.keepProcesses?.requireGitWorkdir !== false;
|
|
5579
|
-
|
|
5580
|
-
|
|
5720
|
+
const allowedCwdRoot = keepProcessesAllowedCwdRoot;
|
|
5721
|
+
if (schemaResult.exists && schemaResult.accepted && (workdirGateOn || allowedCwdRoot)) {
|
|
5722
|
+
const workdirResult = keepProcessSweep.evaluateKeepPidsAcceptance(agentId, {
|
|
5723
|
+
requireGitWorkdir: workdirGateOn,
|
|
5724
|
+
...(allowedCwdRoot ? { allowedCwdRoot } : {}),
|
|
5725
|
+
});
|
|
5581
5726
|
if (workdirResult.exists && !workdirResult.accepted && workdirResult.isWorkdirRejection) {
|
|
5582
5727
|
evalResult = workdirResult;
|
|
5583
5728
|
isWorkdirRejection = true;
|
|
@@ -5672,6 +5817,22 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5672
5817
|
} catch (alertErr) {
|
|
5673
5818
|
log('warn', `keep-processes acceptance: failed to emit inbox alert for ${agentId}: ${alertErr.message}`);
|
|
5674
5819
|
}
|
|
5820
|
+
} else if (evalResult.exists && evalResult.accepted) {
|
|
5821
|
+
try {
|
|
5822
|
+
const enriched = {
|
|
5823
|
+
...(evalResult.parsedRaw || {}),
|
|
5824
|
+
cwd: evalResult.recordedCwd || '',
|
|
5825
|
+
project: project?.name || '',
|
|
5826
|
+
work_type: type,
|
|
5827
|
+
checkout_mode: checkoutModeAtDispatch,
|
|
5828
|
+
dispatch_root: allowedCwdRoot
|
|
5829
|
+
? shared.realPathForComparison(allowedCwdRoot)
|
|
5830
|
+
: '',
|
|
5831
|
+
};
|
|
5832
|
+
fs.writeFileSync(evalResult.filePath, JSON.stringify(enriched, null, 2));
|
|
5833
|
+
} catch (e) {
|
|
5834
|
+
log('warn', `keep-processes acceptance: could not persist checkout provenance for ${agentId}: ${e.message}`);
|
|
5835
|
+
}
|
|
5675
5836
|
}
|
|
5676
5837
|
} catch (e) {
|
|
5677
5838
|
log('warn', `keep-processes acceptance check failed for ${agentId} (${id}): ${e.message}`);
|
|
@@ -5699,7 +5860,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5699
5860
|
if (_msEnabled) {
|
|
5700
5861
|
try {
|
|
5701
5862
|
const managedSpawn = require('./engine/managed-spawn');
|
|
5702
|
-
const
|
|
5863
|
+
const allowedCwdRoot = keepProcessesAllowedCwdRoot;
|
|
5864
|
+
const evalResult = managedSpawn.evaluateManagedSpawnAcceptance(agentId, {
|
|
5865
|
+
projects: getProjects(config),
|
|
5866
|
+
project,
|
|
5867
|
+
...(allowedCwdRoot ? { allowedCwdRoot } : {}),
|
|
5868
|
+
});
|
|
5703
5869
|
if (evalResult.exists && !evalResult.accepted) {
|
|
5704
5870
|
managedSpawnAcceptanceFailure = {
|
|
5705
5871
|
reason: evalResult.reason,
|
|
@@ -5757,6 +5923,9 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
5757
5923
|
owner_agent: agentId,
|
|
5758
5924
|
owner_wi: dispatchItem.meta?.item?.id || '',
|
|
5759
5925
|
owner_project: project?.name || '',
|
|
5926
|
+
owner_work_type: type,
|
|
5927
|
+
dispatch_root: allowedCwdRoot || '',
|
|
5928
|
+
checkout_mode: checkoutModeAtDispatch,
|
|
5760
5929
|
};
|
|
5761
5930
|
const spawnedItems = [];
|
|
5762
5931
|
let spawnFailureReason = null;
|
|
@@ -6097,7 +6266,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
6097
6266
|
// crashed dispatch leaks `work/W-<id>` worktree entries that block
|
|
6098
6267
|
// every retry with `fatal: 'work/W-<id>' is already used by worktree
|
|
6099
6268
|
// at …` until the 2-hour age sweep in cleanup.js eventually catches up.
|
|
6100
|
-
if (worktreePath && rootDir
|
|
6269
|
+
if (worktreePath && rootDir) {
|
|
6101
6270
|
try {
|
|
6102
6271
|
const _wgc = require('./engine/worktree-gc');
|
|
6103
6272
|
const _wtRoot = path.resolve(rootDir, engineConfig.worktreeRoot || ENGINE_DEFAULTS.worktreeRoot);
|
|
@@ -6144,7 +6313,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
6144
6313
|
// restore is a plain `git checkout <originalRef>` — never --force/reset/
|
|
6145
6314
|
// clean/stash — and is best-effort: a thrown restore never alters the
|
|
6146
6315
|
// dispatch result.
|
|
6147
|
-
if (liveMode && branchName) {
|
|
6316
|
+
if (liveMode && branchName && shared.isLiveCheckoutDispatchRecord(dispatchItem, getConfig())) {
|
|
6148
6317
|
try {
|
|
6149
6318
|
const _liveRestore = require('./engine/live-checkout');
|
|
6150
6319
|
await _liveRestore.restoreLiveCheckoutAtDispatchEnd({
|
|
@@ -6335,6 +6504,10 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
6335
6504
|
// ACP worker isn't spawned detached and a fresh pool cold-starts empty —
|
|
6336
6505
|
// a live worker PID must never be mistaken for a resumable dispatch.
|
|
6337
6506
|
if (useAgentPool) item.pooled = true;
|
|
6507
|
+
else delete item.pooled;
|
|
6508
|
+
item.checkoutMode = checkoutModeAtDispatch;
|
|
6509
|
+
item.executionCwd = cwd;
|
|
6510
|
+
if (resolvedProjectMeta && item.meta) item.meta.project = resolvedProjectMeta;
|
|
6338
6511
|
// W-mq5rwwss000f30a7 — persist the worktree path so the live-worktree
|
|
6339
6512
|
// guard (shared.isWorktreePathLive) can correlate destructive callers
|
|
6340
6513
|
// (removeWorktree, cleanup orphan-dir sweep, pool-return, quarantine)
|
|
@@ -6613,73 +6786,6 @@ function safePrdProjectSlug(projectName) {
|
|
|
6613
6786
|
return slug || 'project';
|
|
6614
6787
|
}
|
|
6615
6788
|
|
|
6616
|
-
// Fleet branches the engine/CC create. A worktree-mode operator checkout should
|
|
6617
|
-
// NEVER sit on one of these (or any branch); when it does, a flow ran raw
|
|
6618
|
-
// `git checkout`/`merge` in the live checkout instead of a worktree.
|
|
6619
|
-
const _FLEET_BRANCH_RE = /^(cc-pr|backport|e2e|verify|sched-|work)\//;
|
|
6620
|
-
|
|
6621
|
-
// Self-heal: a worktree-mode project's operator checkout (project.localPath) must
|
|
6622
|
-
// always sit on its mainBranch — agents use isolated worktrees, CC Create-PR uses
|
|
6623
|
-
// prepare-create-pr-worktree. But fleet flows (CC raw git, backport, sync)
|
|
6624
|
-
// sometimes run `git checkout <branch>` + `git merge` THERE despite the rules,
|
|
6625
|
-
// leaving it stuck on a feature branch (often mid-conflict) — which restores
|
|
6626
|
-
// git-tracked dead PRDs and pollutes the operator's main, then the materializer
|
|
6627
|
-
// re-runs finished work. Detect + restore. Conservative: only acts on a stuck
|
|
6628
|
-
// merge/rebase OR a recognizably fleet-created branch, so a human's deliberate
|
|
6629
|
-
// `git checkout my-feature` in their checkout is left alone. Branch commits are
|
|
6630
|
-
// preserved on their ref (reset --hard only drops the uncommitted hijack state).
|
|
6631
|
-
function restoreHijackedOperatorCheckouts(config) {
|
|
6632
|
-
if (config?.engine?.autoRestoreOperatorCheckout === false) return;
|
|
6633
|
-
let projects;
|
|
6634
|
-
try { projects = getProjects(config); } catch { return; }
|
|
6635
|
-
for (const project of projects) {
|
|
6636
|
-
try {
|
|
6637
|
-
const localPath = project?.localPath;
|
|
6638
|
-
if (!localPath || !fs.existsSync(localPath)) continue;
|
|
6639
|
-
if (shared.resolveCheckoutMode(project) === shared.CHECKOUT_MODES.LIVE) continue; // live-mode: feature branch is intentional
|
|
6640
|
-
const main = project.mainBranch || 'main';
|
|
6641
|
-
const opts = { cwd: localPath };
|
|
6642
|
-
// W-mrcvh5hw000o6cf1: use the argv-form (shell: false) git helper instead of
|
|
6643
|
-
// execSilent(). execSilent() shells out via cmd.exe, which cannot chdir into
|
|
6644
|
-
// a UNC path (`//wsl.localhost/...`, `\\server\share\...`) — it silently
|
|
6645
|
-
// falls back to the Windows directory and every git call then fails with
|
|
6646
|
-
// "fatal: not a git repository", every tick, forever, for WSL/UNC-hosted
|
|
6647
|
-
// projects. shellSafeGitSync spawns git.exe directly (execFileSync,
|
|
6648
|
-
// shell:false) so `cwd` is honored even for UNC paths.
|
|
6649
|
-
let isRepo;
|
|
6650
|
-
try { isRepo = String(shared.shellSafeGitSync(['rev-parse', '--is-inside-work-tree'], opts) || '').trim(); }
|
|
6651
|
-
catch { continue; } // not a git repo (or transiently unreachable) — nothing to restore, skip quietly
|
|
6652
|
-
if (isRepo !== 'true') continue;
|
|
6653
|
-
const cur = String(shared.shellSafeGitSync(['rev-parse', '--abbrev-ref', 'HEAD'], opts) || '').trim();
|
|
6654
|
-
if (!cur || cur === 'HEAD' || cur === main) continue; // on main or detached — fine
|
|
6655
|
-
const gitDir = String(shared.shellSafeGitSync(['rev-parse', '--git-dir'], opts) || '').trim();
|
|
6656
|
-
const absGitDir = gitDir ? (path.isAbsolute(gitDir) ? gitDir : path.join(localPath, gitDir)) : path.join(localPath, '.git');
|
|
6657
|
-
const mergeStuck = fs.existsSync(path.join(absGitDir, 'MERGE_HEAD'));
|
|
6658
|
-
const rebaseStuck = fs.existsSync(path.join(absGitDir, 'rebase-merge')) || fs.existsSync(path.join(absGitDir, 'rebase-apply'));
|
|
6659
|
-
if (!mergeStuck && !rebaseStuck && !_FLEET_BRANCH_RE.test(cur)) continue; // likely a human's deliberate checkout — leave it
|
|
6660
|
-
const why = mergeStuck ? ' (stuck merge)' : rebaseStuck ? ' (stuck rebase)' : ' (fleet branch in live checkout)';
|
|
6661
|
-
log('warn', `Operator checkout for ${project.name} is on '${cur}'${why}; worktree-mode requires ${main} — restoring`);
|
|
6662
|
-
if (mergeStuck) shared.shellSafeGitSync(['merge', '--abort'], opts);
|
|
6663
|
-
if (rebaseStuck) shared.shellSafeGitSync(['rebase', '--abort'], opts);
|
|
6664
|
-
shared.shellSafeGitSync(['reset', '--hard', 'HEAD'], opts); // drop uncommitted hijack state; branch commits stay on the ref
|
|
6665
|
-
try { shared.validateGitRef(main); } catch (ve) { throw new Error(`restoreHijackedOperatorCheckouts: invalid mainBranch ${JSON.stringify(main)}: ${ve.message}`); }
|
|
6666
|
-
shared.shellSafeGitSync(['checkout', main], opts);
|
|
6667
|
-
const now = String(shared.shellSafeGitSync(['rev-parse', '--abbrev-ref', 'HEAD'], opts) || '').trim();
|
|
6668
|
-
if (now === main) {
|
|
6669
|
-
try {
|
|
6670
|
-
writeInboxAlert(`operator-checkout-restored-${project.name}`,
|
|
6671
|
-
`Operator checkout ${localPath} was hijacked onto '${cur}'${why} and restored to ${main}. ` +
|
|
6672
|
-
'A fleet flow ran git checkout/merge in the LIVE operator checkout instead of an isolated worktree ' +
|
|
6673
|
-
'(playbooks/shared-rules.md: "Do NOT checkout branches in the main working tree — use worktrees"). ' +
|
|
6674
|
-
`The '${cur}' branch commits are preserved on its ref.`);
|
|
6675
|
-
} catch { /* alert is best-effort */ }
|
|
6676
|
-
} else {
|
|
6677
|
-
log('warn', `Operator checkout restore for ${project.name} did not reach ${main} (now '${now}') — manual intervention needed`);
|
|
6678
|
-
}
|
|
6679
|
-
} catch (e) { log('warn', `restoreHijackedOperatorCheckouts ${project?.name || '?'}: ${e?.message || e}`); }
|
|
6680
|
-
}
|
|
6681
|
-
}
|
|
6682
|
-
|
|
6683
6789
|
function materializePlansAsWorkItems(config) {
|
|
6684
6790
|
const writePrdLocked = (fileName, data) => {
|
|
6685
6791
|
return prdStore.writePrd(fileName, data);
|
|
@@ -10556,6 +10662,7 @@ async function tickInner() {
|
|
|
10556
10662
|
catch (e) { log('warn', `lastTickAt write: ${e.message}`); }
|
|
10557
10663
|
|
|
10558
10664
|
const config = getConfig();
|
|
10665
|
+
_syncAgentWorkerPoolConfig(config.engine);
|
|
10559
10666
|
tickCount++;
|
|
10560
10667
|
const now = Date.now();
|
|
10561
10668
|
const tickIntervalMs = Math.max(1, Number(config.engine?.tickInterval) || ENGINE_DEFAULTS.tickInterval);
|
|
@@ -11588,9 +11695,6 @@ async function tickInner() {
|
|
|
11588
11695
|
const maxC = resolveMaxConcurrent(config);
|
|
11589
11696
|
setTempBudget(Math.max(0, maxC - activeCountPre));
|
|
11590
11697
|
}
|
|
11591
|
-
// Self-heal a hijacked operator checkout BEFORE discovery/materialization, so a
|
|
11592
|
-
// dead PRD restored by a stray `git checkout` can't be re-materialized this tick.
|
|
11593
|
-
try { restoreHijackedOperatorCheckouts(config); } catch (e) { log('warn', 'restoreHijackedOperatorCheckouts: ' + e.message); }
|
|
11594
11698
|
let discoveryOk = true;
|
|
11595
11699
|
try { await withTickTimeout(() => discoverWork(config), 'discoverWork', tickOpTimeoutMs); } catch (e) { log('warn', 'discoverWork: ' + e.message); discoveryOk = false; }
|
|
11596
11700
|
if (_isTickStale(myGeneration)) return;
|
|
@@ -11691,7 +11795,6 @@ module.exports = {
|
|
|
11691
11795
|
discoverWork, discoverFromPrs, discoverFromWorkItems, discoverCentralWorkItems,
|
|
11692
11796
|
materializePlansAsWorkItems,
|
|
11693
11797
|
materializeSpecsAsWorkItems, // exported for testing (P-f7-git-log)
|
|
11694
|
-
restoreHijackedOperatorCheckouts, // exported for testing — operator-checkout self-heal
|
|
11695
11798
|
|
|
11696
11799
|
// Shared helpers (used by lifecycle.js and tests)
|
|
11697
11800
|
reconcileItemsWithPrs, detectDependencyCycles,
|
|
@@ -11704,7 +11807,7 @@ module.exports = {
|
|
|
11704
11807
|
buildDepConflictFixItem, deriveConflictFixKey, // exported for testing (W-mpcwojgr000a0244)
|
|
11705
11808
|
runWorktreeAdd, // exported for testing (W-mqvaxv65000m76f2 — GVFS partial-checkout behavioral test)
|
|
11706
11809
|
_fetchWithTransientRetry, _classifyFreshBaseFetchFailure, _probeBranchLocally, // exported for testing
|
|
11707
|
-
isWorktreeRetryableError,
|
|
11810
|
+
isWorktreeRetryableError, syncReusedWorktree, pushCleanAheadBranch, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
|
|
11708
11811
|
_statusPorcelainCmd, _killGitDescendantsForWorktree, _bumpQuarantineOutcome, // exported for testing (W-mq5n1zx5)
|
|
11709
11812
|
_reapWorktreeHolders, _findTerminalWorktreeOwners, // exported for testing (W-mqila0t5 — CWD-pinned holder reap)
|
|
11710
11813
|
pruneStaleWorktreeForBranch, // exported for testing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2385",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|