From 9f1cbbd385877304276c555bfc101330f87a9550 Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 16 Oct 2026 01:57:58 +0900 Subject: [PATCH] fix: inherit parent profile for default subworkers --- crates/worker/src/spawn/tool.rs | 62 +++++++++++++++---- .../sub_worker_spawn_tool_description.md | 5 +- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/crates/worker/src/spawn/tool.rs b/crates/worker/src/spawn/tool.rs index cf4d3d8f4..07edca94f 100644 --- a/crates/worker/src/spawn/tool.rs +++ b/crates/worker/src/spawn/tool.rs @@ -45,9 +45,9 @@ use protocol::Method; 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 + /// Profile selector for child role configuration. Omit or use `inherit` + /// to derive reusable config from the spawner, use `default` for the + /// effective registry default profile, 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)] @@ -204,7 +204,7 @@ enum SpawnProfileSelector { fn parse_spawn_profile_selector(raw: Option<&str>) -> Result { let Some(raw) = raw.map(str::trim).filter(|s| !s.is_empty()) else { - return Ok(SpawnProfileSelector::Default); + return Ok(SpawnProfileSelector::Inherit); }; if raw == "default" { return Ok(SpawnProfileSelector::Default); @@ -2298,7 +2298,7 @@ max_tokens = 3333 } #[test] - fn omitted_profile_resolves_effective_registry_default() { + fn explicit_default_profile_resolves_effective_registry_default() { let tmp = TempDir::new().unwrap(); let project = tmp.path().join("project"); let delegated = tmp.path().join("delegated"); @@ -2322,7 +2322,7 @@ max_tokens = 3333 "child-default", None, &scope, - SpawnProfileSelector::Default, + parse_spawn_profile_selector(Some("default")).unwrap(), ); assert_eq!(config.worker.name.as_deref(), Some("child-default")); @@ -2373,6 +2373,44 @@ max_tokens = 3333 assert!(config.scope.deny.is_empty()); } + #[test] + fn omitted_profile_inherits_parent_ticket_features_instead_of_registry_default() { + let tmp = TempDir::new().unwrap(); + let mut parent = parent_manifest(tmp.path(), None); + let scope = vec![abs_rule(tmp.path(), Permission::Read)]; + let available = AvailableProfiles { + registry: Some( + ProfileDiscovery::with_sources(None, None) + .discover() + .unwrap(), + ), + diagnostic: None, + }; + let input: SubWorkerSpawnInput = serde_json::from_value(serde_json::json!({ + "name": "child", "task": "inspect", "scope": [] + })) + .unwrap(); + for enabled in [false, true] { + parent.feature.ticket.enabled = enabled; + parent.feature.ticket.thread = enabled; + let config = child_config_from_profile( + &parent, + &available, + tmp.path(), + "child", + None, + &scope, + parse_spawn_profile_selector(input.profile.as_deref()).unwrap(), + ); + assert_eq!(config.model.model_id.as_deref(), Some("parent-model")); + assert_eq!(config.engine.language.as_deref(), Some("Parentish")); + let ticket = config.feature.ticket.unwrap(); + assert_eq!(ticket.enabled, Some(enabled)); + assert_eq!(ticket.thread, Some(enabled)); + assert_eq!(config.scope.allow, scope); + } + } + #[test] fn inherit_copies_reusable_parent_fields_and_replaces_runtime_authority() { let tmp = TempDir::new().unwrap(); @@ -2581,14 +2619,16 @@ max_tokens = 3333 #[test] fn spawn_profile_selector_accepts_default_inherit_and_registry() { + for raw in [None, Some(""), Some(" "), Some("inherit")] { + assert_eq!( + parse_spawn_profile_selector(raw).unwrap(), + SpawnProfileSelector::Inherit + ); + } assert_eq!( - parse_spawn_profile_selector(None).unwrap(), + parse_spawn_profile_selector(Some("default")).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( diff --git a/resources/prompts/internal/sub_worker_spawn_tool_description.md b/resources/prompts/internal/sub_worker_spawn_tool_description.md index d6d50df11..edd7d9ded 100644 --- a/resources/prompts/internal/sub_worker_spawn_tool_description.md +++ b/resources/prompts/internal/sub_worker_spawn_tool_description.md @@ -2,9 +2,10 @@ Spawn a parent-owned Internal SubWorker session to split context for a delegated Optional `cwd`: when provided, the spawned SubWorker's tool default working directory only. It must be a Workdir-relative existing directory covered by the child's readable scope, and it does not change workspace/Profile/memory/Ticket roots or grant authority. `name` must be unique among this Worker's direct children. -Profile selection: `profile` may be omitted or set to `default` to use the effective child default profile, set to `inherit` to derive reusable child configuration from this Worker, or set to one of the registry selectors below. Raw/path profile selectors are not accepted by SubWorkerSpawn. `scope` is the child's only filesystem capability and replaces profile scope. `command` is a separate explicit grant, defaults to false, and is accepted only with a writable scope; writable scope alone does not grant command execution. +Profile selection: omit `profile` or set it to `inherit` to derive reusable child configuration from this Worker. Set it to `default` to use the effective registry default profile, or to one of the registry selectors below. Raw/path profile selectors are not accepted by SubWorkerSpawn. `scope` is the child's only filesystem capability and replaces profile scope. `command` is a separate explicit grant, defaults to false, and is accepted only with a writable scope; writable scope alone does not grant command execution. -Default profile: {{ default_profile }} +Default when omitted: inherit. +Registry default (`profile = "default"`): {{ default_profile }} Special selector: inherit — derive reusable model/worker/tool policy from the spawner while replacing worker.name and scope. Available registry profiles: {{ available_profiles }}{% if profile_diagnostic %}