trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
//! In-project session preparation and the resume-path compiled-prompt refresh.
//!
//! Why: split out of `lifecycle.rs` (#4752), which sits at its frozen SLOC
//! budget. Both items here answer one question — "what must be on disk before
//! this session's process starts?" — so they belong together rather than
//! padding the lifecycle state machine.
//! What: [`prepare_inproject_session`] (the #1913 preparation call the
//! in-project spawn paths make directly) and
//! [`refresh_resume_compiled_prompt_in`] (the #4752 fatal compiled-prompt
//! write the resume path needs because it never runs `prepare_session*`).
//! Test: `prepare_inproject_session_writes_statusline`,
//! `prepare_inproject_session_emits_stage_events_in_order`,
//! `refresh_resume_compiled_prompt_writes_the_project_local_file`,
//! `refresh_resume_compiled_prompt_reports_an_actionable_failure`.

use tracing::{info, warn};

use crate::session_manager::ManagedSessionId;

/// Run the session-preparation pipeline for an in-project worktree, logging
/// (never propagating) any failure.
///
/// Why (#1913): [`spawn_managed_inproject`] has no clone step to wrap this call
/// in — unlike `spawn_managed`'s clone branch and `spawn_managed_local`, which
/// both got preparation "for free" as part of the clone provisioner
/// — so it must invoke [`crate::core::session_launch::prepare_session_with_repo_url`]
/// directly. Extracted to a named, `fw`-parameterised function (rather than
/// inlined) so it is unit-testable against a hermetic [`crate::core::paths::FrameworkPaths::under`]
/// tempdir without touching the operator's real `~/.trusty-mpm`/`~/.claude`.
/// What: calls `prepare_session_for_managed(fw, worktree, repo_url,
/// session_id)` — `repo_url` is `None` for a local-only repository (#8934) — #4832 threads the id so the compiled prompt lands in this
/// session's own directory rather than the unmanaged `local` bucket.
/// On success, logs the deployed-agent count AND the deployed-skill count
/// (`report.skill_deploy.deployed.len()` — #1917; previously only the agent
/// count was logged, so a skill-deploy no-op was invisible here too). On
/// failure, logs a `tracing::warn!` and returns — mirroring
/// the clone provisioner's non-fatal handling of the identical
/// call, so a prep failure never blocks the session from spawning.
///
/// #7685: returns the trusty-memory reachability this preparation resolved, so
/// the caller can hand it to `build_adapter` instead of making the runtime
/// adapter pay a second probe against a daemon already known to be hung. `None`
/// means preparation never got far enough to resolve one.
/// Test: `prepare_inproject_session_writes_statusline` in this module's `tests`
/// submodule.
pub(super) fn prepare_inproject_session(
    fw: &crate::core::paths::FrameworkPaths,
    session_id: &ManagedSessionId,
    worktree: &std::path::Path,
    repo_url: Option<&str>,
) -> Result<Option<bool>, String> {
    let mut memory_reachable = None;
    match crate::core::session_launch::prepare_session_for_managed(
        fw,
        worktree,
        repo_url,
        &session_id.to_string(),
    ) {
        Ok(report) => {
            memory_reachable = Some(report.memory_reachable);
            info!(
                id = %session_id,
                deployed = report.deploy.deployed.len(),
                skills_deployed = report.skill_deploy.deployed.len(),
                worktree = %worktree.display(),
                "spawn_managed (inproject): session prepared"
            );
            // Issue #2149: a roster-deploy failure no longer aborts
            // preparation, so surface it loudly here rather than letting it
            // hide behind a low `deployed`/`skills_deployed` count.
            // #6649 folded the asset-hygiene lines in beside them, through the
            // one shared reporter.
            crate::core::session_launch::log_prep_findings(
                &report.roster_errors,
                &report.asset_notices,
                crate::core::session_launch::PrepScope {
                    kind: "spawn_managed_inproject",
                    session: Some(session_id),
                    dir: worktree,
                },
            );
        }
        // #4752: a compiled-prompt write failure refuses the spawn — the caller
        // propagates this string. Every other prep failure stays non-fatal
        // (#2149) and only warns.
        Err(e) if e.is_fatal() => {
            warn!(
                id = %session_id,
                worktree = %worktree.display(),
                "spawn_managed (inproject): refusing to spawn: {e}"
            );
            return Err(e.to_string());
        }
        Err(e) => {
            warn!(
                id = %session_id,
                worktree = %worktree.display(),
                "spawn_managed (inproject): session prep failed (non-fatal): {e}"
            );
        }
    }
    Ok(memory_reachable)
}

/// Refresh the project's compiled PM prompt on the RESUME path.
///
/// Why (#4752): `resume_managed` never calls `prepare_session*` on its healthy
/// path (`resume_self_heal` → `ensure_status_line` → `ensure_deployment_complete`
/// → `spawn_resume`), so without this a resumed session would run against
/// whatever the last start wrote — or nothing at all. Composing through the
/// SAME seam the spawn uses means the bytes written here are the bytes
/// `spawn_resume` is about to pass to `--append-system-prompt-file`.
/// What: composes the project-resolved prompt, writes it to
/// [`crate::core::instruction_pipeline::compiled_prompt_path`], and on failure
/// returns the operator-facing message from
/// [`crate::core::instruction_pipeline::instructions_failure_message`] —
/// the caller refuses the resume with it — via
/// [`crate::core::instruction_pipeline::refresh_compiled_prompt_in`] with the
/// resumed session's own id as the scope, so the daemon-resume, fresh-start and
/// bare-`tm` in-place relaunch paths cannot drift apart (#4752) and a resume
/// refreshes the file the spawn will read rather than a sibling's (#4832).
///
/// #7514/#8233: the framework root is a PARAMETER, never re-derived from the
/// process home. It anchors the usage/savings ledger, and the root-free wrapper
/// that used to sit here wrote that ledger into the operator's own
/// `~/.trusty-mpm/usage/` from every test that drove the resume route. The one
/// production caller, `lifecycle::resume_managed`, passes
/// `DaemonState::framework_root`, which IS `~/.trusty-mpm` in production.
/// Test: `refresh_resume_compiled_prompt_writes_the_project_local_file`,
/// `refresh_resume_compiled_prompt_reports_an_actionable_failure`.
pub(super) fn refresh_resume_compiled_prompt_in(
    framework_root: &std::path::Path,
    workspace: &std::path::Path,
    session_id: &ManagedSessionId,
) -> Result<(), String> {
    crate::core::instruction_pipeline::refresh_compiled_prompt_in(
        framework_root,
        workspace,
        &session_id.to_string(),
    )
}

#[cfg(test)]
#[path = "session_prep_tests.rs"]
mod tests;