malvin 0.2.5

Non-interactive research and coding agent
#[path = "default_files.rs"]
mod default_files;

pub use default_files::{
    ROUTER_A_MD, ROUTER_A_NO_KPOP_MD, ROUTER_B_CREATIVE_MD, ROUTER_B_MD, ROUTER_B_NO_KPOP_MD,
    RouterBPromptFlags, default_file, header_prompt_file, kpop_common_prompt_file,
    router_a_prompt_file, router_b_prompt_file,
};

pub const HEADER_MD: &str = "header.md";
pub const KPOP_COMMON_MD: &str = "kpop_common.md";
pub const KPOP_COMMON_NO_KPOP_MD: &str = "kpop_common_no_kpop.md";
pub const DO_HEADER_MD: &str = "do_header.md";
pub const ROUTER_CODE_EXTRA_MD: &str = "router_code_extra.md";
pub const ROUTER_SUMMARIZE_MD: &str = "router_summarize.md";
pub const WRITE_A_MD: &str = "write_a.md";
pub const WRITE_B_MD: &str = "write_b.md";

pub const REQUIRED_PROMPTS: &[&str] = &[HEADER_MD];

pub const DEFAULT_PROMPTS: &[&str] = &[
    "mbc2.md",
    "init_constraints.md",
    HEADER_MD,
    KPOP_COMMON_MD,
    KPOP_COMMON_NO_KPOP_MD,
    DO_HEADER_MD,
    ROUTER_A_MD,
    ROUTER_A_NO_KPOP_MD,
    ROUTER_B_MD,
    ROUTER_B_CREATIVE_MD,
    ROUTER_B_NO_KPOP_MD,
    ROUTER_CODE_EXTRA_MD,
    ROUTER_SUMMARIZE_MD,
    WRITE_A_MD,
    WRITE_B_MD,
];

#[cfg(test)]
mod review_plan_embed_tests {
    use super::DEFAULT_PROMPTS;
    use super::default_file;
    use crate::prompts::malformed_brace_placeholders;

    #[test]
    fn embedded_defaults_exclude_mpc_blocks() {
        for name in DEFAULT_PROMPTS {
            assert!(
                !name.starts_with("mpc_block_"),
                "DEFAULT_PROMPTS must not embed mpc_block files: {name}"
            );
        }
        let count = std::fs::read_dir("default_prompts")
            .expect("default_prompts dir")
            .filter_map(Result::ok)
            .filter(|e| {
                e.file_name()
                    .to_str()
                    .is_some_and(|n| n.starts_with("mpc_block_"))
            })
            .count();
        assert_eq!(
            count, 0,
            "default_prompts/ must contain no mpc_block_*.md files"
        );
    }

    #[test]
    fn embedded_default_prompts_use_spaced_brace_placeholders() {
        for name in DEFAULT_PROMPTS {
            let text = default_file(name).unwrap_or_else(|| panic!("{name} must be embedded"));
            let bad = malformed_brace_placeholders(text);
            assert!(bad.is_empty(), "{name}: {bad:?}");
        }
    }
}

#[cfg(test)]
mod advice_path_embed_tests {
    use std::path::Path;

    use crate::artifacts::create_run_artifacts;
    use crate::config::DEFAULT_CLI_MODEL;
    use crate::orchestrator::workflow_context_paths_only;
    use crate::prompts::{PromptStore, render_header};

    #[test]
    fn embedded_header_render_without_unresolved_braces() {
        let tmp = tempfile::tempdir().expect("tempdir");
        std::process::Command::new("git")
            .args(["init"])
            .current_dir(tmp.path())
            .status()
            .expect("git init");
        crate::seed_malvin_checks(tmp.path(), "true\n");
        let plan_path = tmp.path().join("plan.md");
        std::fs::write(&plan_path, "plan body\n").expect("write plan");
        let artifacts =
            create_run_artifacts(Path::new(&plan_path), Some(tmp.path())).expect("artifacts");
        let store = PromptStore::default_store();
        store.ensure_defaults().expect("defaults");
        let ctx = workflow_context_paths_only(&artifacts, DEFAULT_CLI_MODEL, false);
        let header = render_header(&store, ctx.as_map()).expect("header");
        assert!(
            !header.contains("{{"),
            "header must expand all placeholders"
        );
        assert!(
            header.contains(".malvin_home/logs"),
            "header must render logs_dir to home logs bucket"
        );
        let workspace = ctx.get("workspace_dir").expect("workspace_dir");
        assert_eq!(
            Some(workspace.as_str()),
            ctx.get("malvin_output_path").map(String::as_str),
            "workspace_dir should be the per-run log directory"
        );
        assert!(
            header.contains(workspace),
            "header must render workspace_dir to the per-run log directory"
        );
        assert!(
            header.contains("User:"),
            "header must render current_state from workflow context"
        );
    }
}

#[cfg(test)]
mod router_header_embed_tests {
    use std::path::Path;

    use super::{
        DO_HEADER_MD, HEADER_MD, ROUTER_A_MD, ROUTER_B_CREATIVE_MD, ROUTER_B_MD,
        ROUTER_SUMMARIZE_MD, default_file,
    };
    use crate::artifacts::create_run_artifacts;
    use crate::config::DEFAULT_CLI_MODEL;
    use crate::orchestrator::workflow_context_paths_only;
    use crate::prompts::{PromptStore, render_header};
    use crate::router_flow::router_flow_prompt::{
        RouterAPromptInput, RouterBPromptInput, RouterHeaderPromptInput,
        RouterKpopCommonPromptInput, RouterSummarizePromptInput, build_router_a_prompt,
        build_router_b_prompt, build_router_header_prompt, build_router_kpop_common_prompt,
        build_router_summarize_prompt, prepare_router_prompt_store,
    };

    fn embedded_router_fixture() -> (
        tempfile::TempDir,
        crate::artifacts::RunArtifacts,
        PromptStore,
    ) {
        let tmp = tempfile::tempdir().expect("tempdir");
        std::process::Command::new("git")
            .args(["init"])
            .current_dir(tmp.path())
            .status()
            .expect("git init");
        crate::seed_malvin_checks(tmp.path(), "true\n");
        let plan_path = tmp.path().join("plan.md");
        std::fs::write(&plan_path, "plan body\n").expect("write plan");
        let artifacts =
            create_run_artifacts(Path::new(&plan_path), Some(tmp.path())).expect("artifacts");
        let store = prepare_router_prompt_store().expect("store");
        (tmp, artifacts, store)
    }

    #[test]
    fn embedded_default_files_cover_router_templates() {
        assert!(default_file(ROUTER_A_MD).is_some());
        assert!(default_file(ROUTER_B_MD).is_some());
        assert!(default_file(ROUTER_B_CREATIVE_MD).is_some());
        assert!(default_file(ROUTER_SUMMARIZE_MD).is_some());
        assert!(default_file(DO_HEADER_MD).is_some());
        assert!(default_file(HEADER_MD).is_some());
    }

    #[test]
    fn embedded_header_and_router_render_without_unresolved_braces() {
        let (_tmp, artifacts, store) = embedded_router_fixture();
        let ctx = workflow_context_paths_only(&artifacts, DEFAULT_CLI_MODEL, false);
        let header = render_header(&store, ctx.as_map()).expect("header");
        assert!(
            !header.contains("{{"),
            "header must expand all placeholders"
        );
        let header_turn = build_router_header_prompt(RouterHeaderPromptInput {
            store: &store,
            artifacts: &artifacts,
            model: DEFAULT_CLI_MODEL,
            git: false,
        })
        .expect("header turn");
        assert!(!header_turn.contains("{{"));
        let kpop_turn = build_router_kpop_common_prompt(RouterKpopCommonPromptInput {
            store: &store,
            artifacts: &artifacts,
            model: DEFAULT_CLI_MODEL,
            git: false,
            max_hypotheses: 5,
            no_kpop: false,
        })
        .expect("kpop common turn");
        assert!(!kpop_turn.contains("{{"));
        let a = build_router_a_prompt(RouterAPromptInput {
            store: &store,
            artifacts: &artifacts,
            model: DEFAULT_CLI_MODEL,
            git: false,
            gates: false,
            no_kpop: false,
        })
        .expect("router_a");
        assert!(!a.contains("{{"));
        let a_gates = build_router_a_prompt(RouterAPromptInput {
            store: &store,
            artifacts: &artifacts,
            model: DEFAULT_CLI_MODEL,
            git: false,
            gates: true,
            no_kpop: false,
        })
        .expect("router_a gates");
        assert!(!a_gates.contains("{{"));
        let b = build_router_b_prompt(RouterBPromptInput {
            store: &store,
            artifacts: &artifacts,
            model: DEFAULT_CLI_MODEL,
            git: false,
            creative: false,
            no_kpop: false,
        })
        .expect("router_b");
        assert!(!b.contains("{{"));
        let b_creative = build_router_b_prompt(RouterBPromptInput {
            store: &store,
            artifacts: &artifacts,
            model: DEFAULT_CLI_MODEL,
            git: false,
            creative: true,
            no_kpop: false,
        })
        .expect("router_b_creative");
        assert!(!b_creative.contains("{{"));
        assert!(
            b_creative.contains("MBC2"),
            "creative router_b must mention MBC2: {b_creative}"
        );
        assert!(
            !b.contains("MBC2"),
            "default router_b must not mention MBC2: {b}"
        );
        let summarize = build_router_summarize_prompt(RouterSummarizePromptInput {
            store: &store,
            artifacts: &artifacts,
            model: DEFAULT_CLI_MODEL,
            git: false,
        })
        .expect("summarize");
        assert!(!summarize.contains("{{"));
        assert!(
            summarize.contains("Write a summary of this entire session"),
            "router_summarize.md body must be rendered: {summarize}"
        );
    }
}