/**
 * NERF Auto Mode — Fresh Session Per Unit
 *
 * State machine driven by .nerf/ files on disk. Each "unit" of work
 * (plan slice, execute task, complete slice) gets a fresh session via
 * the stashed ctx.newSession() pattern.
 *
 * The extension reads disk state after each agent_end, determines the
 * next unit type, creates a fresh session, and injects a focused prompt
 * telling the LLM which files to read and what to do.
 */
import { deriveState } from "./state.js";
import { parseUnitId } from "./unit-id.js";
import { getManifestStatus } from "./files.js";
export { inlinePriorMilestoneSummary } from "./files.js";
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
import { nerfRoot, resolveMilestoneFile, resolveMilestonePath, resolveDir, milestonesDir, } from "./paths.js";
import { invalidateAllCaches } from "./cache.js";
import { clearActivityLogState } from "./activity-log.js";
import { synthesizeCrashRecovery, getDeepDiagnostic, readActiveMilestoneId, } from "./session-forensics.js";
import { writeLock, clearLock, readCrashLock, isLockProcessAlive, } from "./crash-recovery.js";
import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
import { resolveAutoSupervisorConfig, loadEffectiveNerfPreferences, getIsolationMode, } from "./preferences.js";
import { sendDesktopNotification } from "./notifications.js";
import { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, } from "./auto-budget.js";
import { markToolStart as _markToolStart, markToolEnd as _markToolEnd, getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs, clearInFlightTools, } from "./auto-tool-tracking.js";
import { closeoutUnit } from "./auto-unit-closeout.js";
import { selectAndApplyModel, resolveModelId } from "./auto-model-selection.js";
import { resetRoutingHistory, recordOutcome } from "./routing-history.js";
import { resetHookState, runPreDispatchHooks, restoreHookState, clearPersistedHookState, } from "./post-unit-hooks.js";
import { runNerfDoctor, rebuildState } from "./doctor.js";
import { preDispatchHealthGate, resetProactiveHealing, setLevelChangeCallback, } from "./doctor-proactive.js";
import { clearSkillSnapshot } from "./skill-discovery.js";
import { captureAvailableSkills, resetSkillTelemetry, } from "./skill-telemetry.js";
import { getRtkSessionSavings } from "../shared/rtk-session-stats.js";
import { getLastCacheOptimizationResult } from "./auto-prompts.js";
import { initMetrics, resetMetrics, getLedger, getProjectTotals, formatCost, formatTokenCount, } from "./metrics.js";
import { setLogBasePath } from "./workflow-logger.js";
import { join } from "node:path";
import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
import { atomicWriteSync } from "./atomic-write.js";
import { autoCommitCurrentBranch, captureIntegrationBranch, detectWorktreeName, getCurrentBranch, getMainBranch, setActiveMilestoneId, } from "./worktree.js";
import { GitServiceImpl } from "./git-service.js";
import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
import { createAutoWorktree, enterAutoWorktree, teardownAutoWorktree, isInAutoWorktree, getAutoWorktreePath, mergeMilestoneToMain, autoWorktreeBranch, syncWorktreeStateBack, syncProjectRootToWorktree, checkResourcesStale, escapeStaleWorktree, } from "./auto-worktree.js";
import { pruneQueueOrder } from "./queue-order.js";
import { debugLog, isDebugEnabled, writeDebugSummary } from "./debug-logger.js";
import { reconcileMergeState, } from "./auto-recovery.js";
import { resolveDispatch, DISPATCH_RULES } from "./auto-dispatch.js";
import { initRegistry, convertDispatchRules } from "./rule-registry.js";
import { emitJournalEvent as _emitJournalEvent } from "./journal.js";
import { updateProgressWidget as _updateProgressWidget, updateSliceProgressCache, clearSliceProgressCache, hideFooter, } from "./auto-dashboard.js";
import { registerSigtermHandler as _registerSigtermHandler, deregisterSigtermHandler as _deregisterSigtermHandler, } from "./auto-supervisor.js";
import { isDbAvailable } from "./nerf-db.js";
import { countPendingCaptures } from "./captures.js";
import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
// ── Extracted modules ──────────────────────────────────────────────────────
import { startUnitSupervision } from "./auto-timers.js";
import { runPostUnitVerification } from "./auto-verification.js";
import { postUnitPreVerification, postUnitPostVerification, } from "./auto-post-unit.js";
import { bootstrapAutoSession } from "./auto-start.js";
import { autoLoop, resolveAgentEnd, resolveAgentEndCancelled, _resetPendingResolve, isSessionSwitchInFlight } from "./auto-loop.js";
import { WorktreeResolver, } from "./worktree-resolver.js";
import { reorderForCaching } from "./prompt-ordering.js";
// ─── Session State ─────────────────────────────────────────────────────────
import { AutoSession, } from "./auto/session.js";
export { MAX_UNIT_DISPATCHES, STUB_RECOVERY_THRESHOLD, MAX_LIFETIME_DISPATCHES, NEW_SESSION_TIMEOUT_MS, } from "./auto/session.js";
// ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
// ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
// This file must NOT declare module-level `let` or `var` variables for state.
// The single `s` instance below is the only mutable module-level binding.
//
// When adding features or fixing bugs:
//   - New mutable state → add a property to AutoSession, not a module-level variable
//   - New constants → module-level `const` is fine (immutable)
//   - New state that needs reset on stopAuto → add to AutoSession.reset()
//
// Tests in auto-session-encapsulation.test.ts enforce this invariant.
// ─────────────────────────────────────────────────────────────────────────────
const s = new AutoSession();
/** Throttle STATE.md rebuilds — at most once per 30 seconds */
const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
export function shouldUseWorktreeIsolation() {
    const prefs = loadEffectiveNerfPreferences()?.preferences?.git;
    if (prefs?.isolation === "worktree")
        return true;
    // Default is false — worktree isolation requires explicit opt-in
    return false;
}
/** Crash recovery prompt — set by startAuto, consumed by the main loop */
/** Pending verification retry — set when gate fails with retries remaining, consumed by autoLoop */
/** Verification retry count per unitId — separate from s.unitDispatchCount which tracks artifact-missing retries */
/** Session file path captured at pause — used to synthesize recovery briefing on resume */
/** Dashboard tracking */
/** Track dynamic routing decision for the current unit (for metrics) */
/** Queue of quick-task captures awaiting dispatch after triage resolution */
/**
 * Model captured at auto-mode start. Used to prevent model bleed between
 * concurrent NERF instances sharing the same global settings.json (#650).
 * When preferences don't specify a model for a unit type, this ensures
 * the session's original model is re-applied instead of reading from
 * the shared global settings (which another instance may have overwritten).
 */
/** Track current milestone to detect transitions */
/** Model the user had selected before auto-mode started */
/** Progress-aware timeout supervision */
/** Context-pressure continue-here monitor — fires once when context usage >= 70% */
/** Prompt character measurement for token savings analysis (R051). */
/** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
/**
 * Tool calls currently being executed — prevents false idle detection during long-running tools.
 * Maps toolCallId → start timestamp (ms) so the idle watchdog can detect tools that have been
 * running suspiciously long (e.g., a Bash command hung because `&` kept stdout open).
 */
// Re-export budget utilities for external consumers
export { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, } from "./auto-budget.js";
/** Wrapper: register SIGTERM handler and store reference. */
function registerSigtermHandler(currentBasePath) {
    s.sigtermHandler = _registerSigtermHandler(currentBasePath, s.sigtermHandler);
}
/** Wrapper: deregister SIGTERM handler and clear reference. */
function deregisterSigtermHandler() {
    _deregisterSigtermHandler(s.sigtermHandler);
    s.sigtermHandler = null;
}
export function getAutoDashboardData() {
    const ledger = getLedger();
    const totals = ledger ? getProjectTotals(ledger.units) : null;
    const sessionId = s.cmdCtx?.sessionManager?.getSessionId?.() ?? null;
    const rtkSavings = sessionId && s.basePath
        ? getRtkSessionSavings(s.basePath, sessionId)
        : null;
    const rtkEnabled = loadEffectiveNerfPreferences()?.preferences.experimental?.rtk === true;
    // Pending capture count — lazy check, non-fatal
    let pendingCaptureCount = 0;
    try {
        if (s.basePath) {
            pendingCaptureCount = countPendingCaptures(s.basePath);
        }
    }
    catch {
        // Non-fatal — captures module may not be loaded
    }
    return {
        active: s.active,
        paused: s.paused,
        stepMode: s.stepMode,
        startTime: s.autoStartTime,
        elapsed: s.active || s.paused
            ? (s.autoStartTime > 0 ? Date.now() - s.autoStartTime : 0)
            : 0,
        currentUnit: s.currentUnit ? { ...s.currentUnit } : null,
        basePath: s.basePath,
        totalCost: totals?.cost ?? 0,
        totalTokens: totals?.tokens.total ?? 0,
        pendingCaptureCount,
        rtkSavings,
        rtkEnabled,
        fallbackActive: s.fallbackActive,
        fallbackModelId: s.fallbackModelId,
    };
}
// ─── Public API ───────────────────────────────────────────────────────────────
export function isAutoActive() {
    return s.active;
}
export function isAutoPaused() {
    return s.paused;
}
export function setActiveEngineId(id) {
    s.activeEngineId = id;
}
export function getActiveEngineId() {
    return s.activeEngineId;
}
export function setActiveRunDir(runDir) {
    s.activeRunDir = runDir;
}
export function getActiveRunDir() {
    return s.activeRunDir;
}
/**
 * Return the model captured at auto-mode start for this session.
 * Used by error-recovery to fall back to the session's own model
 * instead of reading (potentially stale) preferences from disk (#1065).
 */
export function getAutoModeStartModel() {
    return s.autoModeStartModel;
}
// ── Fallback state ────────────────────────────────────────────────────────────
/** Mark that the current model is a fallback. */
export function setFallbackActive(modelId) {
    s.fallbackActive = true;
    s.fallbackModelId = modelId;
}
/** Clear fallback state (model restored to primary). */
export function clearFallbackActive() {
    s.fallbackActive = false;
    s.fallbackModelId = null;
}
/** Check if fallback is currently active. */
export function isFallbackActive() {
    return s.fallbackActive;
}
// Tool tracking — delegates to auto-tool-tracking.ts
export function markToolStart(toolCallId, toolName) {
    _markToolStart(toolCallId, s.active, toolName);
}
export function markToolEnd(toolCallId) {
    _markToolEnd(toolCallId);
}
export function getOldestInFlightToolAgeMs() {
    return _getOldestInFlightToolAgeMs();
}
/**
 * Return the base path to use for the auto.lock file.
 * Always uses the original project root (not the worktree) so that
 * a second terminal can discover and stop a running auto-mode session.
 *
 * Delegates to AutoSession.lockBasePath — the single source of truth.
 */
function lockBase() {
    return s.lockBasePath;
}
/**
 * Attempt to stop a running auto-mode session from a different process.
 * Reads the lock file at the project root, checks if the PID is alive,
 * and sends SIGTERM to gracefully stop it.
 *
 * Returns true if a remote session was found and signaled, false otherwise.
 */
export function stopAutoRemote(projectRoot) {
    const lock = readCrashLock(projectRoot);
    if (!lock)
        return { found: false };
    // Never SIGTERM ourselves — a stale lock with our own PID is not a remote
    // session, it is leftover from a prior loop exit in this process. (#2730)
    if (lock.pid === process.pid) {
        clearLock(projectRoot);
        return { found: false };
    }
    if (!isLockProcessAlive(lock)) {
        // Stale lock — clean it up
        clearLock(projectRoot);
        return { found: false };
    }
    // Send SIGTERM — the auto-mode process has a handler that clears the lock and exits
    try {
        process.kill(lock.pid, "SIGTERM");
        return { found: true, pid: lock.pid };
    }
    catch (err) {
        return { found: false, error: err.message };
    }
}
/**
 * Check if a remote auto-mode session is running (from a different process).
 * Reads the crash lock, checks PID liveness, and returns session details.
 * Used by the guard in commands.ts to prevent bare /nerf, /nerf next, and
 * /nerf auto from stealing the session lock.
 */
export function checkRemoteAutoSession(projectRoot) {
    const lock = readCrashLock(projectRoot);
    if (!lock)
        return { running: false };
    // Our own PID is not a "remote" session — it is a stale lock left by this
    // process (e.g. after step-mode exit without full cleanup). (#2730)
    if (lock.pid === process.pid)
        return { running: false };
    if (!isLockProcessAlive(lock)) {
        // Stale lock from a dead process — not a live remote session
        return { running: false };
    }
    return {
        running: true,
        pid: lock.pid,
        unitType: lock.unitType,
        unitId: lock.unitId,
        startedAt: lock.startedAt,
    };
}
export function isStepMode() {
    return s.stepMode;
}
function clearUnitTimeout() {
    if (s.unitTimeoutHandle) {
        clearTimeout(s.unitTimeoutHandle);
        s.unitTimeoutHandle = null;
    }
    if (s.wrapupWarningHandle) {
        clearTimeout(s.wrapupWarningHandle);
        s.wrapupWarningHandle = null;
    }
    if (s.idleWatchdogHandle) {
        clearInterval(s.idleWatchdogHandle);
        s.idleWatchdogHandle = null;
    }
    if (s.continueHereHandle) {
        clearInterval(s.continueHereHandle);
        s.continueHereHandle = null;
    }
    clearInFlightTools();
}
/** Build snapshot metric opts. */
function buildSnapshotOpts(_unitType, _unitId) {
    const cacheResult = getLastCacheOptimizationResult();
    return {
        promptCharCount: s.lastPromptCharCount,
        baselineCharCount: s.lastBaselineCharCount,
        ...(cacheResult?.cacheEfficiency != null ? { cacheEfficiency: cacheResult.cacheEfficiency } : {}),
        ...(s.currentUnitRouting ?? {}),
    };
}
function handleLostSessionLock(ctx, lockStatus) {
    debugLog("session-lock-lost", {
        lockBase: lockBase(),
        reason: lockStatus?.failureReason,
        existingPid: lockStatus?.existingPid,
        expectedPid: lockStatus?.expectedPid,
    });
    // Force to stopped — session lock is gone, no orderly shutdown possible
    if (s.engineState !== 'stopped' && s.engineState !== 'idle') {
        s.engineState = 'stopped';
    }
    clearUnitTimeout();
    deregisterSigtermHandler();
    clearCmuxSidebar(loadEffectiveNerfPreferences()?.preferences);
    const base = lockBase();
    const lockFilePath = base ? join(nerfRoot(base), "auto.lock") : "unknown";
    const recoverySuggestion = "\nTo recover, run: nerf doctor --fix";
    const message = lockStatus?.failureReason === "pid-mismatch"
        ? lockStatus.existingPid
            ? `Session lock (${lockFilePath}) moved to PID ${lockStatus.existingPid} — another NERF process appears to have taken over. Stopping gracefully.${recoverySuggestion}`
            : `Session lock (${lockFilePath}) moved to a different process — another NERF process appears to have taken over. Stopping gracefully.${recoverySuggestion}`
        : lockStatus?.failureReason === "missing-metadata"
            ? `Session lock metadata (${lockFilePath}) disappeared, so ownership could not be confirmed. Stopping gracefully.${recoverySuggestion}`
            : lockStatus?.failureReason === "compromised"
                ? `Session lock (${lockFilePath}) was compromised during heartbeat checks (PID ${process.pid}). This can happen after long event loop stalls during subagent execution.${recoverySuggestion}`
                : `Session lock lost (${lockFilePath}). Stopping gracefully.${recoverySuggestion}`;
    ctx?.ui.notify(message, "error");
    ctx?.ui.setStatus("nerf-auto", undefined);
    ctx?.ui.setWidget("nerf-progress", undefined);
    ctx?.ui.setFooter(undefined);
}
/**
 * Lightweight cleanup after autoLoop exits via step-wizard break.
 *
 * Unlike stopAuto (which tears down the entire session), this only clears
 * the stale unit state, progress widget, status badge, and restores CWD so
 * the dashboard does not show an orphaned timer and the shell is usable.
 */
function cleanupAfterLoopExit(ctx) {
    s.currentUnit = null;
    // Loop exited — ensure engine reaches stopped state for clean lifecycle
    if (s.engineState === 'running' || s.engineState === 'starting') {
        s.transition('stopping');
    }
    if (s.engineState === 'stopping') {
        s.transition('stopped');
    }
    clearUnitTimeout();
    // Clear crash lock and release session lock so the next `/nerf next` does
    // not see a stale lock with the current PID and treat it as a "remote"
    // session (which would cause it to SIGTERM itself). (#2730)
    try {
        if (lockBase())
            clearLock(lockBase());
        if (lockBase())
            releaseSessionLock(lockBase());
    }
    catch {
        /* best-effort — mirror stopAuto cleanup */
    }
    ctx.ui.setStatus("nerf-auto", undefined);
    ctx.ui.setWidget("nerf-progress", undefined);
    ctx.ui.setFooter(undefined);
    // Restore CWD out of worktree back to original project root
    if (s.originalBasePath) {
        s.basePath = s.originalBasePath;
        try {
            process.chdir(s.basePath);
        }
        catch {
            /* best-effort */
        }
    }
}
export async function stopAuto(ctx, pi, reason) {
    if (s.engineState === 'idle' || s.engineState === 'stopped')
        return;
    // Signal that cleanup is in progress
    if (s.engineState !== 'stopping') {
        if (s.engineState === 'running' || s.engineState === 'starting' || s.engineState === 'paused') {
            s.transition('stopping');
        }
        else {
            // Edge case: force to stopping for orderly shutdown
            s.engineState = 'stopping';
        }
    }
    const loadedPreferences = loadEffectiveNerfPreferences()?.preferences;
    const reasonSuffix = reason ? ` — ${reason}` : "";
    try {
        // ── Step 1: Timers and locks ──
        try {
            clearUnitTimeout();
            if (lockBase())
                clearLock(lockBase());
            if (lockBase())
                releaseSessionLock(lockBase());
        }
        catch (e) {
            debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
        }
        // ── Step 2: Skill state ──
        try {
            clearSkillSnapshot();
            resetSkillTelemetry();
        }
        catch (e) {
            debugLog("stop-cleanup-skills", { error: e instanceof Error ? e.message : String(e) });
        }
        // ── Step 3: SIGTERM handler ──
        try {
            deregisterSigtermHandler();
        }
        catch (e) {
            debugLog("stop-cleanup-sigterm", { error: e instanceof Error ? e.message : String(e) });
        }
        // ── Step 4: Auto-worktree exit ──
        // When the milestone is complete (has a SUMMARY), merge the worktree branch
        // back to main so code isn't stranded on the worktree branch (#2317).
        // For incomplete milestones, preserve the branch for later resumption.
        //
        // Skip if phases.ts already merged this milestone — avoids the double
        // mergeAndExit that fails because the branch was already deleted (#2645).
        try {
            if (s.currentMilestoneId && !s.milestoneMergedInPhases) {
                const notifyCtx = ctx
                    ? { notify: ctx.ui.notify.bind(ctx.ui) }
                    : { notify: () => { } };
                const resolver = buildResolver();
                // Check if the milestone is complete — SUMMARY file is the authoritative signal.
                let milestoneComplete = false;
                try {
                    const summaryPath = resolveMilestoneFile(s.originalBasePath || s.basePath, s.currentMilestoneId, "SUMMARY");
                    if (!summaryPath) {
                        // Also check in the worktree path (SUMMARY may not be synced yet)
                        const wtSummaryPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "SUMMARY");
                        milestoneComplete = wtSummaryPath !== null;
                    }
                    else {
                        milestoneComplete = true;
                    }
                }
                catch {
                    // Non-fatal — fall through to preserveBranch path
                }
                if (milestoneComplete) {
                    // Milestone is complete — merge worktree branch back to main
                    resolver.mergeAndExit(s.currentMilestoneId, notifyCtx);
                }
                else {
                    // Milestone still in progress — preserve branch for later resumption
                    resolver.exitMilestone(s.currentMilestoneId, notifyCtx, {
                        preserveBranch: true,
                    });
                }
            }
        }
        catch (e) {
            debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });
        }
        // ── Step 5: DB cleanup ──
        if (isDbAvailable()) {
            try {
                const { closeDatabase } = await import("./nerf-db.js");
                closeDatabase();
            }
            catch (e) {
                debugLog("db-close-failed", {
                    error: e instanceof Error ? e.message : String(e),
                });
            }
        }
        // ── Step 6: Restore basePath and chdir ──
        try {
            if (s.originalBasePath) {
                s.basePath = s.originalBasePath;
                try {
                    process.chdir(s.basePath);
                }
                catch {
                    /* best-effort */
                }
            }
        }
        catch (e) {
            debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
        }
        // ── Step 7: Ledger notification ──
        try {
            const ledger = getLedger();
            if (ledger && ledger.units.length > 0) {
                const totals = getProjectTotals(ledger.units);
                ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`, "info");
            }
            else {
                ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
            }
        }
        catch (e) {
            debugLog("stop-cleanup-ledger", { error: e instanceof Error ? e.message : String(e) });
        }
        // ── Step 8: Rebuild state ──
        if (s.basePath) {
            try {
                await rebuildState(s.basePath);
            }
            catch (e) {
                debugLog("stop-rebuild-state-failed", {
                    error: e instanceof Error ? e.message : String(e),
                });
            }
        }
        // ── Step 9: Cmux sidebar / event log ──
        try {
            clearCmuxSidebar(loadedPreferences);
            logCmuxEvent(loadedPreferences, `Auto-mode stopped${reasonSuffix || ""}.`, reason?.startsWith("Blocked:") ? "warning" : "info");
        }
        catch (e) {
            debugLog("stop-cleanup-cmux", { error: e instanceof Error ? e.message : String(e) });
        }
        // ── Step 10: Debug summary ──
        try {
            if (isDebugEnabled()) {
                const logPath = writeDebugSummary();
                if (logPath) {
                    ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
                }
            }
        }
        catch (e) {
            debugLog("stop-cleanup-debug", { error: e instanceof Error ? e.message : String(e) });
        }
        // ── Step 11: Reset metrics, routing, hooks ──
        try {
            resetMetrics();
            resetRoutingHistory();
            resetHookState();
            if (s.basePath)
                clearPersistedHookState(s.basePath);
        }
        catch (e) {
            debugLog("stop-cleanup-metrics", { error: e instanceof Error ? e.message : String(e) });
        }
        // ── Step 12: Remove paused-session metadata (#1383) ──
        try {
            const pausedPath = join(nerfRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
            if (existsSync(pausedPath))
                unlinkSync(pausedPath);
        }
        catch { /* non-fatal */ }
        // ── Step 13: Restore original model (before reset clears IDs) ──
        try {
            if (pi && ctx && s.originalModelId && s.originalModelProvider) {
                const original = ctx.modelRegistry.find(s.originalModelProvider, s.originalModelId);
                if (original)
                    await pi.setModel(original);
            }
        }
        catch (e) {
            debugLog("stop-cleanup-model", { error: e instanceof Error ? e.message : String(e) });
        }
        // ── Step 14: Unblock pending unitPromise (#1799) ──
        // resolveAgentEnd unblocks autoLoop's `await unitPromise` so it can see
        // s.active === false and exit cleanly. Without this, autoLoop hangs
        // forever and the interactive loop is blocked.
        try {
            resolveAgentEnd({ messages: [] });
            _resetPendingResolve();
        }
        catch (e) {
            debugLog("stop-cleanup-pending-resolve", { error: e instanceof Error ? e.message : String(e) });
        }
    }
    finally {
        // ── Critical invariants: these MUST execute regardless of errors ──
        // Browser teardown — prevent orphaned Chrome processes across retries (#1733)
        try {
            const { getBrowser } = await import("../browser-tools/state.js");
            if (getBrowser()) {
                const { closeBrowser } = await import("../browser-tools/lifecycle.js");
                await closeBrowser();
            }
        }
        catch { /* non-fatal: browser-tools may not be loaded */ }
        // External cleanup (not covered by session reset)
        clearInFlightTools();
        clearSliceProgressCache();
        clearActivityLogState();
        setLevelChangeCallback(null);
        resetProactiveHealing();
        // UI cleanup
        ctx?.ui.setStatus("nerf-auto", undefined);
        ctx?.ui.setWidget("nerf-progress", undefined);
        ctx?.ui.setFooter(undefined);
        // Reset all session state in one call
        s.reset();
    }
}
/**
 * Pause auto-mode without destroying state. Context is preserved.
 * The user can interact with the agent, then `/nerf auto` resumes
 * from disk state. Called when the user presses Escape during auto-mode.
 */
export async function pauseAuto(ctx, _pi, _errorContext) {
    if (!s.active)
        return;
    clearUnitTimeout();
    // Unblock any pending unit promise so the auto-loop is not orphaned.
    // Pass errorContext so runUnitPhase can distinguish user-initiated pause
    // from provider-error pause and avoid hard-stopping (#2762).
    resolveAgentEndCancelled(_errorContext);
    s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
    // Persist paused-session metadata so resume survives /exit (#1383).
    // The fresh-start bootstrap checks for this file and restores worktree context.
    try {
        const pausedMeta = {
            milestoneId: s.currentMilestoneId,
            worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
            originalBasePath: s.originalBasePath,
            stepMode: s.stepMode,
            pausedAt: new Date().toISOString(),
            sessionFile: s.pausedSessionFile,
            activeEngineId: s.activeEngineId,
            activeRunDir: s.activeRunDir,
        };
        const runtimeDir = join(nerfRoot(s.originalBasePath || s.basePath), "runtime");
        mkdirSync(runtimeDir, { recursive: true });
        writeFileSync(join(runtimeDir, "paused-session.json"), JSON.stringify(pausedMeta, null, 2), "utf-8");
    }
    catch {
        // Non-fatal — resume will still work via full bootstrap, just without worktree context
    }
    // Close out the current unit so its runtime record doesn't stay at "dispatched"
    if (s.currentUnit && ctx) {
        try {
            await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt);
        }
        catch {
            // Non-fatal — best-effort closeout on pause
        }
        s.currentUnit = null;
    }
    if (lockBase()) {
        releaseSessionLock(lockBase());
        clearLock(lockBase());
    }
    deregisterSigtermHandler();
    // Unblock pending unitPromise so autoLoop exits cleanly (#1799)
    resolveAgentEnd({ messages: [] });
    _resetPendingResolve();
    s.transition('paused');
    s.pendingVerificationRetry = null;
    s.verificationRetryCount.clear();
    ctx?.ui.setStatus("nerf-auto", "paused");
    ctx?.ui.setWidget("nerf-progress", undefined);
    ctx?.ui.setFooter(undefined);
    const resumeCmd = s.stepMode ? "/nerf next" : "/nerf auto";
    ctx?.ui.notify(`${s.stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`, "info");
}
/**
 * Build a WorktreeResolverDeps from auto.ts private scope.
 * Shared by buildResolver() and buildLoopDeps().
 */
function buildResolverDeps() {
    return {
        isInAutoWorktree,
        shouldUseWorktreeIsolation,
        getIsolationMode,
        mergeMilestoneToMain,
        syncWorktreeStateBack,
        teardownAutoWorktree,
        createAutoWorktree,
        enterAutoWorktree,
        getAutoWorktreePath,
        autoCommitCurrentBranch,
        getCurrentBranch,
        autoWorktreeBranch,
        resolveMilestoneFile,
        readFileSync: (path, encoding) => readFileSync(path, encoding),
        GitServiceImpl: GitServiceImpl,
        loadEffectiveNerfPreferences: loadEffectiveNerfPreferences,
        invalidateAllCaches,
        captureIntegrationBranch,
    };
}
/**
 * Build a WorktreeResolver wrapping the current session.
 * Cheap to construct — it's just a thin wrapper over `s` + deps.
 * Used by stopAuto(), resume path, and buildLoopDeps().
 */
function buildResolver() {
    return new WorktreeResolver(s, buildResolverDeps());
}
/**
 * Build the LoopDeps object from auto.ts private scope.
 * This bundles all private functions that autoLoop needs without exporting them.
 */
function buildLoopDeps() {
    // Initialize the unified rule registry with converted dispatch rules.
    // Must happen before LoopDeps is assembled so facade functions
    // (resolveDispatch, runPreDispatchHooks, etc.) delegate to the registry.
    initRegistry(convertDispatchRules(DISPATCH_RULES));
    return {
        lockBase,
        buildSnapshotOpts,
        stopAuto,
        pauseAuto,
        clearUnitTimeout,
        updateProgressWidget,
        syncCmuxSidebar,
        logCmuxEvent,
        // State and cache
        invalidateAllCaches,
        deriveState,
        rebuildState,
        loadEffectiveNerfPreferences,
        // Pre-dispatch health gate
        preDispatchHealthGate,
        // Worktree sync
        syncProjectRootToWorktree,
        // Resource version guard
        checkResourcesStale,
        // Session lock
        validateSessionLock: getSessionLockStatus,
        updateSessionLock,
        handleLostSessionLock,
        // Milestone transition
        sendDesktopNotification,
        setActiveMilestoneId,
        pruneQueueOrder,
        isInAutoWorktree,
        shouldUseWorktreeIsolation,
        mergeMilestoneToMain,
        teardownAutoWorktree,
        createAutoWorktree,
        captureIntegrationBranch,
        getIsolationMode,
        getCurrentBranch,
        autoWorktreeBranch,
        resolveMilestoneFile,
        reconcileMergeState,
        // Budget/context/secrets
        getLedger,
        getProjectTotals,
        formatCost,
        getBudgetAlertLevel,
        getNewBudgetAlertLevel,
        getBudgetEnforcementAction,
        getManifestStatus,
        collectSecretsFromManifest,
        // Dispatch
        resolveDispatch,
        runPreDispatchHooks,
        getPriorSliceCompletionBlocker,
        getMainBranch,
        // Unit closeout + runtime records
        closeoutUnit,
        recordOutcome,
        writeLock,
        captureAvailableSkills,
        ensurePreconditions,
        updateSliceProgressCache,
        // Model selection + supervision
        selectAndApplyModel,
        resolveModelId,
        startUnitSupervision,
        // Prompt helpers
        getDeepDiagnostic: (basePath) => {
            const mid = readActiveMilestoneId(basePath);
            const wtPath = mid ? getAutoWorktreePath(basePath, mid) : undefined;
            return getDeepDiagnostic(basePath, wtPath ?? undefined);
        },
        isDbAvailable,
        reorderForCaching,
        // Filesystem
        existsSync,
        readFileSync: (path, encoding) => readFileSync(path, encoding),
        atomicWriteSync,
        // Git
        GitServiceImpl: GitServiceImpl,
        // WorktreeResolver
        resolver: buildResolver(),
        // Post-unit processing
        postUnitPreVerification,
        runPostUnitVerification,
        postUnitPostVerification,
        // Session manager
        getSessionFile: (ctx) => {
            try {
                return ctx.sessionManager?.getSessionFile() ?? "";
            }
            catch {
                return "";
            }
        },
        // Journal
        emitJournalEvent: (entry) => _emitJournalEvent(s.basePath, entry),
    };
}
export async function startAuto(ctx, pi, base, verboseMode, options) {
    if (s.active) {
        debugLog("startAuto", { phase: "already-active", skipping: true });
        return;
    }
    const requestedStepMode = options?.step ?? false;
    // Escape stale worktree cwd from a previous milestone (#608).
    base = escapeStaleWorktree(base);
    // If resuming from paused state, just re-activate and dispatch next unit.
    // Check persisted paused-session first (#1383) — survives /exit.
    if (!s.paused) {
        try {
            const pausedPath = join(nerfRoot(base), "runtime", "paused-session.json");
            if (existsSync(pausedPath)) {
                const meta = JSON.parse(readFileSync(pausedPath, "utf-8"));
                if (meta.activeEngineId && meta.activeEngineId !== "dev") {
                    // Custom workflow resume — restore engine state
                    s.activeEngineId = meta.activeEngineId;
                    s.activeRunDir = meta.activeRunDir ?? null;
                    s.originalBasePath = meta.originalBasePath || base;
                    s.stepMode = meta.stepMode ?? requestedStepMode;
                    // Restore paused state from disk — direct assignment since
                    // there's no valid idle→paused transition (this is state restoration)
                    s.engineState = 'paused';
                    try {
                        unlinkSync(pausedPath);
                    }
                    catch { /* non-fatal */ }
                    ctx.ui.notify(`Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`, "info");
                }
                else if (meta.milestoneId) {
                    // Validate the milestone still exists and isn't already complete (#1664).
                    const mDir = resolveMilestonePath(base, meta.milestoneId);
                    const summaryFile = resolveMilestoneFile(base, meta.milestoneId, "SUMMARY");
                    if (!mDir || summaryFile) {
                        // Stale milestone — clean up and fall through to fresh bootstrap
                        try {
                            unlinkSync(pausedPath);
                        }
                        catch { /* non-fatal */ }
                        ctx.ui.notify(`Paused milestone ${meta.milestoneId} is ${!mDir ? "missing" : "already complete"}. Starting fresh.`, "info");
                    }
                    else {
                        s.currentMilestoneId = meta.milestoneId;
                        s.originalBasePath = meta.originalBasePath || base;
                        s.stepMode = meta.stepMode ?? requestedStepMode;
                        // Restore paused state from disk — direct assignment (state restoration, not lifecycle transition)
                        s.engineState = 'paused';
                        // Clean up the persisted file — we're consuming it
                        try {
                            unlinkSync(pausedPath);
                        }
                        catch { /* non-fatal */ }
                        ctx.ui.notify(`Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`, "info");
                    }
                }
            }
        }
        catch {
            // Malformed or missing — proceed with fresh bootstrap
        }
    }
    if (s.paused) {
        const resumeLock = acquireSessionLock(base);
        if (!resumeLock.acquired) {
            ctx.ui.notify(`Cannot resume: ${resumeLock.reason}`, "error");
            return;
        }
        s.transition('running');
        s.verbose = verboseMode;
        s.stepMode = requestedStepMode;
        s.cmdCtx = ctx;
        s.basePath = base;
        setLogBasePath(base);
        s.unitDispatchCount.clear();
        s.unitLifetimeDispatches.clear();
        if (!getLedger())
            initMetrics(base);
        if (s.currentMilestoneId)
            setActiveMilestoneId(base, s.currentMilestoneId);
        // ── Auto-worktree: re-enter worktree on resume ──
        if (s.currentMilestoneId &&
            shouldUseWorktreeIsolation() &&
            s.originalBasePath &&
            !isInAutoWorktree(s.basePath) &&
            !detectWorktreeName(s.basePath) &&
            !detectWorktreeName(s.originalBasePath)) {
            buildResolver().enterMilestone(s.currentMilestoneId, {
                notify: ctx.ui.notify.bind(ctx.ui),
            });
        }
        registerSigtermHandler(lockBase());
        ctx.ui.setStatus("nerf-auto", s.stepMode ? "next" : "auto");
        ctx.ui.setFooter(hideFooter);
        ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
        restoreHookState(s.basePath);
        try {
            await rebuildState(s.basePath);
            syncCmuxSidebar(loadEffectiveNerfPreferences()?.preferences, await deriveState(s.basePath));
        }
        catch (e) {
            debugLog("resume-rebuild-state-failed", {
                error: e instanceof Error ? e.message : String(e),
            });
        }
        try {
            const report = await runNerfDoctor(s.basePath, { fix: true });
            if (report.fixesApplied.length > 0) {
                ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
            }
        }
        catch (e) {
            debugLog("resume-doctor-failed", {
                error: e instanceof Error ? e.message : String(e),
            });
        }
        invalidateAllCaches();
        if (s.pausedSessionFile) {
            const activityDir = join(nerfRoot(s.basePath), "activity");
            const recovery = synthesizeCrashRecovery(s.basePath, s.currentUnit?.type ?? "unknown", s.currentUnit?.id ?? "unknown", s.pausedSessionFile ?? undefined, activityDir);
            if (recovery && recovery.trace.toolCallCount > 0) {
                s.pendingCrashRecovery = recovery.prompt;
                ctx.ui.notify(`Recovered ${recovery.trace.toolCallCount} tool calls from paused session. Resuming with context.`, "info");
            }
            s.pausedSessionFile = null;
        }
        updateSessionLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown");
        writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown");
        logCmuxEvent(loadEffectiveNerfPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
        await autoLoop(ctx, pi, s, buildLoopDeps());
        cleanupAfterLoopExit(ctx);
        return;
    }
    // ── Fresh start path — delegated to auto-start.ts ──
    const bootstrapDeps = {
        shouldUseWorktreeIsolation,
        registerSigtermHandler,
        lockBase,
        buildResolver,
    };
    const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps);
    if (!ready)
        return;
    try {
        syncCmuxSidebar(loadEffectiveNerfPreferences()?.preferences, await deriveState(s.basePath));
    }
    catch {
        // Best-effort only — sidebar sync must never block auto-mode startup
    }
    logCmuxEvent(loadEffectiveNerfPreferences()?.preferences, requestedStepMode ? "Step-mode started." : "Auto-mode started.", "progress");
    // Dispatch the first unit
    await autoLoop(ctx, pi, s, buildLoopDeps());
    cleanupAfterLoopExit(ctx);
}
// ─── Agent End Handler ────────────────────────────────────────────────────────
/**
 * Deprecated thin wrapper — kept as export for backward compatibility.
 * The actual agent_end processing now happens via resolveAgentEnd() in auto-loop.ts,
 * which is called directly from index.ts. The autoLoop() while loop handles all
 * post-unit processing (verification, hooks, dispatch) that this function used to do.
 *
 * If called by straggler code, it simply resolves the pending promise so the loop
 * can continue.
 */
export async function handleAgentEnd(ctx, pi) {
    if (!s.active || !s.cmdCtx) {
        // Even when inactive, resolve any pending promise so the loop is unblocked.
        resolveAgentEndCancelled();
        return;
    }
    clearUnitTimeout();
    resolveAgentEnd({ messages: [] });
}
// describeNextUnit is imported from auto-dashboard.ts and re-exported
export { describeNextUnit } from "./auto-dashboard.js";
/** Thin wrapper: delegates to auto-dashboard.ts, passing state accessors. */
function updateProgressWidget(ctx, unitType, unitId, state) {
    const badge = s.currentUnitRouting?.tier
        ? ({ light: "L", standard: "S", heavy: "H" }[s.currentUnitRouting.tier] ??
            undefined)
        : undefined;
    _updateProgressWidget(ctx, unitType, unitId, state, widgetStateAccessors, badge);
}
/** State accessors for the widget — closures over module globals. */
const widgetStateAccessors = {
    getAutoStartTime: () => s.autoStartTime,
    isStepMode: () => s.stepMode,
    getCmdCtx: () => s.cmdCtx,
    getBasePath: () => s.basePath,
    isVerbose: () => s.verbose,
    isSessionSwitching: isSessionSwitchInFlight,
    isFallbackActive: () => s.fallbackActive,
    getFallbackModelId: () => s.fallbackModelId,
};
// ─── Preconditions ────────────────────────────────────────────────────────────
/**
 * Ensure directories, branches, and other prerequisites exist before
 * dispatching a unit. The LLM should never need to mkdir or git checkout.
 */
function ensurePreconditions(unitType, unitId, base, state) {
    const { milestone: mid, slice: sid } = parseUnitId(unitId);
    const mDir = resolveMilestonePath(base, mid);
    if (!mDir) {
        const newDir = join(milestonesDir(base), mid);
        mkdirSync(join(newDir, "slices"), { recursive: true });
    }
    if (sid !== undefined) {
        const mDirResolved = resolveMilestonePath(base, mid);
        if (mDirResolved) {
            const slicesDir = join(mDirResolved, "slices");
            const sDir = resolveDir(slicesDir, sid);
            if (!sDir) {
                mkdirSync(join(slicesDir, sid, "tasks"), { recursive: true });
            }
            const resolvedSliceDir = resolveDir(slicesDir, sid) ?? sid;
            const tasksDir = join(slicesDir, resolvedSliceDir, "tasks");
            if (!existsSync(tasksDir)) {
                mkdirSync(tasksDir, { recursive: true });
            }
        }
    }
}
// ─── Diagnostics ──────────────────────────────────────────────────────────────
/** Build recovery context from module state for recoverTimedOutUnit */
function buildRecoveryContext() {
    return {
        basePath: s.basePath,
        verbose: s.verbose,
        currentUnitStartedAt: s.currentUnit?.startedAt ?? Date.now(),
        unitRecoveryCount: s.unitRecoveryCount,
    };
}
/**
 * Test-only: expose skip-loop state for unit tests.
 * Not part of the public API.
 */
/**
 * Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
 * Used for manual hook triggers via /nerf run-hook.
 */
export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, triggerUnitId, hookPrompt, hookModel, targetBasePath) {
    if (!s.active) {
        // Bootstrap engine for hook dispatch — idle→starting→running
        if (s.engineState === 'stopped')
            s.transition('idle');
        if (s.engineState === 'idle')
            s.transition('starting');
        if (s.engineState === 'starting')
            s.transition('running');
        s.stepMode = true;
        s.cmdCtx = ctx;
        s.basePath = targetBasePath;
        s.autoStartTime = Date.now();
        s.currentUnit = null;
        s.pendingQuickTasks = [];
    }
    const hookUnitType = `hook/${hookName}`;
    const hookStartedAt = Date.now();
    s.currentUnit = {
        type: triggerUnitType,
        id: triggerUnitId,
        startedAt: hookStartedAt,
    };
    const result = await s.cmdCtx.newSession();
    if (result.cancelled) {
        await stopAuto(ctx, pi);
        return false;
    }
    s.currentUnit = {
        type: hookUnitType,
        id: triggerUnitId,
        startedAt: hookStartedAt,
    };
    if (hookModel) {
        const availableModels = ctx.modelRegistry.getAvailable();
        const match = resolveModelId(hookModel, availableModels, ctx.model?.provider);
        if (match) {
            try {
                await pi.setModel(match);
            }
            catch {
                /* non-fatal */
            }
        }
        else {
            ctx.ui.notify(`Hook model "${hookModel}" not found in available models. Falling back to current session model. ` +
                `Ensure the model is defined in models.json and has auth configured.`, "warning");
        }
    }
    const sessionFile = ctx.sessionManager.getSessionFile();
    writeLock(lockBase(), hookUnitType, triggerUnitId, sessionFile);
    clearUnitTimeout();
    const supervisor = resolveAutoSupervisorConfig();
    const hookHardTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
    s.unitTimeoutHandle = setTimeout(async () => {
        s.unitTimeoutHandle = null;
        if (!s.active)
            return;
        ctx.ui.notify(`Hook ${hookName} exceeded ${supervisor.hard_timeout_minutes ?? 30}min timeout. Pausing auto-mode.`, "warning");
        resetHookState();
        await pauseAuto(ctx, pi);
    }, hookHardTimeoutMs);
    ctx.ui.setStatus("nerf-auto", s.stepMode ? "next" : "auto");
    ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
    // Ensure cwd matches basePath before hook dispatch (#1389)
    try {
        if (process.cwd() !== s.basePath)
            process.chdir(s.basePath);
    }
    catch { }
    debugLog("dispatchHookUnit", {
        phase: "send-message",
        promptLength: hookPrompt.length,
    });
    pi.sendMessage({ customType: "nerf-auto", content: hookPrompt, display: true }, { triggerTurn: true });
    return true;
}
// Direct phase dispatch → auto-direct-dispatch.ts
export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
// Re-export recovery functions for external consumers
export { buildLoopRemediationSteps, } from "./auto-recovery.js";
export { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";
