//! AI runtime profile and its context-injection strategy. //! //! A profile is *declarative configuration* (see CONTEXT.md §9): adding an AI //! means adding data, not code (Open/Closed). The [`crate::ports::AgentRuntime`] //! port is parameterised by an [`AgentProfile`]. use serde::{Deserialize, Serialize}; use crate::error::DomainError; use crate::ids::ProfileId; /// Strategy for injecting an agent's `.md` context into the launched CLI. /// /// Invariants: /// - `ConventionFile.target` is a relative file name without `..` and not absolute, /// - `Env.var` is a valid environment-variable identifier, /// - `Flag.flag` is non-empty. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "strategy")] pub enum ContextInjection { /// Write/symlink the `.md` to a conventional file (e.g. `CLAUDE.md`). ConventionFile { /// Relative target file name. target: String, }, /// Pass the context file path through a CLI flag. Flag { /// The flag template (e.g. `--context-file {path}` or `-f`). flag: String, }, /// Pipe the Markdown content on stdin. Stdin, /// Pass the context via an environment variable. Env { /// Environment variable name. var: String, }, } impl ContextInjection { /// Validated `ConventionFile` constructor. /// /// # Errors /// Returns [`DomainError::PathNotRelativeSafe`] if `target` is absolute or /// contains `..`. pub fn convention_file(target: impl Into) -> Result { let target = target.into(); crate::validation::relative_safe(&target)?; Ok(Self::ConventionFile { target }) } /// Validated `Flag` constructor. /// /// # Errors /// Returns [`DomainError::EmptyField`] if `flag` is empty. pub fn flag(flag: impl Into) -> Result { let flag = flag.into(); crate::validation::non_empty(&flag, "contextInjection.flag")?; Ok(Self::Flag { flag }) } /// `Stdin` constructor (no validation needed). #[must_use] pub const fn stdin() -> Self { Self::Stdin } /// Validated `Env` constructor. /// /// # Errors /// Returns [`DomainError::InvalidEnvVar`] if `var` is not a valid identifier. pub fn env(var: impl Into) -> Result { let var = var.into(); crate::validation::valid_env_var(&var)?; Ok(Self::Env { var }) } } /// Declarative runtime configuration for one AI CLI. /// /// Invariants: /// - `name` and `command` non-empty, /// - `context_injection` is itself valid (guaranteed by its constructors). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AgentProfile { /// Stable identifier. pub id: ProfileId, /// Display name. pub name: String, /// Launch command (e.g. `claude`, `codex`, `gemini`, `aider`). pub command: String, /// Static arguments. pub args: Vec, /// Context-injection strategy. pub context_injection: ContextInjection, /// Optional detection command (e.g. `claude --version`). pub detect: Option, /// Working-directory template. Always `"{agentRunDir}"` (ARCHITECTURE §14.1): /// an agent's PTY cwd is its isolated `.ideai/run//` directory, /// **never** the project root, so that N agents of the same profile never /// collide on a single conventional file (`CLAUDE.md`, …) at the root. pub cwd_template: String, } impl AgentProfile { /// Builds a validated profile. /// /// # Errors /// Returns [`DomainError::EmptyField`] if `name` or `command` is empty. #[allow(clippy::too_many_arguments)] pub fn new( id: ProfileId, name: impl Into, command: impl Into, args: Vec, context_injection: ContextInjection, detect: Option, cwd_template: impl Into, ) -> Result { let name = name.into(); let command = command.into(); let cwd_template = cwd_template.into(); crate::validation::non_empty(&name, "profile.name")?; crate::validation::non_empty(&command, "profile.command")?; Ok(Self { id, name, command, args, context_injection, detect, cwd_template, }) } }