malvin 0.2.5

Non-interactive research and coding agent
mod create;
mod md_request;
mod startup_tag;

use std::path::{Path, PathBuf};

pub use create::{
    create_run_artifacts, create_run_artifacts_from_text, create_run_artifacts_from_text_opts,
    create_run_artifacts_opts,
};
pub(crate) use create::{
    ensure_gate_exp_log_file, ensure_quality_gates_log_file, init_quality_gates_log_pending,
};

pub use crate::session_dotfile_backup::{
    GitignoreBackup, MalvinChecksBackup, MalvinConfigWorkspaceBackup, SessionDotfileBackups,
    SessionDotfileParts, VisionBackup, backup_workspace_gitignore_if_present,
    backup_workspace_gitignore_if_present_with_id, backup_workspace_malvin_checks_if_present,
    backup_workspace_malvin_checks_if_present_with_id,
    backup_workspace_malvin_config_workspace_if_present,
    backup_workspace_malvin_config_workspace_if_present_with_id,
    backup_workspace_vision_if_present, backup_workspace_vision_if_present_with_id,
    merge_and_sanitize_for_gate_restore, merge_for_gate_restore,
    repair_invalid_malvin_home_config_on_disk, restore_workspace_gitignore_backup,
    restore_workspace_malvin_checks_backup, restore_workspace_malvin_config_workspace_backup,
    restore_workspace_session_dotfiles, restore_workspace_vision_backup,
};

pub use md_request::{
    is_existing_md_file_path, looks_like_md_file_path_arg, resolve_user_md_request,
};
pub use startup_tag::startup_request_tag_label;

pub use crate::malvin_constants::{QUALITY_GATES_LOG, SANDBOX_OOM_JSON, STDOUT_LOG, TRACE_JSONL};

#[derive(Debug, Clone)]
pub struct RunArtifacts {
    pub run_dir: PathBuf,
    pub plan_path: PathBuf,
    pub work_dir: PathBuf,
}

impl RunArtifacts {
    #[must_use]
    pub fn log_path(&self, name: &str) -> PathBuf {
        let safe = name.replace(['/', '\\'], "_");
        self.run_dir.join(format!("{safe}.log"))
    }

    #[must_use]
    pub fn artifact_review_md(&self) -> PathBuf {
        self.run_dir.join("review.md")
    }

    #[must_use]
    pub fn review_prep_md(&self) -> PathBuf {
        self.run_dir.join("review_prep.md")
    }

    #[must_use]
    pub fn artifact_result_md(&self) -> PathBuf {
        self.run_dir.join("result.md")
    }

    #[must_use]
    pub fn exp_log_path(&self) -> PathBuf {
        self.gate_exp_log_path(0)
    }

    #[must_use]
    pub fn gate_exp_log_path(&self, iteration: usize) -> PathBuf {
        let slug = self
            .run_dir
            .file_name()
            .and_then(|s| s.to_str())
            .unwrap_or("run");
        let name = if iteration == 0 {
            format!("exp_log_{slug}.md")
        } else {
            format!("exp_log_{slug}_g{iteration}.md")
        };
        self.run_dir.join("_run").join(name)
    }

    #[must_use]
    pub fn quality_gates_log_path(&self) -> PathBuf {
        self.run_dir.join(QUALITY_GATES_LOG)
    }

    #[must_use]
    pub fn stdout_log_path(&self) -> PathBuf {
        self.run_dir.join(STDOUT_LOG)
    }

    #[must_use]
    pub fn sandbox_oom_json_path(&self) -> PathBuf {
        self.run_dir.join(SANDBOX_OOM_JSON)
    }
}

#[must_use]
pub fn user_request_path(artifacts: &RunArtifacts) -> PathBuf {
    artifacts.run_dir.join("user_request.md")
}

#[must_use]
pub fn review_requirements_json(artifacts: &RunArtifacts) -> PathBuf {
    artifacts.run_dir.join("review_requirements.json")
}

pub(crate) fn work_dir_for_path(path: &Path) -> PathBuf {
    path.parent()
        .filter(|p| !p.as_os_str().is_empty())
        .map_or_else(
            || PathBuf::from("."),
            |parent| {
                parent
                    .canonicalize()
                    .unwrap_or_else(|_| parent.to_path_buf())
            },
        )
}

#[cfg(test)]
mod tests;

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

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