use std::path::{Path, PathBuf};
use crate::config::Config;
pub const BUILTIN_STYLES: &[(&str, &str)] = &[
("default", ""),
("none", ""),
(
"proactive",
"Work ahead of the request: when a task obviously implies adjacent work \
(a test for the code you just wrote, the caller you just broke), do it in \
the same turn and say you did. Never stop to ask whether to continue.",
),
(
"explanatory",
"Explain as you go. Before a non-obvious change, state in one or two \
sentences what you are about to do and why that approach over the \
alternative. After it, name what the reader should look at to check it. \
Keep the explanations inline with the work, never as a separate essay.",
),
(
"learning",
"Teach while you work. Prefer showing the reasoning over showing only the \
result: name the concept behind each decision, and leave the smallest \
well-marked piece of the work for the user to finish themselves, marked \
`TODO(human)` with the context they need to do it.",
),
(
"friendly",
"Write like a warm, plain-spoken colleague: short sentences, no jargon \
where a common word will do, and a sentence of context before anything \
surprising. Never perform enthusiasm you do not have.",
),
(
"pragmatic",
"Answer directly and stop. Lead with the result, then only the detail \
needed to act on it. No preamble, no summary of what you are about to \
say, no restating the request.",
),
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Resolved {
pub name: String,
pub text: String,
pub source: Option<PathBuf>,
pub replaces_base: bool,
}
impl Resolved {
pub fn section(&self) -> String {
if self.text.trim().is_empty() {
return String::new();
}
format!("\n\n# Output style: {}\n{}", self.name, self.text.trim())
}
}
pub fn resolve(config: &Config) -> Option<Resolved> {
let name = config.output_style.trim();
if name.is_empty() {
return None;
}
if let Some(found) = style_roots(config)
.into_iter()
.find_map(|root| read_style_file(&root, name))
{
return Some(found);
}
BUILTIN_STYLES
.iter()
.find(|(builtin, _)| builtin.eq_ignore_ascii_case(name))
.map(|(builtin, text)| Resolved {
name: (*builtin).to_string(),
text: (*text).to_string(),
source: None,
replaces_base: false,
})
}
fn style_roots(config: &Config) -> Vec<PathBuf> {
if config.skills_harness.as_deref() != Some(crate::HarnessId::CLAUDE_CODE) {
return Vec::new();
}
let homes = crate::skills::SkillHomes::default();
vec![
homes.claude_code.join("output-styles"),
config.cwd.join(".claude").join("output-styles"),
]
}
fn read_style_file(root: &Path, name: &str) -> Option<Resolved> {
let path = root.join(format!("{name}.md"));
let text = std::fs::read_to_string(&path).ok()?;
let front = crate::skills::read_frontmatter(&path);
let keep = front
.get("keep-coding-instructions")
.map(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "true" | "yes" | "1"))
.unwrap_or(false);
Some(Resolved {
name: front
.get("name")
.cloned()
.unwrap_or_else(|| name.to_string()),
text: crate::skills::strip_frontmatter(&text),
source: Some(path),
replaces_base: !keep,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nothing_selected_resolves_to_nothing() {
let config = Config::builder().build();
assert!(resolve(&config).is_none());
}
#[test]
fn a_neutral_builtin_resolves_and_contributes_no_text() {
let config = Config::builder().output_style("default").build();
let style = resolve(&config).expect("`default` is a built-in style");
assert!(style.section().is_empty());
assert!(!style.replaces_base);
}
#[test]
fn a_named_builtin_contributes_a_section() {
let config = Config::builder().output_style("explanatory").build();
let style = resolve(&config).expect("`explanatory` is a built-in style");
assert!(style
.section()
.starts_with("\n\n# Output style: explanatory\n"));
assert!(style.section().contains("Explain as you go"));
}
#[test]
fn an_unknown_name_never_fails_a_session() {
let config = Config::builder().output_style("no-such-style").build();
assert!(resolve(&config).is_none());
}
}