//! `SubWorkerSpawn` tool — start a parent-owned Internal Worker session.
//!
//! Resolves a child profile, validates filesystem delegation, constructs a normal Worker with the
//! parent's explicit Workspace authority, installs its enabled features, and hands it to the
//! in-process Internal Worker session actor. No Runtime Worker record, OS process, PID, Unix socket,
//! or machine-wide child allocation is created.

use std::path::{Path, PathBuf};
use std::sync::Arc;

use agen::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use arc_swap::ArcSwap;
use async_trait::async_trait;
use fs_operation::FsPath;
use manifest::{
    CompactionConfigPartial, EngineManifestConfig, FileUploadLimitsPartial,
    PermissionConfigPartial, ProfileDiscovery, ProfileError, ProfileRegistry,
    ProfileRegistrySource, ProfileResolveOptions, ProfileResolver, ProfileSelector, ScopeConfig,
    ScopeRule, SessionConfigPartial, ToolOutputLimitsPartial, WorkerManifest, WorkerManifestConfig,
    WorkerMetaConfig,
};
use serde::Deserialize;
use tokio::sync::mpsc;
use workdir::{
    WorkdirDelegationPermission, WorkdirDelegationRequest, WorkdirDelegationRule,
    WorkdirSessionHandle,
};

use crate::PromptCatalogSource;
use crate::controller::register_worker_tools;
use crate::internal_worker::{
    EphemeralSessionStore, InternalWorkerSessionStatus, InternalWorkerVisibility,
    prepare_internal_worker_session,
};
use crate::prompt::catalog::PromptCatalog;
use crate::spawn::registry::SpawnedWorkerRegistry;
use crate::worker::{
    ReviewerChildWorkspaceClient, ReviewerContext, Worker, WorkerFilesystemAuthority,
    WorkspaceRequest, WorkspaceRequestMethod,
};
use protocol::Method;

#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct SubWorkerSpawnInput {
    /// Identifier for the spawned Internal SubWorker. Must be unique among this Worker's direct children.
    name: String,
    /// Profile selector for child role configuration. Omit or use `default`
    /// for the effective child default profile, use `inherit` to derive
    /// reusable config from the spawner, or use a registry selector such as
    /// `project:coder`, `project:reviewer`, `builtin:companion`, or an
    /// unambiguous profile slug. Raw/path selectors are rejected.
    #[serde(default)]
    profile: Option<String>,
    /// Exact catalog-root dotted Prompt name (for example `default` or `role.coder`).
    #[serde(default)]
    instruction: Option<String>,
    /// Logical Workdir-relative child tool working directory. This path is not
    /// a host path and grants no authority. When omitted, the Workdir root is used.
    #[serde(default)]
    cwd: Option<String>,
    /// First message sent to the spawned SubWorker via `Method::Run`.
    task: String,
    /// Allow rules delegated to the spawned SubWorker. Must be a subset of the
    /// spawner's explicit delegation authority; direct tool scope alone is not
    /// sufficient. Omit `recursive` for normal workspace/worktree delegation; it defaults to true.
    scope: Vec<ScopeRuleInput>,
    /// Binds an actual read-only builtin Reviewer child to the current Merge Request candidate.
    /// Review capability material is generated by the trusted spawn layer.
    #[serde(default)]
    review: Option<ReviewerHandoffInput>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct ReviewerHandoffInput {
    ticket_id: String,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct ScopeRuleInput {
    /// Logical Workdir-relative target such as `.` or `src`. Absolute host
    /// paths and parent traversal are rejected.
    target: String,
    /// `"read"` or `"write"`.
    permission: PermissionInput,
    /// When `false`, the rule matches the target itself and its direct
    /// children only. Defaults to `true`.
    #[serde(default = "default_true")]
    recursive: bool,
}

#[derive(Debug, Deserialize, schemars::JsonSchema, Clone, Copy)]
#[serde(rename_all = "lowercase")]
enum PermissionInput {
    Read,
    Write,
}

fn default_true() -> bool {
    true
}

#[derive(Debug, Clone)]
struct AvailableProfiles {
    registry: Option<ProfileRegistry>,
    diagnostic: Option<String>,
}

impl AvailableProfiles {
    fn discover(cwd: &Path) -> Self {
        match ProfileDiscovery::for_cwd(cwd).discover() {
            Ok(registry) => Self {
                registry: Some(registry),
                diagnostic: None,
            },
            Err(error) => Self {
                registry: None,
                diagnostic: Some(error.to_string()),
            },
        }
    }

    fn compact_list(&self) -> String {
        let Some(registry) = &self.registry else {
            return "- profile discovery failed; use `inherit` or retry after fixing discovery"
                .into();
        };
        if registry.entries().is_empty() {
            return "- no registry profiles discovered; `inherit` is still available".into();
        }
        registry
            .entries()
            .iter()
            .map(|entry| {
                let default = if entry.is_default { " (default)" } else { "" };
                let desc = entry
                    .description
                    .as_deref()
                    .map(|d| format!(" — {d}"))
                    .unwrap_or_default();
                format!("- `{}`{}{}", entry.qualified_name(), default, desc)
            })
            .collect::<Vec<_>>()
            .join("\n")
    }

    fn default_label(&self) -> String {
        self.registry
            .as_ref()
            .and_then(|registry| registry.default_entry().ok())
            .map(|entry| entry.qualified_name())
            .unwrap_or_else(|| "none resolved".into())
    }

    fn diagnostic(&self) -> &str {
        self.diagnostic.as_deref().unwrap_or("")
    }

    fn error_suffix(&self) -> String {
        format!(
            "\nUse `default`, `inherit`, or one of these registry selectors:\n{}",
            self.compact_list()
        )
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum SpawnProfileSelector {
    Default,
    Inherit,
    Registry(ProfileSelector),
}

fn parse_spawn_profile_selector(raw: Option<&str>) -> Result<SpawnProfileSelector, String> {
    let Some(raw) = raw.map(str::trim).filter(|s| !s.is_empty()) else {
        return Ok(SpawnProfileSelector::Default);
    };
    if raw == "default" {
        return Ok(SpawnProfileSelector::Default);
    }
    if raw == "inherit" {
        return Ok(SpawnProfileSelector::Inherit);
    }
    if raw.starts_with("path:")
        || raw.starts_with('/')
        || raw.starts_with("./")
        || raw.starts_with("../")
        || raw.contains('/')
        || raw.ends_with(".dcdl")
        || raw.ends_with(".json")
        || raw.ends_with(".toml")
        || raw.ends_with(".nix")
    {
        return Err(format!(
            "SubWorkerSpawn.profile accepts `default`, `inherit`, or registry selectors only; path-like selector `{raw}` is not allowed"
        ));
    }
    if let Some((prefix, name)) = raw.split_once(':') {
        let source = match prefix {
            "builtin" => ProfileRegistrySource::Builtin,
            "user" => ProfileRegistrySource::User,
            "project" => ProfileRegistrySource::Project,
            _ => {
                return Err(format!(
                    "unsupported SubWorkerSpawn.profile selector prefix `{prefix}`; use builtin:, user:, project:, default, or inherit"
                ));
            }
        };
        if name.is_empty() {
            return Err(
                "SubWorkerSpawn.profile registry selector has an empty profile name".into(),
            );
        }
        return Ok(SpawnProfileSelector::Registry(
            ProfileSelector::source_named(source, name),
        ));
    }
    Ok(SpawnProfileSelector::Registry(ProfileSelector::named(raw)))
}

#[derive(Clone)]
pub(crate) enum ParentNotificationTarget {
    Controller(mpsc::WeakSender<Method>),
    Buffer(crate::ipc::notify_buffer::NotifyBuffer),
}

impl ParentNotificationTarget {
    fn notify(&self, message: String, auto_run: bool) {
        match self {
            Self::Controller(parent_method_tx) => {
                let Some(parent_method_tx) = parent_method_tx.upgrade() else {
                    tracing::warn!(
                        "parent Worker controller closed before Internal SubWorker completion notification"
                    );
                    return;
                };
                tokio::spawn(async move {
                    if let Err(error) = parent_method_tx
                        .send(Method::Notify { message, auto_run })
                        .await
                    {
                        tracing::warn!(
                            %error,
                            "failed to notify parent Worker about Internal SubWorker completion"
                        );
                    }
                });
            }
            Self::Buffer(parent_notifies) => parent_notifies.push_notify(message, auto_run),
        }
    }
}

/// Runtime dependencies the `SubWorkerSpawn` tool needs in order to launch a
/// child SubWorker and record the handoff locally. Constructed by the Worker
/// controller once per Worker lifetime.
pub struct SubWorkerSpawnTool {
    /// Spawner's own Worker name, used for direct-child identity collision checks.
    spawner_name: String,
    workspace_context: crate::worker::WorkerWorkspaceContext,
    parent_notifications: ParentNotificationTarget,
    /// Runtime-owned root used only for bounded Internal Worker tool artifacts such as Bash spill
    /// output. It is not an Internal Worker identity or catalog location.
    runtime_base: PathBuf,
    /// Inherited runtime workspace root for Profile/project/Ticket/workflow/
    /// memory context. SubWorkerSpawn `cwd` must not affect this value.
    workspace_root: PathBuf,
    /// Directory the spawned SubWorker's tools should use when the LLM did not
    /// override it. Defaults to the spawner's cwd.
    /// Active provider-backed Workdir session from which child leases are captured.
    source_workdir_session: Option<WorkdirSessionHandle>,
    /// Parent-owned in-memory registry shared by the five SubWorker tools.
    registry: Arc<SpawnedWorkerRegistry>,
    /// Spawner's resolved Manifest. `profile = "inherit"` derives the
    /// child config from reusable fields here, and selected profiles are
    /// merged into the same internal handoff shape before launch.
    spawner_manifest: WorkerManifest,
    prompt_loader: PromptCatalogSource,
    /// Compact selector list shared by tool description and diagnostics.
    available_profiles: AvailableProfiles,
    internal_client_override: Option<Box<dyn agen::llm_client::LlmClient>>,
}

impl SubWorkerSpawnTool {
    #[cfg(test)]
    fn with_internal_client(mut self, client: Box<dyn agen::llm_client::LlmClient>) -> Self {
        self.internal_client_override = Some(client);
        self
    }

    fn new(
        spawner_name: String,
        workspace_context: crate::worker::WorkerWorkspaceContext,
        parent_notifications: ParentNotificationTarget,
        runtime_base: PathBuf,
        workspace_root: PathBuf,
        source_workdir_session: Option<WorkdirSessionHandle>,
        registry: Arc<SpawnedWorkerRegistry>,
        spawner_manifest: WorkerManifest,
        prompt_loader: PromptCatalogSource,
        available_profiles: AvailableProfiles,
    ) -> Self {
        Self {
            spawner_name,
            workspace_context,
            parent_notifications,
            runtime_base,
            workspace_root,
            source_workdir_session,
            registry,
            spawner_manifest,
            prompt_loader,
            available_profiles,
            internal_client_override: None,
        }
    }
}

fn validate_reviewer_handoff(input: &SubWorkerSpawnInput) -> Result<(), ToolError> {
    let Some(review) = &input.review else {
        return Ok(());
    };
    if review.ticket_id.trim().is_empty() {
        return Err(ToolError::InvalidArgument(
            "reviewer handoff requires non-empty ticket_id".to_string(),
        ));
    }
    if input.profile.as_deref() != Some("builtin:reviewer") {
        return Err(ToolError::InvalidArgument(
            "reviewer handoff requires the explicit effective profile builtin:reviewer".to_string(),
        ));
    }
    if !input
        .scope
        .iter()
        .any(|rule| matches!(rule.permission, PermissionInput::Write))
    {
        return Err(ToolError::InvalidArgument(
            "Merge Request Reviewer SubWorkers must include writable delegated scope".to_string(),
        ));
    }
    Ok(())
}

#[async_trait]
impl Tool for SubWorkerSpawnTool {
    async fn execute(
        &self,
        input_json: &str,
        _ctx: agen::tool::ToolExecutionContext,
    ) -> Result<ToolOutput, ToolError> {
        let input: SubWorkerSpawnInput = serde_json::from_str(input_json).map_err(|e| {
            ToolError::InvalidArgument(format!("invalid SubWorkerSpawn input: {e}"))
        })?;

        // `delegate_scope` catches this too (as `DuplicateWorkerName`), but
        // the dedicated message is kinder to the LLM — which gets the
        // error back verbatim — than the generic duplicate-name error.
        if input.name == self.spawner_name {
            return Err(ToolError::InvalidArgument(format!(
                "spawned worker name `{}` collides with spawner's own name",
                input.name
            )));
        }
        validate_reviewer_handoff(&input)?;
        let name_reservation = self
            .registry
            .reserve_internal_name(input.name.clone())
            .map_err(|error| ToolError::InvalidArgument(error.to_string()))?;

        let workdir_rules = parse_workdir_scope(&input.scope)?;
        let source_workdir_session =
            require_active_workdir_session(self.source_workdir_session.as_ref())?;
        let delegation_request = workdir_delegation_request(input.cwd.as_deref(), workdir_rules)?;
        let workdir_delegation = source_workdir_session
            .delegate(delegation_request)
            .await
            .map_err(|error| {
                ToolError::InvalidArgument(format!("delegate Workdir session: {error}"))
            })?;

        let spawn_selector =
            parse_spawn_profile_selector(input.profile.as_deref()).map_err(|msg| {
                ToolError::InvalidArgument(format!(
                    "{msg}{}",
                    self.available_profiles.error_suffix()
                ))
            })?;
        let scope_allow = Vec::new();
        let spawn_config_json = self
            .build_spawn_config_json(
                &input.name,
                input.instruction.as_deref(),
                &scope_allow,
                spawn_selector,
            )
            .map_err(|e| ToolError::InvalidArgument(format!("{e}")))?;

        let mut child_config: WorkerManifestConfig = serde_json::from_str(&spawn_config_json)
            .map_err(|error| {
                ToolError::ExecutionFailed(format!("resolve child manifest: {error}"))
            })?;
        child_config.delegation_scope = ScopeConfig {
            allow: scope_allow.clone(),
            deny: Vec::new(),
        };
        let mut child_manifest =
            WorkerManifest::try_from(WorkerManifestConfig::builtin_defaults().merge(child_config))
                .map_err(|error| {
                    ToolError::ExecutionFailed(format!("resolve child manifest: {error}"))
                })?;
        // Delegated children stay bound to their scoped session and cannot use
        // Workspace attachment tools to replace it with parent-level authority.
        child_manifest.feature.manage_workdir.enabled = false;
        let reviewer_capability = input.review.as_ref().map(|review| {
            (
                review.ticket_id.clone(),
                format!(
                    "{}{}",
                    uuid::Uuid::now_v7().simple(),
                    uuid::Uuid::now_v7().simple()
                ),
            )
        });
        let child_workspace_context =
            if let Some((ticket_id, capability_token)) = &reviewer_capability {
                let workspace_id =
                    self.workspace_context
                        .workspace_id()
                        .cloned()
                        .ok_or_else(|| {
                            ToolError::InvalidArgument(
                                "reviewer handoff requires Workspace identity".to_string(),
                            )
                        })?;
                let parent_client = self.workspace_context.client_handle();
                if !parent_client.is_available() {
                    return Err(ToolError::InvalidArgument(
                        "reviewer handoff requires Workspace API authority".to_string(),
                    ));
                }
                let child_client: Arc<dyn crate::worker::WorkspaceClient> =
                    Arc::new(ReviewerChildWorkspaceClient::new(
                        parent_client.clone(),
                        ReviewerContext {
                            ticket_id: ticket_id.clone(),
                        },
                        capability_token.clone(),
                    ));
                crate::worker::WorkerWorkspaceContext::with_client(Some(workspace_id), child_client)
            } else {
                self.workspace_context.clone()
            };
        let store = EphemeralSessionStore::default();
        let filesystem_authority = WorkerFilesystemAuthority::None;
        let mut child = Worker::<Box<dyn agen::llm_client::LlmClient>, EphemeralSessionStore>::from_internal_manifest_with_context(
            child_manifest,
            store.clone(),
            self.prompt_loader.clone(),
            child_workspace_context,
            filesystem_authority,
            self.internal_client_override
                .as_ref()
                .map(|client| client.clone_boxed()),
        )
        .await
        .map_err(|error| ToolError::ExecutionFailed(format!("build Internal Worker: {error}")))?;
        child.bind_workdir_session(Some(workdir_delegation.scoped_session.clone()));
        let child_scope = child.scope().clone();
        let child_registry = SpawnedWorkerRegistry::new_internal(input.name.clone(), child_scope);
        register_worker_tools(
            &mut child,
            self.runtime_base
                .join("internal-workers")
                .join(&input.name)
                .join("bash-output"),
            self.runtime_base.clone(),
            child_registry.clone(),
            None,
        )
        .await
        .map_err(|error| {
            ToolError::ExecutionFailed(format!("install Internal Worker features: {error}"))
        })?;
        let child_change_tracker = child.tracker().cloned();
        #[cfg(test)]
        let installed_tools = child
            .engine()
            .tool_server_handle()
            .tool_definitions_sorted()
            .into_iter()
            .map(|definition| definition.name)
            .collect();
        let child_name = input.name.clone();
        let registry = Arc::downgrade(&self.registry);
        let parent_notifications = self.parent_notifications.clone();
        let session_result = prepare_internal_worker_session(
            child,
            store,
            InternalWorkerVisibility::ParentClient,
            Some(child_registry.clone()),
            Some(Arc::new(move |status| {
                if status == InternalWorkerSessionStatus::Failed {
                    if let Some(registry) = registry.upgrade() {
                        if let Err(error) = registry.reclaim_internal_scope(&child_name) {
                            tracing::warn!(
                                child_name,
                                %error,
                                "failed to reclaim delegated scope after Internal SubWorker failure"
                            );
                        }
                    }
                }
                let message = format!(
                    "SubWorker `{child_name}` turn ended with status {status:?}. Inspect its committed session with worker-observation tools before making completion decisions."
                );
                parent_notifications.notify(message, true);
            })),
        )
        .await;
        let session = session_result.map_err(|error| {
            ToolError::ExecutionFailed(format!("prepare Internal Worker session: {error}"))
        })?;
        child_registry
            .attach_parent_protocol(session.protocol_sender(), session.session_id_string());

        if let Some((ticket_id, capability_token)) = &reviewer_capability {
            let workspace_id = self.workspace_context.workspace_id().ok_or_else(|| {
                ToolError::ExecutionFailed("review capability lost Workspace identity".to_string())
            })?;
            let child_session_id = session.session_id_string();
            let child_registration = WorkspaceRequest::json(
                WorkspaceRequestMethod::Post,
                format!(
                    "/api/w/{}/internal/reviewer-child-sessions",
                    workspace_id.as_str()
                ),
                serde_json::json!({"child_session_id": child_session_id}).to_string(),
            );
            let child_response = self
                .workspace_context
                .client()
                .execute(child_registration)
                .map_err(|error| {
                    ToolError::ExecutionFailed(format!(
                        "register Runtime-owned Reviewer child session: {error}"
                    ))
                })?;
            if !child_response.is_success() {
                let _ = session.stop().await;
                return Err(ToolError::ExecutionFailed(format!(
                    "register Runtime-owned Reviewer child session failed with status {}: {}",
                    child_response.status, child_response.body
                )));
            }
            let body = serde_json::json!({
                "child_session_id": child_session_id,
                "capability_token": capability_token,
            });
            let request = WorkspaceRequest::json(
                WorkspaceRequestMethod::Post,
                format!(
                    "/api/w/{}/tickets/{}/merge-request/review-capabilities",
                    workspace_id.as_str(),
                    ticket_id
                ),
                body.to_string(),
            );
            let response = self
                .workspace_context
                .client()
                .execute(request)
                .map_err(|error| {
                    ToolError::ExecutionFailed(format!("register review capability: {error}"))
                })?;
            if !response.is_success() {
                let _ = session.stop().await;
                return Err(ToolError::ExecutionFailed(format!(
                    "register review capability failed with status {}: {}",
                    response.status, response.body
                )));
            }
        }

        let record = crate::spawn::registry::InternalSpawnedWorkerRecord::new(
            input.name.clone(),
            scope_allow,
            workdir_delegation,
            #[cfg(test)]
            installed_tools,
            session.clone(),
            child_change_tracker,
        );
        if let Err(error) = name_reservation.commit(record) {
            let _ = session.stop().await;
            return Err(ToolError::ExecutionFailed(format!(
                "register Internal Worker session: {error}"
            )));
        }
        if let Err(error) = session.send(input.task).await {
            let _ = session.stop().await;
            let _ = self.registry.remove_internal(&input.name).await;
            return Err(ToolError::ExecutionFailed(format!(
                "start Internal Worker session: {error}"
            )));
        }

        Ok(ToolOutput {
            summary: format!("spawned internal worker `{}`", input.name),
            content: None,

            attachments: Vec::new(),
        })
    }
}

fn logical_workdir_path(value: &str, field: &str) -> Result<FsPath, ToolError> {
    let path = Path::new(value);
    if path.is_absolute() {
        return Err(ToolError::InvalidArgument(format!(
            "{field} must be Workdir-relative, got `{value}`"
        )));
    }
    let normalized = path
        .components()
        .filter_map(|component| match component {
            std::path::Component::CurDir => None,
            other => Some(other.as_os_str()),
        })
        .collect::<PathBuf>();
    let normalized = normalized.to_str().ok_or_else(|| {
        ToolError::InvalidArgument(format!("{field} `{value}` is not valid UTF-8"))
    })?;
    FsPath::new(normalized).map_err(|error| {
        ToolError::InvalidArgument(format!(
            "{field} `{value}` is not a valid logical Workdir path: {error}"
        ))
    })
}

fn parse_workdir_scope(rules: &[ScopeRuleInput]) -> Result<Vec<WorkdirDelegationRule>, ToolError> {
    if rules.is_empty() {
        return Err(ToolError::InvalidArgument("scope must not be empty".into()));
    }
    rules
        .iter()
        .map(|rule| {
            Ok(WorkdirDelegationRule {
                target: logical_workdir_path(&rule.target, "scope.target")?,
                permission: match rule.permission {
                    PermissionInput::Read => WorkdirDelegationPermission::Read,
                    PermissionInput::Write => WorkdirDelegationPermission::Write,
                },
                recursive: rule.recursive,
            })
        })
        .collect()
}

fn workdir_delegation_request(
    cwd: Option<&str>,
    rules: Vec<WorkdirDelegationRule>,
) -> Result<WorkdirDelegationRequest, ToolError> {
    Ok(WorkdirDelegationRequest {
        rules,
        cwd: logical_workdir_path(cwd.unwrap_or("."), "cwd")?,
    })
}

fn require_active_workdir_session(
    session: Option<&WorkdirSessionHandle>,
) -> Result<&WorkdirSessionHandle, ToolError> {
    session.ok_or_else(|| {
        ToolError::InvalidArgument(
            "SubWorkerSpawn requires an active Workdir session; attach a Workdir before delegating filesystem access"
                .to_string(),
        )
    })
}

/// Serialise the internal manifest config that gets handed to the child
/// Worker runtime process via the hidden `--spawn-config-json` flag.
/// `WorkerManifestConfig`'s `Serialize` impl is the single source of truth for the
/// internal handoff shape.
///
/// The child's tool working directory is carried separately through
/// the child runtime entrypoint; it is not part of the manifest.
impl SubWorkerSpawnTool {
    fn build_spawn_config_json(
        &self,
        name: &str,
        instruction_override: Option<&str>,
        scope_allow: &[ScopeRule],
        selector: SpawnProfileSelector,
    ) -> Result<String, String> {
        build_spawn_config_json_for_profile(
            &self.spawner_manifest,
            &self.available_profiles,
            &self.workspace_root,
            name,
            instruction_override,
            scope_allow,
            selector,
        )
    }
}

fn build_spawn_config_json_for_profile(
    spawner_manifest: &WorkerManifest,
    available_profiles: &AvailableProfiles,
    workspace_root: &Path,
    name: &str,
    instruction_override: Option<&str>,
    scope_allow: &[ScopeRule],
    selector: SpawnProfileSelector,
) -> Result<String, String> {
    let mut config = match selector {
        SpawnProfileSelector::Inherit => manifest_to_reusable_config(spawner_manifest),
        SpawnProfileSelector::Default | SpawnProfileSelector::Registry(_) => {
            let registry = available_profiles.registry.as_ref().ok_or_else(|| {
                format!(
                    "profile discovery failed for SubWorkerSpawn: {}{}",
                    available_profiles.diagnostic().if_empty("unknown error"),
                    available_profiles.error_suffix()
                )
            })?;
            let profile_selector = match selector {
                SpawnProfileSelector::Default => ProfileSelector::Default,
                SpawnProfileSelector::Registry(selector) => selector,
                SpawnProfileSelector::Inherit => unreachable!(),
            };
            let resolved = ProfileResolver::new()
                .with_workspace_base(workspace_root)
                .resolve_from_registry(
                    &profile_selector,
                    registry,
                    ProfileResolveOptions::with_worker_name(name),
                )
                .map_err(|e| profile_error_with_available(e, available_profiles))?;
            manifest_to_reusable_config(&resolved.manifest)
        }
    };
    config.worker.name = Some(name.to_string());
    config.scope = ScopeConfig {
        allow: scope_allow.to_vec(),
        deny: Vec::new(),
    };
    if let Some(instruction) = instruction_override {
        config.engine.instruction = Some(instruction.to_string());
    }
    serde_json::to_string(&config).map_err(|e| format!("spawn config serialisation: {e}"))
}

#[cfg(test)]
fn build_spawn_config_json(
    name: &str,
    instruction: &str,
    scope_allow: &[ScopeRule],
    model: &manifest::ModelManifest,
    record_event_trace: bool,
) -> Result<String, serde_json::Error> {
    let config = WorkerManifestConfig {
        worker: WorkerMetaConfig {
            name: Some(name.to_string()),
        },
        model: model.clone(),
        engine: EngineManifestConfig {
            instruction: Some(instruction.to_string()),
            ..Default::default()
        },
        scope: ScopeConfig {
            allow: scope_allow.to_vec(),
            deny: Vec::new(),
        },
        session: record_event_trace.then_some(SessionConfigPartial {
            record_event_trace: Some(true),
        }),
        ..Default::default()
    };
    serde_json::to_string(&config)
}

trait IfEmpty {
    fn if_empty(&self, fallback: &str) -> String;
}
impl IfEmpty for str {
    fn if_empty(&self, fallback: &str) -> String {
        if self.is_empty() {
            fallback.into()
        } else {
            self.into()
        }
    }
}

fn profile_error_with_available(error: ProfileError, available: &AvailableProfiles) -> String {
    format!(
        "invalid SubWorkerSpawn.profile: {error}{}",
        available.error_suffix()
    )
}

fn manifest_to_reusable_config(manifest: &WorkerManifest) -> WorkerManifestConfig {
    WorkerManifestConfig {
        worker: WorkerMetaConfig {
            name: Some(manifest.worker.name.clone()),
        },
        model: manifest.model.clone(),
        engine: EngineManifestConfig {
            instruction: Some(manifest.engine.instruction.clone()),
            language: Some(manifest.engine.language.clone()),
            max_tokens: manifest.engine.max_tokens,
            max_turns: manifest.engine.max_turns,
            temperature: manifest.engine.temperature,
            top_p: manifest.engine.top_p,
            top_k: manifest.engine.top_k,
            stop_sequences: (!manifest.engine.stop_sequences.is_empty())
                .then_some(manifest.engine.stop_sequences.clone()),
            reasoning: manifest.engine.reasoning.clone(),
            tool_output: ToolOutputLimitsPartial {
                default_max_bytes: Some(manifest.engine.tool_output.default_max_bytes),
                per_tool: manifest.engine.tool_output.per_tool.clone(),
            },
            file_upload: FileUploadLimitsPartial {
                max_bytes: Some(manifest.engine.file_upload.max_bytes),
            },
        },
        scope: ScopeConfig {
            allow: manifest.scope.allow.clone(),
            deny: manifest.scope.deny.clone(),
        },
        // `inherit` reuses behavioral configuration, not subdelegation authority.
        delegation_scope: ScopeConfig::default(),
        session: Some(SessionConfigPartial {
            record_event_trace: Some(manifest.session.record_event_trace),
        }),
        permissions: manifest
            .permissions
            .as_ref()
            .map(|p| PermissionConfigPartial {
                default_action: Some(p.default_action),
                rules: p.rules.clone(),
            }),
        feature: manifest.feature.clone().into(),
        plugins: manifest.plugins.clone(),
        mcp: manifest.mcp.clone(),
        compaction: manifest
            .compaction
            .as_ref()
            .map(|c| CompactionConfigPartial {
                prune_protected_tokens: Some(c.prune_protected_tokens),
                prune_min_savings: Some(c.prune_min_savings),
                threshold: c.threshold,
                request_threshold: c.request_threshold,
                retained_tokens: Some(c.retained_tokens),
                overview_target_tokens: Some(c.overview_target_tokens),
                overview_warning_tokens: Some(c.overview_warning_tokens),
                overview_deadline_tokens: Some(c.overview_deadline_tokens),
                worker_context_max_tokens: Some(c.worker_context_max_tokens),
                finish_warning_remaining_tokens: Some(c.finish_warning_remaining_tokens),
                final_reserve_tokens: Some(c.final_reserve_tokens),
                worker_max_turns: c.worker_max_turns,
                summary_target_tokens: Some(c.summary_target_tokens),
                summary_max_tokens: Some(c.summary_max_tokens),
                auto_read_budget_tokens: Some(c.auto_read_budget_tokens),
                result_context_max_tokens: Some(c.result_context_max_tokens),
                model: c.model.clone(),
            }),
        web: manifest.web.clone(),
        memory: manifest.memory.clone(),
        skills: manifest.skills.clone(),
    }
}

/// Tail of the spawned child's `stderr.log` to splice into a startup
/// failure message. Capped so a chatty child can't blow up the LLM's
/// tool-result budget — debugging beyond this should read the file
/// directly.
/// Factory for the `SubWorkerSpawn` tool.
pub(crate) fn sub_worker_spawn_tool(
    spawner_name: String,
    workspace_context: crate::worker::WorkerWorkspaceContext,
    parent_notifications: ParentNotificationTarget,
    runtime_base: PathBuf,
    workspace_root: PathBuf,
    source_workdir_session: Option<WorkdirSessionHandle>,
    registry: Arc<SpawnedWorkerRegistry>,
    spawner_manifest: WorkerManifest,
    prompts: Arc<ArcSwap<PromptCatalog>>,
) -> ToolDefinition {
    sub_worker_spawn_tool_impl(
        spawner_name,
        workspace_context,
        parent_notifications,
        runtime_base,
        workspace_root,
        source_workdir_session,
        registry,
        spawner_manifest,
        prompts,
    )
}

fn sub_worker_spawn_tool_impl(
    spawner_name: String,
    workspace_context: crate::worker::WorkerWorkspaceContext,
    parent_notifications: ParentNotificationTarget,
    runtime_base: PathBuf,
    workspace_root: PathBuf,
    source_workdir_session: Option<WorkdirSessionHandle>,
    registry: Arc<SpawnedWorkerRegistry>,
    spawner_manifest: WorkerManifest,
    prompts: Arc<ArcSwap<PromptCatalog>>,
) -> ToolDefinition {
    Arc::new(move || {
        let schema = schemars::schema_for!(SubWorkerSpawnInput);
        let schema_value = serde_json::to_value(schema).unwrap_or(serde_json::json!({}));
        let available_profiles = AvailableProfiles::discover(&workspace_root);
        let description = prompts
            .load_full()
            .sub_worker_spawn_tool_description(
                &available_profiles.compact_list(),
                &available_profiles.default_label(),
                available_profiles.diagnostic(),
            )
            .unwrap_or_else(|e| {
                format!(
                    "Spawn an in-process Internal SubWorker session to split context for a delegated task. Profile description rendering failed: {e}. Available profiles:\n{}",
                    available_profiles.compact_list()
                )
            });
        let meta = ToolMeta::new("SubWorkerSpawn")
            .description(description)
            .input_schema(schema_value);
        let tool: Arc<dyn Tool> = Arc::new(SubWorkerSpawnTool::new(
            spawner_name.clone(),
            workspace_context.clone(),
            parent_notifications.clone(),
            runtime_base.clone(),
            workspace_root.clone(),
            source_workdir_session.clone(),
            registry.clone(),
            spawner_manifest.clone(),
            prompts.load_full().source(),
            available_profiles,
        ));
        (meta, tool)
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use manifest::{DelegationScope, Permission, Scope, SharedScope};
    use std::pin::Pin;
    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
    use std::time::Duration;

    use crate::WorkspaceId;
    use agen::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent};
    use agen::llm_client::types::ContentPart;
    use agen::llm_client::{ClientError, LlmClient, Request};
    use agen::{Item, Role};
    use async_trait::async_trait;
    use futures::Stream;
    use manifest::{AuthRef, ModelManifest, SchemeKind, WorkerManifest};
    use tempfile::TempDir;

    use crate::worker::{
        WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceResponse,
    };

    #[test]
    fn missing_active_workdir_session_fails_deterministically() {
        let error = require_active_workdir_session(None).unwrap_err();
        assert!(matches!(
            error,
            ToolError::InvalidArgument(message)
                if message.contains("requires an active Workdir session")
        ));
    }

    #[test]
    fn workdir_scope_uses_logical_relative_paths() {
        let rules = parse_workdir_scope(&[
            ScopeRuleInput {
                target: ".".to_string(),
                permission: PermissionInput::Read,
                recursive: true,
            },
            ScopeRuleInput {
                target: "src".to_string(),
                permission: PermissionInput::Write,
                recursive: false,
            },
        ])
        .unwrap();
        assert_eq!(rules[0].target.as_str(), "");
        assert_eq!(rules[1].target.as_str(), "src");
        for target in ["/host/path", "../escape"] {
            let error = parse_workdir_scope(&[ScopeRuleInput {
                target: target.to_string(),
                permission: PermissionInput::Read,
                recursive: true,
            }])
            .unwrap_err();
            assert!(matches!(error, ToolError::InvalidArgument(_)));
        }
    }

    #[test]
    fn reviewer_handoff_requires_explicit_builtin_profile_and_writable_scope() {
        let valid: SubWorkerSpawnInput = serde_json::from_value(serde_json::json!({
            "name":"reviewer","task":"review","profile":"builtin:reviewer",
            "scope":[{"target":"work","permission":"write"}],
            "review":{"ticket_id":"T1"}
        }))
        .unwrap();
        assert!(validate_reviewer_handoff(&valid).is_ok());
        let wrong_profile: SubWorkerSpawnInput = serde_json::from_value(serde_json::json!({
            "name":"reviewer","task":"review","profile":"builtin:coder",
            "scope":[{"target":"work","permission":"write"}],
            "review":{"ticket_id":"T1"}
        }))
        .unwrap();
        assert!(validate_reviewer_handoff(&wrong_profile).is_err());
        let read_only: SubWorkerSpawnInput = serde_json::from_value(serde_json::json!({
            "name":"reviewer","task":"review","profile":"builtin:reviewer",
            "scope":[{"target":"work","permission":"read"}],
            "review":{"ticket_id":"T1"}
        }))
        .unwrap();
        assert!(validate_reviewer_handoff(&read_only).is_err());
    }

    fn abs_rule(path: &Path, permission: Permission) -> ScopeRule {
        ScopeRule {
            target: path.to_path_buf(),
            permission,
            recursive: true,
        }
    }

    const INTERNAL_REVIEWER_PROFILE: &str = r#"
slug = "reviewer"
scope = "workspace_read"

[model]
scheme = "anthropic"
model_id = "reviewer-model"

[model.auth]
kind = "none"

[engine]
instruction = "role.reviewer"
language = "Reviewerish"
max_tokens = 3333

[feature.ticket]
enabled = true
thread = true

[feature.memory]
enabled = true

[memory]
extract_threshold = 4000
"#;

    #[tokio::test]
    async fn parent_controller_notification_target_does_not_keep_channel_open() {
        let (parent_method_tx, mut parent_method_rx) = mpsc::channel(1);
        let target = ParentNotificationTarget::Controller(parent_method_tx.downgrade());

        drop(parent_method_tx);

        assert!(parent_method_rx.recv().await.is_none());
        target.notify("late completion".to_string(), true);
    }

    #[tokio::test]
    async fn reviewer_profile_write_scope_exposes_command_tools_and_notifies_parent_controller() {
        let runtime = TempDir::new().unwrap();
        let workspace_root = runtime.path().join("project");
        let available_profiles = write_project_profile_registry(
            &workspace_root,
            Some("reviewer"),
            &[("reviewer", "reviewer.toml", INTERNAL_REVIEWER_PROFILE)],
        );
        let mut manifest = parent_manifest(&workspace_root, None);
        manifest.delegation_scope = ScopeConfig {
            allow: vec![abs_rule(&workspace_root, Permission::Write)],
            deny: Vec::new(),
        };
        let spawner_scope = SharedScope::new(Scope::from_config(&manifest.scope).unwrap());
        let registry = SpawnedWorkerRegistry::new_internal("parent".into(), spawner_scope.clone());
        let workspace_context = crate::worker::WorkerWorkspaceContext::with_client(
            Some(WorkspaceId::new("workspace-test").unwrap()),
            Arc::new(AvailableWorkspaceClient),
        );
        let calls = Arc::new(AtomicUsize::new(0));
        let observed_parent_write_revoked = Arc::new(AtomicBool::new(false));
        let observed_instruction_override = Arc::new(AtomicBool::new(false));
        let fail_requests = Arc::new(AtomicBool::new(false));
        let prompt_loader = PromptCatalogSource::builtins_only();
        let (parent_method_tx, mut parent_method_rx) = mpsc::channel(8);
        let source_workdir_session = workdir::delegation_capable_session(Arc::new(
            workdir::LocalWorkdirSession::materialized_bound(
                workdir::Workdir::new("test-workdir"),
                workspace_root.clone(),
                workspace_root.clone(),
                spawner_scope.clone(),
                workdir::WorkdirSessionCapabilities::ALL,
            ),
        ));
        let tool = SubWorkerSpawnTool::new(
            "parent".into(),
            workspace_context,
            ParentNotificationTarget::Controller(parent_method_tx.downgrade()),
            runtime.path().to_path_buf(),
            workspace_root.clone(),
            Some(source_workdir_session),
            registry.clone(),
            manifest.clone(),
            prompt_loader,
            available_profiles,
        )
        .with_internal_client(Box::new(ScriptedInternalClient {
            calls: calls.clone(),
            parent_scope: spawner_scope.clone(),
            delegated_path: workspace_root.clone(),
            observed_parent_write_revoked: observed_parent_write_revoked.clone(),
            observed_instruction_override: observed_instruction_override.clone(),
            fail_requests: fail_requests.clone(),
        }));
        let input = serde_json::json!({
            "name": "reviewer-child",
            "profile": "project:reviewer",
            "instruction": "role.reviewer",
            "task": "review immutable commit",
            "scope": [{
                "target": ".",
                "permission": "write",
                "recursive": true
            }]
        });

        assert!(spawner_scope.snapshot().is_writable(&workspace_root));

        let mut invalid_input = input.clone();
        invalid_input["scope"][0]["target"] =
            serde_json::json!(runtime.path().join("outside-parent-scope"));
        tool.execute(
            &serde_json::to_string(&invalid_input).unwrap(),
            agen::tool::ToolExecutionContext::direct(),
        )
        .await
        .expect_err("invalid delegation must fail before child preparation");
        assert_eq!(calls.load(Ordering::SeqCst), 0);
        assert!(spawner_scope.snapshot().is_writable(&workspace_root));

        let output = tool
            .execute(
                &serde_json::to_string(&input).unwrap(),
                agen::tool::ToolExecutionContext::direct(),
            )
            .await
            .expect("spawn project reviewer as Internal Worker");
        assert!(output.summary.contains("internal worker `reviewer-child`"));
        assert!(spawner_scope.snapshot().is_writable(&workspace_root));
        let record = registry
            .get_internal("reviewer-child")
            .expect("Internal reviewer registry record");
        for required in ["Read", "Write", "Edit", "Glob", "Grep", "Bash"] {
            assert!(
                record.installed_tools.iter().any(|name| name == required),
                "write-scoped child is missing {required}: {:?}",
                record.installed_tools
            );
        }
        assert!(
            !record
                .installed_tools
                .iter()
                .any(|name| matches!(name.as_str(), "WorkdirAttachSelf" | "WorkdirDetachSelf"))
        );
        assert_eq!(
            record.session.wait_until_idle().await,
            crate::internal_worker::InternalWorkerSessionStatus::Idle
        );
        assert_eq!(calls.load(Ordering::SeqCst), 1);
        assert!(!observed_parent_write_revoked.load(Ordering::SeqCst));
        assert!(observed_instruction_override.load(Ordering::SeqCst));
        let completion = tokio::time::timeout(Duration::from_secs(1), parent_method_rx.recv())
            .await
            .expect("SubWorker completion must wake the parent method channel")
            .expect("parent method channel remains open");
        assert!(matches!(
            completion,
            Method::Notify {
                message,
                auto_run: true,
            } if message.contains("SubWorker `reviewer-child` turn ended with status Idle")
        ));
        assert!(!runtime.path().join("reviewer-child/sock").exists());

        let duplicate_error = tool
            .execute(
                &serde_json::to_string(&input).unwrap(),
                agen::tool::ToolExecutionContext::direct(),
            )
            .await
            .expect_err("duplicate child name must be rejected before a first turn starts");
        assert!(
            format!("{duplicate_error:?}").contains("already registered"),
            "unexpected duplicate error: {duplicate_error:?}"
        );
        assert_eq!(
            calls.load(Ordering::SeqCst),
            1,
            "duplicate rejection must not invoke the child provider"
        );
        assert!(matches!(
            parent_method_rx.try_recv(),
            Err(tokio::sync::mpsc::error::TryRecvError::Empty)
        ));

        let context = agen::tool::ToolExecutionContext::direct();
        let list = (crate::spawn::comm_tools::sub_worker_list_tool(registry.clone()))().1;
        let listed = list.execute("{}", context.clone()).await.unwrap();
        assert!(
            listed
                .content
                .unwrap_or_default()
                .contains("reviewer-child")
        );

        let observation =
            crate::feature::builtin::worker_observation::SpawnedSubWorkerObservationProvider::new(
                registry.clone(),
            );
        let observed_child =
            crate::feature::builtin::worker_observation::WorkerObservationSubjectRef::SubWorker {
                name: "reviewer-child".to_string(),
            };
        let first_capture = crate::feature::builtin::worker_observation::WorkerObservationProvider::capture_worker_session(
            &observation,
            &observed_child,
        )
        .await
        .unwrap();
        assert!(first_capture.items.iter().any(|item| {
            matches!(item, Item::Message { role: Role::Assistant, content, .. } if content.iter().any(|part| matches!(part, ContentPart::Text { text } if text.contains("reviewed"))))
        }));

        let send = (crate::spawn::comm_tools::sub_worker_send_tool(registry.clone()))().1;
        send.execute(
            r#"{"name":"reviewer-child","message":"review follow-up"}"#,
            context.clone(),
        )
        .await
        .unwrap();
        assert_eq!(
            record.session.wait_until_idle().await,
            crate::internal_worker::InternalWorkerSessionStatus::Idle
        );
        assert_eq!(calls.load(Ordering::SeqCst), 2);
        let latest_capture = crate::feature::builtin::worker_observation::WorkerObservationProvider::capture_worker_session(
            &observation,
            &observed_child,
        )
        .await
        .unwrap();
        assert!(latest_capture.items.len() > first_capture.items.len());

        fail_requests.store(true, Ordering::SeqCst);
        send.execute(
            r#"{"name":"reviewer-child","message":"trigger terminal failure"}"#,
            context.clone(),
        )
        .await
        .unwrap();
        assert_eq!(
            record.session.wait_until_idle().await,
            InternalWorkerSessionStatus::Failed
        );
        assert_eq!(calls.load(Ordering::SeqCst), 3);
        assert!(
            spawner_scope.snapshot().is_writable(&workspace_root),
            "Failed terminal child must release its delegated Workdir session"
        );
        assert!(
            !record.workdir_delegation.is_active(),
            "failed child must revoke cloned scoped sessions"
        );
        assert!(registry.get_internal("reviewer-child").is_some());

        let stop = (crate::spawn::comm_tools::sub_worker_stop_tool(registry.clone()))().1;
        stop.execute(r#"{"name":"reviewer-child"}"#, context)
            .await
            .unwrap();
        assert!(registry.get_internal("reviewer-child").is_none());
        assert!(spawner_scope.snapshot().is_writable(&workspace_root));

        fail_requests.store(false, Ordering::SeqCst);
        let mut teardown_input = input;
        teardown_input["name"] = serde_json::json!("reviewer-child-parent-drop");
        tool.execute(
            &serde_json::to_string(&teardown_input).unwrap(),
            agen::tool::ToolExecutionContext::direct(),
        )
        .await
        .unwrap();
        assert!(spawner_scope.snapshot().is_writable(&workspace_root));
        drop(list);
        drop(send);
        drop(stop);
        drop(observation);
        drop(tool);
        drop(registry);
        assert!(spawner_scope.snapshot().is_writable(&workspace_root));
    }

    #[test]
    fn spawn_worker_input_schema_includes_optional_cwd() {
        let schema = serde_json::to_value(schemars::schema_for!(SubWorkerSpawnInput)).unwrap();
        let properties = schema
            .get("properties")
            .and_then(serde_json::Value::as_object)
            .expect("schema properties");
        assert!(properties.contains_key("cwd"), "schema: {schema}");
        let required = schema
            .get("required")
            .and_then(serde_json::Value::as_array)
            .expect("schema required list");
        assert!(
            !required.iter().any(|value| value.as_str() == Some("cwd")),
            "cwd must remain optional: {schema}"
        );
    }

    #[test]
    fn orchestration_delegation_allows_root_read_and_worktree_writes_not_root_writes() {
        let tmp = TempDir::new().unwrap();
        let workspace_root = tmp.path().join("original");
        let implementation_worktree = workspace_root.join(".worktree/ticket-1");
        std::fs::create_dir_all(&implementation_worktree).unwrap();
        let delegation = DelegationScope::from_config(&ScopeConfig {
            allow: vec![
                abs_rule(&workspace_root, Permission::Read),
                abs_rule(&workspace_root.join(".worktree"), Permission::Write),
            ],
            deny: Vec::new(),
        })
        .unwrap();

        let coder_scope = vec![
            abs_rule(&workspace_root, Permission::Read),
            abs_rule(&implementation_worktree, Permission::Write),
        ];
        assert!(
            coder_scope
                .iter()
                .all(|rule| delegation.allows_rule(rule).unwrap())
        );

        let reviewer_scope = vec![abs_rule(&workspace_root, Permission::Read)];
        assert!(
            reviewer_scope
                .iter()
                .all(|rule| delegation.allows_rule(rule).unwrap())
        );

        let root_writer_scope = vec![abs_rule(&workspace_root, Permission::Write)];
        assert!(
            root_writer_scope
                .iter()
                .any(|rule| !delegation.allows_rule(rule).unwrap())
        );
    }

    #[derive(Clone)]
    struct ScriptedInternalClient {
        calls: Arc<AtomicUsize>,
        parent_scope: SharedScope,
        delegated_path: PathBuf,
        observed_parent_write_revoked: Arc<std::sync::atomic::AtomicBool>,
        observed_instruction_override: Arc<std::sync::atomic::AtomicBool>,
        fail_requests: Arc<AtomicBool>,
    }

    #[async_trait]
    impl LlmClient for ScriptedInternalClient {
        fn clone_boxed(&self) -> Box<dyn LlmClient> {
            Box::new(self.clone())
        }

        async fn stream(
            &self,
            request: Request,
        ) -> Result<Pin<Box<dyn Stream<Item = Result<LlmEvent, ClientError>> + Send>>, ClientError>
        {
            self.calls.fetch_add(1, Ordering::SeqCst);
            self.observed_parent_write_revoked.store(
                !self
                    .parent_scope
                    .snapshot()
                    .is_writable(&self.delegated_path),
                Ordering::SeqCst,
            );
            self.observed_instruction_override.store(
                request
                    .system_prompt
                    .as_deref()
                    .is_some_and(|prompt| prompt.contains("review")),
                Ordering::SeqCst,
            );
            if self.fail_requests.load(Ordering::SeqCst) {
                return Err(ClientError::Config(
                    "scripted Internal Worker failure".into(),
                ));
            }
            Ok(Box::pin(futures::stream::iter(vec![
                Ok(LlmEvent::text_block_start(0)),
                Ok(LlmEvent::text_delta(0, "reviewed")),
                Ok(LlmEvent::text_block_stop(0, None)),
                Ok(LlmEvent::Status(StatusEvent {
                    status: ResponseStatus::Completed,
                })),
            ])))
        }
    }

    #[derive(Debug)]
    struct AvailableWorkspaceClient;

    impl WorkspaceClient for AvailableWorkspaceClient {
        fn workspace_id(&self) -> Option<&str> {
            Some("workspace-test")
        }

        fn kind(&self) -> &str {
            "test"
        }

        fn is_available(&self) -> bool {
            true
        }

        fn execute(
            &self,
            _request: WorkspaceRequest,
        ) -> Result<WorkspaceResponse, WorkspaceClientError> {
            Err(WorkspaceClientError::Unavailable("not invoked".into()))
        }
    }

    fn parent_manifest(root: &Path, deny: Option<&Path>) -> WorkerManifest {
        WorkerManifestConfig {
            worker: WorkerMetaConfig {
                name: Some("parent".into()),
            },
            model: ModelManifest {
                scheme: Some(SchemeKind::Anthropic),
                model_id: Some("parent-model".into()),
                auth: Some(AuthRef::None),
                ..Default::default()
            },
            engine: EngineManifestConfig {
                instruction: Some("default".into()),
                language: Some("Parentish".into()),
                max_tokens: Some(1234),
                stop_sequences: Some(vec!["STOP".into()]),
                ..Default::default()
            },
            scope: ScopeConfig {
                allow: vec![abs_rule(root, Permission::Write)],
                deny: deny
                    .map(|path| vec![abs_rule(path, Permission::Read)])
                    .unwrap_or_default(),
            },
            session: Some(SessionConfigPartial {
                record_event_trace: Some(true),
            }),
            ..Default::default()
        }
        .try_into()
        .unwrap()
    }

    fn write_project_profile_registry(
        project: &Path,
        default: Option<&str>,
        profiles: &[(&str, &str, &str)],
    ) -> AvailableProfiles {
        let profile_dir = project.join("explicit-project-profiles");
        std::fs::create_dir_all(&profile_dir).unwrap();
        let mut registry_toml = String::new();
        if let Some(default) = default {
            registry_toml.push_str(&format!("default = \"{default}\"\n"));
        }
        registry_toml.push_str("[profile]\n");
        for (name, file, body) in profiles {
            std::fs::write(profile_dir.join(file), body).unwrap();
            registry_toml.push_str(&format!("{name} = \"explicit-project-profiles/{file}\"\n"));
        }
        let registry_path = project.join("explicit-project-profiles.toml");
        std::fs::write(&registry_path, registry_toml).unwrap();
        AvailableProfiles {
            registry: Some(
                ProfileDiscovery::with_sources(None, Some(registry_path))
                    .discover()
                    .unwrap(),
            ),
            diagnostic: None,
        }
    }

    fn child_config_from_profile(
        spawner_manifest: &WorkerManifest,
        available: &AvailableProfiles,
        cwd: &Path,
        name: &str,
        instruction_override: Option<&str>,
        scope: &[ScopeRule],
        selector: SpawnProfileSelector,
    ) -> WorkerManifestConfig {
        let json = build_spawn_config_json_for_profile(
            spawner_manifest,
            available,
            cwd,
            name,
            instruction_override,
            scope,
            selector,
        )
        .unwrap();
        serde_json::from_str(&json).unwrap()
    }

    const CODER_PROFILE: &str = r#"
slug = "coder"
scope = "workspace_write"

[model]
scheme = "anthropic"
model_id = "coder-model"

[engine]
instruction = "role.coder"
language = "Coderish"
max_tokens = 2222
"#;

    const REVIEWER_PROFILE: &str = r#"
slug = "reviewer"
scope = "workspace_write"

[model]
scheme = "anthropic"
model_id = "reviewer-model"

[engine]
instruction = "role.reviewer"
language = "Reviewerish"
max_tokens = 3333
"#;

    #[test]
    fn spawn_config_inherits_inline_spawner_model() {
        let model = ModelManifest {
            scheme: Some(SchemeKind::Anthropic),
            base_url: Some("https://example.test".into()),
            model_id: Some("claude-sonnet-4".into()),
            auth: Some(AuthRef::ApiKey {
                file: Some(PathBuf::from("/etc/keys/anthropic")),
            }),
            ..Default::default()
        };

        let config_json = build_spawn_config_json("child", "default", &[], &model, false).unwrap();
        let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();

        assert_eq!(parsed.model.scheme, Some(SchemeKind::Anthropic));
        assert_eq!(parsed.model.model_id.as_deref(), Some("claude-sonnet-4"));
        assert_eq!(
            parsed.model.base_url.as_deref(),
            Some("https://example.test")
        );
        let file = match parsed.model.auth {
            Some(AuthRef::ApiKey { file, .. }) => file,
            _ => panic!("expected ApiKey"),
        };
        assert_eq!(file.as_deref(), Some(Path::new("/etc/keys/anthropic")));
    }

    #[test]
    fn spawn_config_inherits_ref_spawner_model() {
        let model = ModelManifest {
            ref_: Some("anthropic/claude-sonnet-4-6".into()),
            ..Default::default()
        };
        let config_json = build_spawn_config_json("child", "default", &[], &model, false).unwrap();
        let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();
        assert_eq!(
            parsed.model.ref_.as_deref(),
            Some("anthropic/claude-sonnet-4-6")
        );
    }

    #[test]
    fn spawn_config_preserves_record_event_trace_when_enabled() {
        let model = ModelManifest {
            ref_: Some("anthropic/claude-sonnet-4-6".into()),
            ..Default::default()
        };
        let scope = vec![ScopeRule {
            target: PathBuf::from("/tmp/child"),
            permission: Permission::Read,
            recursive: true,
        }];

        let config_json =
            build_spawn_config_json("child", "default", &scope, &model, true).unwrap();
        let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();
        assert_eq!(
            parsed.session.as_ref().and_then(|s| s.record_event_trace),
            Some(true)
        );

        let manifest: WorkerManifest = WorkerManifestConfig::builtin_defaults()
            .merge(parsed)
            .try_into()
            .unwrap();
        assert!(manifest.session.record_event_trace);
    }

    #[test]
    fn spawn_config_omits_record_event_trace_when_disabled() {
        let model = ModelManifest {
            ref_: Some("anthropic/claude-sonnet-4-6".into()),
            ..Default::default()
        };
        let config_json = build_spawn_config_json("child", "default", &[], &model, false).unwrap();
        let parsed: WorkerManifestConfig = serde_json::from_str(&config_json).unwrap();

        assert!(parsed.session.is_none());
    }

    #[test]
    fn omitted_profile_resolves_effective_registry_default() {
        let tmp = TempDir::new().unwrap();
        let project = tmp.path().join("project");
        let delegated = tmp.path().join("delegated");
        std::fs::create_dir_all(&project).unwrap();
        std::fs::create_dir_all(&delegated).unwrap();
        let available = write_project_profile_registry(
            &project,
            Some("reviewer"),
            &[
                ("coder", "coder.toml", CODER_PROFILE),
                ("reviewer", "reviewer.toml", REVIEWER_PROFILE),
            ],
        );
        let parent = parent_manifest(&project, None);
        let scope = vec![abs_rule(&delegated, Permission::Read)];

        let config = child_config_from_profile(
            &parent,
            &available,
            &project,
            "child-default",
            None,
            &scope,
            SpawnProfileSelector::Default,
        );

        assert_eq!(config.worker.name.as_deref(), Some("child-default"));
        assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model"));
        assert_eq!(config.engine.instruction.as_deref(), Some("role.reviewer"));
        assert_eq!(config.engine.language.as_deref(), Some("Reviewerish"));
        assert_eq!(config.scope.allow, scope);
        assert!(config.scope.deny.is_empty());
    }

    #[test]
    fn source_qualified_profile_role_config_reaches_spawn_config() {
        let tmp = TempDir::new().unwrap();
        let project = tmp.path().join("project");
        let delegated = tmp.path().join("delegated");
        std::fs::create_dir_all(&project).unwrap();
        std::fs::create_dir_all(&delegated).unwrap();
        let available = write_project_profile_registry(
            &project,
            Some("coder"),
            &[
                ("coder", "coder.toml", CODER_PROFILE),
                ("reviewer", "reviewer.toml", REVIEWER_PROFILE),
            ],
        );
        let parent = parent_manifest(&project, None);
        let scope = vec![abs_rule(&delegated, Permission::Write)];

        let config = child_config_from_profile(
            &parent,
            &available,
            &project,
            "review-child",
            None,
            &scope,
            SpawnProfileSelector::Registry(ProfileSelector::source_named(
                ProfileRegistrySource::Project,
                "reviewer",
            )),
        );

        assert_eq!(config.worker.name.as_deref(), Some("review-child"));
        assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model"));
        assert_eq!(config.engine.instruction.as_deref(), Some("role.reviewer"));
        assert_eq!(config.engine.language.as_deref(), Some("Reviewerish"));
        assert_eq!(config.engine.max_tokens, Some(3333));
        assert_eq!(config.scope.allow, scope);
        assert!(config.scope.deny.is_empty());
    }

    #[test]
    fn inherit_copies_reusable_parent_fields_and_replaces_runtime_authority() {
        let tmp = TempDir::new().unwrap();
        let parent_root = tmp.path().join("parent-root");
        let parent_deny = parent_root.join("secret");
        let delegated = tmp.path().join("delegated");
        std::fs::create_dir_all(&parent_deny).unwrap();
        std::fs::create_dir_all(&delegated).unwrap();
        let parent = parent_manifest(&parent_root, Some(&parent_deny));
        let scope = vec![abs_rule(&delegated, Permission::Read)];
        let available = AvailableProfiles {
            registry: None,
            diagnostic: None,
        };

        let config = child_config_from_profile(
            &parent,
            &available,
            tmp.path(),
            "inherited-child",
            None,
            &scope,
            SpawnProfileSelector::Inherit,
        );

        assert_eq!(config.worker.name.as_deref(), Some("inherited-child"));
        assert_eq!(config.model.model_id.as_deref(), Some("parent-model"));
        assert_eq!(config.engine.instruction.as_deref(), Some("default"));
        assert_eq!(config.engine.language.as_deref(), Some("Parentish"));
        assert_eq!(config.engine.max_tokens, Some(1234));
        assert_eq!(
            config.engine.stop_sequences.as_deref(),
            Some(&["STOP".to_string()][..])
        );
        assert_eq!(
            config.session.as_ref().and_then(|s| s.record_event_trace),
            Some(true)
        );
        assert_eq!(config.scope.allow, scope);
        assert!(config.scope.deny.is_empty());
    }

    #[test]
    fn instruction_override_changes_only_worker_instruction() {
        let tmp = TempDir::new().unwrap();
        let project = tmp.path().join("project");
        let delegated = tmp.path().join("delegated");
        std::fs::create_dir_all(&project).unwrap();
        std::fs::create_dir_all(&delegated).unwrap();
        let available = write_project_profile_registry(
            &project,
            Some("reviewer"),
            &[("reviewer", "reviewer.toml", REVIEWER_PROFILE)],
        );
        let parent = parent_manifest(&project, None);
        let scope = vec![abs_rule(&delegated, Permission::Write)];

        let config = child_config_from_profile(
            &parent,
            &available,
            &project,
            "override-child",
            Some("role.reviewer"),
            &scope,
            SpawnProfileSelector::Default,
        );

        assert_eq!(config.engine.instruction.as_deref(), Some("role.reviewer"));
        assert_eq!(config.model.model_id.as_deref(), Some("reviewer-model"));
        assert_eq!(config.engine.language.as_deref(), Some("Reviewerish"));
        assert_eq!(config.engine.max_tokens, Some(3333));
        assert_eq!(config.scope.allow, scope);
    }

    #[test]
    fn profile_and_inherited_scope_are_replaced_by_delegated_scope() {
        let tmp = TempDir::new().unwrap();
        let project = tmp.path().join("project");
        let delegated = tmp.path().join("delegated");
        let parent_root = tmp.path().join("parent-root");
        std::fs::create_dir_all(&project).unwrap();
        std::fs::create_dir_all(&delegated).unwrap();
        std::fs::create_dir_all(&parent_root).unwrap();
        let available = write_project_profile_registry(
            &project,
            Some("reviewer"),
            &[("reviewer", "reviewer.toml", REVIEWER_PROFILE)],
        );
        let parent = parent_manifest(&parent_root, Some(&parent_root.join("deny")));
        let scope = vec![abs_rule(&delegated, Permission::Read)];

        let profile_config = child_config_from_profile(
            &parent,
            &available,
            &project,
            "profile-child",
            None,
            &scope,
            SpawnProfileSelector::Default,
        );
        let inherit_config = child_config_from_profile(
            &parent,
            &available,
            &project,
            "inherit-child",
            None,
            &scope,
            SpawnProfileSelector::Inherit,
        );

        for config in [profile_config, inherit_config] {
            assert_eq!(config.scope.allow, scope);
            assert!(config.scope.deny.is_empty());
            assert!(!config.scope.allow.iter().any(|rule| rule.target == project));
            assert!(
                !config
                    .scope
                    .allow
                    .iter()
                    .any(|rule| rule.target == parent_root)
            );
        }
    }

    #[test]
    fn invalid_ambiguous_and_no_default_diagnostics_include_available_selectors() {
        let tmp = TempDir::new().unwrap();
        let project = tmp.path().join("project");
        std::fs::create_dir_all(&project).unwrap();
        let available = write_project_profile_registry(
            &project,
            None,
            &[("coder", "coder.toml", CODER_PROFILE)],
        );
        let parent = parent_manifest(&project, None);
        let scope = vec![abs_rule(&project, Permission::Read)];

        let invalid = parse_spawn_profile_selector(Some("./reviewer.toml"))
            .map_err(|msg| format!("{msg}{}", available.error_suffix()))
            .unwrap_err();
        assert!(invalid.contains("Use `default`, `inherit`"));
        assert!(invalid.contains("`project:coder`"));

        let default_error = build_spawn_config_json_for_profile(
            &parent,
            &available,
            &project,
            "child",
            None,
            &scope,
            SpawnProfileSelector::Default,
        )
        .unwrap_err();
        assert!(default_error.contains("no default profile is configured"));

        let user_config = tmp.path().join("user-profiles.toml");
        std::fs::write(&user_config, "[profile]\ncoder = \"user-coder.toml\"\n").unwrap();
        let project_config = project.join("explicit-project-profiles.toml");
        let ambiguous = AvailableProfiles {
            registry: Some(
                ProfileDiscovery::with_sources(Some(user_config), Some(project_config))
                    .discover()
                    .unwrap(),
            ),
            diagnostic: None,
        };
        let ambiguous_error = build_spawn_config_json_for_profile(
            &parent,
            &ambiguous,
            &project,
            "child",
            None,
            &scope,
            SpawnProfileSelector::Registry(ProfileSelector::named("coder")),
        )
        .unwrap_err();
        assert!(ambiguous_error.contains("ambiguous"), "{ambiguous_error}");
        assert!(ambiguous_error.contains("user:coder"));
        assert!(ambiguous_error.contains("project:coder"));
        assert!(ambiguous_error.contains("Use `default`, `inherit`"));
    }

    #[test]
    fn spawn_profile_selector_rejects_path_like_values() {
        for raw in [
            "./reviewer.toml",
            "path:./reviewer.toml",
            "/tmp/reviewer.toml",
            "legacy.nix",
        ] {
            let err = parse_spawn_profile_selector(Some(raw)).unwrap_err();
            assert!(err.contains("registry selectors only"), "{raw}: {err}");
        }
    }

    #[test]
    fn spawn_profile_selector_accepts_default_inherit_and_registry() {
        assert_eq!(
            parse_spawn_profile_selector(None).unwrap(),
            SpawnProfileSelector::Default
        );
        assert_eq!(
            parse_spawn_profile_selector(Some("inherit")).unwrap(),
            SpawnProfileSelector::Inherit
        );
        assert_eq!(
            parse_spawn_profile_selector(Some("project:reviewer")).unwrap(),
            SpawnProfileSelector::Registry(ProfileSelector::source_named(
                ProfileRegistrySource::Project,
                "reviewer"
            ))
        );
        assert_eq!(
            parse_spawn_profile_selector(Some("coder")).unwrap(),
            SpawnProfileSelector::Registry(ProfileSelector::named("coder"))
        );
    }
}
