/**
 * MentorOnboardingRunner — thin glue that assembles the real services into the
 * pure runMentorTick core (FRAMEWORK-ONBOARDING-MENTOR-SPEC §19.4).
 *
 * The runner holds no orchestration logic of its own — that lives in
 * runMentorTick (pure, fully unit-tested). The runner's only job is to (a) read
 * the mentor config and short-circuit when the feature is off, and (b) wire the
 * injected services (ledger, Stage-A spawn, Stage-B forensics, mentee-busy check,
 * budget gate, surface builder) into the tick's callbacks. Every service is
 * injected, so the runner is testable with fakes and carries no hard dependency
 * on SessionManager/tmux/LLM at construction.
 *
 * Ships dormant: `mentor.enabled=false` / `mentor.mode='off'` by default (§16).
 */
import { runMentorTick } from './MentorOnboardingTick.js';
import { llmCircuitAvailable } from '../core/LlmCircuitBreaker.js';
import { runAutonomousGuardian, } from './MentorAutonomousGuardian.js';
/**
 * The Telegram topic the mentor exchange flows through. Prefers a dedicated
 * `mentorTopicId` (so mentor a2a stays OFF the human↔mentee conversation
 * topic), falling back to `menteeTopicId` for backward compatibility. This one
 * id drives both the /a2a/inbox body (mentee session/reply binding) and the
 * Telegram fallback, so the whole exchange moves together.
 */
export function resolveMentorDeliveryTopic(cfg) {
    return cfg.mentorTopicId ?? cfg.menteeTopicId;
}
export const DEFAULT_MENTOR_CONFIG = {
    enabled: false,
    mode: 'off',
    menteeFramework: 'codex-cli',
    minIntervalMs: 600_000, // 10 min floor
    maxRoundsPerDay: 24,
    dailySpendCapUsd: 0.5,
    // botToken / menteeBotId / menteeTopicId default undefined → mentor wiring stays
    // dark until they are explicitly configured (per /mentor/bot-setup, future PR).
};
export class MentorOnboardingRunner {
    services;
    getConfig;
    constructor(services, getConfig) {
        this.services = services;
        this.getConfig = getConfig;
        // Hydrate the last outcome from durable state so a server restart doesn't
        // erase the loop's only observability record. Best-effort: a corrupt or
        // missing file is just null (in-memory start, the old behavior).
        try {
            this.lastResult = services.loadLastResult?.() ?? null;
        }
        catch { /* @silent-fallback-ok — hydration is best-effort; null = old behavior */ }
    }
    inFlight = false;
    lastResult = null;
    /** Single write funnel for lastResult: assigns + persists best-effort. */
    setLastResult(r) {
        this.lastResult = r;
        try {
            this.services.saveLastResult?.(r);
        }
        catch { /* @silent-fallback-ok — persistence is best-effort; in-memory value is already set */ }
    }
    status() {
        const cfg = this.getConfig();
        return {
            enabled: cfg.enabled,
            mode: cfg.mode,
            menteeFramework: cfg.menteeFramework,
            inFlight: this.inFlight,
            lastResult: this.lastResult,
        };
    }
    /**
     * Fire-and-forget tick for the heartbeat route (§19.4 live-readiness). Returns
     * immediately (202 semantics) so a slow Stage-A spawn can't hang the HTTP
     * request (the gate-latency-vs-client-timeout failure mode). A single in-flight
     * guard prevents overlapping ticks; the outcome lands in `status().lastResult`.
     * Disabled config still short-circuits synchronously to `disabled`.
     */
    startTick() {
        const cfg = this.getConfig();
        // The autonomous-fix guardian is a distinct execution path: it runs whenever
        // `enabled && autonomousFix.enabled`, regardless of `mode` (mode gates only
        // the haiku observe-pipeline). So `mode:'off'` does NOT disable it.
        const autonomous = cfg.autonomousFix?.enabled === true;
        if (!cfg.enabled || (cfg.mode === 'off' && !autonomous)) {
            this.setLastResult({ ran: false, reason: 'disabled', at: (this.services.now ?? Date.now)() });
            return { accepted: false, reason: 'disabled' };
        }
        if (this.inFlight)
            return { accepted: false, reason: 'in-flight' };
        this.inFlight = true;
        // One line per accepted tick — without it the loop is invisible in
        // server.log (success was fully silent; only failures warned), which made
        // "is the mentor running at all?" unanswerable from the logs.
        // eslint-disable-next-line no-console
        console.log(`[mentor] tick accepted (mode=${cfg.mode}, framework=${cfg.menteeFramework}${autonomous ? ', autonomous-fix' : ''})`);
        void this.tick()
            .then((r) => {
            this.setLastResult({ ...r, at: (this.services.now ?? Date.now)() });
            // eslint-disable-next-line no-console
            console.log(`[mentor] tick result: ran=${r.ran}, reason=${r.reason ?? 'ok'}`);
        })
            .catch((err) => {
            const message = err instanceof Error ? err.message : String(err);
            this.setLastResult({
                ran: false,
                reason: 'stage-a-failed',
                // Surface the real failure so GET /mentor/status.lastResult.error is
                // diagnosable, instead of an opaque 'stage-a-failed' with the cause
                // only in a console.warn that may never reach the readable logs.
                error: message,
                at: (this.services.now ?? Date.now)(),
            });
            // eslint-disable-next-line no-console
            console.warn('[mentor] tick failed:', message);
        })
            .finally(() => {
            this.inFlight = false;
        });
        return { accepted: true };
    }
    /** Run one tick. Short-circuits to `disabled` when the feature is off (§16).
     *  Branches to the autonomous-fix guardian when `autonomousFix.enabled`. */
    async tick() {
        const cfg = this.getConfig();
        const autonomous = cfg.autonomousFix?.enabled === true;
        if (!cfg.enabled || (cfg.mode === 'off' && !autonomous)) {
            return { ran: false, reason: 'disabled' };
        }
        if (autonomous)
            return this.autonomousTick(cfg);
        const framework = cfg.menteeFramework;
        const mode = cfg.mode === 'live' ? 'live' : 'dry-run';
        const result = await runMentorTick({
            framework,
            mode,
            surface: this.services.getSurface(framework),
            // Safe window = mentee at rest AND the min-interval floor elapsed (§12 Q3).
            safeWindowOpen: !this.services.isMenteeBusy() && this.services.minIntervalElapsed(),
            budgetOk: this.services.budgetOk(),
            llmAvailable: llmCircuitAvailable(),
            spawnStageA: this.services.spawnStageA,
            runStageBForensics: () => this.services.runStageBForensics(framework),
            capture: this.services.capture,
            recordCycle: this.services.recordCycle,
            deliverToMentee: this.services.deliverToMentee,
            now: this.services.now,
        });
        if (result.ran)
            this.services.onTickRan?.();
        return result;
    }
    /**
     * The "just be Echo" autonomous-fix path. Delegates to the pure
     * {@link runAutonomousGuardian}: keep ONE full-tool Opus loop session alive on
     * the manual dogfooding loop. The runner wires the injected guardian services
     * (alive-check, spawn, goal builder) and advances the run counters when a
     * cycle actually spawns. A host that enabled `autonomousFix` but failed to
     * wire `spawnLoopSession` surfaces a clear `spawn-failed` result rather than a
     * silent no-op.
     */
    async autonomousTick(cfg) {
        const af = cfg.autonomousFix;
        const framework = cfg.menteeFramework;
        const result = await runAutonomousGuardian({
            framework,
            enabled: af.enabled,
            budgetOk: this.services.budgetOk(),
            loopSessionAlive: this.services.loopSessionAlive?.() ?? false,
            minIntervalElapsed: this.services.minIntervalElapsed(),
            model: af.model || 'opus',
            buildGoal: () => af.goalTemplate ?? this.services.buildAutoloopGoal?.(framework) ?? '',
            spawnLoopSession: this.services.spawnLoopSession ??
                (async () => {
                    throw new Error('spawnLoopSession not wired — autonomousFix.enabled but the host injected no loop-session spawner');
                }),
            now: this.services.now,
        });
        // A spawned cycle counts as a run (advances the per-day cap + interval clock).
        if (result.reason === 'spawned')
            this.services.onTickRan?.();
        return {
            ran: result.ran,
            reason: result.reason,
            sessionName: result.sessionName,
            error: result.error,
        };
    }
}
//# sourceMappingURL=MentorOnboardingRunner.js.map