use anyhow::{Context, Result};
use colored::Colorize;
use std::path::{Path, PathBuf};
const SKILL_MD: &str = include_str!("../../assets/SKILL.md");
const SKILL_FILENAME: &str = "SKILL.md";
const AGENTS_FILENAME: &str = "AGENTS.md";
const CLAUDE_FILENAME: &str = "CLAUDE.md";
pub fn agent_docs_command(dir: Option<PathBuf>, force: bool) -> Result<()> {
let dir = dir.unwrap_or_else(|| PathBuf::from("."));
write_agent_docs(&dir, force, None)
}
pub fn write_agent_docs(dir: &Path, force: bool, project_name: Option<&str>) -> Result<()> {
std::fs::create_dir_all(dir)
.with_context(|| format!("failed to create directory {}", dir.display()))?;
let mut written = Vec::new();
let mut kept = Vec::new();
for (name, content) in agent_docs_files(project_name) {
let path = dir.join(name);
if path.exists() && !force {
kept.push(path);
continue;
}
std::fs::write(&path, content)
.with_context(|| format!("failed to write {}", path.display()))?;
written.push(path);
}
for path in &written {
eprintln!(" {} {}", "✓".green().bold(), path.display());
}
for path in &kept {
eprintln!(
" {} {} (existing kept; --force to refresh)",
"•".dimmed(),
path.display()
);
}
Ok(())
}
fn agent_docs_files(project_name: Option<&str>) -> [(String, String); 3] {
let version = env!("CARGO_PKG_VERSION");
let title = project_name.unwrap_or("this oxo-flow project");
let agents = format!(
r#"# Agent Context — {title}
<!-- Generated by oxo-flow {version}. This file belongs to the project owner:
edit it freely; `oxo-flow agent-docs` never overwrites it without --force.
Re-run `oxo-flow agent-docs --force` to refresh the generated skeleton
after an oxo-flow upgrade. -->
Read [SKILL.md](./SKILL.md) before operating in this project — it is the
canonical oxo-flow operating manual (workflow format, static gates, execution,
monitoring, QC, and delivery), shipped alongside this file by
`oxo-flow agent-docs`.
## Non-negotiable data safety (full contract: SKILL.md → Data Safety)
- Input data is read-only. Never delete, move, or overwrite an input without
explicit human authorization.
- Snapshot before mutating (`cp -a` + checksum); write new files and rename;
never truncate an existing file with `>`.
- All probes and scratch work live under `.oxo-flow/agent/<date>-<task>/`
inside this project; journal every mutating command to
`.oxo-flow/agent/operations.log`.
- Prefer `oxo-flow status`, `oxo-flow report`, `oxo-flow provenance verify`,
and `oxo-flow dry-run` over ad-hoc shell.
## Project notes (edit freely)
<!-- Describe what an agent cannot infer from the files. Examples:
- Raw data: raw/ (vendor FASTQs — read-only)
- References: reference/ (genome build, indexes)
- Cluster profile: see [cluster] in the workflow
- Sample naming: {{sample}}_R1.fastq.gz / {{sample}}_R2.fastq.gz
-->
"#
);
let claude = format!(
r#"# Claude Code
<!-- Generated by oxo-flow {version}; re-run `oxo-flow agent-docs --force`
to refresh. Claude Code reads this file and imports the lines below. -->
@AGENTS.md
"#
);
[
(SKILL_FILENAME.to_string(), SKILL_MD.to_string()),
(AGENTS_FILENAME.to_string(), agents),
(CLAUDE_FILENAME.to_string(), claude),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writes_three_files_with_relative_links() {
let dir = tempfile::tempdir().unwrap();
write_agent_docs(dir.path(), false, Some("demo")).unwrap();
let skill = std::fs::read_to_string(dir.path().join(SKILL_FILENAME)).unwrap();
assert_eq!(
skill, SKILL_MD,
"SKILL.md must be the embedded copy verbatim"
);
assert!(
skill.contains("Data Safety"),
"manual carries the safety contract"
);
let agents = std::fs::read_to_string(dir.path().join(AGENTS_FILENAME)).unwrap();
assert!(
agents.contains("[SKILL.md](./SKILL.md)"),
"relative link to the manual"
);
assert!(agents.contains("demo"), "project name is spliced in");
assert!(agents.contains("read-only"), "safety summary is inline");
assert!(
agents.contains(env!("CARGO_PKG_VERSION")),
"version marker for refreshes"
);
let claude = std::fs::read_to_string(dir.path().join(CLAUDE_FILENAME)).unwrap();
assert!(claude.contains("@AGENTS.md"), "Claude Code import line");
}
#[test]
fn keeps_existing_files_without_force() {
let dir = tempfile::tempdir().unwrap();
write_agent_docs(dir.path(), false, None).unwrap();
std::fs::write(dir.path().join(AGENTS_FILENAME), "my notes\n").unwrap();
write_agent_docs(dir.path(), false, None).unwrap();
let agents = std::fs::read_to_string(dir.path().join(AGENTS_FILENAME)).unwrap();
assert_eq!(agents, "my notes\n", "user edits survive a re-run");
}
#[test]
fn force_refreshes_existing_files() {
let dir = tempfile::tempdir().unwrap();
write_agent_docs(dir.path(), false, None).unwrap();
std::fs::write(dir.path().join(AGENTS_FILENAME), "my notes\n").unwrap();
write_agent_docs(dir.path(), true, None).unwrap();
let agents = std::fs::read_to_string(dir.path().join(AGENTS_FILENAME)).unwrap();
assert_ne!(agents, "my notes\n");
assert!(agents.contains("[SKILL.md](./SKILL.md)"));
}
}