use std::path::{Path, PathBuf};
pub struct PromptContext<'a> {
pub cwd: &'a Path,
pub os: &'a str,
pub tool_names: &'a [String],
pub project_context: Option<String>,
pub custom_prompt: Option<String>,
pub append_prompt: Option<String>,
pub capabilities: Option<String>,
pub cognitive_core: bool,
}
const FULL_PREAMBLE: &str = "\
You are Cordy, an expert autonomous coding agent operating in the user's terminal. You help by \
reading files, running commands, editing code, and writing new files — working directly in the \
project with the tools provided.
## How you work
- Understand before you act: read the relevant files and search the codebase before proposing or \
making changes.
- Edit precisely: use `edit` with an exact, unique `old` string; keep diffs minimal and focused; \
never reformat or touch unrelated code.
- Match the surroundings: follow the project's existing style, naming, and conventions.
- Verify your work: after changes, build/test/run what's relevant and report the actual result — \
if something fails, say so with the output.
- Prefer tools over recall: read the code, grep, run commands, and search the web for current \
facts instead of relying on memory.
- Work one concrete step at a time; don't guess when you can check.
## Style
- Be concise and factual. Lead with the answer or the action; skip preamble and filler.
- Show file paths clearly (e.g. `src/main.rs:42`) so they are easy to follow.
- When you change files, state what changed and where.
- Ask only when you are genuinely blocked on a decision that is the user's to make.
## Safety
- Destructive or irreversible actions (deleting files, force operations, mass edits) need care — \
confirm intent unless clearly authorized.
- Respect the permission system: some tools are gated and will prompt the user before running.";
const LEAN_PREAMBLE: &str = "\
You are Cordy, a compact coding agent that drives tools. You hold little built-in knowledge — \
rely on tools: read files, grep, run commands, and search the web instead of recalling. Take one \
concrete step at a time and keep output terse. Use `edit` with an exact, unique `old` string; \
read before you edit; verify every change.";
pub fn build_system_prompt(ctx: &PromptContext) -> String {
let mut s = String::new();
match &ctx.custom_prompt {
Some(custom) if !custom.trim().is_empty() => s.push_str(custom.trim()),
_ => s.push_str(if ctx.cognitive_core {
LEAN_PREAMBLE
} else {
FULL_PREAMBLE
}),
}
if !ctx.tool_names.is_empty() {
s.push_str("\n\n## Tools\n");
s.push_str(&ctx.tool_names.join(", "));
s.push_str(
"\nOther tools may be available depending on the project (skills, MCP servers).",
);
}
s.push_str("\n\n## Environment\n");
s.push_str(&format!("cwd: {}\nos: {}\n", ctx.cwd.display(), ctx.os));
if let Some(pc) = &ctx.project_context
&& !pc.trim().is_empty()
{
s.push_str("\n<project_context>\n");
s.push_str(pc.trim());
s.push_str("\n</project_context>\n");
}
if let Some(cap) = &ctx.capabilities
&& !cap.trim().is_empty()
{
s.push('\n');
s.push_str(cap.trim());
s.push('\n');
}
if let Some(app) = &ctx.append_prompt
&& !app.trim().is_empty()
{
s.push_str("\n\n");
s.push_str(app.trim());
s.push('\n');
}
s
}
const CONTEXT_FILES: [&str; 3] = ["AGENTS.md", "CLAUDE.md", ".cordy/CORDY.md"];
pub fn load_project_context(cwd: &Path) -> Option<String> {
let mut chain: Vec<PathBuf> = Vec::new();
let mut cur = Some(cwd.to_path_buf());
while let Some(dir) = cur {
chain.push(dir.clone());
cur = dir.parent().map(Path::to_path_buf);
}
chain.reverse();
let mut parts: Vec<String> = Vec::new();
for dir in chain {
for name in CONTEXT_FILES {
let path = dir.join(name);
if let Ok(text) = std::fs::read_to_string(&path) {
let trimmed = text.trim();
if !trimmed.is_empty() {
parts.push(format!(
"<file path=\"{}\">\n{}\n</file>",
path.display(),
trimmed
));
}
}
}
}
if parts.is_empty() {
None
} else {
Some(parts.join("\n"))
}
}
pub fn load_system_override(cwd: &Path, user_dir: Option<&Path>) -> Option<String> {
read_nonempty(&cwd.join(".cordy").join("SYSTEM.md"))
.or_else(|| user_dir.and_then(|d| read_nonempty(&d.join("SYSTEM.md"))))
}
pub fn load_system_append(cwd: &Path, user_dir: Option<&Path>) -> Option<String> {
let mut parts: Vec<String> = Vec::new();
if let Some(d) = user_dir
&& let Some(t) = read_nonempty(&d.join("APPEND_SYSTEM.md"))
{
parts.push(t);
}
if let Some(t) = read_nonempty(&cwd.join(".cordy").join("APPEND_SYSTEM.md")) {
parts.push(t);
}
if parts.is_empty() {
None
} else {
Some(parts.join("\n\n"))
}
}
fn read_nonempty(path: &Path) -> Option<String> {
let text = std::fs::read_to_string(path).ok()?;
let trimmed = text.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx<'a>(cwd: &'a Path, tools: &'a [String]) -> PromptContext<'a> {
PromptContext {
cwd,
os: "linux",
tool_names: tools,
project_context: None,
custom_prompt: None,
append_prompt: None,
capabilities: None,
cognitive_core: false,
}
}
#[test]
fn full_prompt_lists_tools_and_env() {
let tools = vec!["read".to_string(), "edit".to_string()];
let p = build_system_prompt(&ctx(Path::new("/proj"), &tools));
assert!(p.contains("autonomous coding agent"));
assert!(p.contains("read, edit"));
assert!(p.contains("os: linux"));
}
#[test]
fn lean_prompt_for_cognitive_core() {
let tools = vec!["read".to_string()];
let mut c = ctx(Path::new("/p"), &tools);
c.cognitive_core = true;
let p = build_system_prompt(&c);
assert!(p.contains("compact coding agent"));
}
#[test]
fn custom_prompt_overrides_default() {
let tools = vec!["read".to_string()];
let mut c = ctx(Path::new("/p"), &tools);
c.custom_prompt = Some("BESPOKE INSTRUCTIONS".into());
let p = build_system_prompt(&c);
assert!(p.contains("BESPOKE INSTRUCTIONS"));
assert!(!p.contains("autonomous coding agent")); assert!(p.contains("read")); }
#[test]
fn context_and_append_are_placed() {
let tools = vec!["read".to_string()];
let mut c = ctx(Path::new("/p"), &tools);
c.project_context = Some("<file path=\"AGENTS.md\">tabs</file>".into());
c.append_prompt = Some("EXTRA".into());
let p = build_system_prompt(&c);
assert!(p.contains("<project_context>"));
assert!(p.contains("tabs"));
assert!(p.find("EXTRA").unwrap() > p.find("<project_context>").unwrap());
}
#[test]
fn context_files_tagged_with_paths() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("AGENTS.md"), "Follow the style guide.").unwrap();
let pc = load_project_context(dir.path()).unwrap();
assert!(pc.contains("Follow the style guide."));
assert!(pc.contains("<file path="));
assert!(pc.contains("AGENTS.md"));
}
#[test]
fn system_override_prefers_project_then_global() {
let proj = tempfile::tempdir().unwrap();
let user = tempfile::tempdir().unwrap();
std::fs::create_dir_all(proj.path().join(".cordy")).unwrap();
std::fs::write(user.path().join("SYSTEM.md"), "global").unwrap();
assert_eq!(
load_system_override(proj.path(), Some(user.path())).as_deref(),
Some("global")
);
std::fs::write(proj.path().join(".cordy").join("SYSTEM.md"), "project").unwrap();
assert_eq!(
load_system_override(proj.path(), Some(user.path())).as_deref(),
Some("project")
);
}
#[test]
fn append_concatenates_global_then_project() {
let proj = tempfile::tempdir().unwrap();
let user = tempfile::tempdir().unwrap();
std::fs::create_dir_all(proj.path().join(".cordy")).unwrap();
std::fs::write(user.path().join("APPEND_SYSTEM.md"), "G").unwrap();
std::fs::write(proj.path().join(".cordy").join("APPEND_SYSTEM.md"), "P").unwrap();
let a = load_system_append(proj.path(), Some(user.path())).unwrap();
assert!(a.find("G").unwrap() < a.find("P").unwrap());
}
}