pub mod append;
#[cfg(feature = "ast")]
pub mod ast;
pub mod batch;
pub mod create;
pub mod delete;
pub mod doc;
pub mod explain;
pub mod init;
#[cfg(feature = "mcp")]
pub mod mcp;
pub mod md;
pub mod output;
pub mod patch;
pub mod read;
pub mod rename;
pub mod replace;
pub mod schema;
pub mod search;
pub mod status;
pub mod tidy;
pub mod tx;
pub mod undo;
pub mod write_dispatch;
use crate::cli::Cli;
use clap::{Args, Subcommand, ValueEnum};
#[derive(Debug, Subcommand)]
pub enum Command {
#[command(display_order = 10)]
Append(append::AppendArgs),
#[command(display_order = 11)]
Create(create::CreateArgs),
#[command(display_order = 12)]
Delete(delete::DeleteArgs),
#[command(display_order = 13)]
Read(read::ReadArgs),
#[command(display_order = 14)]
Rename(rename::RenameArgs),
#[command(display_order = 20)]
Search(search::SearchArgs),
#[command(display_order = 21)]
Replace(replace::ReplaceArgs),
#[command(display_order = 22)]
Patch(patch::PatchArgs),
#[command(display_order = 23)]
Tidy(tidy::TidyArgs),
#[command(display_order = 24)]
Batch(batch::BatchArgs),
#[command(display_order = 30)]
Doc(doc::DocArgs),
#[command(display_order = 31)]
Md(md::MdArgs),
#[cfg(feature = "ast")]
#[command(display_order = 40)]
Ast(ast::AstArgs),
#[command(display_order = 50)]
Tx(crate::tx::TxArgs),
#[command(display_order = 51)]
Explain(explain::ExplainArgs),
#[command(display_order = 52)]
Undo(undo::UndoArgs),
#[command(display_order = 53)]
Status(status::StatusArgs),
#[command(display_order = 54)]
Schema(schema::SchemaArgs),
#[command(display_order = 55)]
AgentRules(AgentRulesArgs),
#[command(display_order = 56)]
Init(init::InitArgs),
#[cfg(feature = "mcp")]
#[command(display_order = 57)]
McpServer {
#[arg(long)]
log: Option<String>,
#[cfg(feature = "mcp-http")]
#[arg(long)]
http: bool,
#[cfg(feature = "mcp-http")]
#[arg(long, default_value = "127.0.0.1", requires = "http")]
host: String,
#[cfg(feature = "mcp-http")]
#[arg(long, default_value_t = 8080, requires = "http")]
port: u16,
#[cfg(feature = "mcp-http")]
#[arg(long, requires_all = ["http", "tls_key"])]
tls_cert: Option<std::path::PathBuf>,
#[cfg(feature = "mcp-http")]
#[arg(long, requires_all = ["http", "tls_cert"])]
tls_key: Option<std::path::PathBuf>,
},
#[command(display_order = 58)]
Completions {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum AgentMode {
All,
Cli,
Mcp,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum AgentPlatform {
All,
Linux,
Windows,
}
#[derive(Debug, Args)]
#[command(after_help = "\
EXAMPLES:
patchloom agent-rules >> AGENTS.md
patchloom agent-rules --mode mcp
patchloom agent-rules --platform linux")]
pub struct AgentRulesArgs {
#[arg(long, value_enum, default_value = "all")]
pub mode: AgentMode,
#[arg(long, value_enum, default_value = "all")]
pub platform: AgentPlatform,
}
pub(crate) const AGENT_RULES_GENERATED_MARKER: &str = "<!-- Generated by patchloom v";
fn generate_agent_rules(args: &AgentRulesArgs) -> String {
let version = env!("CARGO_PKG_VERSION");
let show_cli = matches!(args.mode, AgentMode::All | AgentMode::Cli);
let show_mcp = matches!(args.mode, AgentMode::All | AgentMode::Mcp);
let show_linux = matches!(args.platform, AgentPlatform::All | AgentPlatform::Linux);
let show_windows = matches!(args.platform, AgentPlatform::All | AgentPlatform::Windows);
let mut out = String::new();
out.push_str(&format!(
"{AGENT_RULES_GENERATED_MARKER}{version} — https://github.com/patchloom/patchloom -->\n\
# Patchloom\n\n"
));
if show_mcp && !show_cli {
out.push_str(
"You have MCP tools for file reads, edits, and searches. \
Use them for ALL file operations.\n\n",
);
}
if show_mcp && show_cli {
out.push_str(
"**Decision rule: always use patchloom MCP tools instead of your native agent \
tools for file edits.** Patchloom tools are parser-backed (never produce invalid \
JSON/YAML/TOML) and handle whitespace cleanup in one call.\n\n",
);
}
if show_cli {
out.push_str(
"**Decision rule: if you are about to make 3+ tool calls for file edits, use \
`patchloom batch` instead.** One call replaces N round-trips.\n\n",
);
}
if show_mcp {
out.push_str(
"## Tool selection guide\n\n\
| Task pattern | Tool to use |\n\
|---|---|\n\
| Set/get a key in JSON, YAML, or TOML | `doc_set`, `doc_get`, `doc_query` |\n\
| Edit markdown section, bullet, or table | `md_replace_section`, `md_upsert_bullet`, `md_table_append` |\n\
| Insert text after/before a heading | `md_insert_after_heading`, `md_insert_before_heading` |\n\
| Move a heading section (same file or cross-file) | `md_move_section` |\n\
| Remove duplicate headings | `md_dedupe_headings` |\n\
| Fix trailing whitespace or missing newlines | `fix_whitespace` (one file) or `batch_tidy` (multiple files) |\n\
| Create, append, prepend, rename, or delete a file | `create_file`, `append_file`, `prepend_file`, `move_file`, `delete_file` |\n\
| Find/replace text in a file | `replace_text` (one file) or `batch_replace` (same replacement across multiple files) |\n\
| Search across files | `search_files` |\n\
| List/read/rename symbols (AST-aware) | `ast_list`, `ast_read`, `ast_rename`, `ast_replace` |\n\
| Insert, wrap, or manage imports | `ast_insert`, `ast_wrap`, `ast_imports` |\n\
| Reorder, group, or move symbols | `ast_reorder`, `ast_group`, `ast_move` |\n\
| Extract or split files by symbol | `ast_extract`, `ast_split` |\n\
| Validate syntax, find refs, or analyze impact | `ast_validate`, `ast_refs`, `ast_impact`, `ast_search` |\n\
| Repo map, imports, or structural diff | `ast_map`, `ast_deps`, `ast_diff` |\n\
| Apply same operation to many files | `execute_plan` with `for_each` glob |\n\n",
);
}
if show_cli {
out.push_str(
"Use patchloom when:\n\
- Editing JSON, YAML, or TOML (parser-backed, preserves comments, output is always valid)\n\
- Editing markdown sections, bullets, or tables by heading\n\
- Batching edits across multiple files in one call\n\
- You need atomic rollback if any edit fails\n\n",
);
if !show_mcp {
out.push_str(
"For single-file read, search, create, delete, or rename, your native agent tools are faster.\n\n",
);
}
}
if show_mcp {
out.push_str(
"## MCP mode\n\n\
**ALWAYS use MCP tools for ALL file edits.**\n\n\
- For any multi-op or multi-file change, **use `execute_plan`** with an inline plan (or plan_path). \
It provides atomic execution + rollback, exactly like the CLI `tx` command.\n\
- Use `batch_replace` or `batch_tidy` only when applying the *exact same* operation to multiple files.\n\
- Do **not** issue parallel write calls against the same path(s) — per-call success does not guarantee a coherent combined result.\n\n",
);
}
if show_cli {
out.push_str("## Batching (the main speed win)\n\n\
Six file edits via native tools = six round-trips. One `batch` call = one round-trip:\n\n");
if show_linux {
out.push_str(
"```bash\n\
patchloom batch --apply <<'EOF'\n\
doc.set config.json version \"2.0.0\"\n\
doc.set config.yaml app.version \"2.0.0\"\n\
replace README.md \"1.0.0\" \"2.0.0\"\n\
file.create hello.txt \"Hello, World!\"\n\
file.rename old.txt new.txt\n\
md.upsert_bullet CHANGELOG.md \"## Changes\" \"- Bumped to 2.0.0\"\n\
EOF\n\
```\n\n\
One line per operation. Double-quote values with spaces.\n\n",
);
}
if show_windows {
if show_linux {
out.push_str(
"On Windows (where heredocs are not available), write operations to a file and pass it:\n\n",
);
}
out.push_str(
"```bash\n\
patchloom batch ops.txt --apply\n\
```\n\n",
);
if !show_linux {
out.push_str(
"One line per operation in the file. Double-quote values with spaces.\n\n",
);
}
}
out.push_str(
"**Note:** Values are parsed as JSON first. An unquoted `1.0` is parsed as a number. \
To force a string, wrap in JSON quotes. Unix-like shells (bash/zsh): \
`doc.set config.json version '\"1.0\"'`. Windows (`cmd.exe`/PowerShell): \
`doc.set config.json version \"\\\"1.0\\\"\"`.\n\n",
);
out.push_str(
"For complex plans needing format/validate lifecycle, regex replace, or `--nth`, use `tx` with JSON:\n\n\
```bash\n\
patchloom tx plan.json --apply\n\
```\n\n",
);
out.push_str("## Structured edits\n\n");
if show_linux {
out.push_str(
"```bash\n\
# Edit a value in JSON/YAML/TOML by key (parser-backed, preserves comments)\n\
patchloom doc set config.json version '\"2.0.0\"' --apply\n\
patchloom doc merge config.yaml --value '{\"db\":{\"pool\":10}}' --apply\n\
\n\
# Append a row to a markdown table\n\
patchloom md table-append README.md --heading \"## API\" --row \"| new | row |\" --apply\n\
```\n\n",
);
}
if show_windows {
if show_linux {
out.push_str("On Windows, use double-quote escaping:\n\n");
}
out.push_str(
"```bash\n\
patchloom doc set config.json version \"\\\"2.0.0\\\"\" --apply\n\
patchloom doc merge config.yaml --value \"{\\\"db\\\":{\\\"pool\\\":10}}\" --apply\n\
patchloom md table-append README.md --heading \"## API\" --row \"| new | row |\" --apply\n\
```\n\n",
);
}
out.push_str(
"Add `--apply` to all write commands. Without it, patchloom previews changes without writing.\n\n",
);
out.push_str(
"## Project configuration\n\n\
Create `.patchloom.toml` in the project root to set defaults for all commands:\n\n\
```toml\n\
[write_policy]\n\
ensure_final_newline = true\n\
normalize_eol = \"lf\"\n\
trim_trailing_whitespace = true\n\
collapse_blanks = true\n\
\n\
[tx]\n\
strict = false\n\
\n\
[exclude]\n\
globs = [\"target/**\", \"node_modules/**\"]\n\
```\n\n\
CLI flags override config values. The file is searched upward from the working directory.\n\n",
);
}
if show_cli {
out.push_str("## Workflow examples\n\n");
out.push_str(
"### Rename a function across a codebase\n\n\
```bash\n\
# Find all occurrences first\n\
patchloom search --count \"old_function_name\" src/\n\n\
# Replace in all matching files\n\
patchloom replace \"old_function_name\" --new \"new_function_name\" src/ --apply\n\
```\n\n",
);
out.push_str(
"### Delete lines matching a pattern\n\n\
```bash\n\
# Delete entire lines containing a pattern; collapse consecutive blanks\n\
patchloom replace 'dbg!' --whole-line --new '' src/ --collapse-blanks --apply\n\n\
# Restrict to a line range (e.g. implementation only, skip tests)\n\
patchloom replace 'TODO' --whole-line --range 10:200 --new '' notes.md --apply\n\
```\n\n",
);
out.push_str(
"### Edit a CI workflow\n\n\
```bash\n\
# Set a value in a YAML workflow by key (preserves comments and formatting)\n\
patchloom doc set .github/workflows/ci.yml jobs.test.timeout-minutes 30 --apply\n\
```\n\n",
);
out.push_str(
"### Stale patch recovery via three-way merge\n\n\
```bash\n\
# Apply a patch using three-way merge when context has drifted\n\
patchloom patch apply changes.patch --on-stale merge --apply\n\
\n\
# Or use the merge subcommand directly\n\
patchloom patch merge changes.patch --apply\n\
\n\
# If merge produces conflicts, allow them to be written as markers\n\
# WARNING: never commit files containing conflict markers\n\
patchloom patch merge changes.patch --apply --allow-conflicts\n\
```\n\n",
);
if show_linux {
out.push_str(
"```bash\n\
# Same via transaction plan (JSON):\n\
patchloom tx - --apply <<'EOF'\n\
{\"version\": \"1\", \"operations\": [\n\
{\"op\": \"patch.apply\", \"diff\": \"...\", \"on_stale\": \"merge\", \"allow_conflicts\": true}\n\
]}\n\
EOF\n\
```\n\n",
);
}
if show_linux {
out.push_str(
"### Bump a version across config files\n\n\
```bash\n\
patchloom batch --apply <<'EOF'\n\
doc.set package.json version \"2.0.0\"\n\
doc.set Cargo.toml package.version \"2.0.0\"\n\
replace README.md \"1.0.0\" \"2.0.0\"\n\
md.upsert_bullet CHANGELOG.md \"## [2.0.0]\" \"- Initial 2.0 release\"\n\
EOF\n\
```\n\n",
);
out.push_str("### Multi-file refactoring with a transaction\n\n\
```bash\n\
patchloom tx - --apply <<'EOF'\n\
{\"version\": 1, \"operations\": [\n\
{\"op\": \"replace\", \"path\": \"src/config.rs\", \"old\": \"old_default\", \"new\": \"new_default\"},\n\
{\"op\": \"doc.set\", \"path\": \"config.toml\", \"key\": \"default_value\", \"value\": \"new_default\"},\n\
{\"op\": \"md.replace_section\", \"path\": \"docs/config.md\", \"heading\": \"## Defaults\",\n\
\"content\": \"The default value is now `new_default`.\\n\"}\n\
]}\n\
EOF\n\
```\n\n\
All operations succeed atomically or roll back together.\n\n");
}
}
#[cfg(feature = "ast")]
if show_cli {
out.push_str(
"## AST-aware operations\n\n\
Tree-sitter-backed operations that understand code structure (20 languages).\n\n\
```bash\n\
# Rename an identifier across a file, skipping strings and comments\n\
patchloom ast rename OldName NewName src/lib.rs --apply\n\n\
# Replace text only within a specific function body\n\
patchloom ast replace src/config.rs --symbol default_timeout --old 30 --new 60 --apply\n\n\
# List all symbol definitions\n\
patchloom ast list src/lib.rs\n\n\
# Find all references to a symbol\n\
patchloom ast refs src/ --name my_function\n\
```\n\n\
AST rename and replace can also be used in batch and tx plans:\n\n\
```bash\n\
# In a batch file:\n\
ast.rename src/lib.rs OldStruct NewStruct\n\
ast.replace src/config.rs default_timeout \"30\" \"60\"\n\
```\n\n",
);
}
out.push_str(
"## Key path syntax\n\n\
All `doc` operations use key paths to address values inside JSON, YAML, and TOML files.\n\n\
| Syntax | Meaning | Example |\n\
|--------|---------|---------|\n\
| `key` | Object key | `database.host` |\n\
| `[N]` | Array index (zero-based) | `servers[0].port` |\n\
| `[*]` | Wildcard (all array elements) | `jobs[*].timeout` |\n\
| `[key=val]` | Predicate (filter by field value) | `deps[name=express].version` |\n\n\
Segments are separated by `.` or adjacent brackets. Examples:\n\n\
```text\n\
scripts.test # simple key path\n\
jobs[0].steps[*].name # index + wildcard\n\
dependencies[name=react].version # predicate filter\n\
```\n\n",
);
if show_cli {
out.push_str(
"## Exit codes\n\n\
| Code | Meaning |\n\
|------|---------|\n\
| 0 | Success (operation completed, or no changes needed) |\n\
| 1 | Failure (error during execution), or tx `rollback_failed` when mid-commit rollback could not fully restore files |\n\
| 2 | Changes detected (`--check` mode found pending changes) |\n\
| 3 | No matches (search/replace found nothing matching the pattern) |\n\
| 4 | Parse error (malformed input file or plan) |\n\
| 5 | Ambiguous (replacement matched multiple locations without `--nth`, or stale/missing patch context) |\n\
| 6 | Validation failed (tx plan validation step returned non-zero; writes may remain when not strict) |\n\
| 7 | Rollback (tx mid-commit failure or strict lifecycle failure; changes were rolled back) |\n\
| 8 | Patch merge conflicts (`patch merge` or `--on-stale merge` without `--allow-conflicts`) |\n\
| 9 | Tx operation staging failure (`operation_failed`) |\n\n",
);
}
out.push_str(
"## Troubleshooting\n\n\
If a command produces unexpected results, enable verbose logging to see \
what patchloom is doing internally:\n\n\
```bash\n\
patchloom --verbose <command> [args]\n\
# or via environment variable:\n\
PATCHLOOM_LOG=1 patchloom <command> [args]\n\
```\n\n\
Diagnostic messages are printed to stderr prefixed with `[patchloom]`.\n",
);
out
}
fn load_project_config(global: &mut crate::cli::global::GlobalFlags) {
let Ok(cwd) = global.resolve_cwd() else {
return;
};
if let Some((config, _)) = crate::config::find_and_load(&cwd) {
crate::config::apply_config(global, &config);
}
}
pub fn dispatch(cli: Cli) -> anyhow::Result<u8> {
let mut global = cli.global;
match cli.command {
#[cfg(feature = "mcp")]
Command::McpServer {
log,
#[cfg(feature = "mcp-http")]
http,
#[cfg(feature = "mcp-http")]
host,
#[cfg(feature = "mcp-http")]
port,
#[cfg(feature = "mcp-http")]
tls_cert,
#[cfg(feature = "mcp-http")]
tls_key,
} => {
load_project_config(&mut global);
#[cfg(feature = "mcp-http")]
if http {
return mcp::run_mcp_http_server(
&global,
log,
&host,
port,
tls_cert.as_deref(),
tls_key.as_deref(),
);
}
mcp::run_mcp_server(&global, log)
}
Command::Schema(args) => schema::run(args, &global),
Command::AgentRules(args) => {
let output = generate_agent_rules(&args);
print!("{output}");
Ok(crate::exit::SUCCESS)
}
Command::Init(args) => init::run(args, &global),
Command::Completions { shell } => {
let mut cmd = <Cli as clap::CommandFactory>::command();
clap_complete::generate(shell, &mut cmd, "patchloom", &mut std::io::stdout());
Ok(crate::exit::SUCCESS)
}
Command::Read(args) => {
load_project_config(&mut global);
read::run(args, &global)
}
Command::Explain(args) => {
load_project_config(&mut global);
explain::run(args, &global)
}
Command::Undo(args) => {
load_project_config(&mut global);
undo::run(args, &global)
}
Command::Search(args) => {
load_project_config(&mut global);
search::run(args, &global)
}
Command::Status(args) => {
load_project_config(&mut global);
status::run(args, &global)
}
Command::Append(args) => {
global.merge_write(&args.write);
load_project_config(&mut global);
append::run(args, &global)
}
Command::Create(args) => {
global.merge_write(&args.write);
load_project_config(&mut global);
create::run(args, &global)
}
Command::Delete(args) => {
global.merge_write(&args.write);
load_project_config(&mut global);
delete::run(args, &global)
}
Command::Rename(args) => {
global.merge_write(&args.write);
load_project_config(&mut global);
rename::run(args, &global)
}
Command::Replace(args) => {
global.merge_write(&args.write);
load_project_config(&mut global);
replace::run(args, &global)
}
Command::Patch(args) => {
global.merge_write(&args.write);
load_project_config(&mut global);
patch::run(args, &global)
}
Command::Md(args) => {
global.merge_write(&args.write);
load_project_config(&mut global);
md::run(args, &global)
}
Command::Doc(args) => {
global.merge_write(&args.write);
load_project_config(&mut global);
doc::run(args, &global)
}
Command::Tidy(args) => {
global.merge_write(&args.write);
load_project_config(&mut global);
tidy::run(args, &global)
}
Command::Tx(args) => {
global.merge_write(&args.write);
load_project_config(&mut global);
tx::run(args, &global)
}
Command::Batch(args) => {
global.merge_write(&args.write);
load_project_config(&mut global);
batch::run(args, &global)
}
#[cfg(feature = "ast")]
Command::Ast(args) => {
match args.command {
ast::AstCommand::Rename(ref a) => global.merge_write(&a.write),
ast::AstCommand::Replace(ref a) => global.merge_write(&a.write),
_ => {}
}
load_project_config(&mut global);
ast::run(args, &global)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn args(mode: AgentMode, platform: AgentPlatform) -> AgentRulesArgs {
AgentRulesArgs { mode, platform }
}
#[test]
fn default_includes_all_sections() {
let out = generate_agent_rules(&args(AgentMode::All, AgentPlatform::All));
assert!(out.contains("# Patchloom"));
assert!(out.contains("## MCP mode"));
assert!(out.contains("## Tool selection guide"));
assert!(out.contains("## Batching"));
assert!(out.contains("## Structured edits"));
assert!(out.contains("## Exit codes"));
assert!(out.contains("<<'EOF'"));
assert!(out.contains("batch ops.txt"));
}
#[test]
fn mode_cli_omits_mcp_keeps_cli() {
let out = generate_agent_rules(&args(AgentMode::Cli, AgentPlatform::All));
assert!(!out.contains("## MCP mode"));
assert!(!out.contains("## Tool selection guide"));
assert!(out.contains("## Batching"));
assert!(out.contains("## Structured edits"));
assert!(out.contains("native agent tools are faster"));
}
#[test]
fn mode_mcp_omits_cli_keeps_mcp() {
let out = generate_agent_rules(&args(AgentMode::Mcp, AgentPlatform::All));
assert!(out.contains("## MCP mode"));
assert!(out.contains("## Tool selection guide"));
assert!(!out.contains("batch({\"operations\":"));
assert!(!out.contains("transaction({\"operations\":"));
assert!(!out.contains("search_replace"));
assert!(!out.contains("run_terminal_command"));
assert!(!out.contains("command line"));
assert!(out.contains("Use them for ALL file operations"));
assert!(!out.contains("\n## Batching"));
assert!(!out.contains("\n## Structured edits"));
assert!(!out.contains("native agent tools are faster"));
}
#[test]
fn platform_linux_omits_windows() {
let out = generate_agent_rules(&args(AgentMode::All, AgentPlatform::Linux));
assert!(out.contains("<<'EOF'"));
assert!(!out.contains("batch ops.txt"));
assert!(out.contains("'\"2.0.0\"'"));
assert!(!out.contains("patchloom doc set config.json version \"\\\"2.0.0\\\"\""));
}
#[test]
fn platform_windows_omits_heredoc() {
let out = generate_agent_rules(&args(AgentMode::All, AgentPlatform::Windows));
assert!(!out.contains("<<'EOF'"));
assert!(out.contains("batch ops.txt"));
assert!(out.contains("patchloom doc set config.json version \"\\\"2.0.0\\\"\""));
assert!(!out.contains("'\"2.0.0\"'"));
}
#[test]
fn exit_codes_present_for_cli_modes() {
for mode in [AgentMode::All, AgentMode::Cli] {
for platform in [
AgentPlatform::All,
AgentPlatform::Linux,
AgentPlatform::Windows,
] {
let out = generate_agent_rules(&args(mode, platform));
assert!(
out.contains("## Exit codes"),
"exit codes missing for mode={mode:?} platform={platform:?}"
);
}
}
let out = generate_agent_rules(&args(AgentMode::Mcp, AgentPlatform::All));
assert!(!out.contains("## Exit codes"));
}
#[test]
fn workflow_includes_whole_line_delete_example() {
let out = generate_agent_rules(&args(AgentMode::Cli, AgentPlatform::All));
assert!(out.contains("--whole-line"));
assert!(out.contains("--collapse-blanks"));
}
#[test]
fn agent_rules_includes_project_config_section() {
let out = generate_agent_rules(&args(AgentMode::Cli, AgentPlatform::All));
assert!(out.contains("## Project configuration"));
assert!(out.contains(".patchloom.toml"));
assert!(out.contains("collapse_blanks"));
assert!(out.contains("[tx]"));
}
#[test]
fn agent_rules_includes_patch_merge_workflow() {
let out = generate_agent_rules(&args(AgentMode::Cli, AgentPlatform::All));
assert!(out.contains("--on-stale merge"));
assert!(out.contains("--allow-conflicts"));
assert!(out.contains("never commit files containing conflict markers"));
}
#[test]
fn agent_rules_exit_codes_include_conflicts_and_rollback_failed() {
let out = generate_agent_rules(&args(AgentMode::Cli, AgentPlatform::All));
assert!(out.contains("| 8 |"));
assert!(out.contains("| 9 |"));
assert!(out.contains("rollback_failed"));
assert!(out.contains("operation_failed"));
}
#[test]
fn version_is_embedded() {
let out = generate_agent_rules(&args(AgentMode::All, AgentPlatform::All));
let version = env!("CARGO_PKG_VERSION");
assert!(out.contains(&format!("patchloom v{version}")));
}
#[test]
fn mcp_and_windows_compose_to_minimal() {
let out = generate_agent_rules(&args(AgentMode::Mcp, AgentPlatform::Windows));
assert!(out.contains("## MCP mode"));
assert!(!out.contains("batch({\"operations\":"));
assert!(!out.contains("transaction({\"operations\":"));
assert!(!out.contains("\n## Batching"));
assert!(!out.contains("batch ops.txt"));
assert!(!out.contains("<<'EOF'"));
}
}