@yemi33/minions 0.1.2382 → 0.1.2384

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.
Files changed (51) hide show
  1. package/bin/minions.js +1 -0
  2. package/dashboard/js/refresh.js +2 -1
  3. package/dashboard/js/settings.js +1 -1
  4. package/dashboard.js +37 -19
  5. package/docs/completion-reports.md +20 -1
  6. package/docs/keep-processes.md +7 -2
  7. package/docs/live-checkout-mode.md +7 -7
  8. package/docs/managed-spawn.md +14 -1
  9. package/docs/runtime-adapters.md +61 -26
  10. package/docs/skills.md +2 -0
  11. package/docs/workspace-manifests.md +1 -1
  12. package/docs/worktree-lifecycle.md +36 -0
  13. package/engine/acp-transport.js +273 -58
  14. package/engine/ado-comment.js +3 -1
  15. package/engine/agent-worker-pool.js +278 -54
  16. package/engine/cc-worker-pool.js +12 -3
  17. package/engine/cleanup.js +15 -11
  18. package/engine/cli.js +56 -28
  19. package/engine/comment-format.js +51 -14
  20. package/engine/create-pr-worktree.js +57 -79
  21. package/engine/dispatch.js +7 -2
  22. package/engine/execution-model.js +68 -0
  23. package/engine/gh-comment.js +6 -3
  24. package/engine/github.js +6 -7
  25. package/engine/keep-process-sweep.js +131 -9
  26. package/engine/lifecycle.js +126 -45
  27. package/engine/live-checkout.js +4 -2
  28. package/engine/llm.js +59 -83
  29. package/engine/managed-spawn.js +112 -5
  30. package/engine/memory-store.js +3 -1
  31. package/engine/playbook.js +4 -6
  32. package/engine/pooled-agent-process.js +512 -45
  33. package/engine/pr-action.js +6 -8
  34. package/engine/process-utils.js +154 -18
  35. package/engine/runtimes/claude.js +94 -25
  36. package/engine/runtimes/codex.js +1 -0
  37. package/engine/runtimes/contract.js +239 -0
  38. package/engine/runtimes/copilot.js +97 -9
  39. package/engine/runtimes/index.js +37 -9
  40. package/engine/shared.js +349 -82
  41. package/engine/spawn-agent.js +85 -116
  42. package/engine/spawn-phase-watchdog.js +8 -37
  43. package/engine/timeout.js +14 -10
  44. package/engine/worktree-gc.js +55 -46
  45. package/engine.js +418 -241
  46. package/package.json +1 -1
  47. package/playbooks/fix.md +20 -0
  48. package/playbooks/review.md +2 -0
  49. package/playbooks/shared-rules.md +2 -2
  50. package/prompts/cc-system.md +11 -11
  51. package/skills/check-self-authored-review-comment/SKILL.md +34 -0
package/engine.js CHANGED
@@ -34,14 +34,20 @@ const shared = require('./engine/shared');
34
34
  const { exec, execAsync, execSilent, runFile, ts, ENGINE_DEFAULTS,
35
35
  WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, REVIEW_STATUS, DISPATCH_RESULT, AGENT_STATUS,
36
36
  FAILURE_CLASS, resolvePollFlag } = shared;
37
- const { resolveRuntime } = require('./engine/runtimes');
38
- const { assertStaleHeadOk, preApproveWorkspaceMcps } = require('./engine/spawn-agent');
37
+ const {
38
+ resolveRuntime,
39
+ resolveInvocationOptions,
40
+ buildSpawnFlags,
41
+ prepareWorkspace,
42
+ } = require('./engine/runtimes');
43
+ const { assertStaleHeadOk } = require('./engine/spawn-agent');
39
44
  // P-1d8f0b93 — fleet ACP worker pool + its child_process-shaped facade.
40
45
  // NOTE: engine/agent-worker-pool.js is a DIFFERENT module from the
41
46
  // dashboard's single-tab CC pool (engine.js must never import that one —
42
47
  // see the cc-tab-pool wiring regression guard test).
43
48
  const agentWorkerPool = require('./engine/agent-worker-pool');
44
49
  const { PooledAgentProcess } = require('./engine/pooled-agent-process');
50
+ const { normalizeExecutionModel } = require('./engine/execution-model');
45
51
  const adoGitAuth = require('./engine/ado-git-auth');
46
52
  const queries = require('./engine/queries');
47
53
  const dispatchEvents = require('./engine/dispatch-events');
@@ -436,22 +442,26 @@ function _maxTurnsForType(type, engineConfig = {}) {
436
442
  // ─── Runtime adapter integration (P-2a6d9c4f) ────────────────────────────────
437
443
 
438
444
  function _buildAgentSpawnFlags(runtime, opts = {}) {
439
- if (runtime && typeof runtime.buildSpawnFlags === 'function') return runtime.buildSpawnFlags(opts);
440
- const caps = (runtime && runtime.capabilities) || {};
441
- const flags = ['--runtime', String(runtime?.name || 'claude')];
442
- if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
443
- if (opts.model) flags.push('--model', String(opts.model));
444
- if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
445
- if (caps.effortLevels && opts.effort) flags.push('--effort', String(opts.effort));
446
- if (caps.sessionResume && opts.sessionId) flags.push('--resume', String(opts.sessionId));
447
- if (caps.budgetCap && opts.maxBudget != null) flags.push('--max-budget-usd', String(opts.maxBudget));
448
- if (caps.bareMode && opts.bare === true) flags.push('--bare');
449
- if (caps.fallbackModel && opts.fallbackModel) flags.push('--fallback-model', String(opts.fallbackModel));
450
- if (opts.stream != null && opts.stream !== '') flags.push('--stream', String(opts.stream));
451
- if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
452
- if (opts.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
453
-
454
- return flags;
445
+ return buildSpawnFlags(runtime, opts);
446
+ }
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;
455
465
  }
456
466
 
457
467
  function _runtimeLogger() {
@@ -603,10 +613,48 @@ function captureSessionIdFromStdoutChunk(agentId, dispatchId, branchName, runtim
603
613
  logger: _runtimeLogger(),
604
614
  });
605
615
  }
616
+
606
617
  return;
607
618
  }
608
619
  }
609
620
 
621
+ function _runtimeModelFromOutputLine(runtime, line) {
622
+ if (!runtime || typeof runtime.parseOutput !== 'function') return null;
623
+ try {
624
+ return normalizeExecutionModel(runtime.parseOutput(String(line), { maxTextLength: 0 })?.model);
625
+ } catch {
626
+ return null;
627
+ }
628
+ }
629
+
630
+ function captureExecutionModelFromStdoutChunk(dispatchItem, dispatchId, runtime, procInfo, chunk, state) {
631
+ if (!procInfo || procInfo.executionModel || !chunk) return null;
632
+ const text = String(state.modelLineBuffer || '') + String(chunk);
633
+ const lines = text.split('\n');
634
+ state.modelLineBuffer = /[\r\n]$/.test(text) ? '' : (lines.pop() || '');
635
+ if (state.modelLineBuffer.length > 65536) state.modelLineBuffer = '';
636
+
637
+ for (const line of lines) {
638
+ const model = _runtimeModelFromOutputLine(runtime, line);
639
+ if (!model) continue;
640
+ procInfo.executionModel = model;
641
+ dispatchItem.executionModel = model;
642
+ try {
643
+ mutateDispatch((dispatch) => {
644
+ for (const section of ['pending', 'active']) {
645
+ const item = (dispatch[section] || []).find(entry => entry.id === dispatchId);
646
+ if (item) item.executionModel = model;
647
+ }
648
+ return dispatch;
649
+ });
650
+ } catch (err) {
651
+ log('warn', `Could not persist runtime model for ${dispatchId}: ${err.message}`);
652
+ }
653
+ return model;
654
+ }
655
+ return null;
656
+ }
657
+
610
658
  function mergePendingSteeringEntries(...groups) {
611
659
  const merged = [];
612
660
  const seen = new Set();
@@ -905,16 +953,14 @@ function _isTransientGitNetworkError(err) {
905
953
  }
906
954
 
907
955
  // Wraps shellSafeGit(['fetch', ...]) with a single retry on transient
908
- // network errors. Preserves the pre-fix convention that fetch failures
909
- // are NON-fatal here: we log + swallow the terminal error so the caller
910
- // falls back to the local ref. The retry just gives transient errors one
911
- // shot to recover before that fallback kicks in empirically eliminates
912
- // most "couldn't find remote ref" cascades downstream caused by a 0-second
913
- // network blip during the initial fetch.
956
+ // network errors. Most existing callers keep the historical non-fatal
957
+ // behavior and receive false on terminal failure. Callers that cannot safely
958
+ // use a stale local ref (ordinary fresh-branch creation) pass throwOnFailure
959
+ // so the terminal git error remains available for accurate classification.
914
960
  //
915
961
  // `args` is the array passed to git AFTER 'fetch' (e.g. ['origin', branch]).
916
962
  // `label` is a short string used in the log line for triage.
917
- async function _fetchWithTransientRetry(args, opts, label) {
963
+ async function _fetchWithTransientRetry(args, opts, label, { throwOnFailure = false } = {}) {
918
964
  const _attempt = async () => shared.shellSafeGit(['fetch', ...args], opts);
919
965
  try {
920
966
  await _attempt();
@@ -930,6 +976,7 @@ async function _fetchWithTransientRetry(args, opts, label) {
930
976
  // incident. _redactBearer keeps any bearer header echoed in the failing
931
977
  // git command out of engine logs.
932
978
  log('warn', `git fetch ${label}: ${adoGitAuth.redactBearer(String(e.message || e))}`);
979
+ if (throwOnFailure) throw e;
933
980
  return false; // swallow non-transient — caller falls back to local ref
934
981
  }
935
982
  const transientMsg = adoGitAuth.redactBearer(String(e.message || e));
@@ -941,19 +988,30 @@ async function _fetchWithTransientRetry(args, opts, label) {
941
988
  return true;
942
989
  } catch (e2) {
943
990
  const retryMsg = adoGitAuth.redactBearer(String(e2.message || e2));
944
- log('warn', `git fetch ${label}: retry also failed (${retryMsg}) — falling back to local ref`);
991
+ log('warn', `git fetch ${label}: retry also failed (${retryMsg})${throwOnFailure ? ' surfacing error' : ' — falling back to local ref'}`);
992
+ if (throwOnFailure) throw e2;
945
993
  return false;
946
994
  }
947
995
  }
948
996
  }
949
997
 
950
- // W-mr3tayu4 — thin wrapper around shared.removeStaleIndexLock. The age-gated
951
- // (>300000ms) stale-lock removal now lives in engine/shared.js so both the
952
- // worktree-add retry paths here and engine/live-checkout.js#prepareLiveCheckout
953
- // share one implementation (no circular import). Kept as a named local because
954
- // three internal call sites and the test harness reference `removeStaleIndexLock`.
955
- function removeStaleIndexLock(rootDir) {
956
- return shared.removeStaleIndexLock(rootDir);
998
+ function _classifyFreshBaseFetchFailure(err) {
999
+ return adoGitAuth.isAdoAuthFailure(err)
1000
+ ? FAILURE_CLASS.AUTH
1001
+ : FAILURE_CLASS.NETWORK_ERROR;
1002
+ }
1003
+
1004
+ async function _probeBranchLocally(rootDir, branchName, gitOpts) {
1005
+ try {
1006
+ await shared.shellSafeGit(
1007
+ ['rev-parse', '--verify', '--quiet', `refs/heads/${branchName}`],
1008
+ { ...gitOpts, cwd: rootDir, timeout: 10000 },
1009
+ );
1010
+ return true;
1011
+ } catch (e) {
1012
+ if (e?.code === 1) return false;
1013
+ throw e;
1014
+ }
957
1015
  }
958
1016
 
959
1017
  async function runWorktreeAdd(rootDir, worktreePath, addArgs, gitOpts, worktreeCreateRetries) {
@@ -970,7 +1028,6 @@ async function runWorktreeAdd(rootDir, worktreePath, addArgs, gitOpts, worktreeC
970
1028
  try {
971
1029
  if (attempt > 0) {
972
1030
  try { await shared.shellSafeGit(['worktree', 'prune'], { ...gitOpts, cwd: rootDir, timeout: 15000 }); } catch (e) { log('warn', 'git: ' + e.message); }
973
- removeStaleIndexLock(rootDir);
974
1031
  log('warn', `Retrying git worktree add (attempt ${attempt + 1}/${retries + 1}) for ${path.basename(worktreePath)}`);
975
1032
  }
976
1033
  await shared.shellSafeGit(['worktree', 'add', worktreePath, ...addArgs], { ...gitOpts, cwd: rootDir });
@@ -2315,7 +2372,6 @@ async function spawnAgent(dispatchItem, config) {
2315
2372
  // (large prompts are written to engine/contexts/<id>.md to keep dispatch rows
2316
2373
  // small — see shared.sidecarDispatchPrompt / #1167).
2317
2374
  let taskPrompt = shared.resolveDispatchPrompt(dispatchItem);
2318
- const claudeConfig = config.claude || {};
2319
2375
  const engineConfig = config.engine || {};
2320
2376
  const startedAt = ts();
2321
2377
  // Phase timing — raw timestamps, emitted as one structured `[spawn-timing]`
@@ -2341,8 +2397,12 @@ async function spawnAgent(dispatchItem, config) {
2341
2397
  updateAgentStatus(id, AGENT_STATUS.FAILED, err.message);
2342
2398
  throw err;
2343
2399
  }
2344
- const metaProjectFields = metaProject && typeof metaProject === 'object' ? metaProject : {};
2345
- const project = projectResolution.project ? { ...projectResolution.project, ...metaProjectFields } : {};
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);
2346
2406
 
2347
2407
  // ── Workspace manifest repo gate (W-mq07avbk000m5543) ────────────────────
2348
2408
  // Dispatch-time enforcement of `agents.<id>.workspace_manifest.allowed_repos`.
@@ -2376,19 +2436,14 @@ async function spawnAgent(dispatchItem, config) {
2376
2436
 
2377
2437
  // W-mp73x32w000l143d: decouple agent cwd from worktree placement.
2378
2438
  // resolveSpawnPaths returns:
2379
- // - read-only types: { cwd: <project dir or MINIONS_DIR>, worktreeRootDir: null }
2380
- // (no drive-root preflight these tasks don't need a worktree)
2439
+ // - project-bound read-only types: detached worktree placement
2440
+ // - project-less read-only types: { cwd: MINIONS_DIR, worktreeRootDir: null }
2381
2441
  // - code-mutating types: { cwd: null, worktreeRootDir: <project root> }
2382
2442
  // (caller defaults cwd to worktreeRootDir; drive-root collapse throws
2383
2443
  // WORKTREE_ROOTDIR_COLLAPSED_TO_DRIVE_ROOT — same fail-fast behavior as
2384
2444
  // the legacy resolveProjectRootDir call this replaced).
2385
- // W-mp8ho6w500034a58: read-only task types (meeting/ask/explore/plan/plan-to-prd)
2386
- // never need a worktree — even when carrying a pipeline branch. Pipeline branches
2387
- // on read-only stages are a no-op label; the stage doesn't commit anything, so
2388
- // the worktree had no functional purpose and was only there to absorb a drive-
2389
- // root preflight that fired against MINIONS_DIR's parent. Read-only pipeline
2390
- // stages now short-circuit alongside any other read-only WI (see the gate at
2391
- // `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.
2392
2447
  const _preBranchName = meta?.branch ? sanitizeBranch(meta.branch) : null;
2393
2448
 
2394
2449
  // ── Per-WI meta.workdir override (P-714ef144) ─────────────────────────
@@ -2435,9 +2490,10 @@ async function spawnAgent(dispatchItem, config) {
2435
2490
  }
2436
2491
  const validatedWorkdir = _wdValidation.value; // null when unset / empty
2437
2492
 
2438
- let cwd, worktreeRootDir, liveMode = false;
2493
+ let cwd, worktreeRootDir, liveMode = false, readOnlyWorktree = false;
2439
2494
  try {
2440
- ({ cwd, worktreeRootDir, liveMode = false } = shared.resolveSpawnPaths(project, type, MINIONS_DIR, { workdir: validatedWorkdir }));
2495
+ ({ cwd, worktreeRootDir, liveMode = false, readOnlyWorktree = false } =
2496
+ shared.resolveSpawnPaths(project, type, MINIONS_DIR, { workdir: validatedWorkdir }));
2441
2497
  } catch (rootErr) {
2442
2498
  if (rootErr?.code === 'WORKTREE_ROOTDIR_COLLAPSED_TO_DRIVE_ROOT' || rootErr?.code === 'WORKTREE_ROOTDIR_MISSING_BASE') {
2443
2499
  log('error', `spawnAgent: project rootDir resolution failed for ${id}: ${rootErr.message}`);
@@ -2486,6 +2542,29 @@ async function spawnAgent(dispatchItem, config) {
2486
2542
  }
2487
2543
  throw rootErr;
2488
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
+ }
2489
2568
  // Legacy local alias: downstream git ops (worktree add, prune, fetch) and
2490
2569
  // the `cwd === rootDir` safety warn at line ~1387 reference `rootDir`. For
2491
2570
  // read-only rootless tasks (no worktree, no branch) this is null — the
@@ -2583,6 +2662,28 @@ async function spawnAgent(dispatchItem, config) {
2583
2662
  });
2584
2663
  } catch (e) { log('warn', `spawnAgent: failed to persist tmpDir for ${id}: ${e.message}`); }
2585
2664
  const _cleanupPromptFiles = () => { shared.removeDispatchTmpDir(dispatchTmpDir); };
2665
+ const _failWorktreeRemotePreparation = (remoteErr, reasonContext, unavailableContext) => {
2666
+ const classificationErr = remoteErr?._probeFailed && remoteErr._cause
2667
+ ? remoteErr._cause
2668
+ : remoteErr;
2669
+ const failureClass = _classifyFreshBaseFetchFailure(classificationErr);
2670
+ const safeRemoteError = adoGitAuth.redactBearer(String(remoteErr?.message || remoteErr));
2671
+ const reason = `${failureClass.toUpperCase().replace(/-/g, '_')}: ${reasonContext}: ${safeRemoteError}`;
2672
+ log('error', `spawnAgent: ${reason}`);
2673
+ _cleanupPromptFiles();
2674
+ completeDispatch(
2675
+ id,
2676
+ DISPATCH_RESULT.ERROR,
2677
+ reason.slice(0, 800),
2678
+ `No worktree or agent was started. The work item remains retryable and will not count against the selected agent because ${unavailableContext}.`,
2679
+ {
2680
+ failureClass,
2681
+ agentRetryable: true,
2682
+ countAgentRetryFailure: false,
2683
+ },
2684
+ );
2685
+ cleanupTempAgent(agentId);
2686
+ };
2586
2687
  // Convert a WORKTREE_NESTED_IN_PROJECT throw into a fail-fast non-retryable
2587
2688
  // dispatch failure (W-mp62taw2000ubcc3). The error's `.code` is set by
2588
2689
  // shared.assertWorktreeOutsideProject so we don't have to parse the message.
@@ -2683,17 +2784,11 @@ async function spawnAgent(dispatchItem, config) {
2683
2784
  _phaseT.afterPrompt = Date.now();
2684
2785
 
2685
2786
  if (branchName && READ_ONLY_ROOT_TASK_TYPES.has(type)) {
2686
- // W-mp7havqf0007ce6b: read-only types (meeting/ask/explore/plan/plan-to-prd)
2687
- // short-circuit BEFORE the worktree-creation block. resolveSpawnPaths returns
2688
- // worktreeRootDir=null for read-only types, and path.resolve(null, ...) throws
2689
- // ("paths[0] must be of type string. Received null"). Pipeline branches used
2690
- // to be exempt — they don't need to be (W-mp8ho6w500034a58): read-only stages
2691
- // never commit, so a pipeline-branch label is meaningless for them and the
2692
- // forced worktree only existed to feed the drive-root preflight that this
2693
- // short-circuit now correctly avoids.
2694
- 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`);
2695
2791
  branchName = null;
2696
- worktreePath = null;
2697
2792
  }
2698
2793
 
2699
2794
  // ── Live-checkout mode handling (P-a3f9b204) ─────────────────────────────
@@ -3452,6 +3547,55 @@ async function spawnAgent(dispatchItem, config) {
3452
3547
  }
3453
3548
  }
3454
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
+
3455
3599
  if (branchName && !liveMode) {
3456
3600
  updateAgentStatus(id, AGENT_STATUS.WORKTREE_SETUP, `Setting up worktree for branch ${branchName}`);
3457
3601
  // W-mpwy3mp5 (#2996): track whether we ended up reusing an existing
@@ -3469,7 +3613,7 @@ async function spawnAgent(dispatchItem, config) {
3469
3613
  // nested worktrees cause glob/grep to match both copies (mirror writes).
3470
3614
  // WORKTREE_NESTED_IN_PROJECT is non-retryable: the recompute on the next
3471
3615
  // tick will produce the same path. Fail fast (W-mp62taw2000ubcc3).
3472
- try { shared.assertWorktreeOutsideProject(worktreePath, rootDir); }
3616
+ try { assertSafeWorktreePath(worktreePath); }
3473
3617
  catch (assertErr) {
3474
3618
  if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
3475
3619
  throw assertErr;
@@ -3483,7 +3627,7 @@ async function spawnAgent(dispatchItem, config) {
3483
3627
  // Same guard for reuse — a previously-created bad worktree must not
3484
3628
  // be silently reused either; the cleanup sweep flags these so the
3485
3629
  // operator can remove them.
3486
- try { shared.assertWorktreeOutsideProject(existingWt, rootDir); }
3630
+ try { assertSafeWorktreePath(existingWt); }
3487
3631
  catch (assertErr) {
3488
3632
  if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
3489
3633
  throw assertErr;
@@ -3535,7 +3679,7 @@ async function spawnAgent(dispatchItem, config) {
3535
3679
  if (!_branchOnRemote) {
3536
3680
  const borrowed = worktreePool.tryBorrow(_poolProject, id);
3537
3681
  if (borrowed && borrowed.path && fs.existsSync(borrowed.path)) {
3538
- try { shared.assertWorktreeOutsideProject(borrowed.path, rootDir); }
3682
+ try { assertSafeWorktreePath(borrowed.path); }
3539
3683
  catch (assertErr) {
3540
3684
  // Always evict before deciding what to do — leaves no BORROWED
3541
3685
  // orphan tied to a dispatch that won't complete normally.
@@ -3597,9 +3741,6 @@ async function spawnAgent(dispatchItem, config) {
3597
3741
  const isSharedBranch = meta?.branchStrategy === 'shared-branch' || meta?.useExistingBranch;
3598
3742
  // Prune stale worktree entries before creating (handles leftover entries from crashed runs)
3599
3743
  try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 10000 }); } catch (e) { log('warn', 'git: ' + e.message); }
3600
- // Remove stale index.lock before creating worktree (Windows crashes can leave this behind)
3601
- removeStaleIndexLock(rootDir);
3602
-
3603
3744
  if (isSharedBranch) {
3604
3745
  log('info', `Creating worktree for shared branch: ${worktreePath} on ${branchName}`);
3605
3746
  await _fetchWithTransientRetry(['origin', branchName], { ..._gitOpts, cwd: rootDir }, `origin/${branchName} (shared-branch pre-create)`);
@@ -3609,7 +3750,7 @@ async function spawnAgent(dispatchItem, config) {
3609
3750
  if (eShared.message?.includes('already used by worktree') || eShared.message?.includes('already checked out')) {
3610
3751
  const existingWtPath = await findExistingWorktree(rootDir, branchName);
3611
3752
  if (existingWtPath && fs.existsSync(existingWtPath)) {
3612
- try { shared.assertWorktreeOutsideProject(existingWtPath, rootDir); }
3753
+ try { assertSafeWorktreePath(existingWtPath); }
3613
3754
  catch (assertErr) {
3614
3755
  if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
3615
3756
  throw assertErr;
@@ -3659,7 +3800,17 @@ async function spawnAgent(dispatchItem, config) {
3659
3800
  // ahead of main) — two consecutive fix dispatches errored on
3660
3801
  // STALE_HEAD over 10 min and the engine then starved the PR for
3661
3802
  // the cooldown window (60 min effective).
3662
- const _branchOnRemote = await probeBranchOnRemote(rootDir, branchName, _gitOpts);
3803
+ let _branchOnRemote;
3804
+ try {
3805
+ _branchOnRemote = await probeBranchOnRemote(rootDir, branchName, _gitOpts);
3806
+ } catch (probeErr) {
3807
+ _failWorktreeRemotePreparation(
3808
+ probeErr,
3809
+ `could not determine whether origin has branch ${branchName}`,
3810
+ `its required origin/${branchName} existence check could not complete`,
3811
+ );
3812
+ return null;
3813
+ }
3663
3814
 
3664
3815
  if (_branchOnRemote) {
3665
3816
  // Mirror shared-branch fetch+add (~line 1157-1159).
@@ -3684,7 +3835,7 @@ async function spawnAgent(dispatchItem, config) {
3684
3835
  log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
3685
3836
  throw eRemote;
3686
3837
  }
3687
- try { shared.assertWorktreeOutsideProject(existingWtPath, rootDir); }
3838
+ try { assertSafeWorktreePath(existingWtPath); }
3688
3839
  catch (assertErr) {
3689
3840
  if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
3690
3841
  throw assertErr;
@@ -3709,7 +3860,7 @@ async function spawnAgent(dispatchItem, config) {
3709
3860
  } else { throw eRemote; }
3710
3861
  }
3711
3862
  } else {
3712
- // Branch isn't on origin (or probe failed) — start a fresh local
3863
+ // Branch is confirmed absent on origin — start a fresh local
3713
3864
  // branch from origin/<mainRef>. This is the dominant path for new
3714
3865
  // implement/decompose dispatches.
3715
3866
  // W-mph6n4p00006ce38: mirror the pool-borrow path (~line 1110-1114)
@@ -3717,27 +3868,43 @@ async function spawnAgent(dispatchItem, config) {
3717
3868
  // not the local ref. Without this, fresh-create dispatches inherit
3718
3869
  // whatever stale local master the engine clone happens to be on
3719
3870
  // (most painful: long-lived engine processes between restarts).
3720
- // Non-fatal: if the fetch fails (network blip, transient auth),
3721
- // fall back to local mainRef so the dispatch still progresses;
3722
- // the dep-merge phase's own fetch + the on-failure
3723
- // `git reset --hard origin/<mainRef>` recovery remain as safety nets.
3871
+ // Fail closed: an ordinary brand-new branch must never inherit a
3872
+ // stale local mainRef when origin/<mainRef> could not be refreshed.
3873
+ // Existing/PR/shared branches are handled above and retain their
3874
+ // continuation semantics.
3875
+ // A local branch with no upstream is an intentional retry/WIP
3876
+ // continuation. Preserve the old `-b` -> "already exists" fallback
3877
+ // below; only truly brand-new branches require a fresh base.
3878
+ const _branchExistsLocally = await _probeBranchLocally(rootDir, branchName, _gitOpts);
3724
3879
  let _freshCreateBase = mainRef;
3725
- const _baseFetchOk = await _fetchWithTransientRetry(
3726
- ['origin', mainRef],
3727
- { ..._gitOpts, cwd: rootDir, timeout: 30000 },
3728
- `origin/${mainRef} (fresh-create base for ${branchName})`
3729
- );
3730
- if (_baseFetchOk) _freshCreateBase = `origin/${mainRef}`;
3880
+ if (!_branchExistsLocally) {
3881
+ try {
3882
+ await _fetchWithTransientRetry(
3883
+ ['origin', mainRef],
3884
+ { ..._gitOpts, cwd: rootDir, timeout: 30000 },
3885
+ `origin/${mainRef} (fresh-create base for ${branchName})`,
3886
+ { throwOnFailure: true },
3887
+ );
3888
+ _freshCreateBase = `origin/${mainRef}`;
3889
+ } catch (freshBaseErr) {
3890
+ _failWorktreeRemotePreparation(
3891
+ freshBaseErr,
3892
+ `could not refresh origin/${mainRef} before creating brand-new branch ${branchName}`,
3893
+ `its required fresh origin/${mainRef} base was unavailable`,
3894
+ );
3895
+ return null;
3896
+ }
3897
+ }
3731
3898
  try {
3732
3899
  await runWorktreeAdd(rootDir, worktreePath, ['-b', branchName, _freshCreateBase], _worktreeGitOpts, worktreeCreateRetries);
3733
3900
  } catch (e1) {
3734
3901
  const branchExists = e1.message?.includes('already exists');
3735
3902
  log('warn', `Worktree -b failed for ${branchName}: ${e1.message?.split('\n')[0]}`);
3736
3903
  if (!branchExists) {
3737
- // Transient error (lock, timeout) — prune, clean, and retry -b once more
3904
+ // Transient error (lock, timeout) — prune and retry -b once more.
3905
+ // Never delete the operator checkout's .git/index.lock.
3738
3906
  log('info', `Retrying -b create after prune for ${branchName}`);
3739
3907
  try { await shared.shellSafeGit(['worktree', 'prune'], { ..._gitOpts, cwd: rootDir, timeout: 15000 }); } catch { /* optional */ }
3740
- removeStaleIndexLock(rootDir);
3741
3908
  // Clean up partial worktree directory from the failed -b
3742
3909
  // attempt. This husk pre-dates `git worktree add`, so git
3743
3910
  // does not yet own it and shared.removeWorktree's
@@ -3799,7 +3966,7 @@ async function spawnAgent(dispatchItem, config) {
3799
3966
  log('warn', `Branch ${branchName} actively used by another agent at ${existingWtPath} — cannot create worktree`);
3800
3967
  throw e2;
3801
3968
  }
3802
- try { shared.assertWorktreeOutsideProject(existingWtPath, rootDir); }
3969
+ try { assertSafeWorktreePath(existingWtPath); }
3803
3970
  catch (assertErr) {
3804
3971
  if (assertErr?.code === 'WORKTREE_NESTED_IN_PROJECT') { _failWorktreePreflight(assertErr); return null; }
3805
3972
  throw assertErr;
@@ -4509,6 +4676,21 @@ async function spawnAgent(dispatchItem, config) {
4509
4676
  }
4510
4677
  }
4511
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
+
4512
4694
  updateAgentStatus(id, AGENT_STATUS.READY, 'Worktree ready, preparing to spawn process');
4513
4695
 
4514
4696
  if (worktreePath && meta?.source === 'work-item' && meta?.item?.branchStrategy === 'shared-branch') {
@@ -4625,40 +4807,18 @@ async function spawnAgent(dispatchItem, config) {
4625
4807
  const resolvedModel = runtime.resolveModel(shared.resolveAgentModel(agentConfig, engineConfig));
4626
4808
  const resolvedMaxBudget = shared.resolveAgentMaxBudget(agentConfig, engineConfig);
4627
4809
  const resolvedBare = shared.resolveAgentBareMode(agentConfig, engineConfig);
4628
- // P-1d8f0b93 pool a copilot dispatch through the persistent ACP worker
4629
- // pool (engine/agent-worker-pool.js) instead of cold-spawning a fresh CLI
4630
- // process, when the capability + config gate allows it. Structurally
4631
- // copilot-only (runtime.capabilities.acpWorkerPool) — see
4632
- // shared.resolveAgentUseWorkerPool for the exact precedence chain.
4633
- const useAgentPool = shared.resolveAgentUseWorkerPool(engineConfig, runtime);
4634
- if (useAgentPool) {
4635
- // Idempotent cheap to call on every pooled dispatch so a live config
4636
- // change (agentAcpPoolSize / maxConcurrent) takes effect without a
4637
- // separate engine-restart-only wiring hook.
4638
- agentWorkerPool.setPoolSize(shared.resolveAgentAcpPoolSize(engineConfig));
4639
- }
4640
-
4641
- // W-mpg6isvy000xca4d — On retry after FAILURE_CLASS.MODEL_UNAVAILABLE, swap
4642
- // to the runtime-appropriate fallback model. Two paths gated on
4643
- // `runtime.capabilities.fallbackModel` (no `runtime.name === ...` branches):
4644
- // - Capability TRUE (Claude): the CLI's own --fallback-model handles
4645
- // the swap on 429. We keep `engineConfig.claudeFallbackModel` plumbed
4646
- // unconditionally via the fallbackModel opt below; no model override
4647
- // needed at the engine layer.
4648
- // - Capability FALSE (Copilot): no --fallback-model flag exists, so we
4649
- // OVERRIDE the --model arg directly with engine.copilotFallbackModel
4650
- // for this retry attempt only. The work item's _lastFailureClass is
4651
- // written by dispatch.js's retry block; the next normal dispatch loop
4652
- // pick-up re-reads it through meta.item.
4653
- let effectiveModel = resolvedModel;
4654
- const prevFailureClass = meta?.item?._lastFailureClass || null;
4655
- if (prevFailureClass === FAILURE_CLASS.MODEL_UNAVAILABLE
4656
- && runtime.capabilities?.fallbackModel === false
4657
- && engineConfig.copilotFallbackModel) {
4658
- effectiveModel = engineConfig.copilotFallbackModel;
4659
- log('info', `MODEL_UNAVAILABLE retry: ${runtimeName} ${id} — overriding --model to ${effectiveModel}`);
4810
+ // Pool through the persistent ACP worker transport when the selected adapter
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`);
4660
4819
  }
4661
4820
 
4821
+ const prevFailureClass = meta?.item?._lastFailureClass || null;
4662
4822
  const requestedEffort = engineConfig.agentEffort || null;
4663
4823
 
4664
4824
  let cachedSessionId = null;
@@ -4667,53 +4827,49 @@ async function spawnAgent(dispatchItem, config) {
4667
4827
  agentId,
4668
4828
  branchName,
4669
4829
  agentsDir: AGENTS_DIR,
4670
- // Pass the working directory so the Claude adapter can probe for the
4671
- // conversation jsonl and avoid `--resume <dead-uuid>` retry loops when
4672
- // the agent died before checkpoint (W-mouugzow00068741).
4830
+ // Adapters may use cwd to verify that their backing session still exists
4831
+ // before returning a resumable ID.
4673
4832
  cwd,
4674
4833
  logger: _runtimeLogger(),
4675
4834
  });
4676
4835
  }
4677
4836
 
4678
- const args = _buildAgentSpawnFlags(runtime, {
4679
- model: effectiveModel,
4837
+ const runtimeOptions = resolveInvocationOptions(runtime, {
4838
+ model: resolvedModel,
4680
4839
  maxTurns: _maxTurnsForType(type, engineConfig),
4681
- allowedTools: shared.mergeManifestAllowedTools(
4682
- claudeConfig.allowedTools,
4683
- shared.resolveAgentManifest(_manifestAgent).allowed_tools,
4684
- ),
4840
+ allowedTools: shared.resolveAgentAllowedTools(config),
4685
4841
  effort: requestedEffort,
4686
4842
  sessionId: cachedSessionId,
4687
4843
  maxBudget: resolvedMaxBudget,
4688
4844
  bare: resolvedBare,
4689
- fallbackModel: engineConfig.claudeFallbackModel,
4690
- stream: engineConfig.copilotStreamMode,
4691
- disableBuiltinMcps: engineConfig.copilotDisableBuiltinMcps,
4692
- reasoningSummaries: engineConfig.copilotReasoningSummaries,
4845
+ }, {
4846
+ config,
4847
+ engineConfig,
4848
+ failureClass: prevFailureClass,
4693
4849
  });
4850
+ runtimeOptions.allowedTools = shared.mergeManifestAllowedTools(
4851
+ runtimeOptions.allowedTools,
4852
+ shared.resolveAgentManifest(_manifestAgent).allowed_tools,
4853
+ );
4854
+ const effectiveModel = runtimeOptions.model;
4855
+ if (prevFailureClass === FAILURE_CLASS.MODEL_UNAVAILABLE
4856
+ && effectiveModel && effectiveModel !== resolvedModel) {
4857
+ log('info', `MODEL_UNAVAILABLE retry: ${runtimeName} ${id} — adapter selected model ${effectiveModel}`);
4858
+ }
4859
+ const args = _buildAgentSpawnFlags(runtime, runtimeOptions);
4694
4860
 
4695
- // MCP servers: agents inherit from ~/.claude.json directly as Claude Code processes.
4696
- // No --mcp-config needed avoids redundant config and ensures agents always have latest servers.
4697
- //
4698
- // P-7d31a06b — When the runtime is Claude AND the worktree has a `.mcp.json`,
4699
- // pre-warm `~/.claude.json` projects.<worktreePath>.enabledMcpjsonServers so
4700
- // Claude's first call doesn't show the project-MCP trust prompt (which
4701
- // --dangerously-skip-permissions silently suppresses → agent runs without the
4702
- // workspace MCPs connected). Helper internally no-ops for non-Claude runtimes
4703
- // and when engine.claudePreApproveWorkspaceMcps is false (no runtime.name
4704
- // check at this call site, per CLAUDE.md rule). Best-effort: NEVER block dispatch.
4705
- //
4861
+ // Let the adapter prepare any runtime-owned workspace state. Best-effort:
4862
+ // harness setup must never block dispatch.
4706
4863
  try {
4707
- const result = preApproveWorkspaceMcps({
4708
- runtimeName,
4864
+ const result = prepareWorkspace(runtime, {
4709
4865
  worktreePath,
4710
4866
  homeDir: os.homedir(),
4711
4867
  engineConfig,
4712
4868
  mutateJsonFileLocked: shared.mutateJsonFileLocked,
4713
4869
  });
4714
4870
  if (result.wrote) {
4715
- log('info', `Pre-approved ${result.servers.length} workspace MCP server(s) in ~/.claude.json for ${worktreePath}: ${result.servers.join(', ')}`);
4716
- } else if (result.reason !== 'no-workspace-mcp' && result.reason !== 'not-claude' && result.reason !== 'no-worktree') {
4871
+ log('info', `Runtime workspace prepared for ${id}${result.target ? ` (${result.target})` : ''}`);
4872
+ } else if (result.reason !== 'no-workspace-mcp' && result.reason !== 'unsupported-runtime' && result.reason !== 'no-worktree') {
4717
4873
  log('debug', `Skipped workspace MCP pre-approval for ${id} (reason=${result.reason})`);
4718
4874
  }
4719
4875
  } catch (err) {
@@ -4724,10 +4880,22 @@ async function spawnAgent(dispatchItem, config) {
4724
4880
 
4725
4881
  log('info', `Spawning agent: ${agentId} (${id}) in ${cwd}`);
4726
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
+ }
4727
4895
 
4728
4896
  // Agent status is derived from dispatch state.
4729
4897
 
4730
- // Spawn the claude process
4898
+ // Spawn the selected harness runtime.
4731
4899
  const childEnv = shared.cleanChildEnv();
4732
4900
  if (completionReportPath) childEnv.MINIONS_COMPLETION_REPORT = completionReportPath;
4733
4901
  if (completionNonce) childEnv.MINIONS_COMPLETION_NONCE = completionNonce;
@@ -4740,6 +4908,11 @@ async function spawnAgent(dispatchItem, config) {
4740
4908
  || dispatchItem.meta?.keep_processes_skip_workdir_check) {
4741
4909
  childEnv.MINIONS_KEEP_PROCESSES_SKIP_WORKDIR_CHECK = '1';
4742
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
+ }
4743
4916
 
4744
4917
  // W-mpg54mi2000n7b7e — suppress Git's interactive credential prompts and
4745
4918
  // Git Credential Manager's GUI dialog for every agent spawn. Without these,
@@ -4787,7 +4960,7 @@ async function spawnAgent(dispatchItem, config) {
4787
4960
 
4788
4961
  // Spawn via wrapper script — node directly (no bash intermediary)
4789
4962
  // spawn-agent.js handles CLAUDECODE env cleanup and claude binary resolution
4790
- const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
4963
+ const spawnScript = runtime.spawnScript || path.join(ENGINE_DIR, 'spawn-agent.js');
4791
4964
 
4792
4965
  const spawnArgs = [spawnScript, promptPath, sysPromptPath, ...args];
4793
4966
  // P-1d8f0b93 — the pooled-dispatch branch further down (agentWorkerPool
@@ -4850,9 +5023,9 @@ async function spawnAgent(dispatchItem, config) {
4850
5023
  }
4851
5024
 
4852
5025
  _phaseT.spawnCallStart = Date.now();
4853
- // P-c5a1f3b8 (Item D — live-mode push/PR-create cwd guard): in live mode
4854
- // worktreePath stays null, so `cwd` here is project.localPath (it is never
4855
- // reassigned to a worktree by the `if (worktreePath ...)` block above). The
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
4856
5029
  // agent's `git push -u origin <branch>` and PR-create therefore run in-place
4857
5030
  // against the operator's checkout. This cwd MUST NEVER be null for a mutating
4858
5031
  // live dispatch — shared.resolveSpawnPaths returns cwd === project.localPath
@@ -4891,6 +5064,7 @@ async function spawnAgent(dispatchItem, config) {
4891
5064
  'MINIONS_LIVE_OUTPUT_PATH',
4892
5065
  'MINIONS_STEERING_ACK_DIR',
4893
5066
  'MINIONS_AGENT_CWD',
5067
+ 'MINIONS_KEEP_PROCESSES_ALLOWED_CWD_ROOT',
4894
5068
  ];
4895
5069
  const pooledProcessEnv = { ...childEnv };
4896
5070
  const pooledSessionContext = {};
@@ -4900,18 +5074,30 @@ async function spawnAgent(dispatchItem, config) {
4900
5074
  }
4901
5075
  proc = new PooledAgentProcess({
4902
5076
  pool: agentWorkerPool,
5077
+ runtime,
4903
5078
  dispatchId: id,
4904
5079
  cwd,
4905
- model: effectiveModel,
4906
- effort: requestedEffort,
5080
+ processCwd: _resolveAgentPoolProcessCwd(project, engineConfig),
5081
+ model: runtimeOptions.model,
5082
+ effort: runtimeOptions.effort,
4907
5083
  // Mirrors the CC worker pool's convention (dashboard.js) — a single
4908
5084
  // flat top-level config array, not per-agent-resolved.
4909
5085
  mcpServers: (engineConfig && engineConfig.mcpServers) || [],
4910
5086
  hermeticDirs: addDirs,
4911
5087
  processEnv: pooledProcessEnv,
5088
+ workerOptions: runtimeOptions,
5089
+ sessionId: cachedSessionId,
4912
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
+ ),
4913
5097
  systemPromptText: systemPrompt,
4914
- promptText: fullTaskPrompt,
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'),
4915
5101
  });
4916
5102
  // The child_process PID file is normally written asynchronously by
4917
5103
  // spawn-agent.js itself (engine/spawn-agent.js:606) once it starts.
@@ -5043,7 +5229,7 @@ async function spawnAgent(dispatchItem, config) {
5043
5229
  let stdout = '';
5044
5230
  let stderr = '';
5045
5231
  let steeringAckStdout = '';
5046
- const sessionCaptureState = { sessionLineBuffer: '' };
5232
+ const sessionCaptureState = { sessionLineBuffer: '', modelLineBuffer: '' };
5047
5233
  let _trustCheckDone = false;
5048
5234
  const _spawnTime = Date.now();
5049
5235
 
@@ -5078,6 +5264,7 @@ async function spawnAgent(dispatchItem, config) {
5078
5264
  // Copilot emits sessionId, so use the runtime-neutral steering helper.
5079
5265
  const procInfo = activeProcesses.get(id);
5080
5266
  markRuntimeResumeOutputSeen(procInfo);
5267
+ captureExecutionModelFromStdoutChunk(dispatchItem, id, runtime, procInfo, chunk, sessionCaptureState);
5081
5268
  captureSessionIdFromStdoutChunk(agentId, id, branchName, runtime, procInfo, chunk, sessionCaptureState);
5082
5269
 
5083
5270
  ackPendingSteeringFiles(agentId, procInfo, chunk);
@@ -5199,22 +5386,17 @@ async function spawnAgent(dispatchItem, config) {
5199
5386
  return;
5200
5387
  }
5201
5388
 
5202
- const resumeArgs = _buildAgentSpawnFlags(runtime, {
5203
- model: effectiveModel,
5389
+ const resumeRuntimeOptions = resolveInvocationOptions(runtime, {
5390
+ ...runtimeOptions,
5204
5391
  maxTurns: engineConfig?.maxTurns || ENGINE_DEFAULTS.maxTurns,
5205
- allowedTools: shared.mergeManifestAllowedTools(
5206
- claudeConfig?.allowedTools,
5207
- shared.resolveAgentManifest(_manifestAgent).allowed_tools,
5208
- ),
5209
5392
  sessionId: steerSessionId,
5210
- maxBudget: resolvedMaxBudget,
5211
- bare: resolvedBare,
5212
- fallbackModel: engineConfig?.claudeFallbackModel,
5213
- stream: engineConfig?.copilotStreamMode,
5214
- disableBuiltinMcps: engineConfig?.copilotDisableBuiltinMcps,
5215
- reasoningSummaries: engineConfig?.copilotReasoningSummaries,
5393
+ }, {
5394
+ config,
5395
+ engineConfig,
5396
+ failureClass: prevFailureClass,
5216
5397
  });
5217
- if (!resumeArgs.includes('--resume')) {
5398
+ const resumeArgs = _buildAgentSpawnFlags(runtime, resumeRuntimeOptions);
5399
+ if (runtime.capabilities?.sessionResume !== true) {
5218
5400
  log('warn', `Steering: runtime ${runtime.name} did not accept session resume — skipping for ${agentId}`);
5219
5401
  try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Runtime ${runtime.name} does not support session resume. Message was: ${steerMsg}\n`); } catch {}
5220
5402
  try { fs.unlinkSync(steerPromptPath); } catch {}
@@ -5224,7 +5406,7 @@ async function spawnAgent(dispatchItem, config) {
5224
5406
  return;
5225
5407
  }
5226
5408
 
5227
- const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
5409
+ const spawnScript = runtime.spawnScript || path.join(ENGINE_DIR, 'spawn-agent.js');
5228
5410
  const childEnv = shared.cleanChildEnv();
5229
5411
  if (completionReportPath) childEnv.MINIONS_COMPLETION_REPORT = completionReportPath;
5230
5412
  // P-d2a8f6c1: preserve the per-dispatch nonce across steering resume so
@@ -5271,6 +5453,24 @@ async function spawnAgent(dispatchItem, config) {
5271
5453
  // Re-expose the engine-resolved cwd on steering resume (matches the
5272
5454
  // initial spawn path) so $MINIONS_AGENT_CWD stays valid across resumes.
5273
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
+ }
5274
5474
  let resumeProc;
5275
5475
  try {
5276
5476
  // detached so the resumed steering session also survives engine death (matches initial spawn)
@@ -5289,6 +5489,12 @@ async function spawnAgent(dispatchItem, config) {
5289
5489
  cleanupTempAgent(agentId);
5290
5490
  return;
5291
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);
5292
5498
 
5293
5499
  // Re-attach to existing tracking — do NOT carry _steeringAt forward (#1052).
5294
5500
  // The kill watcher in timeout.js fires 30s after _steeringAt is set. If we carry it
@@ -5321,6 +5527,7 @@ async function spawnAgent(dispatchItem, config) {
5321
5527
  stderr = '';
5322
5528
  }
5323
5529
  sessionCaptureState.sessionLineBuffer = '';
5530
+ sessionCaptureState.modelLineBuffer = '';
5324
5531
  // Re-wire stdout/stderr handlers (same as original)
5325
5532
  resumeProc.stdout.on('data', (data) => {
5326
5533
  const chunk = data.toString();
@@ -5348,6 +5555,7 @@ async function spawnAgent(dispatchItem, config) {
5348
5555
  } catch { /* best-effort */ }
5349
5556
  }
5350
5557
  markRuntimeResumeOutputSeen(resumeInfo);
5558
+ captureExecutionModelFromStdoutChunk(dispatchItem, id, runtime, resumeInfo, chunk, sessionCaptureState);
5351
5559
  captureSessionIdFromStdoutChunk(agentId, id, branchName, runtime, resumeInfo, chunk, sessionCaptureState);
5352
5560
  ackPendingSteeringFiles(agentId, resumeInfo, chunk);
5353
5561
  });
@@ -5509,8 +5717,12 @@ async function spawnAgent(dispatchItem, config) {
5509
5717
  let evalResult = schemaResult;
5510
5718
  let isWorkdirRejection = false;
5511
5719
  const workdirGateOn = !_kpSkipWorkdir && ENGINE_DEFAULTS.keepProcesses?.requireGitWorkdir !== false;
5512
- if (schemaResult.exists && schemaResult.accepted && workdirGateOn) {
5513
- const workdirResult = keepProcessSweep.evaluateKeepPidsAcceptance(agentId, { requireGitWorkdir: true });
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
+ });
5514
5726
  if (workdirResult.exists && !workdirResult.accepted && workdirResult.isWorkdirRejection) {
5515
5727
  evalResult = workdirResult;
5516
5728
  isWorkdirRejection = true;
@@ -5605,6 +5817,22 @@ async function spawnAgent(dispatchItem, config) {
5605
5817
  } catch (alertErr) {
5606
5818
  log('warn', `keep-processes acceptance: failed to emit inbox alert for ${agentId}: ${alertErr.message}`);
5607
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
+ }
5608
5836
  }
5609
5837
  } catch (e) {
5610
5838
  log('warn', `keep-processes acceptance check failed for ${agentId} (${id}): ${e.message}`);
@@ -5632,7 +5860,12 @@ async function spawnAgent(dispatchItem, config) {
5632
5860
  if (_msEnabled) {
5633
5861
  try {
5634
5862
  const managedSpawn = require('./engine/managed-spawn');
5635
- const evalResult = managedSpawn.evaluateManagedSpawnAcceptance(agentId);
5863
+ const allowedCwdRoot = keepProcessesAllowedCwdRoot;
5864
+ const evalResult = managedSpawn.evaluateManagedSpawnAcceptance(agentId, {
5865
+ projects: getProjects(config),
5866
+ project,
5867
+ ...(allowedCwdRoot ? { allowedCwdRoot } : {}),
5868
+ });
5636
5869
  if (evalResult.exists && !evalResult.accepted) {
5637
5870
  managedSpawnAcceptanceFailure = {
5638
5871
  reason: evalResult.reason,
@@ -5690,6 +5923,9 @@ async function spawnAgent(dispatchItem, config) {
5690
5923
  owner_agent: agentId,
5691
5924
  owner_wi: dispatchItem.meta?.item?.id || '',
5692
5925
  owner_project: project?.name || '',
5926
+ owner_work_type: type,
5927
+ dispatch_root: allowedCwdRoot || '',
5928
+ checkout_mode: checkoutModeAtDispatch,
5693
5929
  };
5694
5930
  const spawnedItems = [];
5695
5931
  let spawnFailureReason = null;
@@ -6030,7 +6266,7 @@ async function spawnAgent(dispatchItem, config) {
6030
6266
  // crashed dispatch leaks `work/W-<id>` worktree entries that block
6031
6267
  // every retry with `fatal: 'work/W-<id>' is already used by worktree
6032
6268
  // at …` until the 2-hour age sweep in cleanup.js eventually catches up.
6033
- if (worktreePath && rootDir && branchName) {
6269
+ if (worktreePath && rootDir) {
6034
6270
  try {
6035
6271
  const _wgc = require('./engine/worktree-gc');
6036
6272
  const _wtRoot = path.resolve(rootDir, engineConfig.worktreeRoot || ENGINE_DEFAULTS.worktreeRoot);
@@ -6077,7 +6313,7 @@ async function spawnAgent(dispatchItem, config) {
6077
6313
  // restore is a plain `git checkout <originalRef>` — never --force/reset/
6078
6314
  // clean/stash — and is best-effort: a thrown restore never alters the
6079
6315
  // dispatch result.
6080
- if (liveMode && branchName) {
6316
+ if (liveMode && branchName && shared.isLiveCheckoutDispatchRecord(dispatchItem, getConfig())) {
6081
6317
  try {
6082
6318
  const _liveRestore = require('./engine/live-checkout');
6083
6319
  await _liveRestore.restoreLiveCheckoutAtDispatchEnd({
@@ -6259,11 +6495,19 @@ async function spawnAgent(dispatchItem, config) {
6259
6495
  // route output parsing through the right adapter. Also surfaces the choice
6260
6496
  // in dispatch state for debugging multi-runtime fleets.
6261
6497
  item.runtimeName = runtimeName;
6498
+ if (effectiveModel) item.requestedModel = effectiveModel;
6499
+ else delete item.requestedModel;
6500
+ if (dispatchItem.executionModel) item.executionModel = dispatchItem.executionModel;
6501
+ else delete item.executionModel;
6262
6502
  // P-3f5b7d26 — persist pooled on the DISK record (activeProcesses/_pooled
6263
6503
  // is wiped on restart); cli.js's reattach path needs this since a pooled
6264
6504
  // ACP worker isn't spawned detached and a fresh pool cold-starts empty —
6265
6505
  // a live worker PID must never be mistaken for a resumable dispatch.
6266
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;
6267
6511
  // W-mq5rwwss000f30a7 — persist the worktree path so the live-worktree
6268
6512
  // guard (shared.isWorktreePathLive) can correlate destructive callers
6269
6513
  // (removeWorktree, cleanup orphan-dir sweep, pool-return, quarantine)
@@ -6542,73 +6786,6 @@ function safePrdProjectSlug(projectName) {
6542
6786
  return slug || 'project';
6543
6787
  }
6544
6788
 
6545
- // Fleet branches the engine/CC create. A worktree-mode operator checkout should
6546
- // NEVER sit on one of these (or any branch); when it does, a flow ran raw
6547
- // `git checkout`/`merge` in the live checkout instead of a worktree.
6548
- const _FLEET_BRANCH_RE = /^(cc-pr|backport|e2e|verify|sched-|work)\//;
6549
-
6550
- // Self-heal: a worktree-mode project's operator checkout (project.localPath) must
6551
- // always sit on its mainBranch — agents use isolated worktrees, CC Create-PR uses
6552
- // prepare-create-pr-worktree. But fleet flows (CC raw git, backport, sync)
6553
- // sometimes run `git checkout <branch>` + `git merge` THERE despite the rules,
6554
- // leaving it stuck on a feature branch (often mid-conflict) — which restores
6555
- // git-tracked dead PRDs and pollutes the operator's main, then the materializer
6556
- // re-runs finished work. Detect + restore. Conservative: only acts on a stuck
6557
- // merge/rebase OR a recognizably fleet-created branch, so a human's deliberate
6558
- // `git checkout my-feature` in their checkout is left alone. Branch commits are
6559
- // preserved on their ref (reset --hard only drops the uncommitted hijack state).
6560
- function restoreHijackedOperatorCheckouts(config) {
6561
- if (config?.engine?.autoRestoreOperatorCheckout === false) return;
6562
- let projects;
6563
- try { projects = getProjects(config); } catch { return; }
6564
- for (const project of projects) {
6565
- try {
6566
- const localPath = project?.localPath;
6567
- if (!localPath || !fs.existsSync(localPath)) continue;
6568
- if (shared.resolveCheckoutMode(project) === shared.CHECKOUT_MODES.LIVE) continue; // live-mode: feature branch is intentional
6569
- const main = project.mainBranch || 'main';
6570
- const opts = { cwd: localPath };
6571
- // W-mrcvh5hw000o6cf1: use the argv-form (shell: false) git helper instead of
6572
- // execSilent(). execSilent() shells out via cmd.exe, which cannot chdir into
6573
- // a UNC path (`//wsl.localhost/...`, `\\server\share\...`) — it silently
6574
- // falls back to the Windows directory and every git call then fails with
6575
- // "fatal: not a git repository", every tick, forever, for WSL/UNC-hosted
6576
- // projects. shellSafeGitSync spawns git.exe directly (execFileSync,
6577
- // shell:false) so `cwd` is honored even for UNC paths.
6578
- let isRepo;
6579
- try { isRepo = String(shared.shellSafeGitSync(['rev-parse', '--is-inside-work-tree'], opts) || '').trim(); }
6580
- catch { continue; } // not a git repo (or transiently unreachable) — nothing to restore, skip quietly
6581
- if (isRepo !== 'true') continue;
6582
- const cur = String(shared.shellSafeGitSync(['rev-parse', '--abbrev-ref', 'HEAD'], opts) || '').trim();
6583
- if (!cur || cur === 'HEAD' || cur === main) continue; // on main or detached — fine
6584
- const gitDir = String(shared.shellSafeGitSync(['rev-parse', '--git-dir'], opts) || '').trim();
6585
- const absGitDir = gitDir ? (path.isAbsolute(gitDir) ? gitDir : path.join(localPath, gitDir)) : path.join(localPath, '.git');
6586
- const mergeStuck = fs.existsSync(path.join(absGitDir, 'MERGE_HEAD'));
6587
- const rebaseStuck = fs.existsSync(path.join(absGitDir, 'rebase-merge')) || fs.existsSync(path.join(absGitDir, 'rebase-apply'));
6588
- if (!mergeStuck && !rebaseStuck && !_FLEET_BRANCH_RE.test(cur)) continue; // likely a human's deliberate checkout — leave it
6589
- const why = mergeStuck ? ' (stuck merge)' : rebaseStuck ? ' (stuck rebase)' : ' (fleet branch in live checkout)';
6590
- log('warn', `Operator checkout for ${project.name} is on '${cur}'${why}; worktree-mode requires ${main} — restoring`);
6591
- if (mergeStuck) shared.shellSafeGitSync(['merge', '--abort'], opts);
6592
- if (rebaseStuck) shared.shellSafeGitSync(['rebase', '--abort'], opts);
6593
- shared.shellSafeGitSync(['reset', '--hard', 'HEAD'], opts); // drop uncommitted hijack state; branch commits stay on the ref
6594
- try { shared.validateGitRef(main); } catch (ve) { throw new Error(`restoreHijackedOperatorCheckouts: invalid mainBranch ${JSON.stringify(main)}: ${ve.message}`); }
6595
- shared.shellSafeGitSync(['checkout', main], opts);
6596
- const now = String(shared.shellSafeGitSync(['rev-parse', '--abbrev-ref', 'HEAD'], opts) || '').trim();
6597
- if (now === main) {
6598
- try {
6599
- writeInboxAlert(`operator-checkout-restored-${project.name}`,
6600
- `Operator checkout ${localPath} was hijacked onto '${cur}'${why} and restored to ${main}. ` +
6601
- 'A fleet flow ran git checkout/merge in the LIVE operator checkout instead of an isolated worktree ' +
6602
- '(playbooks/shared-rules.md: "Do NOT checkout branches in the main working tree — use worktrees"). ' +
6603
- `The '${cur}' branch commits are preserved on its ref.`);
6604
- } catch { /* alert is best-effort */ }
6605
- } else {
6606
- log('warn', `Operator checkout restore for ${project.name} did not reach ${main} (now '${now}') — manual intervention needed`);
6607
- }
6608
- } catch (e) { log('warn', `restoreHijackedOperatorCheckouts ${project?.name || '?'}: ${e?.message || e}`); }
6609
- }
6610
- }
6611
-
6612
6789
  function materializePlansAsWorkItems(config) {
6613
6790
  const writePrdLocked = (fileName, data) => {
6614
6791
  return prdStore.writePrd(fileName, data);
@@ -7990,6 +8167,7 @@ async function discoverFromPrs(config, project) {
7990
8167
  const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
7991
8168
  pr_id: pr.id, pr_branch: prBranch,
7992
8169
  review_note: pr.minionsReview?.note || pr.reviewNote || 'See PR thread comments',
8170
+ review_finding_id: shared.prReviewFindingIdentity(pr),
7993
8171
  }, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, {
7994
8172
  dispatchKey: key, cooldownKey: key, automationCauseKey: reviewCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta,
7995
8173
  // W-mpg58wv3 — closure-loop binding. Carries the originating minion review
@@ -10484,6 +10662,7 @@ async function tickInner() {
10484
10662
  catch (e) { log('warn', `lastTickAt write: ${e.message}`); }
10485
10663
 
10486
10664
  const config = getConfig();
10665
+ _syncAgentWorkerPoolConfig(config.engine);
10487
10666
  tickCount++;
10488
10667
  const now = Date.now();
10489
10668
  const tickIntervalMs = Math.max(1, Number(config.engine?.tickInterval) || ENGINE_DEFAULTS.tickInterval);
@@ -11516,9 +11695,6 @@ async function tickInner() {
11516
11695
  const maxC = resolveMaxConcurrent(config);
11517
11696
  setTempBudget(Math.max(0, maxC - activeCountPre));
11518
11697
  }
11519
- // Self-heal a hijacked operator checkout BEFORE discovery/materialization, so a
11520
- // dead PRD restored by a stray `git checkout` can't be re-materialized this tick.
11521
- try { restoreHijackedOperatorCheckouts(config); } catch (e) { log('warn', 'restoreHijackedOperatorCheckouts: ' + e.message); }
11522
11698
  let discoveryOk = true;
11523
11699
  try { await withTickTimeout(() => discoverWork(config), 'discoverWork', tickOpTimeoutMs); } catch (e) { log('warn', 'discoverWork: ' + e.message); discoveryOk = false; }
11524
11700
  if (_isTickStale(myGeneration)) return;
@@ -11619,7 +11795,6 @@ module.exports = {
11619
11795
  discoverWork, discoverFromPrs, discoverFromWorkItems, discoverCentralWorkItems,
11620
11796
  materializePlansAsWorkItems,
11621
11797
  materializeSpecsAsWorkItems, // exported for testing (P-f7-git-log)
11622
- restoreHijackedOperatorCheckouts, // exported for testing — operator-checkout self-heal
11623
11798
 
11624
11799
  // Shared helpers (used by lifecycle.js and tests)
11625
11800
  reconcileItemsWithPrs, detectDependencyCycles,
@@ -11631,13 +11806,15 @@ module.exports = {
11631
11806
  gitOutputToString, gitErrorOutput, classifyDepMergeFailureOutput, listUnmergedFiles, // exported for testing
11632
11807
  buildDepConflictFixItem, deriveConflictFixKey, // exported for testing (W-mpcwojgr000a0244)
11633
11808
  runWorktreeAdd, // exported for testing (W-mqvaxv65000m76f2 — GVFS partial-checkout behavioral test)
11634
- isWorktreeRetryableError, removeStaleIndexLock, syncReusedWorktree, pushCleanAheadBranch, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
11809
+ _fetchWithTransientRetry, _classifyFreshBaseFetchFailure, _probeBranchLocally, // exported for testing
11810
+ isWorktreeRetryableError, syncReusedWorktree, pushCleanAheadBranch, assertCleanSharedWorktree, _quarantineDirtyWorktree, // exported for testing
11635
11811
  _statusPorcelainCmd, _killGitDescendantsForWorktree, _bumpQuarantineOutcome, // exported for testing (W-mq5n1zx5)
11636
11812
  _reapWorktreeHolders, _findTerminalWorktreeOwners, // exported for testing (W-mqila0t5 — CWD-pinned holder reap)
11637
11813
  pruneStaleWorktreeForBranch, // exported for testing
11638
11814
  findExistingWorktree, // exported for testing
11639
11815
  probeBranchOnRemote, // exported for testing (W-mphnm6a1000281b8)
11640
11816
  _maxTurnsForType, buildProjectContext, normalizeAc, _buildAgentSpawnFlags, _classifyAgentFailure, // exported for testing
11817
+ _runtimeModelFromOutputLine, captureExecutionModelFromStdoutChunk, // exported for testing
11641
11818
  _tryAutoResolveLiveCheckoutWorktreeConflict, // exported for testing (W-mr28h2j2000y0de1 review — auto-resolve worktree conflicts)
11642
11819
  _isOutputTruncated, AGENT_OUTPUT_CAP_BYTES, // P-8e4c2a17: exported for testing (OUTPUT_TRUNCATED detection)
11643
11820
  promoteCheckpointSteeringForClose, // exported for testing