use std::fmt;
use std::path::PathBuf;
use crate::core::delegation_authority::{generate_authority, scan_agents};
pub(crate) const SECTION_SEPARATOR: &str = "\n\n---\n\n";
pub(crate) const PM_INSTRUCTIONS: &str = include_str!("../assets/instructions/PM_INSTRUCTIONS.md");
pub(crate) const WORKFLOW: &str = include_str!("../assets/instructions/WORKFLOW.md");
pub(crate) const AGENT_DELEGATION: &str =
include_str!("../assets/instructions/AGENT_DELEGATION.md");
pub(crate) const BASE_PM: &str = include_str!("../assets/instructions/BASE_PM.md");
pub fn assemble_system_prompt() -> String {
[PM_INSTRUCTIONS, WORKFLOW, AGENT_DELEGATION, BASE_PM].join("\n\n---\n\n")
}
pub fn install_system_prompt_to(dest: &std::path::Path) -> std::io::Result<()> {
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(dest, assemble_system_prompt())
}
pub fn install_system_prompt() -> std::io::Result<std::path::PathBuf> {
let home = dirs::home_dir()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no home dir"))?;
let out_dir = home.join(".trusty-mpm/framework/instructions");
let out_path = out_dir.join("INSTRUCTIONS.md");
install_system_prompt_to(&out_path)?;
Ok(out_path)
}
const CLAUDE_MD_STUB: &str = "# Project Instructions
<!-- trusty-mpm: created by `trusty-mpm session start` — customize for your project -->
<!-- This file is yours: trusty-mpm will never overwrite it after creation. -->
## Project Context
<!-- Describe your project, tech stack, and any conventions the agent should know. -->
## Preferences
<!-- Any agent behavior preferences specific to this project. -->
";
#[derive(Debug, Clone)]
pub struct PipelineInput {
pub framework_instructions_path: PathBuf,
pub agents_dir: PathBuf,
pub claude_md_path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct PipelineOutput {
pub merged: String,
pub instructions_loaded: bool,
pub agent_count: usize,
pub claude_md_created: bool,
}
#[derive(Debug)]
pub enum PipelineError {
Io {
path: PathBuf,
source: std::io::Error,
},
}
impl fmt::Display for PipelineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io { path, source } => {
write!(f, "io error for {}: {source}", path.display())
}
}
}
}
impl std::error::Error for PipelineError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
}
}
}
pub fn build_instructions(input: &PipelineInput) -> Result<PipelineOutput, PipelineError> {
let (framework, instructions_loaded) =
match std::fs::read_to_string(&input.framework_instructions_path) {
Ok(text) => (text, true),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => (String::new(), false),
Err(err) => {
return Err(PipelineError::Io {
path: input.framework_instructions_path.clone(),
source: err,
});
}
};
let agents = scan_agents(&input.agents_dir);
let agent_count = agents.len();
let authority = generate_authority(&agents);
let (claude_md, claude_md_created) = load_or_create_claude_md(&input.claude_md_path)?;
let merged = merge_sections(&framework, &authority, &claude_md);
Ok(PipelineOutput {
merged,
instructions_loaded,
agent_count,
claude_md_created,
})
}
fn load_or_create_claude_md(path: &PathBuf) -> Result<(String, bool), PipelineError> {
match std::fs::read_to_string(path) {
Ok(text) => Ok((text, false)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent).map_err(|source| PipelineError::Io {
path: parent.to_path_buf(),
source,
})?;
}
std::fs::write(path, CLAUDE_MD_STUB).map_err(|source| PipelineError::Io {
path: path.clone(),
source,
})?;
Ok((CLAUDE_MD_STUB.to_string(), true))
}
Err(err) => Err(PipelineError::Io {
path: path.clone(),
source: err,
}),
}
}
fn merge_sections(framework: &str, authority: &str, claude_md: &str) -> String {
let sections: Vec<&str> = [framework.trim(), authority.trim(), claude_md.trim()]
.into_iter()
.filter(|s| !s.is_empty())
.collect();
let mut merged = sections.join(SECTION_SEPARATOR);
if !merged.is_empty() {
merged.push('\n');
}
merged
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn write_file(path: &PathBuf, content: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create parent");
}
fs::write(path, content).expect("write file");
}
fn input_in(tmp: &TempDir) -> PipelineInput {
PipelineInput {
framework_instructions_path: tmp.path().join("INSTRUCTIONS.md"),
agents_dir: tmp.path().join("agents"),
claude_md_path: tmp.path().join("project").join("CLAUDE.md"),
}
}
#[test]
fn pipeline_full() {
let tmp = TempDir::new().unwrap();
let input = input_in(&tmp);
write_file(
&input.framework_instructions_path,
"# Framework\n\nFRAMEWORK SECTION\n",
);
fs::create_dir_all(&input.agents_dir).unwrap();
write_file(
&input.agents_dir.join("engineer.md"),
"---\nname: engineer\nrole: engineer\ndescription: Builds things.\n---\n\n# Engineer\n",
);
write_file(&input.claude_md_path, "# Project\n\nPROJECT SECTION\n");
let out = build_instructions(&input).unwrap();
assert!(out.instructions_loaded);
assert_eq!(out.agent_count, 1);
assert!(
!out.claude_md_created,
"existing CLAUDE.md is not recreated"
);
let fw = out.merged.find("FRAMEWORK SECTION").expect("framework");
let auth = out
.merged
.find("## Delegation Authority")
.expect("authority");
let proj = out.merged.find("PROJECT SECTION").expect("project");
assert!(fw < auth, "framework precedes delegation authority");
assert!(auth < proj, "delegation authority precedes project notes");
assert!(out.merged.contains("### engineer"));
}
#[test]
fn pipeline_missing_instructions() {
let tmp = TempDir::new().unwrap();
let input = input_in(&tmp);
fs::create_dir_all(&input.agents_dir).unwrap();
write_file(
&input.agents_dir.join("qa.md"),
"---\nname: qa\nrole: qa\ndescription: Tests things.\n---\n\n# QA\n",
);
write_file(&input.claude_md_path, "# Project\n\nPROJECT NOTES\n");
let out = build_instructions(&input).unwrap();
assert!(!out.instructions_loaded);
assert!(out.merged.contains("## Delegation Authority"));
assert!(out.merged.contains("PROJECT NOTES"));
assert!(!out.merged.starts_with("---"));
}
#[test]
fn pipeline_creates_claude_md() {
let tmp = TempDir::new().unwrap();
let input = input_in(&tmp);
write_file(&input.framework_instructions_path, "# Framework\n");
fs::create_dir_all(&input.agents_dir).unwrap();
assert!(!input.claude_md_path.exists());
let out = build_instructions(&input).unwrap();
assert!(out.claude_md_created);
assert!(input.claude_md_path.exists());
let on_disk = fs::read_to_string(&input.claude_md_path).unwrap();
assert!(on_disk.contains("# Project Instructions"));
assert!(on_disk.contains("trusty-mpm will never overwrite it"));
assert!(out.merged.contains("# Project Instructions"));
}
#[test]
fn pipeline_claude_md_not_overwritten() {
let tmp = TempDir::new().unwrap();
let input = input_in(&tmp);
write_file(&input.framework_instructions_path, "# Framework\n");
fs::create_dir_all(&input.agents_dir).unwrap();
let custom = "# My Project\n\nCUSTOM HAND-WRITTEN CONTENT\n";
write_file(&input.claude_md_path, custom);
let out = build_instructions(&input).unwrap();
assert!(!out.claude_md_created);
let on_disk = fs::read_to_string(&input.claude_md_path).unwrap();
assert_eq!(on_disk, custom, "custom CLAUDE.md must be untouched");
assert!(out.merged.contains("CUSTOM HAND-WRITTEN CONTENT"));
}
#[test]
fn assemble_system_prompt_contains_all_sections() {
let prompt = assemble_system_prompt();
assert!(prompt.contains("# PM Agent -- Claude MPM"));
assert!(prompt.contains("# BASE_PM Framework Floor"));
assert!(prompt.contains("# PM Workflow Configuration"));
assert!(prompt.contains("# Agent Delegation Routing"));
assert!(prompt.contains("## Trusty Tool Priority (Non-Overridable)"));
assert!(prompt.contains("\n\n---\n\n"));
let base = prompt.find("# BASE_PM Framework Floor").expect("base_pm");
let delegation = prompt
.find("# Agent Delegation Routing")
.expect("delegation");
assert!(base > delegation, "BASE_PM floor must be appended last");
assert!(!prompt.contains("mcp__mcp-ticketer__"));
assert!(!prompt.contains("ticketing_agent"));
}
#[test]
fn install_system_prompt_writes_file() {
let out = install_system_prompt().expect("install succeeds");
assert!(out.ends_with("INSTRUCTIONS.md"));
assert!(out.exists());
let on_disk = fs::read_to_string(&out).unwrap();
assert_eq!(on_disk, assemble_system_prompt());
assert!(!on_disk.is_empty());
}
#[test]
fn install_system_prompt_to_writes_assembled() {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("instructions").join("INSTRUCTIONS.md");
install_system_prompt_to(&dest).expect("write succeeds");
assert!(
dest.exists(),
"INSTRUCTIONS.md must exist after install_system_prompt_to"
);
let on_disk = fs::read_to_string(&dest).unwrap();
assert_eq!(
on_disk,
assemble_system_prompt(),
"content must equal assembled prompt"
);
assert!(
!on_disk.trim().eq("# trusty-mpm Framework Instructions\n\nThis Claude Code instance is managed by trusty-mpm.\nDaemon endpoint: ${TRUSTY_MPM_URL:-http://localhost:7799}"),
"stub content must not be written — full assembled prompt required"
);
assert!(
on_disk.contains("# PM Agent -- Claude MPM"),
"PM_INSTRUCTIONS section must be present"
);
assert!(
on_disk.contains("# BASE_PM Framework Floor"),
"BASE_PM floor must be present"
);
}
#[test]
fn pipeline_no_agents_still_succeeds() {
let tmp = TempDir::new().unwrap();
let input = input_in(&tmp);
write_file(&input.framework_instructions_path, "# Framework\n");
fs::create_dir_all(&input.agents_dir).unwrap();
let out = build_instructions(&input).unwrap();
assert_eq!(out.agent_count, 0);
assert!(out.merged.to_lowercase().contains("no delegatable agents"));
}
}