Guidance for AI coding agents operating in this repository.
agentfiles is a Rust CLI tool that installs agent files (skills, agents, commands) across multiple agentic coding providers (Claude Code, OpenCode, Codex, Cursor) from a unified agentfiles.json manifest. Rust 2024 edition, purely synchronous, no unsafe code.
cargo build # Build the project
cargo run # Build and run the binary
cargo test # Run ALL tests
cargo test <name> # Run a single test by substring match
cargo test <name> -- --exact # Run a single test by exact name
cargo clippy -- -D warnings # Lint (CI treats warnings as errors)
cargo fmt # Format code
cargo fmt -- --check # Check formatting without modifyingCI runs on every PR: fmt --check, clippy -D warnings, test, build across Linux/macOS/Windows. All four must pass.
src/
lib.rs -- pub mod re-exports only
types.rs -- Core enums (FileScope, FileKind, FileStrategy, AgentProvider)
provider.rs -- Provider directory layout resolution, compatibility matrix
manifest.rs -- Manifest/FileMapping structs, JSON load/save (serde)
scanner.rs -- Auto-discovery of agent files from directory structures
installer.rs -- File installation (copy/symlink) to provider directories
git.rs -- Remote git: URL detection, @ref parsing, clone/cache
cli.rs -- CLI argument parsing (clap derive)
main.rs -- Binary entry point, command handlers (cmd_install, cmd_init, etc.)
Dependency flow: types <- provider, manifest <- scanner, installer. git and cli are standalone. main wires everything together via the lib crate.
Default rustfmt settings (no .rustfmt.toml). No configuration overrides. Just run cargo fmt.
Three groups separated by blank lines:
// 1. Standard library
use std::fs;
use std::path::{Path, PathBuf};
// 2. External crates
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
// 3. Crate-internal modules
use crate::manifest::FileMapping;
use crate::types::{AgentProvider, FileKind};Exception: types.rs has no crate-internal imports so groups 1 and 2 may appear together.
- Functions:
snake_case. Public:scan_agent_files,load_manifest. Private helpers:scan_kind_dir,normalize_url. Command handlers in main:cmd_install,cmd_init. - Types/Structs/Enums:
PascalCase. Examples:FileMapping,InstallResult,AgentProvider. - Enum variants:
PascalCase. Examples:FileScope::Project,FileKind::Skill. - Constants:
UPPER_SNAKE_CASE. Example:const KIND_DIRS: &[(&str, FileKind)]. - Modules:
snake_case, flat structure (all insrc/), no nested module directories.
Uses anyhow exclusively. No custom error types. All Result types are anyhow::Result<T>. Three patterns:
// 1. Static context
let content = std::fs::read_to_string(path).context("failed to read manifest")?;
// 2. Dynamic context with formatting
source.canonicalize()
.with_context(|| format!("failed to resolve: {}", source.display()))?;
// 3. Early-return errors
anyhow::bail!("source file not found: {}", path.display());Error message style: lowercase, no trailing punctuation, descriptive. All FromStr impls use type Err = anyhow::Error. Never use unwrap() in production code -- only in tests.
- Derive set for domain types:
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] - Serde:
#[serde(rename_all = "lowercase")]on enums,skip_serializing_iffor optional/default fields - Builder-like pattern on
Manifest:Manifest::default().with_name(n).with_files(f) - No trait abstractions -- concrete types throughout. Standard traits (
Display,FromStr,Default) implemented as needed.
Gate with #[cfg(unix)] / #[cfg(windows)]. Used in installer.rs for symlink creation and in tests.
Uses println!() directly for user-facing output. No logging framework, no structured logging, no tracing.
Let-chains are used (e.g., in scanner.rs):
if entry_path.is_file()
&& let Some(ext) = entry_path.extension()
&& ext == "md"
{Inline #[cfg(test)] mod tests blocks within each module. No separate tests/ directory, no test fixture files.
Two patterns:
// Flat (types.rs, provider.rs, scanner.rs):
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn descriptive_name() -> Result<()> { ... }
}
// Nested sub-modules for grouping (manifest.rs, git.rs, installer.rs):
#[cfg(test)]
mod tests {
mod load_manifest {
use super::super::*;
#[test]
fn from_directory() -> Result<()> { ... }
}
}- Return type: Most tests return
Result<()>and use?for propagation. Tests checking error cases useassert!(result.is_err()). - Filesystem: Use
tempfile::TempDirfor all filesystem tests. Create test data inline -- no fixture files. - Platform gates: Symlink tests use
#[cfg(unix)] #[test]. - Naming: Descriptive
snake_case. No mandatorytest_prefix. Examples:save_and_roundtrip,install_command_skips_codex,shorthand_gets_https. - Helpers: Define local helper functions within test modules (e.g.,
setup_skill,make_manifest).
cargo test save_and_roundtrip # Match by substring
cargo test tests::load_manifest::from_dir # Match nested module path
cargo test -p agentfiles save_and_roundtrip -- --exact # Exact match- Flat module structure -- all source in
src/*.rs, no nested directories. - Minimal dependencies -- 5 runtime deps, 1 dev dep. Shell out to
gitrather than adding a git library. - Single source of truth --
ProviderLayoutinprovider.rsdefines all provider config. Adding a provider means adding one layout entry. - Compatibility matrix drives behavior -- one manifest file, multiple providers handled automatically.
- No async -- entirely synchronous. No tokio/async-std.
- Section separators in larger files use comment banners:
// --------------------------------------------------------------------------- // Internal helpers // ---------------------------------------------------------------------------