supercode_harness/
output_style.rs1use std::path::{Path, PathBuf};
37
38use crate::config::Config;
39
40pub const BUILTIN_STYLES: &[(&str, &str)] = &[
46 ("default", ""),
47 ("none", ""),
48 (
49 "proactive",
50 "Work ahead of the request: when a task obviously implies adjacent work \
51 (a test for the code you just wrote, the caller you just broke), do it in \
52 the same turn and say you did. Never stop to ask whether to continue.",
53 ),
54 (
55 "explanatory",
56 "Explain as you go. Before a non-obvious change, state in one or two \
57 sentences what you are about to do and why that approach over the \
58 alternative. After it, name what the reader should look at to check it. \
59 Keep the explanations inline with the work, never as a separate essay.",
60 ),
61 (
62 "learning",
63 "Teach while you work. Prefer showing the reasoning over showing only the \
64 result: name the concept behind each decision, and leave the smallest \
65 well-marked piece of the work for the user to finish themselves, marked \
66 `TODO(human)` with the context they need to do it.",
67 ),
68 (
69 "friendly",
70 "Write like a warm, plain-spoken colleague: short sentences, no jargon \
71 where a common word will do, and a sentence of context before anything \
72 surprising. Never perform enthusiasm you do not have.",
73 ),
74 (
75 "pragmatic",
76 "Answer directly and stop. Lead with the result, then only the detail \
77 needed to act on it. No preamble, no summary of what you are about to \
78 say, no restating the request.",
79 ),
80];
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Resolved {
85 pub name: String,
87 pub text: String,
89 pub source: Option<PathBuf>,
91 pub replaces_base: bool,
95}
96
97impl Resolved {
98 pub fn section(&self) -> String {
102 if self.text.trim().is_empty() {
103 return String::new();
104 }
105 format!("\n\n# Output style: {}\n{}", self.name, self.text.trim())
106 }
107}
108
109pub fn resolve(config: &Config) -> Option<Resolved> {
116 let name = config.output_style.trim();
117 if name.is_empty() {
118 return None;
119 }
120 if let Some(found) = style_roots(config)
121 .into_iter()
122 .find_map(|root| read_style_file(&root, name))
123 {
124 return Some(found);
125 }
126 BUILTIN_STYLES
127 .iter()
128 .find(|(builtin, _)| builtin.eq_ignore_ascii_case(name))
129 .map(|(builtin, text)| Resolved {
130 name: (*builtin).to_string(),
131 text: (*text).to_string(),
132 source: None,
133 replaces_base: false,
134 })
135}
136
137fn style_roots(config: &Config) -> Vec<PathBuf> {
145 if config.skills_harness.as_deref() != Some(crate::HarnessId::CLAUDE_CODE) {
146 return Vec::new();
147 }
148 let homes = crate::skills::SkillHomes::default();
149 vec![
150 homes.claude_code.join("output-styles"),
151 config.cwd.join(".claude").join("output-styles"),
152 ]
153}
154
155fn read_style_file(root: &Path, name: &str) -> Option<Resolved> {
157 let path = root.join(format!("{name}.md"));
158 let text = std::fs::read_to_string(&path).ok()?;
159 let front = crate::skills::read_frontmatter(&path);
160 let keep = front
161 .get("keep-coding-instructions")
162 .map(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "true" | "yes" | "1"))
163 .unwrap_or(false);
164 Some(Resolved {
165 name: front
166 .get("name")
167 .cloned()
168 .unwrap_or_else(|| name.to_string()),
169 text: crate::skills::strip_frontmatter(&text),
170 source: Some(path),
171 replaces_base: !keep,
172 })
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn nothing_selected_resolves_to_nothing() {
181 let config = Config::builder().build();
182 assert!(resolve(&config).is_none());
183 }
184
185 #[test]
186 fn a_neutral_builtin_resolves_and_contributes_no_text() {
187 let config = Config::builder().output_style("default").build();
188 let style = resolve(&config).expect("`default` is a built-in style");
189 assert!(style.section().is_empty());
190 assert!(!style.replaces_base);
191 }
192
193 #[test]
194 fn a_named_builtin_contributes_a_section() {
195 let config = Config::builder().output_style("explanatory").build();
196 let style = resolve(&config).expect("`explanatory` is a built-in style");
197 assert!(style
198 .section()
199 .starts_with("\n\n# Output style: explanatory\n"));
200 assert!(style.section().contains("Explain as you go"));
201 }
202
203 #[test]
204 fn an_unknown_name_never_fails_a_session() {
205 let config = Config::builder().output_style("no-such-style").build();
206 assert!(resolve(&config).is_none());
207 }
208}