1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
//! BP-5 (catalog D2 "Output style / personality module": *swappable
//! response-style instruction layer*; cc§7 "Output styles"; cx§2
//! "Personality layer") — the named response-style layer.
//!
//! **This is a prompt-assembly INPUT, not a module.** It has no state, no
//! tools, no capability table and nothing in the loop consults it after
//! construction: `[core.output_style]` names a style, this file resolves the
//! name to text, and `Agent::with_parts` appends one section to the system
//! prompt. That is the whole mechanism, and it is exactly what both
//! harnesses do — Claude Code's output styles "append custom instructions to
//! the end of the system prompt" (`docs:output-styles#how-output-styles-work`)
//! and Codex's personality is one more spliced instruction block
//! (`codex-rs/core/src/context/personality_spec_instructions.rs`).
//!
//! **Two resolution sources, in this order.**
//! 1. A markdown file under the style roots of the harness `[core.skills]
//! harness` names — for Claude Code, `<CLAUDE_CONFIG_DIR>/output-styles/`
//! then `<cwd>/.claude/output-styles/` (`docs:output-styles`). A user's
//! own file WINS over a built-in of the same name, which is the only
//! ordering that lets someone replace a shipped style.
//! 2. A [`BUILTIN_STYLES`] entry.
//!
//! **The built-in texts are supercode's own.** Each is a short transcription
//! of what the named style is DOCUMENTED to do, written here; none is a copy
//! of a harness's shipped prompt. `default` (cc) and `none` (cx) are the
//! neutral selections both harnesses ship with — they resolve, and
//! contribute no text, which is precisely their documented behavior.
//!
//! **`keep-coding-instructions`.** A custom cc style may DROP the built-in
//! software-engineering instructions unless its frontmatter says
//! `keep-coding-instructions: true` (`docs:output-styles`). That is the one
//! place a style does more than append: [`Resolved::replaces_base`] reports
//! it, and prompt assembly then uses the style text as the base prompt
//! instead of appending it.
use std::path::{Path, PathBuf};
use crate::config::Config;
/// The named styles this build ships, `name → instruction text`.
///
/// `default` (Claude Code's own default selection) and `none` (Codex's)
/// carry empty text on purpose: selecting them is a real, resolvable
/// selection that adds nothing to the prompt.
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.",
),
];
/// A resolved output style.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Resolved {
/// The name as the config asked for it.
pub name: String,
/// The instruction text (possibly empty, for a neutral built-in).
pub text: String,
/// The markdown file it came from, when it came from disk.
pub source: Option<PathBuf>,
/// Whether this style REPLACES the base coding instructions rather than
/// appending to them (cc: a custom style without
/// `keep-coding-instructions: true`).
pub replaces_base: bool,
}
impl Resolved {
/// The prompt section this style contributes — empty for a neutral
/// style, so an agent that selects `default`/`none` keeps exactly the
/// prompt it had before this existed.
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())
}
}
/// Resolve `[core.output_style]` for `config`, or `None` when it names
/// nothing (the default) or nothing by that name exists.
///
/// An unknown name resolves to `None` rather than an error: a style is a
/// presentation layer, and a typo in it must never stop a session from
/// starting.
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,
})
}
/// The directories a style file is looked for in, most specific LAST is not
/// the rule here — cc's own precedence is personal before project, the same
/// order its skill roots use, so the first hit wins.
///
/// Only Claude Code publishes a file-based style root; Codex's personality
/// is a config value with no on-disk form (cx§6 `personality`), so a
/// `codex` config resolves built-ins only.
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"),
]
}
/// Read `<root>/<name>.md` as a style, if it is there.
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());
}
}