supercode_harness/output_style.rs
1//! BP-5 (catalog D2 "Output style / personality module": *swappable
2//! response-style instruction layer*; cc§7 "Output styles"; cx§2
3//! "Personality layer") — the named response-style layer.
4//!
5//! **This is a prompt-assembly INPUT, not a module.** It has no state, no
6//! tools, no capability table and nothing in the loop consults it after
7//! construction: `[core.output_style]` names a style, this file resolves the
8//! name to text, and `Agent::with_parts` appends one section to the system
9//! prompt. That is the whole mechanism, and it is exactly what both
10//! harnesses do — Claude Code's output styles "append custom instructions to
11//! the end of the system prompt" (`docs:output-styles#how-output-styles-work`)
12//! and Codex's personality is one more spliced instruction block
13//! (`codex-rs/core/src/context/personality_spec_instructions.rs`).
14//!
15//! **Two resolution sources, in this order.**
16//! 1. A markdown file under the style roots of the harness `[core.skills]
17//! harness` names — for Claude Code, `<CLAUDE_CONFIG_DIR>/output-styles/`
18//! then `<cwd>/.claude/output-styles/` (`docs:output-styles`). A user's
19//! own file WINS over a built-in of the same name, which is the only
20//! ordering that lets someone replace a shipped style.
21//! 2. A [`BUILTIN_STYLES`] entry.
22//!
23//! **The built-in texts are supercode's own.** Each is a short transcription
24//! of what the named style is DOCUMENTED to do, written here; none is a copy
25//! of a harness's shipped prompt. `default` (cc) and `none` (cx) are the
26//! neutral selections both harnesses ship with — they resolve, and
27//! contribute no text, which is precisely their documented behavior.
28//!
29//! **`keep-coding-instructions`.** A custom cc style may DROP the built-in
30//! software-engineering instructions unless its frontmatter says
31//! `keep-coding-instructions: true` (`docs:output-styles`). That is the one
32//! place a style does more than append: [`Resolved::replaces_base`] reports
33//! it, and prompt assembly then uses the style text as the base prompt
34//! instead of appending it.
35
36use std::path::{Path, PathBuf};
37
38use crate::config::Config;
39
40/// The named styles this build ships, `name → instruction text`.
41///
42/// `default` (Claude Code's own default selection) and `none` (Codex's)
43/// carry empty text on purpose: selecting them is a real, resolvable
44/// selection that adds nothing to the prompt.
45pub 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/// A resolved output style.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Resolved {
85 /// The name as the config asked for it.
86 pub name: String,
87 /// The instruction text (possibly empty, for a neutral built-in).
88 pub text: String,
89 /// The markdown file it came from, when it came from disk.
90 pub source: Option<PathBuf>,
91 /// Whether this style REPLACES the base coding instructions rather than
92 /// appending to them (cc: a custom style without
93 /// `keep-coding-instructions: true`).
94 pub replaces_base: bool,
95}
96
97impl Resolved {
98 /// The prompt section this style contributes — empty for a neutral
99 /// style, so an agent that selects `default`/`none` keeps exactly the
100 /// prompt it had before this existed.
101 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
109/// Resolve `[core.output_style]` for `config`, or `None` when it names
110/// nothing (the default) or nothing by that name exists.
111///
112/// An unknown name resolves to `None` rather than an error: a style is a
113/// presentation layer, and a typo in it must never stop a session from
114/// starting.
115pub 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
137/// The directories a style file is looked for in, most specific LAST is not
138/// the rule here — cc's own precedence is personal before project, the same
139/// order its skill roots use, so the first hit wins.
140///
141/// Only Claude Code publishes a file-based style root; Codex's personality
142/// is a config value with no on-disk form (cx§6 `personality`), so a
143/// `codex` config resolves built-ins only.
144fn 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
155/// Read `<root>/<name>.md` as a style, if it is there.
156fn 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}