use std::path::{Path, PathBuf};
use crate::core::instruction_pipeline::SECTION_SEPARATOR;
pub(crate) const SM_INSTRUCTIONS: &str =
include_str!("../../assets/sm_instructions/SM_INSTRUCTIONS.md");
pub(crate) const SM_WORKFLOW: &str = include_str!("../../assets/sm_instructions/SM_WORKFLOW.md");
pub(crate) const SM_TOOLS: &str = include_str!("../../assets/sm_instructions/SM_TOOLS.md");
pub(crate) const BASE_SM: &str = include_str!("../../assets/sm_instructions/BASE_SM.md");
pub const SM_OVERRIDE_SUBDIR: &str = "sm";
pub const FILE_SM_INSTRUCTIONS: &str = "SM_INSTRUCTIONS.md";
pub const FILE_SM_WORKFLOW: &str = "SM_WORKFLOW.md";
pub const FILE_SM_TOOLS: &str = "SM_TOOLS.md";
pub fn assemble_sm_prompt() -> String {
[SM_INSTRUCTIONS, SM_WORKFLOW, SM_TOOLS, BASE_SM]
.iter()
.map(|s| s.trim())
.collect::<Vec<_>>()
.join(SECTION_SEPARATOR)
}
pub fn sm_override_dir() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".trusty-mpm").join(SM_OVERRIDE_SUBDIR))
}
fn read_override(dir: &Path, name: &str) -> Option<String> {
let path = dir.join(name);
match std::fs::read_to_string(&path) {
Ok(text) => {
let trimmed = text.trim();
if trimmed.is_empty() {
tracing::warn!(
path = %path.display(),
"SM instruction override file is empty; using bundled default"
);
None
} else {
tracing::info!(path = %path.display(), "applying SM instruction override");
Some(trimmed.to_string())
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
Err(err) => {
tracing::warn!(
path = %path.display(),
%err,
"SM instruction override file unreadable; using bundled default"
);
None
}
}
}
pub fn resolve_sm_prompt(dir: &Path) -> String {
let instructions = read_override(dir, FILE_SM_INSTRUCTIONS)
.unwrap_or_else(|| SM_INSTRUCTIONS.trim().to_string());
let workflow =
read_override(dir, FILE_SM_WORKFLOW).unwrap_or_else(|| SM_WORKFLOW.trim().to_string());
let tools = read_override(dir, FILE_SM_TOOLS).unwrap_or_else(|| SM_TOOLS.trim().to_string());
let sections = [instructions, workflow, tools, BASE_SM.trim().to_string()];
sections
.iter()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(SECTION_SEPARATOR)
}
pub fn resolve_sm_prompt_default() -> String {
match sm_override_dir() {
Some(dir) => resolve_sm_prompt(&dir),
None => assemble_sm_prompt(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn write_override(dir: &Path, name: &str, content: &str) {
fs::create_dir_all(dir).expect("create override dir");
fs::write(dir.join(name), content).expect("write override");
}
#[test]
fn assemble_sm_prompt_contains_all_sections() {
let prompt = assemble_sm_prompt();
assert!(prompt.contains("# Session Manager (SM) -- trusty-mpm"));
assert!(prompt.contains("| SP1 |"));
assert!(prompt.contains("| SP7 |"));
assert!(prompt.contains("You MAY do directly (Allowlist)"));
assert!(prompt.contains("# SM Workflow -- the delegation loop"));
assert!(prompt.contains("1. **INTAKE.**"));
assert!(prompt.contains("6. **REPORT & PERSIST.**"));
assert!(prompt.contains("Verification gate (BLOCKING)"));
assert!(prompt.contains("# SM Tools -- the verbs you may call"));
assert!(prompt.contains("# BASE_SM Framework Floor"));
assert!(prompt.contains(SECTION_SEPARATOR));
}
#[test]
fn assemble_sm_prompt_base_floor_is_last() {
let prompt = assemble_sm_prompt();
let base = prompt.find("# BASE_SM Framework Floor").expect("base_sm");
let tools = prompt
.find("# SM Tools -- the verbs you may call")
.expect("tools");
let workflow = prompt
.find("# SM Workflow -- the delegation loop")
.expect("workflow");
assert!(workflow < tools, "workflow precedes tools");
assert!(base > tools, "BASE_SM floor must be appended last");
}
#[test]
fn no_overrides_uses_bundled() {
let tmp = TempDir::new().unwrap();
let prompt = resolve_sm_prompt(tmp.path());
assert!(prompt.contains("# Session Manager (SM) -- trusty-mpm"));
assert!(prompt.contains("# SM Workflow -- the delegation loop"));
assert!(prompt.contains("# SM Tools -- the verbs you may call"));
assert!(prompt.contains("# BASE_SM Framework Floor"));
let base = prompt.find("# BASE_SM Framework Floor").expect("base");
let tools = prompt
.find("# SM Tools -- the verbs you may call")
.expect("tools");
assert!(base > tools, "BASE_SM floor must be last");
}
#[test]
fn sm_instructions_override_replaces() {
let tmp = TempDir::new().unwrap();
write_override(
tmp.path(),
FILE_SM_INSTRUCTIONS,
"# Custom Identity\n\nDELEGATE_EVERYTHING_TO_ALICE\n",
);
let prompt = resolve_sm_prompt(tmp.path());
assert!(prompt.contains("DELEGATE_EVERYTHING_TO_ALICE"));
assert!(
!prompt.contains("# Session Manager (SM) -- trusty-mpm"),
"bundled SM_INSTRUCTIONS heading must be replaced"
);
assert!(prompt.contains("# SM Workflow -- the delegation loop"));
assert!(prompt.contains("# SM Tools -- the verbs you may call"));
assert!(prompt.contains("# BASE_SM Framework Floor"));
let body = prompt.find("DELEGATE_EVERYTHING_TO_ALICE").expect("body");
let base = prompt.find("# BASE_SM Framework Floor").expect("base");
assert!(body < base, "BASE_SM floor follows the override body");
}
#[test]
fn workflow_override_replaces() {
let tmp = TempDir::new().unwrap();
write_override(
tmp.path(),
FILE_SM_WORKFLOW,
"# Custom Loop\n\nTWO_PHASE_ONLY\n",
);
let prompt = resolve_sm_prompt(tmp.path());
assert!(prompt.contains("TWO_PHASE_ONLY"));
assert!(
!prompt.contains("# SM Workflow -- the delegation loop"),
"bundled workflow heading must be replaced"
);
assert!(prompt.contains("# Session Manager (SM) -- trusty-mpm"));
assert!(prompt.contains("# SM Tools -- the verbs you may call"));
assert!(prompt.contains("# BASE_SM Framework Floor"));
}
#[test]
fn tools_override_replaces() {
let tmp = TempDir::new().unwrap();
write_override(
tmp.path(),
FILE_SM_TOOLS,
"# Custom Verbs\n\nONLY_LAUNCH_AND_STOP\n",
);
let prompt = resolve_sm_prompt(tmp.path());
assert!(prompt.contains("ONLY_LAUNCH_AND_STOP"));
assert!(
!prompt.contains("# SM Tools -- the verbs you may call"),
"bundled tools heading must be replaced"
);
assert!(prompt.contains("# Session Manager (SM) -- trusty-mpm"));
assert!(prompt.contains("# SM Workflow -- the delegation loop"));
assert!(prompt.contains("# BASE_SM Framework Floor"));
}
#[test]
fn base_sm_floor_is_never_overridable() {
let tmp = TempDir::new().unwrap();
write_override(
tmp.path(),
"BASE_SM.md",
"# Fake Floor\n\nNO_PROHIBITIONS_ANYMORE\n",
);
write_override(
tmp.path(),
FILE_SM_INSTRUCTIONS,
"# Custom Identity\n\nCUSTOM_IDENTITY_BODY\n",
);
let prompt = resolve_sm_prompt(tmp.path());
assert!(prompt.contains("CUSTOM_IDENTITY_BODY"));
assert!(
prompt.contains("# BASE_SM Framework Floor"),
"bundled BASE_SM floor must always be appended"
);
assert!(prompt.contains("Trusty Tool Priority (Non-Overridable)"));
assert!(
!prompt.contains("NO_PROHIBITIONS_ANYMORE"),
"override-dir BASE_SM.md must be ignored"
);
assert!(!prompt.contains("# Fake Floor"));
let body = prompt.find("CUSTOM_IDENTITY_BODY").expect("body");
let base = prompt.find("# BASE_SM Framework Floor").expect("base");
assert!(body < base, "bundled BASE_SM floor must come last");
}
#[test]
fn missing_override_dir_uses_bundled() {
let tmp = TempDir::new().unwrap();
let missing = tmp.path().join("does-not-exist");
assert!(!missing.exists());
let prompt = resolve_sm_prompt(&missing);
assert!(prompt.contains("# Session Manager (SM) -- trusty-mpm"));
assert!(prompt.contains("# BASE_SM Framework Floor"));
}
#[test]
fn empty_override_falls_back() {
let tmp = TempDir::new().unwrap();
write_override(tmp.path(), FILE_SM_WORKFLOW, " \n\t\n");
let prompt = resolve_sm_prompt(tmp.path());
assert!(prompt.contains("# SM Workflow -- the delegation loop"));
assert!(prompt.contains("# BASE_SM Framework Floor"));
}
#[test]
fn unreadable_override_falls_back() {
let tmp = TempDir::new().unwrap();
fs::create_dir(tmp.path().join(FILE_SM_WORKFLOW)).unwrap();
let prompt = resolve_sm_prompt(tmp.path());
assert!(prompt.contains("# SM Workflow -- the delegation loop"));
assert!(prompt.contains("# BASE_SM Framework Floor"));
}
#[test]
fn separators_are_consistent() {
let tmp = TempDir::new().unwrap();
let prompt = resolve_sm_prompt(tmp.path());
assert!(prompt.contains(SECTION_SEPARATOR));
}
#[test]
fn resolve_with_no_overrides_matches_assembled_sections() {
let tmp = TempDir::new().unwrap();
let resolved = resolve_sm_prompt(tmp.path());
let assembled = assemble_sm_prompt();
assert_eq!(
resolved, assembled,
"no-override resolve must be byte-identical to assemble"
);
let section_count = assembled.split(SECTION_SEPARATOR).count();
assert_eq!(section_count, 4, "four sections expected");
}
#[test]
fn sm_override_dir_under_home() {
match sm_override_dir() {
Some(dir) => {
assert!(dir.ends_with("sm"), "override dir must end with `sm`");
assert!(
dir.to_string_lossy().contains(".trusty-mpm"),
"override dir must be anchored under `.trusty-mpm`"
);
}
None => {
eprintln!(
"sm_override_dir_under_home: no home resolved; \
graceful-None fallback exercised"
);
}
}
}
}