use std::path::Path;
use crate::skills::manifest::{skill_file_path, SkillsManifest};
use crate::skills::parser::parse_skill_md;
pub fn build_serve_system_prompt(
skill_names: &[String],
data_dir: &Path,
mode: Option<&str>,
) -> String {
let base = match mode {
Some("evaluate") | Some("review") => {
"You are an evaluation agent running in headless server mode. \
Analyze the provided input and return a structured assessment. \
Be objective, cite specific evidence for every claim, and never \
assert facts you cannot verify from the provided context."
}
_ => {
"You are a task agent running in headless server mode. \
Complete the requested task and return your output. \
Only assert facts you can verify from the provided context."
}
};
if skill_names.is_empty() {
return base.to_string();
}
let manifest = SkillsManifest::load(data_dir);
let mut skill_lines: Vec<String> = Vec::new();
for name in skill_names {
let Some(installed) = manifest.find(name) else {
tracing::warn!(skill = %name, "requested skill not installed, skipping");
continue;
};
let Some(path) = skill_file_path(data_dir, &installed.name) else {
tracing::warn!(
skill = %name,
stored_name = %installed.name,
"manifest entry has invalid name (path traversal attempt?), skipping"
);
continue;
};
let raw = match std::fs::read_to_string(&path) {
Ok(r) => r,
Err(e) => {
tracing::warn!(skill = %name, error = %e, "failed to read skill file, skipping");
continue;
}
};
let parsed = match parse_skill_md(&raw) {
Ok(p) => p,
Err(e) => {
tracing::warn!(skill = %name, error = %e, "failed to parse skill, skipping");
continue;
}
};
let domain_tag = parsed.domain.map(|d| format!(" [{d}]")).unwrap_or_default();
let trigger_tag = if parsed.triggers.is_empty() {
String::new()
} else {
format!(" triggers: {}", parsed.triggers.join(", "))
};
let tool_tag = if parsed.toolbox.is_empty() {
String::new()
} else {
let names = parsed
.toolbox
.iter()
.map(|t| t.name.as_str())
.collect::<Vec<_>>()
.join(", ");
format!(" tools: [{names}]")
};
skill_lines.push(format!(
"- **{}**{} [{}]: {}{}{}",
parsed.name, domain_tag, installed.trust, parsed.description, trigger_tag, tool_tag
));
}
if skill_lines.is_empty() {
return base.to_string();
}
let mut prompt = base.to_string();
prompt.push_str("\n\n## Active Skills\n");
for line in &skill_lines {
prompt.push('\n');
prompt.push_str(line);
}
prompt
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_skills_returns_base_prompt() {
let dir = tempfile::tempdir().unwrap();
let tmp = dir.path();
let prompt = build_serve_system_prompt(&[], tmp, None);
assert!(prompt.contains("task agent"));
assert!(!prompt.contains("Active Skills"));
}
#[test]
fn evaluate_mode_changes_base_prompt() {
let dir = tempfile::tempdir().unwrap();
let tmp = dir.path();
let prompt = build_serve_system_prompt(&[], tmp, Some("evaluate"));
assert!(prompt.contains("evaluation agent"));
assert!(!prompt.contains("task agent"));
}
#[test]
fn review_mode_is_evaluation_variant() {
let dir = tempfile::tempdir().unwrap();
let tmp = dir.path();
let prompt = build_serve_system_prompt(&[], tmp, Some("review"));
assert!(prompt.contains("evaluation agent"));
}
#[test]
fn unknown_skill_is_skipped() {
let dir = tempfile::tempdir().unwrap();
let tmp = dir.path();
let skill_names = vec!["nonexistent-skill".to_string()];
let prompt = build_serve_system_prompt(&skill_names, tmp, None);
assert!(!prompt.contains("Active Skills"));
assert!(prompt.contains("task agent"));
}
}