supercode-harness 0.4.18

The optional native Supercode agent and tool harness
Documentation
//! 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());
    }
}