//! Agent definition types — parsed from `.kigi/agents/*.md` files.
use crate::error::AgentBuildError;
use crate::prompt::context::TemplateOverride;
use crate::prompt::user_message::UserMessageTemplate;
use kigi_tools::implementations::codex;
use kigi_tools::implementations::kigi;
use kigi_tools::implementations::kigi_concise;
use kigi_tools::implementations::memory;
use kigi_tools::implementations::opencode;
use kigi_tools::implementations::search_tool;
use kigi_tools::implementations::use_tool;
use kigi_tools::registry::types::{ToolConfig, ToolServerConfig};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use strum::{AsRefStr, Display, EnumIter, EnumString, IntoStaticStr};
/// Process-global registry of externally-provided toolset presets.
///
/// # Visibility
/// Each preset is registered as either **public** or **internal**:
/// - **Public** presets are product presets: they are enumerated by
///   [`preset_names`] / [`all_toolset_presets`] (so they appear in the
///   workspace manifest, preset sets, etc.) *and* resolvable via
///   [`toolset_for_preset`].
/// - **Internal** presets are resolved by name at runtime by the shell /
///   orchestrator spawn path via [`toolset_for_preset`], but are deliberately
///   NOT enumerated, so a harness-internal preset never leaks into public
///   preset enumeration.
///
/// # Ordering contract
/// [`register_toolset_preset`] / [`register_internal_toolset_preset`] MUST run
/// before the first preset resolution in the process. Presets registered later
/// are still visible to subsequent `toolset_for_preset` / `preset_names` /
/// `all_toolset_presets` calls, but any config resolved before registration
/// will not see them.
/// A toolset preset builder: a function producing a [`ToolServerConfig`].
pub type ToolsetPresetBuilder = fn() -> ToolServerConfig;
/// Whether a registered preset is enumerated publicly or resolved by name only.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum PresetVisibility {
    /// A product preset: enumerated by `preset_names()` / `all_toolset_presets()`.
    Public,
    /// A harness-internal preset: resolvable by name but never enumerated.
    Internal,
}
static TOOLSET_PRESETS: OnceLock<Mutex<HashMap<String, (ToolsetPresetBuilder, PresetVisibility)>>> =
    OnceLock::new();
fn toolset_preset_registry()
-> &'static Mutex<HashMap<String, (ToolsetPresetBuilder, PresetVisibility)>> {
    TOOLSET_PRESETS.get_or_init(|| Mutex::new(HashMap::new()))
}
/// Register an out-of-tree **public** (product) toolset preset by name. Public
/// presets are enumerated by [`preset_names`] / [`all_toolset_presets`] and
/// resolvable via [`toolset_for_preset`]. See [`TOOLSET_PRESETS`].
pub fn register_toolset_preset(name: &str, builder: ToolsetPresetBuilder) {
    toolset_preset_registry()
        .lock()
        .expect("toolset preset registry poisoned")
        .insert(name.to_string(), (builder, PresetVisibility::Public));
}
/// Register an out-of-tree **internal** toolset preset by name. Internal presets
/// are resolvable via [`toolset_for_preset`] (the shell / orchestrator spawn
/// path resolves them by name) but are deliberately NOT enumerated by
/// [`preset_names`] / [`all_toolset_presets`], so they never leak into public
/// preset enumeration (manifest generation, product preset sets, …). See
/// [`TOOLSET_PRESETS`].
pub fn register_internal_toolset_preset(name: &str, builder: ToolsetPresetBuilder) {
    toolset_preset_registry()
        .lock()
        .expect("toolset preset registry poisoned")
        .insert(name.to_string(), (builder, PresetVisibility::Internal));
}
/// Look up an externally-registered toolset preset by (already-normalized) name.
/// Resolves BOTH public and internal presets.
fn registered_toolset_preset(name: &str) -> Option<ToolServerConfig> {
    toolset_preset_registry()
        .lock()
        .expect("toolset preset registry poisoned")
        .get(name)
        .map(|(f, _)| f())
}
/// Names of externally-registered **public** presets only (internal presets are
/// intentionally excluded from enumeration).
fn registered_public_toolset_preset_names() -> Vec<String> {
    toolset_preset_registry()
        .lock()
        .expect("toolset preset registry poisoned")
        .iter()
        .filter(|(_, (_, visibility))| *visibility == PresetVisibility::Public)
        .map(|(name, _)| name.clone())
        .collect()
}
/// Orchestrator-specific prompt body appended to the standard Kigi
/// system prompt (`prompt.md`). Instructs the GBL model to delegate
/// coding and exploration work to subagents.
const ORCHESTRATOR_PROMPT_BODY: &str = "\
## Orchestrator Mode

You are a technical lead orchestrating a team of senior-engineer subagents. Your subagents \
are highly capable \u{2014} treat them as expert peers, not junior helpers. Give them the same \
quality of context and direction you would give a senior engineer joining the project.

Your job is to think, plan, coordinate, and review. Their job is to explore, implement, \
and execute. Use them aggressively and liberally \u{2014} spawn subagents early and often.

### Your direct responsibilities:
- High-level planning and architecture decisions
- Reading files for quick context (${{ tools.by_kind.read }}, ${{ tools.by_kind.search }}, ${{ tools.by_kind.list }})
- Running quick terminal commands for orientation (${{ tools.by_kind.execute }})
- Invoking skills and MCP tools (${{ tools.by_kind.skill }}, ${{ tools.by_kind.search_tool }}, ${{ tools.by_kind.use_tool }})
- Web research (${{ tools.by_kind.web_search }}, ${{ tools.by_kind.web_fetch }})
- Asking the user questions (${{ tools.by_kind.ask_user }})
- Managing task lists and tracking progress (${{ tools.by_kind.plan }})
- Reviewing subagent results and synthesizing responses for the user

### ALWAYS delegate to subagents:
- **ALL file modifications** \u{2014} creating, editing, deleting files (`general-purpose`)
- **ALL builds, tests, and verification** \u{2014} running test suites, linters, compilers (`general-purpose`)
- **Deep codebase exploration** \u{2014} searching across many files, understanding patterns (`explore`)
- **Multi-step implementation** \u{2014} any task involving more than reading (`general-purpose`)
- **Any research requiring thoroughness** \u{2014} don\u{2019}t do shallow searches yourself, spawn an `explore` subagent

### How to talk to subagents:
Write prompts the way you would brief a senior engineer:
- Explain WHAT you need done and WHY (the context behind the task)
- Share what you already know \u{2014} file paths, function names, architectural decisions
- Describe the end state, not step-by-step commands \u{2014} trust their judgment on HOW
- If you have opinions on approach, share them as guidance, not rigid instructions
- Include acceptance criteria: what does \"done\" look like?

### Parallelism:
- Break independent tasks into separate subagents and run them in parallel
- Use `explore` subagents to investigate multiple areas simultaneously
- Launch implementation subagents for independent files/modules at the same time
- Do NOT wait for one subagent before spawning others that don\u{2019}t depend on it

### Anti-patterns to avoid:
- Do NOT do shallow 1-2 file reads yourself when an `explore` agent would be more thorough
- Do NOT implement code changes yourself \u{2014} you have no file editing tools
- Do NOT give subagents overly prescriptive step-by-step instructions \u{2014} trust their expertise
- Do NOT summarize or re-explain what the user said \u{2014} get to work immediately";
/// Bash tool with clearer model-facing names:
/// `run_terminal_cmd` → `run_terminal_command`, `is_background` → `background`.
fn bash_tool_config() -> ToolConfig {
    ToolConfig::from(&kigi::BashTool)
        .with_name("run_terminal_command")
        .with_param_rename("is_background", "background")
}
/// Task/subagent tool with clearer model-facing names:
/// `task` → `spawn_subagent`, `run_in_background` → `background`.
fn task_tool_config() -> ToolConfig {
    ToolConfig::from(&kigi::TaskTool)
        .with_name("spawn_subagent")
        .with_param_rename("run_in_background", "background")
}
/// Task output tool renamed for clarity:
/// `get_task_output` → `get_command_or_subagent_output`.
fn task_output_tool_config() -> ToolConfig {
    ToolConfig::from(&kigi::TaskOutputTool).with_name("get_command_or_subagent_output")
}
/// `wait_tasks` → `wait_commands_or_subagents`.
fn wait_tasks_tool_config() -> ToolConfig {
    ToolConfig::from(&kigi::WaitTasksTool).with_name("wait_commands_or_subagents")
}
/// `kill_task` → `kill_command_or_subagent`.
fn kill_task_tool_config() -> ToolConfig {
    ToolConfig::from(&kigi::KillTaskTool).with_name("kill_command_or_subagent")
}
/// Complete workspace-executable toolset for hub registration.
///
/// Extends `default_kigi_toolset()` with tools that are dynamically
/// injected by `AgentBuilder::build()` or only available in specific modes.
/// In proxy mode, the workspace server executes ALL tools — the shell has
/// zero local dispatch.
pub fn workspace_kigi_toolset() -> ToolServerConfig {
    let mut tools = default_kigi_toolset().tools;
    tools.push((&opencode::OpenCodeWriteTool).into());
    tools.push((&kigi::EnterPlanModeTool).into());
    tools.push((&kigi::ExitPlanModeTool).into());
    tools.push((&kigi::AskUserQuestionTool).into());
    tools.push((&kigi::WebSearchTool).into());
    tools.push((&kigi::WebFetchTool).into());
    tools.push((&memory::search_tool::MemorySearchImpl).into());
    tools.push((&memory::get_tool::MemoryGetImpl).into());
    tools.push((&kigi::LspTool).into());
    ToolServerConfig {
        tools,
        behavior_preset: None,
    }
}
/// Toolset for the `kigi-computer` (workspace/sandbox) preset.
fn kigi_computer_toolset() -> ToolServerConfig {
    #[allow(unused_mut)]
    let mut tools = vec![
        bash_tool_config(),
        (&kigi::ReadFileTool).into(),
        (&kigi::SearchReplaceTool).into(),
        (&opencode::OpenCodeWriteTool).into(),
        (&kigi::ListDirTool).into(),
        (&kigi::GrepTool).into(),
        (&kigi::KillTerminalCommandTool).into(),
        (&kigi::GetTerminalCommandOutputTool).into(),
    ];
    ToolServerConfig {
        tools,
        behavior_preset: None,
    }
}
/// Every named toolset preset, as `(normalized_name, config)` pairs.
///
/// Single source of truth: [`toolset_for_preset`] resolves through this
/// table, and the preset-coverage tests iterate it, so a new preset is
/// automatically covered the moment it becomes resolvable.
/// Native (in-crate) toolset presets.
fn native_toolset_presets() -> Vec<(&'static str, ToolServerConfig)> {
    vec![
        ("kigi", workspace_kigi_toolset()),
        ("kigi-concise", kigi_concise_toolset()),
        ("kigi-plan", kigi_plan_toolset()),
        ("codex", codex_toolset()),
        ("explore", explore_toolset()),
        ("plan", plan_toolset()),
        ("kigi-computer", kigi_computer_toolset()),
    ]
}
/// Every named **public** toolset preset (native + externally registered public
/// presets), as `(name, config)` pairs. Harness-internal registered presets are
/// intentionally excluded — resolve them by name via [`toolset_for_preset`].
fn all_toolset_presets() -> Vec<(String, ToolServerConfig)> {
    let mut out: Vec<(String, ToolServerConfig)> = native_toolset_presets()
        .into_iter()
        .map(|(name, cfg)| (name.to_string(), cfg))
        .collect();
    for name in registered_public_toolset_preset_names() {
        if !out.iter().any(|(n, _)| *n == name)
            && let Some(cfg) = registered_toolset_preset(&name)
        {
            out.push((name, cfg));
        }
    }
    out
}
/// Names of every named toolset preset (native first, then registered).
pub fn preset_names() -> Vec<String> {
    all_toolset_presets()
        .into_iter()
        .map(|(name, _)| name)
        .collect()
}
/// Resolve a named toolset preset to its [`ToolServerConfig`], or `None` if unknown.
pub fn toolset_for_preset(preset: &str) -> Option<ToolServerConfig> {
    let normalized = preset.trim().to_ascii_lowercase().replace([' ', '_'], "-");
    native_toolset_presets()
        .into_iter()
        .find(|(name, _)| *name == normalized)
        .map(|(_, toolset)| toolset)
        .or_else(|| registered_toolset_preset(&normalized))
}
fn default_kigi_toolset() -> ToolServerConfig {
    ToolServerConfig {
        tools: vec![
            bash_tool_config(),
            (&kigi::ReadFileTool).into(),
            (&kigi::SearchReplaceTool).into(),
            (&kigi::ListDirTool).into(),
            (&kigi::GrepTool).into(),
            kill_task_tool_config(),
            (&kigi::TodoWriteTool).into(),
            task_output_tool_config(),
            wait_tasks_tool_config(),
            task_tool_config(),
            (&kigi::SchedulerCreateTool).into(),
            (&kigi::SchedulerDeleteTool).into(),
            (&kigi::SchedulerListTool).into(),
            (&kigi::MonitorTool).into(),
            (&search_tool::SearchTool).into(),
            (&use_tool::UseTool).into(),
            (&kigi::UpdateGoalTool).into(),
        ],
        behavior_preset: None,
    }
}
fn kigi_concise_toolset() -> ToolServerConfig {
    ToolServerConfig {
        tools: vec![
            (&kigi_concise::BashConciseTool).into(),
            (&kigi_concise::ReadFileConciseTool).into(),
            (&kigi_concise::SearchReplaceConciseTool).into(),
            (&kigi::ListDirTool).into(),
            (&kigi::GrepTool).into(),
            kill_task_tool_config(),
            (&kigi::TodoWriteTool).into(),
            task_output_tool_config(),
            (&kigi::SchedulerCreateTool).into(),
            (&kigi::SchedulerDeleteTool).into(),
            (&kigi::SchedulerListTool).into(),
            (&kigi::MonitorTool).into(),
            (&kigi::UpdateGoalTool).into(),
        ],
        behavior_preset: None,
    }
}
/// Hashline toolset: anchor-based read/edit/search + standard utilities.
///
/// `hashline_tools` should be the 3 hashline `ToolConfig` entries produced by
/// `FileToolset::Hashline.tool_configs(&hashline_config)` — they carry the
/// scheme parameters as tool params.
pub fn kigi_hashline_toolset(
    hashline_tools: Vec<kigi_tools::registry::types::ToolConfig>,
) -> ToolServerConfig {
    let mut tools: Vec<kigi_tools::registry::types::ToolConfig> = vec![bash_tool_config()];
    tools.extend(hashline_tools);
    tools.extend([
        (&kigi::ListDirTool).into(),
        kill_task_tool_config(),
        (&kigi::TodoWriteTool).into(),
        task_output_tool_config(),
        wait_tasks_tool_config(),
        task_tool_config(),
        (&kigi::WebSearchTool).into(),
        (&kigi::SchedulerCreateTool).into(),
        (&kigi::SchedulerDeleteTool).into(),
        (&kigi::SchedulerListTool).into(),
        (&kigi::MonitorTool).into(),
        (&search_tool::SearchTool).into(),
        (&use_tool::UseTool).into(),
        (&kigi::UpdateGoalTool).into(),
    ]);
    ToolServerConfig {
        tools,
        behavior_preset: None,
    }
}
fn codex_toolset() -> ToolServerConfig {
    ToolServerConfig {
        tools: vec![
            bash_tool_config(),
            (&codex::CodexReadFileTool).into(),
            (&codex::ApplyPatchTool).into(),
            (&codex::CodexListDirTool).into(),
            (&codex::CodexGrepFilesTool).into(),
            kill_task_tool_config(),
            (&kigi::TodoWriteTool).into(),
            task_output_tool_config(),
            (&search_tool::SearchTool).into(),
            (&use_tool::UseTool).into(),
        ],
        behavior_preset: None,
    }
}
/// Read-only toolset for the **explore** subagent.
///
/// Genuinely read-only: `read_file` (Read), `list_dir` (Glob), `grep` (Grep).
/// `run_terminal_command` (Bash) is intentionally omitted so exploration cannot
/// mutate the workspace — the read-only guarantee is enforced by the toolset,
/// not merely by the prompt. With no `BashTool`, the background-task helpers
/// (`KillTaskTool`/`TaskOutputTool`) are unnecessary and also omitted.
fn explore_toolset() -> ToolServerConfig {
    ToolServerConfig {
        tools: vec![
            (&kigi::ReadFileTool).into(),
            (&kigi::ListDirTool).into(),
            (&kigi::GrepTool).into(),
        ],
        behavior_preset: None,
    }
}
/// Plan-mode toolset — read-only inspection tools, no shell, no file-editing.
///
/// Enforces read-only at the toolset: the agent may inspect the repo and keep
/// a todo list, but `search_replace` (file edits) and `run_terminal_command`
/// (shell) are both omitted so it cannot mutate the workspace.
fn plan_toolset() -> ToolServerConfig {
    ToolServerConfig {
        tools: vec![
            (&kigi::ReadFileTool).into(),
            (&kigi::ListDirTool).into(),
            (&kigi::GrepTool).into(),
            (&kigi::TodoWriteTool).into(),
        ],
        behavior_preset: None,
    }
}
/// Kigi + plan mode toolset.
///
/// Extends the default `kigi` toolset with plan mode tools:
/// `enter_plan_mode`, `exit_plan_mode`, and `ask_user_question`.
/// This allows the agent to enter a structured planning phase before
/// writing code, with user-approved plans.
fn kigi_plan_toolset() -> ToolServerConfig {
    ToolServerConfig {
        tools: vec![
            bash_tool_config(),
            (&kigi::ReadFileTool).into(),
            (&kigi::SearchReplaceTool).into(),
            (&kigi::ListDirTool).into(),
            (&kigi::GrepTool).into(),
            kill_task_tool_config(),
            (&kigi::TodoWriteTool).into(),
            task_output_tool_config(),
            task_tool_config(),
            (&kigi::SchedulerCreateTool).into(),
            (&kigi::SchedulerDeleteTool).into(),
            (&kigi::SchedulerListTool).into(),
            (&kigi::MonitorTool).into(),
            (&search_tool::SearchTool).into(),
            (&use_tool::UseTool).into(),
            (&kigi::UpdateGoalTool).into(),
            (&kigi::EnterPlanModeTool).into(),
            (&kigi::ExitPlanModeTool).into(),
            (&kigi::AskUserQuestionTool).into(),
        ],
        behavior_preset: None,
    }
}
/// Orchestrator toolset: read/search/orchestration tools only.
///
/// No terminal execution, no file editing. The orchestrator delegates
/// all execution and file modification to subagents. Retains read_file,
/// grep, list_dir for research, plus the full subagent/skill/MCP/plan
/// stack for orchestration.
fn orchestrator_toolset() -> ToolServerConfig {
    ToolServerConfig {
        tools: vec![
            bash_tool_config(),
            (&kigi::ReadFileTool).into(),
            (&kigi::ListDirTool).into(),
            (&kigi::GrepTool).into(),
            task_tool_config(),
            task_output_tool_config(),
            wait_tasks_tool_config(),
            kill_task_tool_config(),
            (&search_tool::SearchTool).into(),
            (&use_tool::UseTool).into(),
            (&kigi::TodoWriteTool).into(),
            (&kigi::EnterPlanModeTool).into(),
            (&kigi::ExitPlanModeTool).into(),
            (&kigi::AskUserQuestionTool).into(),
            (&kigi::UpdateGoalTool).into(),
            (&kigi::SchedulerCreateTool).into(),
            (&kigi::SchedulerDeleteTool).into(),
            (&kigi::SchedulerListTool).into(),
            (&kigi::MonitorTool).into(),
            (&kigi::WebSearchTool).into(),
            (&kigi::WebFetchTool).into(),
            (&memory::MemorySearchImpl).into(),
            (&memory::MemoryGetImpl).into(),
        ],
        behavior_preset: None,
    }
}
/// Kigi + plan mode toolset WITHOUT subagent tools.
///
/// Same as `kigi_plan_toolset` but excludes `TaskTool`,
/// `TaskOutputTool`, and `KillTaskTool`. Use this when the shell
/// does not have subagent infrastructure wired up.
fn kigi_plan_no_subagents_toolset() -> ToolServerConfig {
    ToolServerConfig {
        tools: vec![
            bash_tool_config(),
            (&kigi::ReadFileTool).into(),
            (&kigi::SearchReplaceTool).into(),
            (&kigi::ListDirTool).into(),
            (&kigi::GrepTool).into(),
            kill_task_tool_config(),
            (&kigi::TodoWriteTool).into(),
            task_output_tool_config(),
            (&kigi::SchedulerCreateTool).into(),
            (&kigi::SchedulerDeleteTool).into(),
            (&kigi::SchedulerListTool).into(),
            (&kigi::MonitorTool).into(),
            (&search_tool::SearchTool).into(),
            (&use_tool::UseTool).into(),
            (&kigi::UpdateGoalTool).into(),
            (&kigi::EnterPlanModeTool).into(),
            (&kigi::ExitPlanModeTool).into(),
            (&kigi::AskUserQuestionTool).into(),
        ],
        behavior_preset: None,
    }
}
/// Default Kigi toolset + `ask_user_question`.
///
/// Same as `default_kigi_toolset` with the `AskUserQuestionTool` added,
/// allowing the agent to ask structured questions without full plan mode.
fn kigi_ask_user_toolset() -> ToolServerConfig {
    ToolServerConfig {
        tools: vec![
            bash_tool_config(),
            (&kigi::ReadFileTool).into(),
            (&kigi::SearchReplaceTool).into(),
            (&kigi::ListDirTool).into(),
            (&kigi::GrepTool).into(),
            kill_task_tool_config(),
            (&kigi::TodoWriteTool).into(),
            task_output_tool_config(),
            wait_tasks_tool_config(),
            task_tool_config(),
            (&kigi::SchedulerCreateTool).into(),
            (&kigi::SchedulerDeleteTool).into(),
            (&kigi::SchedulerListTool).into(),
            (&kigi::MonitorTool).into(),
            (&search_tool::SearchTool).into(),
            (&use_tool::UseTool).into(),
            (&kigi::UpdateGoalTool).into(),
            (&kigi::AskUserQuestionTool).into(),
        ],
        behavior_preset: None,
    }
}
fn opencode_toolset() -> ToolServerConfig {
    ToolServerConfig {
        tools: vec![
            (&opencode::OpenCodeBashTool).into(),
            (&opencode::OpenCodeReadTool).into(),
            (&opencode::OpenCodeEditTool).into(),
            (&opencode::OpenCodeWriteTool).into(),
            (&opencode::OpenCodeGrepTool).into(),
            (&opencode::OpenCodeGlobTool).into(),
            (&opencode::OpenCodeTodoWriteTool).into(),
            (&opencode::OpenCodeSkillTool).into(),
            kill_task_tool_config(),
            task_output_tool_config(),
        ],
        behavior_preset: None,
    }
}
/// Model override for an agent definition.
///
/// Two states:
/// - `Inherit` — use the parent session's model (default).
/// - `Override(String)` — use a specific model ID, resolved against
///   available models at subagent spawn time.
///
/// In YAML frontmatter / JSON:
/// - `model: inherit` or omitted → `Inherit`
/// - `model: kigi-3-fast` → `Override("kigi-3-fast")`
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ModelOverride {
    /// Use the parent session's model.
    #[default]
    Inherit,
    /// Use a specific model ID.
    Override(String),
}
impl std::fmt::Display for ModelOverride {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Inherit => f.write_str("inherit"),
            Self::Override(id) => f.write_str(id),
        }
    }
}
impl<'de> Deserialize<'de> for ModelOverride {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let opt = Option::<String>::deserialize(deserializer)?;
        match opt {
            None => Ok(Self::Inherit),
            Some(s) if s.is_empty() || s.eq_ignore_ascii_case("inherit") => Ok(Self::Inherit),
            Some(s) => Ok(Self::Override(s)),
        }
    }
}
impl serde::Serialize for ModelOverride {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            Self::Inherit => serializer.serialize_str("inherit"),
            Self::Override(id) => serializer.serialize_str(id),
        }
    }
}
const AGENT_TASK_KEYWORDS: &str = "Agent|Task";
/// Splits `"Agent(a, b), read_file"` → `["Agent(a, b)", "read_file"]`.
pub static AGENT_TASK_TOKENIZER_RE: std::sync::LazyLock<regex::Regex> =
    std::sync::LazyLock::new(|| {
        regex::Regex::new(&format!(r"(?i:{AGENT_TASK_KEYWORDS})\([^)]*\)|[^,]+"))
            .expect("valid regex")
    });
/// Matches `"Agent(a, b)"` and captures `"a, b"` in group 1. `None` for bare `Agent`.
pub static AGENT_TASK_CLASSIFIER_RE: std::sync::LazyLock<regex::Regex> =
    std::sync::LazyLock::new(|| {
        regex::Regex::new(&format!(r"^(?i:{AGENT_TASK_KEYWORDS})(?:\(([^)]*)\))?$"))
            .expect("valid regex")
    });
/// Accepts `"a, b, c"` or `["a", "b"]`. Trims whitespace.
fn deserialize_string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    use serde::de;
    struct StringOrVec;
    impl<'de> de::Visitor<'de> for StringOrVec {
        type Value = Vec<String>;
        fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
            f.write_str("a comma-separated string or an array of strings")
        }
        fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
            Ok(AGENT_TASK_TOKENIZER_RE
                .find_iter(s)
                .map(|m| m.as_str().trim().to_string())
                .filter(|s| !s.is_empty())
                .collect())
        }
        fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
            let mut v = Vec::new();
            while let Some(item) = seq.next_element::<String>()? {
                v.push(item);
            }
            Ok(v)
        }
        fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
            Ok(Vec::new())
        }
        fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
            Ok(Vec::new())
        }
    }
    deserializer.deserialize_any(StringOrVec)
}
/// Accepts a positive u32 or null/absent. Rejects 0.
fn deserialize_nonzero_u32<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let opt = Option::<u32>::deserialize(deserializer)?;
    if let Some(0) = opt {
        return Err(serde::de::Error::custom("maxTurns must be greater than 0"));
    }
    Ok(opt)
}
/// All built-in agent names as a typed enum.
///
/// Eliminates string matching in discovery and ensures built-in names
/// are defined in exactly one place. The enum covers all built-in
/// agents for centralized name management and `by_name()` dispatch.
///
/// `subagent_variants()` returns only the 3 that are exposed to the LLM
/// via the `TaskTool` description. The remaining 6 are top-level agent
/// profiles resolvable by name but not advertised as subagent types.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, EnumIter, AsRefStr, IntoStaticStr,
)]
#[strum(serialize_all = "kebab-case")]
pub enum BuiltinAgentName {
    Kigi,
    KigiConcise,
    KigiPlan,
    KigiPlanNoSubagents,
    KigiAskUser,
    Codex,
    Opencode,
    GeneralPurpose,
    Explore,
    Plan,
    BrowserUse,
    #[strum(serialize = "kigi-orchestrator")]
    KigiOrchestrator,
}
/// Agent-type prefix written by pre-rebrand builds into session files and
/// model configs (`<prefix>`, `<prefix>-plan`, `<prefix>-concise`, …).
///
/// DOCUMENTED ALIAS: sessions persist agent types, so names written by older
/// builds must keep resolving. The prefix is assembled with `concat!` so the
/// release-gate grep over Rust sources (which forbids the upstream brand
/// string) stays clean while the runtime value is the historical spelling.
pub const LEGACY_AGENT_TYPE_PREFIX: &str = concat!("gr", "ok-build");

/// Map a legacy pre-rebrand agent-type name onto its current `kigi*`
/// equivalent: `<prefix>` → `kigi`, `<prefix>-plan` → `kigi-plan`, and so on.
/// Non-legacy names pass through unchanged. Applied at the name-resolution
/// boundaries ([`crate::discovery::by_name`] and friends,
/// [`is_strict_harness_agent_type`]) so persisted agent types from old
/// session files keep loading.
pub fn canonical_agent_type(name: &str) -> std::borrow::Cow<'_, str> {
    match name.strip_prefix(LEGACY_AGENT_TYPE_PREFIX) {
        Some("") => std::borrow::Cow::Borrowed("kigi"),
        Some(rest) if rest.starts_with('-') => std::borrow::Cow::Owned(format!("kigi{rest}")),
        _ => std::borrow::Cow::Borrowed(name),
    }
}

/// Strict-harness predicate by name. Resolves via `BuiltinAgentName` and
/// delegates to [`AgentDefinition::is_strict_harness`]; unknown names
/// return `false` (conservative — never enforce a harness we can't verify).
/// Accepts legacy pre-rebrand agent-type names via [`canonical_agent_type`].
/// Callers that already hold an `AgentDefinition` should call that method
/// directly so project-level shadowing is honored.
pub fn is_strict_harness_agent_type(name: &str) -> bool {
    use std::str::FromStr;
    BuiltinAgentName::from_str(canonical_agent_type(name).as_ref())
        .map(|b| b.definition().is_strict_harness())
        .unwrap_or(false)
}
impl BuiltinAgentName {
    /// Build the `AgentDefinition` for this built-in agent.
    pub fn definition(self) -> AgentDefinition {
        match self {
            Self::Kigi => AgentDefinition::default_kigi(),
            Self::KigiConcise => AgentDefinition::kigi_concise(),
            Self::KigiPlan => AgentDefinition::kigi_plan(),
            Self::KigiPlanNoSubagents => AgentDefinition::kigi_plan_no_subagents(),
            Self::KigiAskUser => AgentDefinition::kigi_ask_user(),
            Self::Codex => AgentDefinition::codex(),
            Self::Opencode => AgentDefinition::opencode(),
            Self::GeneralPurpose => AgentDefinition::general_purpose(),
            Self::Explore => AgentDefinition::explore(),
            Self::Plan => AgentDefinition::plan(),
            Self::BrowserUse => AgentDefinition::browser_use(),
            Self::KigiOrchestrator => AgentDefinition::kigi_orchestrator(),
        }
    }
    /// Built-in agents available as subagents via the Task tool.
    pub fn subagent_variants() -> &'static [Self] {
        &[Self::GeneralPurpose, Self::Explore, Self::Plan]
    }
}
/// Portable agent identity — parsed from .kigi/agents/*.md.
/// Usable as both a top-level agent and a subagent definition.
///
/// This is the stable, version-controllable contract. It does NOT
/// contain session-level policies (compaction, system reminders).
/// Those are provided by the AgentBuilder at build time.
#[derive(Debug, Clone, Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentDefinition {
    pub name: String,
    pub description: String,
    /// Plugin namespace for plugin-backed agents only.
    #[serde(skip)]
    pub plugin_name: Option<String>,
    #[serde(default = "default_prompt_mode")]
    pub prompt_mode: PromptMode,
    #[serde(default = "default_kigi_toolset")]
    pub tool_config: ToolServerConfig,
    /// Runtime capability mode that constrains which tool kinds the agent
    /// can use. Applied during subagent spawn in `handle_subagent_request`
    /// by filtering the definition's `tool_config` before session creation.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub capability_mode: Option<kigi_tool_types::SubagentCapabilityMode>,
    #[serde(default)]
    pub permission_mode: PermissionMode,
    #[serde(default)]
    pub skills: Vec<String>,
    /// When true (the default), the AgentBuilder discovers skills from CWD
    /// at build time and seeds mid-session skill discovery. When false,
    /// skill discovery is suppressed and the agent gets an empty skill
    /// list with no CWD-based runtime discovery.
    #[serde(default = "default_true")]
    pub discover_skills: bool,
    /// Whether to inherit the parent session's discovered skills when
    /// spawned as a subagent. Ignored for primary sessions.
    #[serde(default = "default_true")]
    pub inherit_skills: bool,
    #[serde(default = "default_true")]
    pub agents_md: bool,
    /// When true (the default), the AgentBuilder layers session-level optional
    /// tools on top of the agent's declared `tool_config`: memory_search/get,
    /// web_search, web_fetch, lsp, OpenCode write
    /// fallback, and the plan-mode tools.
    ///
    /// Set this to `false` for harnesses that need an exact, minimal toolset
    /// (e.g. the compat harness, where every advertised tool must match the
    /// model's trained schema). The agent's `tool_config` is then used
    /// verbatim with only the subagent strip applied.
    #[serde(default = "default_true")]
    pub inject_default_tools: bool,
    /// Tool allowlist. Empty = inherit all. Also carries `Agent(type)` directives.
    #[serde(default, deserialize_with = "deserialize_string_or_vec")]
    pub tools: Vec<String>,
    /// Tool denylist. `Agent(type)` entries strip spawn permissions.
    #[serde(default, deserialize_with = "deserialize_string_or_vec")]
    pub disallowed_tools: Vec<String>,
    #[serde(default)]
    pub effort: Option<Effort>,
    #[serde(default, deserialize_with = "deserialize_nonzero_u32")]
    pub max_turns: Option<u32>,
    #[serde(default)]
    pub isolation: Option<IsolationMode>,
    #[serde(default)]
    pub background: Option<bool>,
    #[serde(default)]
    pub color: Option<AgentColor>,
    #[serde(default)]
    pub initial_prompt: Option<String>,
    #[serde(default)]
    pub mcp_servers: Vec<McpServerRef>,
    #[serde(default)]
    pub mcp_inheritance: McpInheritance,
    #[serde(default, deserialize_with = "deserialize_hooks_map")]
    pub hooks: Option<HooksConfig>,
    #[serde(default)]
    pub memory: Option<MemoryScope>,
    #[serde(default)]
    pub model: ModelOverride,
    /// Completion requirement — declares that this agent must call a
    /// specific tool before the turn ends.
    #[serde(default)]
    pub completion_requirement: Option<CompletionRequirement>,
    /// Subagent types this agent can spawn (derived by builder from `tools`).
    /// `None` = unrestricted, `Some([t1])` = restricted, `Some([])` = blocked.
    #[serde(skip)]
    pub allowed_subagent_types: Option<Vec<String>>,
    /// Session-operator tool restrictions (`--tools` / `--disallowed-tools`),
    /// distinct from the agent author's own `tools`/`disallowed_tools`. The
    /// builder applies them as a final clamp over the fully-assembled toolset
    /// (function + hosted), so they bind regardless of later `tool_config`
    /// mutations and compose with the agent's own filters by intersection.
    /// `None` = no session restriction.
    #[serde(skip)]
    pub session_tools_allowlist: Option<Vec<String>>,
    #[serde(skip)]
    pub session_tools_denylist: Option<Vec<String>>,
    #[serde(skip)]
    pub prompt_body: Option<String>,
    #[serde(skip)]
    pub system_prompt: TemplateOverride,
    /// First-user-message template selector. `Default` (the default) lets
    /// the shell layer build the legacy `<user_info>` + `<git_status>`
    /// prefix; `Custom` uses a caller-supplied template string.
    #[serde(default)]
    pub user_message_template: UserMessageTemplate,
    /// Where this definition was loaded from, optional if built in agent definition
    #[serde(skip)]
    pub source_path: Option<PathBuf>,
    /// Discovery scope (project vs user).
    #[serde(skip)]
    pub scope: AgentScope,
}
/// Declares that the agent must call a specific tool before the turn ends.
#[derive(Debug, Clone, Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionRequirement {
    /// Canonical tool name that must be called.
    pub tool: String,
    /// Reminder text injected when the tool hasn't been called.
    pub reminder: String,
    /// Suggested recovery policy for the harness.
    #[serde(default)]
    pub recovery: Option<RecoveryPolicy>,
}
/// Suggested turn-level recovery policy.
#[derive(Debug, Clone, Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RecoveryPolicy {
    pub max_retries: u32,
    pub base_delay_ms: u64,
    pub max_delay_ms: u64,
}
/// Per-tool execution config.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolExecConfig {
    /// Retry config for this tool. None = no retry (execute once).
    #[serde(default)]
    pub retry: Option<ToolRetryConfig>,
}
/// Retry configuration for a single tool.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolRetryConfig {
    pub max_retries: u32,
    pub base_delay_ms: u64,
    pub max_delay_ms: u64,
}
/// How the Markdown body interacts with the base prompt template.
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PromptMode {
    /// Body is appended to the base template (tool conventions,
    /// formatting rules, user_info). Default.
    #[default]
    Extend,
    /// Body IS the complete system prompt. No base template.
    Full,
}
fn default_prompt_mode() -> PromptMode {
    PromptMode::Extend
}
/// Where the agent definition was discovered.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum AgentScope {
    /// .kigi/agents/ (project-level, highest priority)
    Project,
    /// ~/.kigi/agents/ (user-level)
    User,
    /// ~/.kigi/bundled/agents/ (lowest-priority bundled cache)
    Bundled,
    /// Built-in agent (e.g., default_kigi(), browser_use()).
    #[default]
    BuiltIn,
}
impl AgentScope {
    pub fn label(self) -> &'static str {
        match self {
            Self::Project => "project",
            Self::User => "user",
            Self::Bundled => "bundled",
            Self::BuiltIn => "built-in",
        }
    }
}
impl std::fmt::Display for AgentScope {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.label())
    }
}
/// Controls which parent MCP servers a subagent inherits.
///
/// Deserializes from:
/// - `"all"` / `"none"` (string, case-insensitive)
/// - `{ "named": ["slack", "github"] }` / `{ "except": ["internal"] }` (map)
///
/// The custom `Deserialize` is needed because `serde_yaml` 0.9 uses YAML
/// tags (`!named`) for externally-tagged enum data variants, but agent
/// definition frontmatter uses the mapping style that JSON also expects.
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum McpInheritance {
    #[default]
    All,
    None,
    Named(Vec<String>),
    Except(Vec<String>),
}
impl<'de> Deserialize<'de> for McpInheritance {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de;
        struct McpInheritanceVisitor;
        impl<'de> de::Visitor<'de> for McpInheritanceVisitor {
            type Value = McpInheritance;
            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                f.write_str(r#""all", "none", {"named": [...]}, or {"except": [...]}"#)
            }
            fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
                match s.to_ascii_lowercase().as_str() {
                    "all" => Ok(McpInheritance::All),
                    "none" => Ok(McpInheritance::None),
                    other => Err(de::Error::unknown_variant(other, &["all", "none"])),
                }
            }
            fn visit_map<A: de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
                let key: String = map
                    .next_key()?
                    .ok_or_else(|| de::Error::invalid_length(0, &"a single-key map"))?;
                let value: Vec<String> = map.next_value()?;
                if map.next_key::<de::IgnoredAny>()?.is_some() {
                    return Err(de::Error::custom(
                        "mcpInheritance map must have exactly one key",
                    ));
                }
                match key.as_str() {
                    "named" => Ok(McpInheritance::Named(value)),
                    "except" => Ok(McpInheritance::Except(value)),
                    other => Err(de::Error::unknown_variant(other, &["named", "except"])),
                }
            }
        }
        deserializer.deserialize_any(McpInheritanceVisitor)
    }
}
/// Permission mode. Only `BypassPermissions` is wired at spawn; others are forward-compat.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, serde::Serialize, strum::EnumCount)]
#[serde(rename_all = "camelCase")]
pub enum PermissionMode {
    #[default]
    Default,
    AcceptEdits,
    /// Background classifier reviews tool calls.
    Auto,
    /// Silently deny non-pre-approved tools.
    DontAsk,
    BypassPermissions,
    Plan,
}
impl PermissionMode {
    pub const VALID_VALUES: &[&str] = &[
        "default",
        "acceptEdits",
        "auto",
        "dontAsk",
        "bypassPermissions",
        "plan",
    ];
}
const _: () =
    assert!(PermissionMode::VALID_VALUES.len() == <PermissionMode as strum::EnumCount>::COUNT);
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Deserialize,
    serde::Serialize,
    IntoStaticStr,
    strum::EnumCount,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum Effort {
    Low,
    Medium,
    High,
    #[serde(rename = "xhigh")]
    #[strum(serialize = "xhigh")]
    XHigh,
    Max,
}
impl Effort {
    pub const VALID_VALUES: &[&str] = &["low", "medium", "high", "xhigh", "max"];
}
const _: () = assert!(Effort::VALID_VALUES.len() == <Effort as strum::EnumCount>::COUNT);
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Deserialize,
    serde::Serialize,
    IntoStaticStr,
    strum::EnumCount,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum IsolationMode {
    None,
    Worktree,
}
impl IsolationMode {
    pub const VALID_VALUES: &[&str] = &["none", "worktree"];
}
const _: () =
    assert!(IsolationMode::VALID_VALUES.len() == <IsolationMode as strum::EnumCount>::COUNT);
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Deserialize,
    serde::Serialize,
    IntoStaticStr,
    strum::EnumCount,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum AgentColor {
    Red,
    Blue,
    Green,
    Yellow,
    Purple,
    Orange,
    Pink,
    Cyan,
}
impl AgentColor {
    pub const VALID_VALUES: &[&str] = &[
        "red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan",
    ];
}
const _: () = assert!(AgentColor::VALID_VALUES.len() == <AgentColor as strum::EnumCount>::COUNT);
/// Agent memory scope. Distinct from `storage::MemoryScope` (global-vs-workspace write target).
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Deserialize,
    serde::Serialize,
    IntoStaticStr,
    strum::EnumCount,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum MemoryScope {
    /// `~/.kigi/agent-memory/<name>/`
    User,
    /// `<project>/.kigi/agent-memory/<name>/`
    Project,
    /// `<project>/.kigi/agent-memory-local/<name>/`
    Local,
}
impl MemoryScope {
    pub const VALID_VALUES: &[&str] = &["user", "project", "local"];
}
const _: () = assert!(MemoryScope::VALID_VALUES.len() == <MemoryScope as strum::EnumCount>::COUNT);
#[derive(Debug)]
pub struct ResolvedMemoryDir {
    pub path: std::path::PathBuf,
    /// No workspace hash needed (already project-scoped).
    pub is_project_scoped: bool,
}
impl MemoryScope {
    pub fn resolve_dir(self, agent_name: &str, project_cwd: &std::path::Path) -> ResolvedMemoryDir {
        match self {
            Self::User => ResolvedMemoryDir {
                path: kigi_config::kigi_home()
                    .join("agent-memory")
                    .join(agent_name),
                is_project_scoped: false,
            },
            Self::Project => ResolvedMemoryDir {
                path: project_cwd.join(".kigi/agent-memory").join(agent_name),
                is_project_scoped: true,
            },
            Self::Local => ResolvedMemoryDir {
                path: project_cwd
                    .join(".kigi/agent-memory-local")
                    .join(agent_name),
                is_project_scoped: true,
            },
        }
    }
}
/// Hooks config validated as an object at parse time. Semantic parsing deferred to spawn.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct HooksConfig(pub serde_json::Map<String, serde_json::Value>);
impl HooksConfig {
    pub fn as_value(&self) -> serde_json::Value {
        serde_json::Value::Object(self.0.clone())
    }
}
fn deserialize_hooks_map<'de, D>(deserializer: D) -> Result<Option<HooksConfig>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let opt = Option::<serde_json::Value>::deserialize(deserializer)?;
    match opt {
        None => Ok(None),
        Some(serde_json::Value::Object(map)) => Ok(Some(HooksConfig(map))),
        Some(_) => Err(serde::de::Error::custom("hooks must be an object")),
    }
}
/// MCP server reference — typed to catch config errors at parse time.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum McpServerRef {
    Named(String),
    /// Opaque JSON config resolved to `McpServer` at spawn time.
    Inline {
        name: String,
        config: serde_json::Value,
    },
}
impl<'de> Deserialize<'de> for McpServerRef {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let val = serde_json::Value::deserialize(deserializer)?;
        match val {
            serde_json::Value::String(s) => Ok(McpServerRef::Named(s)),
            serde_json::Value::Object(obj) if obj.len() == 1 => {
                let (name, config) = obj.into_iter().next().unwrap();
                if !config.is_object() {
                    return Err(serde::de::Error::custom(format!(
                        "mcpServers inline config for '{name}' must be an object"
                    )));
                }
                Ok(McpServerRef::Inline { name, config })
            }
            serde_json::Value::Object(obj) => {
                if let Some(name) = obj.get("name").and_then(|v| v.as_str()) {
                    Ok(McpServerRef::Inline {
                        name: name.to_string(),
                        config: serde_json::Value::Object(obj),
                    })
                } else {
                    Err(serde::de::Error::custom(
                        "mcpServers entry must be a string, a {name: config} map, \
                         or an object with a 'name' field",
                    ))
                }
            }
            _ => Err(serde::de::Error::custom(
                "mcpServers entry must be a string or object",
            )),
        }
    }
}
impl serde::Serialize for McpServerRef {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            McpServerRef::Named(s) => serializer.serialize_str(s),
            McpServerRef::Inline { name, config } => {
                use serde::ser::SerializeMap;
                let mut map = serializer.serialize_map(Some(1))?;
                map.serialize_entry(name, config)?;
                map.end()
            }
        }
    }
}
/// Bash tool config overrides (agent-definition layer).
///
/// NOTE: Uses `camelCase` for YAML frontmatter. The `AgentBuilder` maps
/// these into `kigi_tools::registry::types::ToolsetConfig.bash`
/// which uses the tools crate's `BashToolConfig` type.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BashConfig {
    #[serde(default = "default_timeout_secs")]
    pub timeout_secs: f64,
    #[serde(default = "default_output_byte_limit")]
    pub output_byte_limit: usize,
    #[serde(default)]
    pub cmd_prefix: Option<String>,
}
impl Default for BashConfig {
    fn default() -> Self {
        Self {
            timeout_secs: default_timeout_secs(),
            output_byte_limit: default_output_byte_limit(),
            cmd_prefix: None,
        }
    }
}
fn default_timeout_secs() -> f64 {
    120.0
}
fn default_output_byte_limit() -> usize {
    200_000
}
fn default_true() -> bool {
    true
}
/// Strip a tool id's `Namespace:` prefix, yielding its short name.
pub(crate) fn short_tool_name(id: &str) -> &str {
    id.rsplit(':').next().unwrap_or(id)
}
/// Whether an allow/deny `entry` refers to tool `id` (by full id or short name).
pub(crate) fn tool_id_eq(entry: &str, id: &str) -> bool {
    entry == id || entry == short_tool_name(id)
}
/// Whether any `list` entry refers to tool `id`.
pub(crate) fn tool_id_matches(list: &[String], id: &str) -> bool {
    list.iter().any(|e| tool_id_eq(e, id))
}
impl AgentDefinition {
    /// Parse an agent definition from a Markdown file with YAML frontmatter.
    ///
    /// File format:
    /// ```text
    /// ---
    /// name: my-agent
    /// description: A custom agent
    /// # ... other fields
    /// ---
    ///
    /// System prompt body goes here...
    /// ```
    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, AgentBuildError> {
        let path = path.as_ref();
        let content = std::fs::read_to_string(path).map_err(AgentBuildError::IoError)?;
        let mut def = Self::parse(&content)?;
        def.source_path = Some(path.to_path_buf());
        def.plugin_name = None;
        def.scope = Self::scope_from_path(path);
        Ok(def)
    }
    /// Parse only YAML frontmatter from an agent file, leaving prompt_body unset.
    pub fn from_file_frontmatter_only(path: impl AsRef<Path>) -> Result<Self, AgentBuildError> {
        let path = path.as_ref();
        let content = std::fs::read_to_string(path).map_err(AgentBuildError::IoError)?;
        let trimmed = content.trim_start();
        if !trimmed.starts_with("---") {
            return Err(AgentBuildError::ParseError(
                "missing frontmatter delimiters".to_string(),
            ));
        }
        let after_opening = &trimmed[3..];
        let closing_idx = after_opening.find("\n---").ok_or_else(|| {
            AgentBuildError::ParseError("missing closing frontmatter delimiter".to_string())
        })?;
        let yaml_content = &after_opening[..closing_idx];
        let mut def: AgentDefinition = serde_yaml::from_str(yaml_content)
            .map_err(|e| AgentBuildError::ParseError(e.to_string()))?;
        def.prompt_body = None;
        def.system_prompt = TemplateOverride::None;
        def.source_path = Some(path.to_path_buf());
        def.plugin_name = None;
        def.scope = Self::scope_from_path(path);
        Ok(def)
    }
    /// Parse from string content (for testing and inline definitions).
    pub fn parse(content: &str) -> Result<Self, AgentBuildError> {
        let trimmed = content.trim_start();
        if !trimmed.starts_with("---") {
            return Err(AgentBuildError::ParseError(
                "missing frontmatter delimiters".to_string(),
            ));
        }
        let after_opening = &trimmed[3..];
        let closing_idx = after_opening.find("\n---").ok_or_else(|| {
            AgentBuildError::ParseError("missing closing frontmatter delimiter".to_string())
        })?;
        let yaml_content = &after_opening[..closing_idx];
        let after_closing = &after_opening[closing_idx + 4..];
        let body_start = after_closing.find('\n').map(|i| i + 1).unwrap_or(0);
        let body = after_closing[body_start..].trim();
        let prompt_body = if body.is_empty() {
            None
        } else {
            Some(body.to_string())
        };
        let mut def: AgentDefinition = serde_yaml::from_str(yaml_content)
            .map_err(|e| AgentBuildError::ParseError(e.to_string()))?;
        def.prompt_body = prompt_body;
        def.plugin_name = None;
        Ok(def)
    }
    /// Determine the scope of a definition file based on its path.
    fn scope_from_path(path: &Path) -> AgentScope {
        let path_str = path.to_string_lossy();
        let kigi = kigi_config::user_kigi_home();
        let home = dirs::home_dir();
        for (dir, scope) in crate::discovery::user_agent_dirs(home.as_deref(), kigi.as_deref()) {
            if path.starts_with(&dir) {
                return scope;
            }
        }
        if path_str.contains(".kigi/agents/") || path_str.contains(".kigi\\agents\\") {
            return AgentScope::Project;
        }
        if path_str.contains(".kigi/bundled/agents/")
            || path_str.contains(".kigi\\bundled\\agents\\")
        {
            return AgentScope::Bundled;
        }
        AgentScope::BuiltIn
    }
}
impl AgentDefinition {
    /// Whether `id` passes the session-operator clamp: denylist wins, then an
    /// unset allowlist allows all.
    pub(crate) fn session_tools_allowed(&self, id: &str) -> bool {
        if self
            .session_tools_denylist
            .as_deref()
            .is_some_and(|d| tool_id_matches(d, id))
        {
            return false;
        }
        self.session_tools_allowlist
            .as_deref()
            .is_none_or(|a| tool_id_matches(a, id))
    }
    /// Whether a hosted/server-side tool `id` survives the agent's own
    /// `disallowed_tools`/`tools` and the session clamp. Hosted tools aren't in
    /// `tool_config`, so they're gated by name here — and strictly (no
    /// compat-name mapping or unresolved-entry fallback like the function path).
    pub(crate) fn hosted_tool_allowed(&self, id: &str) -> bool {
        if tool_id_matches(&self.disallowed_tools, id) {
            return false;
        }
        if !self.tools.is_empty() && !tool_id_matches(&self.tools, id) {
            return false;
        }
        self.session_tools_allowed(id)
    }
    /// Replace the file-operation tools (read/edit/search) in the tool config
    /// with the given set. Used by the shell layer to swap from standard to
    /// hashline toolset based on `config.toml` / remote settings.
    /// True iff the active system prompt template for `audience` carries
    /// the `<task_completion_discipline>` block.
    ///
    /// Used by the runtime turn-end TodoGate to gate firing on sessions
    /// whose prompt actually references the rules the gate's reminder
    /// text invokes. The block has been removed from every built-in
    /// template, so this returns `false` unconditionally. Kept as a
    /// helper so the gate's call-site stays stable in case the block
    /// is reintroduced behind a future flag.
    pub fn carries_task_completion_discipline(
        &self,
        _audience: crate::prompt::context::PromptAudience,
    ) -> bool {
        false
    }
    /// True iff this agent's wire format is non-interchangeable with the
    /// stock harness, so a client-supplied `_meta.agentProfile` must NOT
    /// override it. Strict iff any of: bespoke `system_prompt` template,
    /// bespoke `user_message_template`, or curated toolset
    /// (`!inject_default_tools`). Stock `kigi*` agents leave all
    /// three at defaults and are non-strict.
    pub fn is_strict_harness(&self) -> bool {
        use crate::prompt::context::TemplateOverride;
        use crate::prompt::user_message::UserMessageTemplate;
        let prompt_is_custom = !matches!(self.system_prompt, TemplateOverride::None);
        let user_template_is_custom =
            !matches!(self.user_message_template, UserMessageTemplate::Default);
        let toolset_is_curated = !self.inject_default_tools;
        prompt_is_custom || user_template_is_custom || toolset_is_curated
    }
    /// Swap the definition's file tools for the equivalents in `file_tools`
    /// (hashline vs standard), slot by slot — never granting a slot the
    /// definition doesn't already have (read-only toolsets stay read-only).
    pub fn override_file_tools(
        &mut self,
        file_tools: Vec<kigi_tools::registry::types::ToolConfig>,
    ) {
        const FILE_TOOL_SLOTS: &[[&str; 2]] = &[
            ["Kigi:read_file", "KigiHashline:hashline_read"],
            ["Kigi:search_replace", "KigiHashline:hashline_edit"],
            ["Kigi:grep", "KigiHashline:hashline_grep"],
        ];
        for tool in self.tool_config.tools.iter_mut() {
            let Some(slot) = FILE_TOOL_SLOTS
                .iter()
                .find(|slot| slot.contains(&tool.id.as_str()))
            else {
                continue;
            };
            if let Some(replacement) = file_tools.iter().find(|ft| slot.contains(&ft.id.as_str())) {
                *tool = replacement.clone();
            }
        }
    }
    /// Shared defaults for built-in constructors.
    fn base(name: BuiltinAgentName, description: &str) -> Self {
        Self::builtin_defaults(name.as_ref(), description)
    }
    /// Shared defaults for out-of-tree built-in agent registrations.
    pub fn builtin_defaults(name: &str, description: &str) -> Self {
        Self {
            name: name.to_owned(),
            description: description.to_string(),
            plugin_name: None,
            prompt_mode: PromptMode::Extend,
            tool_config: default_kigi_toolset(),
            capability_mode: None,
            permission_mode: PermissionMode::Default,
            skills: vec![],
            agents_md: true,
            discover_skills: true,
            inherit_skills: true,
            inject_default_tools: true,
            disallowed_tools: vec![],
            tools: vec![],
            effort: None,
            max_turns: None,
            isolation: None,
            background: None,
            color: None,
            initial_prompt: None,
            mcp_servers: vec![],
            mcp_inheritance: McpInheritance::All,
            hooks: None,
            memory: None,
            allowed_subagent_types: None,
            session_tools_allowlist: None,
            session_tools_denylist: None,
            model: ModelOverride::Inherit,
            completion_requirement: None,
            prompt_body: None,
            system_prompt: TemplateOverride::None,
            source_path: None,
            user_message_template: UserMessageTemplate::Default,
            scope: AgentScope::BuiltIn,
        }
    }
    pub fn default_kigi() -> Self {
        Self::base(
            BuiltinAgentName::Kigi,
            "Kigi agent for software engineering tasks.",
        )
    }
    /// Kigi Concise agent definition — concise output format for SFT/RL.
    pub fn kigi_concise() -> Self {
        Self {
            tool_config: kigi_concise_toolset(),
            agents_md: false,
            ..Self::base(
                BuiltinAgentName::KigiConcise,
                "Kigi agent with concise output format.",
            )
        }
    }
    /// Kigi agent with plan mode tools.
    pub fn kigi_plan() -> Self {
        Self {
            tool_config: kigi_plan_toolset(),
            ..Self::base(
                BuiltinAgentName::KigiPlan,
                "Kigi agent with plan mode support.",
            )
        }
    }
    /// Kigi + plan mode WITHOUT subagent tools.
    pub fn kigi_plan_no_subagents() -> Self {
        Self {
            tool_config: kigi_plan_no_subagents_toolset(),
            ..Self::base(
                BuiltinAgentName::KigiPlanNoSubagents,
                "Kigi agent with plan mode (no subagents).",
            )
        }
    }
    /// Default Kigi agent with the `ask_user_question` tool.
    pub fn kigi_ask_user() -> Self {
        Self {
            tool_config: kigi_ask_user_toolset(),
            ..Self::base(
                BuiltinAgentName::KigiAskUser,
                "Kigi agent with ask-user-question tool.",
            )
        }
    }
    pub fn codex() -> Self {
        Self {
            tool_config: codex_toolset(),
            system_prompt: TemplateOverride::Codex,
            ..Self::base(BuiltinAgentName::Codex, "Codex toolset and prompt")
        }
    }
    pub fn opencode() -> Self {
        Self {
            tool_config: opencode_toolset(),
            ..Self::base(
                BuiltinAgentName::Opencode,
                "OpenCode toolset — opencode-style tools and parameter conventions",
            )
        }
    }
    /// General-purpose subagent definition.
    pub fn general_purpose() -> Self {
        use crate::prompt::subagent_prompts;
        Self {
            description: kigi_tool_types::GENERAL_PURPOSE_SUBAGENT
                .description
                .to_string(),
            prompt_body: Some(subagent_prompts::GENERAL_PURPOSE_PROMPT.to_string()),
            ..Self::base(BuiltinAgentName::GeneralPurpose, "")
        }
    }
    /// Explore subagent — fast, read-only codebase exploration.
    pub fn explore() -> Self {
        use crate::prompt::subagent_prompts;
        Self {
            description: kigi_tool_types::EXPLORE_SUBAGENT.description.to_string(),
            tool_config: explore_toolset(),
            permission_mode: PermissionMode::Plan,
            prompt_body: Some(subagent_prompts::EXPLORE_PROMPT.to_string()),
            inherit_skills: false,
            ..Self::base(BuiltinAgentName::Explore, "")
        }
    }
    /// Plan subagent — read-only architect for implementation plans.
    pub fn plan() -> Self {
        use crate::prompt::subagent_prompts;
        Self {
            description: kigi_tool_types::PLAN_SUBAGENT.description.to_string(),
            tool_config: plan_toolset(),
            permission_mode: PermissionMode::Plan,
            prompt_body: Some(subagent_prompts::PLAN_PROMPT.to_string()),
            inherit_skills: false,
            ..Self::base(BuiltinAgentName::Plan, "")
        }
    }
    /// Browser Use agent definition.
    pub fn browser_use() -> Self {
        Self {
            prompt_mode: PromptMode::Full,
            agents_md: false,
            prompt_body: Some(
                "You are a web browsing agent. You can navigate, interact with, and \
                 extract information from web pages. Use the available browsing tools \
                 to complete the user's request."
                    .to_string(),
            ),
            ..Self::base(
                BuiltinAgentName::BrowserUse,
                "Web browsing and interaction agent.",
            )
        }
    }
    /// Kigi Orchestrator — GBL model with full Kigi tools
    /// (skills, MCPs, plan mode) that delegates coding/exploration to
    /// subagents.
    ///
    /// Subagent overrides are applied in `handle_subagent_request`:
    /// general-purpose children get `implementer_toolset()` and explore
    /// children get `explorer_toolset()`, both with the subagent model.
    pub fn kigi_orchestrator() -> Self {
        Self {
            tool_config: orchestrator_toolset(),
            inject_default_tools: false,
            prompt_body: Some(ORCHESTRATOR_PROMPT_BODY.to_string()),
            ..Self::base(
                BuiltinAgentName::KigiOrchestrator,
                "Kigi orchestrator that delegates coding to specialized subagents",
            )
        }
    }
    /// Deserialize an agent definition from a JSON value (e.g. from ACP `_meta.agentProfile`).
    ///
    /// Unlike `parse()` (which reads YAML frontmatter + Markdown body from a file),
    /// this method accepts a flat JSON object where `promptBody` is an explicit
    /// string field rather than the body below `---` delimiters.
    ///
    /// ```json
    /// {
    ///   "name": "my-agent",
    ///   "description": "A custom agent profile.",
    ///   "promptMode": "extend",
    ///   "permissionMode": "dontAsk",
    ///   "promptBody": "You are a specialized coding assistant..."
    /// }
    /// ```
    pub fn from_json(value: &serde_json::Value) -> Result<Self, AgentBuildError> {
        let mut def: AgentDefinition = serde_json::from_value(value.clone())
            .map_err(|e| AgentBuildError::ParseError(e.to_string()))?;
        if let Some(body) = value.get("promptBody").and_then(|v| v.as_str()) {
            let trimmed = body.trim();
            if !trimmed.is_empty() {
                def.prompt_body = Some(trimmed.to_string());
            }
        }
        if !value.get("toolConfig").is_some_and(|v| v.is_object()) {
            def.tool_config = default_kigi_toolset();
        }
        def.scope = AgentScope::BuiltIn;
        Ok(def)
    }
    /// Serialize to a JSON value suitable for `from_json` roundtrip.
    /// Handles `prompt_body` which is `#[serde(skip)]` on the struct.
    pub fn to_json_value(&self) -> serde_json::Value {
        let mut value = serde_json::to_value(self).expect("AgentDefinition is always serializable");
        if let Some(ref body) = self.prompt_body {
            value["promptBody"] = serde_json::Value::String(body.clone());
        }
        value
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    /// Native presets only.
    #[test]
    fn toolset_for_preset_resolves_known_names() {
        for name in [
            "kigi",
            "kigi",
            "kigi-concise",
            "kigi-plan",
            "codex",
            "explore",
            "plan",
            "kigi-computer",
            "kigi_computer",
        ] {
            assert!(
                toolset_for_preset(name).is_some(),
                "preset `{name}` should resolve"
            );
        }
        assert!(toolset_for_preset("does-not-exist").is_none());
    }
    #[test]
    fn presets_select_distinct_toolsets_by_size() {
        let gb = toolset_for_preset("kigi").unwrap();
        let plan = toolset_for_preset("plan").unwrap();
        let explore = toolset_for_preset("explore").unwrap();
        assert!(explore.tools.len() < plan.tools.len());
        assert!(plan.tools.len() < gb.tools.len());
    }
    fn kigi_computer_exclusive_ids() -> Vec<String> {
        #[allow(unused_mut)]
        let mut ids: Vec<String> = vec![
            ToolConfig::from(&kigi::GetTerminalCommandOutputTool).id,
            ToolConfig::from(&kigi::KillTerminalCommandTool).id,
        ];
        ids
    }
    #[test]
    fn kigi_computer_preset_is_curated_kigi_subset() {
        let gc = toolset_for_preset("kigi-computer").unwrap();
        let gb = toolset_for_preset("kigi").unwrap();
        let gb_ids: std::collections::HashSet<&str> =
            gb.tools.iter().map(|t| t.id.as_str()).collect();
        let exclusive_ids = kigi_computer_exclusive_ids();
        assert!(!gc.tools.is_empty());
        for t in &gc.tools {
            if exclusive_ids.contains(&t.id) {
                continue;
            }
            assert!(
                gb_ids.contains(t.id.as_str()),
                "kigi-computer tool `{}` must also ship in the kigi preset",
                t.id
            );
        }
        assert!(
            gc.tools.len() < gb.tools.len(),
            "kigi-computer should be a curated subset of kigi"
        );
    }
    #[test]
    fn kigi_computer_uses_subagent_free_background_task_tools() {
        let gc = toolset_for_preset("kigi-computer").unwrap();
        let ids: std::collections::HashSet<&str> = gc.tools.iter().map(|t| t.id.as_str()).collect();
        assert!(
            ids.contains(
                ToolConfig::from(&kigi::GetTerminalCommandOutputTool)
                    .id
                    .as_str()
            )
        );
        assert!(ids.contains(ToolConfig::from(&kigi::KillTerminalCommandTool).id.as_str()));
        assert!(!ids.contains(ToolConfig::from(&kigi::TaskOutputTool).id.as_str()));
        assert!(!ids.contains(ToolConfig::from(&kigi::KillTaskTool).id.as_str()));
        assert!(!ids.contains(ToolConfig::from(&kigi::TaskTool).id.as_str()));
        for t in &gc.tools {
            if t.id == ToolConfig::from(&kigi::GetTerminalCommandOutputTool).id
                || t.id == ToolConfig::from(&kigi::KillTerminalCommandTool).id
            {
                assert!(t.name_override.is_none(), "tool `{}` must not rename", t.id);
            }
        }
    }
    /// The kigi-computer preset must ship a full-file write tool (legacy
    /// `write_file` parity) — the same OpenCode `write` tool the kigi
    /// preset uses. Guards against `search_replace` being the only
    /// file-mutation path, which has no single-tool full-rewrite when the
    /// empty-old_string overwrite guard is enabled.
    #[test]
    fn kigi_computer_preset_includes_write_tool() {
        let gc = toolset_for_preset("kigi-computer").unwrap();
        let write_id = ToolConfig::from(&opencode::OpenCodeWriteTool).id;
        assert!(
            gc.tools.iter().any(|t| t.id == write_id),
            "kigi-computer preset must include the `{write_id}` tool"
        );
    }
    #[test]
    fn kigi_computer_preset_excludes_plan_and_lsp() {
        let gc = toolset_for_preset("kigi-computer").unwrap();
        let gc_ids: std::collections::HashSet<&str> =
            gc.tools.iter().map(|t| t.id.as_str()).collect();
        for excluded in [
            ToolConfig::from(&kigi::LspTool).id,
            ToolConfig::from(&kigi::EnterPlanModeTool).id,
            ToolConfig::from(&kigi::ExitPlanModeTool).id,
        ] {
            assert!(
                !gc_ids.contains(excluded.as_str()),
                "kigi-computer preset must not advertise `{excluded}`"
            );
        }
        let full = workspace_kigi_toolset();
        let full_ids: std::collections::HashSet<&str> =
            full.tools.iter().map(|t| t.id.as_str()).collect();
        for present in [
            ToolConfig::from(&kigi::LspTool).id,
            ToolConfig::from(&kigi::EnterPlanModeTool).id,
            ToolConfig::from(&kigi::ExitPlanModeTool).id,
        ] {
            assert!(
                full_ids.contains(present.as_str()),
                "workspace_kigi_toolset must ship `{present}`"
            );
        }
    }
    /// Exhaustive match → adding a new `BuiltinAgentName` won't compile
    /// until classified.
    fn expected_strict_harness(name: BuiltinAgentName) -> bool {
        match name {
            BuiltinAgentName::Codex | BuiltinAgentName::KigiOrchestrator => true,
            BuiltinAgentName::Kigi
            | BuiltinAgentName::KigiConcise
            | BuiltinAgentName::KigiPlan
            | BuiltinAgentName::KigiPlanNoSubagents
            | BuiltinAgentName::KigiAskUser
            | BuiltinAgentName::GeneralPurpose
            | BuiltinAgentName::Explore
            | BuiltinAgentName::Plan
            | BuiltinAgentName::Opencode
            | BuiltinAgentName::BrowserUse => false,
        }
    }
    /// Invariant: structural `is_strict_harness()` must match the
    /// hand-classified expectation for every built-in variant.
    #[test]
    fn is_strict_harness_matches_structural_classification_for_all_builtins() {
        use strum::IntoEnumIterator;
        for variant in BuiltinAgentName::iter() {
            let structural = variant.definition().is_strict_harness();
            let expected = expected_strict_harness(variant);
            assert_eq!(
                structural, expected,
                "BuiltinAgentName::{variant:?}: structural={structural} but \
                 expected={expected}. Update `expected_strict_harness` if the \
                 change is intentional.",
            );
        }
    }
    #[test]
    fn is_strict_harness_agent_type_classifies_by_name() {
        for strict in ["codex", "kigi-orchestrator"] {
            assert!(
                is_strict_harness_agent_type(strict),
                "{strict} should be strict"
            );
        }
        for non_strict in [
            "kigi",
            "kigi-plan",
            "kigi-concise",
            "kigi-ask-user",
            "opencode",
            "browser-use",
            "custom-user-agent",
            "",
            "kigi-totally-made-up",
        ] {
            assert!(
                !is_strict_harness_agent_type(non_strict),
                "{non_strict} should be non-strict",
            );
        }
    }
    #[test]
    fn test_parse_valid_full_definition() {
        let content = r#"---
name: test-agent
description: A test agent
promptMode: full
tools:
  - read_file
  - grep
permissionMode: plan
agentsMd: false
---

You are a test agent.
"#;
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(def.name, "test-agent");
        assert_eq!(def.description, "A test agent");
        assert_eq!(def.prompt_mode, PromptMode::Full);
        assert_eq!(def.permission_mode, PermissionMode::Plan);
        assert!(!def.agents_md);
        assert_eq!(def.prompt_body.as_deref(), Some("You are a test agent."));
        assert_eq!(
            def.tools,
            vec!["read_file".to_string(), "grep".to_string()],
            "tools allowlist must be parsed from YAML frontmatter"
        );
    }
    #[test]
    fn test_parse_tools_and_disallowed_together() {
        let content = r#"---
name: mixed-tools
description: Both tools and disallowedTools
tools:
  - read_file
  - grep
  - search_replace
  - task
disallowedTools:
  - search_replace
---

Mixed agent.
"#;
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(def.tools.len(), 4, "tools allowlist should have 4 entries");
        assert_eq!(
            def.disallowed_tools,
            vec!["search_replace".to_string()],
            "disallowedTools should have 1 entry"
        );
    }
    #[test]
    fn test_parse_tools_with_agent_parens() {
        let content = r#"---
name: coordinator
description: test
tools: Agent(worker, researcher), Read, Bash
---

Agent.
"#;
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(
            def.tools,
            vec!["Agent(worker, researcher)", "Read", "Bash"],
            "Agent(a, b) must be kept as a single token"
        );
    }
    #[test]
    fn test_parse_tools_comma_separated() {
        let content = r#"---
name: comma-tools
description: Comma-separated tools
tools: read_file, grep, list_dir
disallowedTools: search_replace, write
---

Agent.
"#;
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(
            def.tools,
            vec!["read_file", "grep", "list_dir"],
            "comma-separated tools must parse correctly"
        );
        assert_eq!(
            def.disallowed_tools,
            vec!["search_replace", "write"],
            "comma-separated disallowedTools must parse correctly"
        );
    }
    #[test]
    fn test_parse_tools_case_insensitive_agent() {
        let content = r#"---
name: ci-test
description: test
tools: agent(worker, researcher), Read
---

Agent.
"#;
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(
            def.tools,
            vec!["agent(worker, researcher)", "Read"],
            "lowercase agent(a, b) must be kept as a single token"
        );
    }
    #[test]
    fn test_parse_max_turns_zero_rejected() {
        let content = "---\nname: test\ndescription: Test\nmaxTurns: 0\n---\n";
        let result = AgentDefinition::parse(content);
        assert!(
            result.is_err(),
            "maxTurns: 0 should be rejected at parse time"
        );
    }
    #[test]
    fn test_parse_model_empty_is_inherit() {
        let content = "---\nname: test\ndescription: Test\nmodel: \"\"\n---\n";
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(def.model, ModelOverride::Inherit);
    }
    #[test]
    fn test_parse_model_null_is_inherit() {
        let content = "---\nname: test\ndescription: Test\nmodel: ~\n---\n";
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(def.model, ModelOverride::Inherit);
    }
    #[test]
    fn test_parse_minimal_defaults_none_fields() {
        let content = "---\nname: minimal\ndescription: Test\n---\n";
        let def = AgentDefinition::parse(content).unwrap();
        assert!(def.effort.is_none());
        assert!(def.max_turns.is_none());
        assert!(def.isolation.is_none());
        assert!(def.background.is_none());
        assert!(def.color.is_none());
        assert!(def.initial_prompt.is_none());
        assert!(def.memory.is_none());
        assert!(def.hooks.is_none());
    }
    #[test]
    fn test_model_override_display_shows_id() {
        assert_eq!(ModelOverride::Inherit.to_string(), "inherit");
        assert_eq!(
            ModelOverride::Override("kigi-3-fast".to_string()).to_string(),
            "kigi-3-fast"
        );
    }
    #[test]
    fn test_parse_new_fields() {
        let content = r#"---
name: full-fields
description: All new fields
effort: high
maxTurns: 10
isolation: worktree
background: true
color: blue
initialPrompt: "hello world"
model: kigi-3
---

Agent body.
"#;
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(def.effort, Some(Effort::High));
        assert_eq!(def.max_turns, Some(10));
        assert_eq!(def.isolation, Some(IsolationMode::Worktree));
        assert_eq!(def.background, Some(true));
        assert_eq!(def.color, Some(AgentColor::Blue));
        assert_eq!(def.initial_prompt.as_deref(), Some("hello world"));
        assert_eq!(def.model, ModelOverride::Override("kigi-3".to_string()));
    }
    #[test]
    fn test_parse_minimal_definition() {
        let content = r#"---
name: minimal
description: Minimal agent
---
"#;
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(def.name, "minimal");
        assert_eq!(def.description, "Minimal agent");
        assert_eq!(def.prompt_mode, PromptMode::Extend);
        assert!(def.agents_md);
        assert!(def.prompt_body.is_none());
    }
    #[test]
    fn mcp_server_ref_parse_and_reject() {
        let v: McpServerRef = serde_json::from_value(serde_json::json!("slack")).unwrap();
        assert_eq!(v, McpServerRef::Named("slack".to_string()));
        let v: McpServerRef =
            serde_json::from_value(serde_json::json!({ "s" : { "type" : "stdio" } })).unwrap();
        assert!(matches!(v, McpServerRef::Inline { ref name, .. } if name == "s"));
        let v: McpServerRef =
            serde_json::from_value(serde_json::json!({ "name" : "s", "type" : "stdio" })).unwrap();
        assert!(matches!(v, McpServerRef::Inline { ref name, .. } if name == "s"));
        assert!(
            serde_json::from_value::<McpServerRef>(serde_json::json!({ "type" :
            "stdio" }))
            .is_err()
        );
        assert!(serde_json::from_value::<McpServerRef>(serde_json::json!(42)).is_err());
        assert!(
            serde_json::from_value::<McpServerRef>(serde_json::json!({ "s" : "bad" })).is_err()
        );
    }
    #[test]
    fn memory_scope_resolve_dir() {
        let cwd = std::path::Path::new("/project");
        let user = MemoryScope::User.resolve_dir("a", cwd);
        assert!(user.path.ends_with("agent-memory/a"));
        assert!(!user.is_project_scoped);
        let proj = MemoryScope::Project.resolve_dir("a", cwd);
        assert_eq!(
            proj.path,
            std::path::PathBuf::from("/project/.kigi/agent-memory/a")
        );
        assert!(proj.is_project_scoped);
        let local = MemoryScope::Local.resolve_dir("a", cwd);
        assert_eq!(
            local.path,
            std::path::PathBuf::from("/project/.kigi/agent-memory-local/a")
        );
        assert!(local.is_project_scoped);
    }
    #[test]
    fn all_new_enum_variants_parse() {
        for effort in Effort::VALID_VALUES {
            let c = format!("---\nname: t\ndescription: t\neffort: {effort}\n---\n");
            assert!(
                AgentDefinition::parse(&c).unwrap().effort.is_some(),
                "effort: {effort}"
            );
        }
        for iso in IsolationMode::VALID_VALUES {
            let c = format!("---\nname: t\ndescription: t\nisolation: {iso}\n---\n");
            assert!(
                AgentDefinition::parse(&c).unwrap().isolation.is_some(),
                "isolation: {iso}"
            );
        }
        for color in AgentColor::VALID_VALUES {
            let c = format!("---\nname: t\ndescription: t\ncolor: {color}\n---\n");
            assert!(
                AgentDefinition::parse(&c).unwrap().color.is_some(),
                "color: {color}"
            );
        }
        for memory in MemoryScope::VALID_VALUES {
            let c = format!("---\nname: t\ndescription: t\nmemory: {memory}\n---\n");
            assert!(
                AgentDefinition::parse(&c).unwrap().memory.is_some(),
                "memory: {memory}"
            );
        }
    }
    #[test]
    fn test_parse_missing_name() {
        let content = r#"---
description: No name
---
"#;
        let result = AgentDefinition::parse(content);
        assert!(result.is_err());
        match result.unwrap_err() {
            AgentBuildError::ParseError(msg) => {
                assert!(msg.contains("name"), "Error should mention 'name': {}", msg);
            }
            e => panic!("Expected ParseError, got: {:?}", e),
        }
    }
    #[test]
    fn test_parse_missing_delimiters() {
        let content = "Just some text without frontmatter";
        let result = AgentDefinition::parse(content);
        assert!(result.is_err());
        match result.unwrap_err() {
            AgentBuildError::ParseError(msg) => {
                assert!(msg.contains("delimiter") || msg.contains("frontmatter"));
            }
            e => panic!("Expected ParseError, got: {:?}", e),
        }
    }
    #[test]
    fn test_parse_unknown_fields_ignored() {
        let content = r#"---
name: test
description: Test
hooks:
  PreToolUse:
    - matcher: Bash
      hooks:
        - type: command
          command: echo hi
memory: user
unknownField: value
---
"#;
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(def.name, "test");
    }
    #[test]
    fn test_parse_completion_requirement() {
        let content = r#"---
name: completion-test
description: Test completion requirement parsing
completionRequirement:
  tool: my_agent__complete_task
  reminder: You must call complete_task
  recovery:
    maxRetries: 5
    baseDelayMs: 5000
    maxDelayMs: 60000
---
"#;
        let def = AgentDefinition::parse(content).unwrap();
        let req = def.completion_requirement.unwrap();
        assert_eq!(req.tool, "my_agent__complete_task");
        assert_eq!(req.reminder, "You must call complete_task");
        let recovery = req.recovery.unwrap();
        assert_eq!(recovery.max_retries, 5);
        assert_eq!(recovery.base_delay_ms, 5000);
        assert_eq!(recovery.max_delay_ms, 60000);
    }
    #[test]
    fn test_builtin_browser_use() {
        let def = AgentDefinition::browser_use();
        assert_eq!(def.name, "browser-use");
        assert_eq!(def.prompt_mode, PromptMode::Full);
        assert!(!def.agents_md);
    }
    #[test]
    fn test_completion_requirement_round_trips() {
        let content = r#"---
name: roundtrip
description: Test round-trip
completionRequirement:
  tool: my__complete
  reminder: Please complete
  recovery:
    maxRetries: 3
    baseDelayMs: 1000
    maxDelayMs: 10000
---
"#;
        let def = AgentDefinition::parse(content).unwrap();
        let req = def.completion_requirement.as_ref().unwrap();
        assert_eq!(req.tool, "my__complete");
        assert_eq!(req.reminder, "Please complete");
        let rec = req.recovery.as_ref().unwrap();
        assert_eq!(rec.max_retries, 3);
        assert_eq!(rec.base_delay_ms, 1000);
        assert_eq!(rec.max_delay_ms, 10000);
    }
    #[test]
    fn test_default_tool_config_has_kigi_tools() {
        let content = r#"---
name: default-tools
description: Test default tool config
---
"#;
        let def = AgentDefinition::parse(content).unwrap();
        assert!(
            !def.tool_config.tools.is_empty(),
            "default tool_config should have kigi tools"
        );
    }
    #[test]
    fn test_permission_mode_round_trips() {
        for v in PermissionMode::VALID_VALUES {
            let content = format!("---\nname: test\ndescription: Test\npermissionMode: {v}\n---\n");
            AgentDefinition::parse(&content)
                .unwrap_or_else(|e| panic!("PermissionMode '{v}' failed parse: {e}"));
        }
    }
    #[test]
    fn test_prompt_mode_round_trips() {
        for (yaml_val, expected) in [("extend", PromptMode::Extend), ("full", PromptMode::Full)] {
            let content =
                format!("---\nname: test\ndescription: Test\npromptMode: {yaml_val}\n---\n");
            let def = AgentDefinition::parse(&content).unwrap();
            assert_eq!(def.prompt_mode, expected, "Failed for: {yaml_val}");
        }
    }
    #[test]
    fn test_from_file_sets_scope_and_path() {
        let tmp = tempfile::tempdir().unwrap();
        let file_path = tmp.path().join("test-agent.md");
        std::fs::write(
            &file_path,
            "---\nname: file-test\ndescription: From file\n---\n",
        )
        .unwrap();
        let def = AgentDefinition::from_file(&file_path).unwrap();
        assert_eq!(def.name, "file-test");
        assert_eq!(def.source_path, Some(file_path));
    }
    #[test]
    fn test_scope_from_path_detects_bundled_agents() {
        let tmp = tempfile::tempdir().unwrap();
        let bundled = tmp
            .path()
            .join("nested")
            .join(".kigi")
            .join("bundled")
            .join("agents")
            .join("bundled-agent.md");
        std::fs::create_dir_all(bundled.parent().unwrap()).unwrap();
        std::fs::write(
            &bundled,
            "---\nname: bundled-agent\ndescription: Bundled agent\n---\n",
        )
        .unwrap();
        let def = AgentDefinition::from_file(&bundled).unwrap();
        assert_eq!(def.scope, AgentScope::Bundled);
        assert_eq!(AgentScope::Bundled.label(), "bundled");
        assert_eq!(AgentScope::BuiltIn.label(), "built-in");
    }
    #[test]
    fn test_from_json_minimal() {
        let json = serde_json::json!(
            { "name" : "acp-agent", "description" : "An agent from ACP" }
        );
        let def = AgentDefinition::from_json(&json).unwrap();
        assert_eq!(def.name, "acp-agent");
        assert_eq!(def.description, "An agent from ACP");
        assert_eq!(def.prompt_mode, PromptMode::Extend);
        assert!(def.agents_md);
        assert!(def.prompt_body.is_none());
        assert_eq!(def.scope, AgentScope::BuiltIn);
    }
    #[test]
    fn test_from_json_has_default_toolset_with_task_tool() {
        let json = serde_json::json!(
            { "name" : "kigi", "description" : "Multi-surface coding agent.",
            "promptMode" : "extend", "permissionMode" : "dontAsk", "agentsMd" : true,
            "promptBody" : "You are a coding assistant." }
        );
        let def = AgentDefinition::from_json(&json).unwrap();
        let task_tool_id = "Kigi:task";
        assert!(
            def.tool_config.tools.iter().any(|tc| tc.id == task_tool_id),
            "from_json() without toolConfig should include TaskTool in default toolset, \
             got tool IDs: {:?}",
            def.tool_config
                .tools
                .iter()
                .map(|tc| &tc.id)
                .collect::<Vec<_>>()
        );
    }
    #[test]
    fn test_from_json_with_prompt_body() {
        let json = serde_json::json!(
            { "name" : "custom-agent", "description" : "Agent with prompt body",
            "promptBody" : "You are a specialized coding assistant.\n\nFocus on Rust." }
        );
        let def = AgentDefinition::from_json(&json).unwrap();
        assert_eq!(def.name, "custom-agent");
        assert_eq!(
            def.prompt_body.as_deref(),
            Some("You are a specialized coding assistant.\n\nFocus on Rust.")
        );
    }
    #[test]
    fn test_from_json_with_permission_mode() {
        let json = serde_json::json!(
            { "name" : "auto-accept-agent", "description" :
            "Agent with dontAsk permission mode", "permissionMode" : "dontAsk",
            "promptBody" : "## Auto-accept Mode" }
        );
        let def = AgentDefinition::from_json(&json).unwrap();
        assert_eq!(def.permission_mode, PermissionMode::DontAsk);
        assert_eq!(def.prompt_body.as_deref(), Some("## Auto-accept Mode"));
    }
    #[test]
    fn test_from_json_empty_prompt_body_is_none() {
        let json = serde_json::json!(
            { "name" : "test", "description" : "Test", "promptBody" : "   " }
        );
        let def = AgentDefinition::from_json(&json).unwrap();
        assert!(
            def.prompt_body.is_none(),
            "Whitespace-only promptBody should be None"
        );
    }
    #[test]
    fn test_from_json_missing_required_fields() {
        let json = serde_json::json!({ "description" : "Missing name" });
        let result = AgentDefinition::from_json(&json);
        assert!(result.is_err());
    }
    #[test]
    fn test_from_json_ignores_unknown_fields() {
        let json = serde_json::json!(
            { "name" : "test", "description" : "Test", "unknownField" : "value",
            "futureFeature" : true }
        );
        let def = AgentDefinition::from_json(&json).unwrap();
        assert_eq!(def.name, "test");
    }
    #[test]
    fn to_json_value_roundtrips_through_from_json() {
        let mut original = AgentDefinition::parse(
                "---\nname: test-agent\ndescription: A test\npermissionMode: dontAsk\n---\nYou are a helper.",
            )
            .unwrap();
        original.tools = vec!["read_file".to_string(), "grep".to_string()];
        original.disallowed_tools = vec!["web_search".to_string()];
        let json = original.to_json_value();
        let recovered = AgentDefinition::from_json(&json).unwrap();
        assert_eq!(recovered.name, "test-agent");
        assert_eq!(recovered.description, "A test");
        assert_eq!(recovered.prompt_body.as_deref(), Some("You are a helper."));
        assert_eq!(recovered.permission_mode, PermissionMode::DontAsk);
        assert_eq!(recovered.tools, vec!["read_file", "grep"]);
        assert_eq!(recovered.disallowed_tools, vec!["web_search"]);
    }
    #[test]
    fn test_model_override_default_is_inherit() {
        assert_eq!(ModelOverride::default(), ModelOverride::Inherit);
    }
    #[test]
    fn test_model_override_serde_inherit() {
        let yaml = "\"inherit\"";
        let m: ModelOverride = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(m, ModelOverride::Inherit);
    }
    #[test]
    fn test_model_override_serde_inherit_case_insensitive() {
        let yaml = "\"Inherit\"";
        let m: ModelOverride = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(m, ModelOverride::Inherit);
    }
    #[test]
    fn test_model_override_serde_explicit_model_id() {
        let yaml = "\"kigi-3-fast\"";
        let m: ModelOverride = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(m, ModelOverride::Override("kigi-3-fast".to_string()));
    }
    #[test]
    fn test_model_override_serialize_inherit() {
        let m = ModelOverride::Inherit;
        let s = serde_json::to_string(&m).unwrap();
        assert_eq!(s, "\"inherit\"");
    }
    #[test]
    fn test_model_override_serialize_override() {
        let m = ModelOverride::Override("kigi-3-fast".to_string());
        let s = serde_json::to_string(&m).unwrap();
        assert_eq!(s, "\"kigi-3-fast\"");
    }
    #[test]
    fn test_model_override_in_frontmatter() {
        let content = "---\nname: test\ndescription: Test\nmodel: kigi-3-fast\n---\n";
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(
            def.model,
            ModelOverride::Override("kigi-3-fast".to_string())
        );
    }
    #[test]
    fn test_model_override_in_frontmatter_inherit() {
        let content = "---\nname: test\ndescription: Test\nmodel: inherit\n---\n";
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(def.model, ModelOverride::Inherit);
    }
    #[test]
    fn test_model_override_omitted_defaults_to_inherit() {
        let content = "---\nname: test\ndescription: Test\n---\n";
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(def.model, ModelOverride::Inherit);
    }
    #[test]
    fn test_model_override_in_json() {
        let json = serde_json::json!(
            { "name" : "test", "description" : "Test", "model" : "kigi-code-fast-1" }
        );
        let def = AgentDefinition::from_json(&json).unwrap();
        assert_eq!(
            def.model,
            ModelOverride::Override("kigi-code-fast-1".to_string())
        );
    }
    #[test]
    fn test_builtin_agent_name_strum_round_trip() {
        use std::str::FromStr;
        for (s, expected) in [
            ("kigi", BuiltinAgentName::Kigi),
            ("kigi-concise", BuiltinAgentName::KigiConcise),
            ("kigi-ask-user", BuiltinAgentName::KigiAskUser),
            ("codex", BuiltinAgentName::Codex),
            ("opencode", BuiltinAgentName::Opencode),
            ("general-purpose", BuiltinAgentName::GeneralPurpose),
            ("explore", BuiltinAgentName::Explore),
            ("plan", BuiltinAgentName::Plan),
            ("browser-use", BuiltinAgentName::BrowserUse),
        ] {
            let parsed = BuiltinAgentName::from_str(s).unwrap();
            assert_eq!(parsed, expected, "from_str failed for: {s}");
            assert_eq!(parsed.as_ref(), s, "as_ref failed for: {s}");
        }
    }
    #[test]
    fn test_builtin_agent_name_unknown_returns_err() {
        use std::str::FromStr;
        assert!(BuiltinAgentName::from_str("nonexistent").is_err());
        assert!(BuiltinAgentName::from_str("not-a-builtin-agent").is_err());
    }
    /// Legacy pre-rebrand agent types (as persisted in old session files)
    /// must map onto the current `kigi*` names; everything else passes
    /// through unchanged.
    #[test]
    fn test_canonical_agent_type_maps_legacy_names() {
        use std::str::FromStr;
        let legacy = |suffix: &str| format!("{LEGACY_AGENT_TYPE_PREFIX}{suffix}");
        for (suffix, expected) in [
            ("", "kigi"),
            ("-concise", "kigi-concise"),
            ("-plan", "kigi-plan"),
            ("-plan-no-subagents", "kigi-plan-no-subagents"),
            ("-ask-user", "kigi-ask-user"),
            ("-orchestrator", "kigi-orchestrator"),
        ] {
            let name = legacy(suffix);
            assert_eq!(canonical_agent_type(&name), expected, "for {name}");
            // Every mapped name resolves to a real builtin.
            assert!(
                BuiltinAgentName::from_str(canonical_agent_type(&name).as_ref()).is_ok(),
                "legacy {name} must resolve to a builtin"
            );
        }
        // Pass-through for current and unrelated names.
        assert_eq!(canonical_agent_type("kigi-plan"), "kigi-plan");
        assert_eq!(canonical_agent_type("general-purpose"), "general-purpose");
        // A prefix match without a `-` separator is NOT a legacy alias.
        let not_alias = legacy("ish");
        assert_eq!(canonical_agent_type(&not_alias), not_alias.as_str());
    }
    /// The legacy default agent type resolves through discovery `by_name`,
    /// which is the boundary session-restore and model `agent_type` strings
    /// pass through.
    #[test]
    fn test_by_name_resolves_legacy_agent_type() {
        let legacy_plan = format!("{LEGACY_AGENT_TYPE_PREFIX}-plan");
        let def = crate::discovery::by_name(&legacy_plan).expect("legacy agent type resolves");
        assert_eq!(def.name, "kigi-plan");
        assert!(
            is_strict_harness_agent_type(&format!("{LEGACY_AGENT_TYPE_PREFIX}-concise"))
                == is_strict_harness_agent_type("kigi-concise")
        );
    }
    #[test]
    fn test_builtin_agent_name_definition_names_match() {
        use strum::IntoEnumIterator;
        for variant in BuiltinAgentName::iter() {
            let def = variant.definition();
            assert_eq!(
                def.name,
                variant.as_ref(),
                "definition().name doesn't match as_ref() for {:?}",
                variant
            );
        }
    }
    #[test]
    fn test_builtin_agent_name_subagent_variants() {
        let variants = BuiltinAgentName::subagent_variants();
        assert_eq!(variants.len(), 3);
        assert!(variants.contains(&BuiltinAgentName::GeneralPurpose));
        assert!(variants.contains(&BuiltinAgentName::Explore));
        assert!(variants.contains(&BuiltinAgentName::Plan));
    }
    #[test]
    fn test_all_builtins_have_inherit_model() {
        use strum::IntoEnumIterator;
        for variant in BuiltinAgentName::iter() {
            let def = variant.definition();
            assert_eq!(
                def.model,
                ModelOverride::Inherit,
                "Built-in {:?} should default to Inherit",
                variant
            );
        }
    }
    #[test]
    fn mcp_inheritance_default_when_omitted() {
        let def = AgentDefinition::parse("---\nname: t\ndescription: t\n---\n").unwrap();
        assert_eq!(def.mcp_inheritance, McpInheritance::All);
    }
    #[test]
    fn mcp_inheritance_all_parses() {
        let def =
            AgentDefinition::parse("---\nname: t\ndescription: t\nmcpInheritance: all\n---\n")
                .unwrap();
        assert_eq!(def.mcp_inheritance, McpInheritance::All);
    }
    #[test]
    fn mcp_inheritance_none_parses() {
        let def =
            AgentDefinition::parse("---\nname: t\ndescription: t\nmcpInheritance: none\n---\n")
                .unwrap();
        assert_eq!(def.mcp_inheritance, McpInheritance::None);
    }
    #[test]
    fn mcp_inheritance_named_parses() {
        let content = "---\nname: t\ndescription: t\nmcpInheritance:\n  named:\n    - slack\n    - github\n---\n";
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(
            def.mcp_inheritance,
            McpInheritance::Named(vec!["slack".into(), "github".into()])
        );
    }
    #[test]
    fn mcp_inheritance_except_parses() {
        let content =
            "---\nname: t\ndescription: t\nmcpInheritance:\n  except:\n    - internal\n---\n";
        let def = AgentDefinition::parse(content).unwrap();
        assert_eq!(
            def.mcp_inheritance,
            McpInheritance::Except(vec!["internal".into()])
        );
    }
    #[test]
    fn mcp_inheritance_round_trips_via_json() {
        let json = serde_json::json!(
            { "name" : "t", "description" : "t", "mcpInheritance" : { "named" : ["a",
            "b"] } }
        );
        let def = AgentDefinition::from_json(&json).unwrap();
        assert_eq!(
            def.mcp_inheritance,
            McpInheritance::Named(vec!["a".into(), "b".into()])
        );
        let serialized = def.to_json_value();
        let recovered = AgentDefinition::from_json(&serialized).unwrap();
        assert_eq!(recovered.mcp_inheritance, def.mcp_inheritance);
    }
    fn def_with_template(tpl: crate::prompt::context::TemplateOverride) -> AgentDefinition {
        let mut def = AgentDefinition::default_kigi();
        def.system_prompt = tpl;
        def
    }
    #[test]
    fn carries_discipline_false_for_every_template_and_audience() {
        for tpl in [
            crate::prompt::context::TemplateOverride::None,
            crate::prompt::context::TemplateOverride::Codex,
            crate::prompt::context::TemplateOverride::Custom("fake".to_string()),
        ] {
            let def = def_with_template(tpl.clone());
            for audience in [
                crate::prompt::context::PromptAudience::Primary,
                crate::prompt::context::PromptAudience::Subagent,
            ] {
                assert!(
                    !def.carries_task_completion_discipline(audience),
                    "discipline block was removed; helper must return false \
                     (template: {tpl:?}, audience: {audience:?})"
                );
            }
        }
    }
}
