diff --git a/crates/manifest/src/config.rs b/crates/manifest/src/config.rs index c3003053..6ff27827 100644 --- a/crates/manifest/src/config.rs +++ b/crates/manifest/src/config.rs @@ -87,8 +87,6 @@ pub struct FeatureConfigPartial { #[serde(default)] pub ticket: Option, #[serde(default)] - pub ticket_orchestration: Option, - #[serde(default)] pub plugins: Option, } @@ -100,11 +98,6 @@ impl FeatureConfigPartial { web: merge_option(self.web, other.web, FeatureFlagConfigPartial::merge), workers: merge_option(self.workers, other.workers, FeatureFlagConfigPartial::merge), ticket: merge_option(self.ticket, other.ticket, TicketFeatureConfigPartial::merge), - ticket_orchestration: merge_option( - self.ticket_orchestration, - other.ticket_orchestration, - FeatureFlagConfigPartial::merge, - ), plugins: merge_option(self.plugins, other.plugins, FeatureFlagConfigPartial::merge), } } @@ -128,10 +121,6 @@ impl FeatureFlagConfigPartial { pub struct TicketFeatureConfigPartial { #[serde(default)] pub enabled: Option, - /// Legacy access field. Prefer `preset` for new profile/DCDL authoring. - #[serde(default)] - pub access: Option, - /// Semantic Ticket access preset for profile/DCDL authoring. #[serde(default)] pub preset: Option, } @@ -140,7 +129,6 @@ impl TicketFeatureConfigPartial { fn merge(self, other: Self) -> Self { Self { enabled: other.enabled.or(self.enabled), - access: other.access.or(self.access), preset: other.preset.or(self.preset), } } @@ -163,10 +151,6 @@ impl From for FeatureConfig { .ticket .map(TicketFeatureConfig::from) .unwrap_or_default(), - ticket_orchestration: value - .ticket_orchestration - .map(FeatureFlagConfig::from) - .unwrap_or_default(), plugins: value .plugins .map(FeatureFlagConfig::from) @@ -195,7 +179,7 @@ impl From for TicketFeatureConfig { fn from(value: TicketFeatureConfigPartial) -> Self { Self { enabled: value.enabled.unwrap_or_default(), - access: value.preset.or(value.access).unwrap_or_default(), + preset: value.preset.unwrap_or_default(), } } } @@ -204,8 +188,7 @@ impl From for TicketFeatureConfigPartial { fn from(value: TicketFeatureConfig) -> Self { Self { enabled: Some(value.enabled), - access: Some(value.access), - preset: None, + preset: Some(value.preset), } } } @@ -218,7 +201,6 @@ impl From for FeatureConfigPartial { web: Some(value.web.into()), workers: Some(value.workers.into()), ticket: Some(value.ticket.into()), - ticket_orchestration: Some(value.ticket_orchestration.into()), plugins: Some(value.plugins.into()), } } @@ -1764,7 +1746,6 @@ worker_max_turns = 7 assert!(!manifest.feature.web.enabled); assert!(!manifest.feature.workers.enabled); assert!(!manifest.feature.ticket.enabled); - assert!(!manifest.feature.ticket_orchestration.enabled); } #[test] @@ -1776,10 +1757,7 @@ enabled = true [feature.ticket] enabled = true -access = "read_only" - -[feature.ticket_orchestration] -enabled = true +preset = "read_only" "#, ) .unwrap(); @@ -1810,10 +1788,9 @@ enabled = true assert!(manifest.feature.task.enabled); assert!(manifest.feature.ticket.enabled); assert_eq!( - manifest.feature.ticket.access, + manifest.feature.ticket.preset, TicketFeatureAccessConfig::ReadOnly ); - assert!(manifest.feature.ticket_orchestration.enabled); assert!(!manifest.feature.memory.enabled); } @@ -1826,14 +1803,14 @@ enabled = true [feature.ticket] enabled = true -access = "read_only" +preset = "read_only" "#, ) .unwrap(); let upper = WorkerManifestConfig::from_toml( r#" [feature.ticket] -access = "lifecycle" +preset = "orchestration_control" [feature.web] enabled = true @@ -1868,8 +1845,8 @@ enabled = true assert!(manifest.feature.memory.enabled); assert!(manifest.feature.ticket.enabled); assert_eq!( - manifest.feature.ticket.access, - TicketFeatureAccessConfig::Lifecycle + manifest.feature.ticket.preset, + TicketFeatureAccessConfig::OrchestrationControl ); assert!(manifest.feature.web.enabled); assert!(!manifest.feature.workers.enabled); diff --git a/crates/manifest/src/lib.rs b/crates/manifest/src/lib.rs index 7891cd49..41f17280 100644 --- a/crates/manifest/src/lib.rs +++ b/crates/manifest/src/lib.rs @@ -115,8 +115,6 @@ pub struct FeatureConfig { #[serde(default)] pub ticket: TicketFeatureConfig, #[serde(default)] - pub ticket_orchestration: FeatureFlagConfig, - #[serde(default)] pub plugins: FeatureFlagConfig, } @@ -128,7 +126,6 @@ impl Default for FeatureConfig { web: FeatureFlagConfig::disabled(), workers: FeatureFlagConfig::disabled(), ticket: TicketFeatureConfig::default(), - ticket_orchestration: FeatureFlagConfig::disabled(), plugins: FeatureFlagConfig::disabled(), } } @@ -160,18 +157,15 @@ impl Default for FeatureFlagConfig { pub struct TicketFeatureConfig { #[serde(default)] pub enabled: bool, - /// Which non-orchestration Ticket surface to expose when `enabled = true`. - /// Orchestration-plan/relation tools are controlled independently by - /// `[feature.ticket_orchestration].enabled`. #[serde(default)] - pub access: TicketFeatureAccessConfig, + pub preset: TicketFeatureAccessConfig, } impl Default for TicketFeatureConfig { fn default() -> Self { Self { enabled: false, - access: TicketFeatureAccessConfig::Lifecycle, + preset: TicketFeatureAccessConfig::default(), } } } @@ -185,13 +179,11 @@ pub enum TicketFeatureAccessConfig { OrchestrationControl, WorkReport, Review, - /// Legacy broad mutation preset retained as a migration shim. - Lifecycle, } impl Default for TicketFeatureAccessConfig { fn default() -> Self { - Self::Lifecycle + Self::WorkspaceAuthoring } } diff --git a/crates/manifest/src/profile.rs b/crates/manifest/src/profile.rs index acd8026e..63a4d59a 100644 --- a/crates/manifest/src/profile.rs +++ b/crates/manifest/src/profile.rs @@ -894,7 +894,6 @@ fn builtin_profile_artifact(label: &str) -> Option { true, true, true, - false, ); Some(value) } @@ -908,7 +907,6 @@ fn builtin_profile_artifact(label: &str) -> Option { true, true, false, - false, ); Some(value) } @@ -922,7 +920,6 @@ fn builtin_profile_artifact(label: &str) -> Option { true, true, true, - true, ); Some(value) } @@ -936,7 +933,6 @@ fn builtin_profile_artifact(label: &str) -> Option { true, true, false, - false, ); Some(value) } @@ -950,7 +946,6 @@ fn builtin_profile_artifact(label: &str) -> Option { true, true, false, - false, ); Some(value) } @@ -976,8 +971,7 @@ fn builtin_default_profile_artifact() -> serde_json::Value { "memory": { "enabled": true }, "web": { "enabled": true }, "workers": { "enabled": true }, - "ticket": { "enabled": true, "access": "lifecycle" }, - "ticket_orchestration": { "enabled": false } + "ticket": { "enabled": true, "preset": "workspace_authoring" } }, "memory": { "extract_threshold": 50000, @@ -1004,7 +998,6 @@ fn apply_role_profile( memory: bool, web: bool, workers: bool, - ticket_orchestration: bool, ) { value["slug"] = serde_json::Value::String(slug.to_string()); value["description"] = serde_json::Value::String(description.to_string()); @@ -1018,14 +1011,12 @@ fn apply_role_profile( "orchestrator" => TicketFeatureAccessConfig::OrchestrationControl, "coder" => TicketFeatureAccessConfig::WorkReport, "reviewer" => TicketFeatureAccessConfig::Review, - _ => TicketFeatureAccessConfig::Lifecycle, + _ => TicketFeatureAccessConfig::WorkspaceAuthoring, }; value["feature"]["ticket"] = serde_json::json!({ "enabled": true, "preset": ticket_access, }); - value["feature"]["ticket_orchestration"] = - serde_json::json!({ "enabled": ticket_orchestration }); } fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), ProfileError> { @@ -1452,10 +1443,9 @@ mod tests { assert!(companion.web.is_some()); assert!(companion.feature.ticket.enabled); assert_eq!( - companion.feature.ticket.access, + companion.feature.ticket.preset, TicketFeatureAccessConfig::WorkspaceAuthoring ); - assert!(!companion.feature.ticket_orchestration.enabled); assert_eq!( companion.compaction.as_ref().unwrap().threshold, Some(240000) @@ -1478,7 +1468,7 @@ mod tests { assert!(!intake.feature.workers.enabled); assert!(intake.feature.ticket.enabled); assert_eq!( - intake.feature.ticket.access, + intake.feature.ticket.preset, TicketFeatureAccessConfig::Intake ); assert!(intake.scope.allow.is_empty()); @@ -1486,17 +1476,15 @@ mod tests { assert_eq!(intake.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); assert!(intake.web.is_some()); assert!(intake.compaction.is_some()); - assert!(!intake.feature.ticket_orchestration.enabled); let orchestrator = resolve("orchestrator"); assert!(orchestrator.feature.task.enabled); assert!(orchestrator.feature.workers.enabled); assert!(orchestrator.feature.ticket.enabled); assert_eq!( - orchestrator.feature.ticket.access, + orchestrator.feature.ticket.preset, TicketFeatureAccessConfig::OrchestrationControl ); - assert!(orchestrator.feature.ticket_orchestration.enabled); assert!(orchestrator.scope.allow.is_empty()); assert!(orchestrator.delegation_scope.allow.is_empty()); assert_eq!( @@ -1516,20 +1504,18 @@ mod tests { assert!(coder.compaction.is_some()); assert!(coder.feature.ticket.enabled); assert_eq!( - coder.feature.ticket.access, + coder.feature.ticket.preset, TicketFeatureAccessConfig::WorkReport ); - assert!(!coder.feature.ticket_orchestration.enabled); let reviewer = resolve("reviewer"); assert!(reviewer.feature.task.enabled); assert!(!reviewer.feature.workers.enabled); assert!(reviewer.feature.ticket.enabled); assert_eq!( - reviewer.feature.ticket.access, + reviewer.feature.ticket.preset, TicketFeatureAccessConfig::Review ); - assert!(!reviewer.feature.ticket_orchestration.enabled); assert!(reviewer.scope.allow.is_empty()); assert!(reviewer.delegation_scope.allow.is_empty()); assert_eq!(reviewer.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); @@ -1678,10 +1664,7 @@ enabled = true [feature.ticket] enabled = true -access = "read_only" - -[feature.ticket_orchestration] -enabled = false +preset = "read_only" "#, ); let workspace = tmp.path().join("workspace"); @@ -1700,10 +1683,9 @@ enabled = false assert!(resolved.manifest.feature.workers.enabled); assert!(resolved.manifest.feature.ticket.enabled); assert_eq!( - resolved.manifest.feature.ticket.access, + resolved.manifest.feature.ticket.preset, crate::TicketFeatureAccessConfig::ReadOnly ); - assert!(!resolved.manifest.feature.ticket_orchestration.enabled); assert_eq!( resolved.manifest.delegation_scope.allow[0].target, workspace @@ -1791,10 +1773,9 @@ worker_context_max_tokens = 68000 assert!(resolved.manifest.session.record_event_trace); assert!(resolved.manifest.feature.ticket.enabled); assert_eq!( - resolved.manifest.feature.ticket.access, - crate::TicketFeatureAccessConfig::Lifecycle + resolved.manifest.feature.ticket.preset, + crate::TicketFeatureAccessConfig::WorkspaceAuthoring ); - assert!(!resolved.manifest.feature.ticket_orchestration.enabled); assert_eq!( resolved.profile.as_ref().unwrap().name.as_deref(), Some("default") diff --git a/crates/tui/src/setup_model.rs b/crates/tui/src/setup_model.rs index f7d2670e..0c4d6230 100644 --- a/crates/tui/src/setup_model.rs +++ b/crates/tui/src/setup_model.rs @@ -238,10 +238,7 @@ enabled = false [feature.ticket] enabled = true -access = "lifecycle" - -[feature.ticket_orchestration] -enabled = false +preset = "workspace_authoring" [memory] extract_threshold = 50000 @@ -298,8 +295,9 @@ mod tests { assert!(profile.contains("slug = \"default\"")); assert!(profile.contains("ref = \"codex-oauth/gpt-5.5\"")); assert!(profile.contains("scope = \"workspace_write\"")); - assert!(profile.contains("[feature.ticket]\nenabled = true\naccess = \"lifecycle\"")); - assert!(profile.contains("[feature.ticket_orchestration]\nenabled = false")); + assert!( + profile.contains("[feature.ticket]\nenabled = true\npreset = \"workspace_authoring\"") + ); } #[test] diff --git a/crates/worker/src/controller.rs b/crates/worker/src/controller.rs index 58e25e20..cc131c6c 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -548,10 +548,7 @@ fn install_ticket_event_companion_notify_hook( let ticket_feature = &worker.manifest().feature.ticket; if !ticket_feature.enabled - || !matches!( - ticket_feature.access, - TicketFeatureAccessConfig::OrchestrationControl | TicketFeatureAccessConfig::Lifecycle - ) + || ticket_feature.preset != TicketFeatureAccessConfig::OrchestrationControl { return; } @@ -677,8 +674,8 @@ where if feature_config.task.enabled { feature_registry.add_module(task_feature); } - if feature_config.ticket.enabled || feature_config.ticket_orchestration.enabled { - let ticket_access = match feature_config.ticket.access { + if feature_config.ticket.enabled { + let ticket_access = match feature_config.ticket.preset { TicketFeatureAccessConfig::ReadOnly => { crate::feature::builtin::ticket::TicketFeatureAccess::ReadOnly } @@ -697,9 +694,6 @@ where TicketFeatureAccessConfig::Review => { crate::feature::builtin::ticket::TicketFeatureAccess::Review } - TicketFeatureAccessConfig::Lifecycle => { - crate::feature::builtin::ticket::TicketFeatureAccess::Lifecycle - } }; // Ticket tools are typed operations over the current workspace Ticket backend. // Runtime-hosted Workers prefer the workspace API URI carried by the @@ -728,10 +722,9 @@ where } }; feature_registry.add_module( - crate::feature::builtin::ticket::ticket_tools_feature_with_options( + crate::feature::builtin::ticket::ticket_tools_feature_with_backend( ticket_backend, - feature_config.ticket.enabled.then_some(ticket_access), - feature_config.ticket_orchestration.enabled, + ticket_access, ), ); } diff --git a/crates/worker/src/feature/builtin.rs b/crates/worker/src/feature/builtin.rs index 82c35226..bef8b017 100644 --- a/crates/worker/src/feature/builtin.rs +++ b/crates/worker/src/feature/builtin.rs @@ -14,5 +14,5 @@ pub(crate) use session_explore::{ pub use task::{TaskFeature, task_tools_feature}; pub use ticket::{ TicketFeature, TicketFeatureAccess, ticket_tools_feature, ticket_tools_feature_with_access, - ticket_tools_feature_with_options, + ticket_tools_feature_with_backend, }; diff --git a/crates/worker/src/feature/builtin/ticket.rs b/crates/worker/src/feature/builtin/ticket.rs index 65c69abf..2d046a05 100644 --- a/crates/worker/src/feature/builtin/ticket.rs +++ b/crates/worker/src/feature/builtin/ticket.rs @@ -14,11 +14,7 @@ use ticket::{ TicketIntakeSummary, TicketListQuery, TicketRef, TicketRelation, TicketRelationKind, TicketRelationView, TicketReview, TicketStateChange, TicketSummary, config::{DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TicketConfig}, - tool::{ - TICKET_BASE_READ_ONLY_TOOL_NAMES, TICKET_BASE_TOOL_NAMES, - TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES, - TicketToolBackend, ticket_tool_description, ticket_tools, - }, + tool::{TicketToolBackend, ticket_tool_description, ticket_tools}, }; use crate::feature::{ @@ -45,11 +41,18 @@ pub enum TicketFeatureAccess { WorkReport, /// Reviewer-style access for reading Tickets and writing review/comment thread events. Review, - /// Legacy full lifecycle access retained as a migration shim. - Lifecycle, } -const WORKSPACE_AUTHORING_BASE_TOOL_NAMES: [&str; 10] = [ +const READ_ONLY_TOOL_NAMES: [&str; 6] = [ + "TicketList", + "TicketShow", + "TicketDependencyCheck", + "TicketDoctor", + "TicketRelationQuery", + "TicketOrchestrationPlanQuery", +]; + +const WORKSPACE_AUTHORING_TOOL_NAMES: [&str; 12] = [ "TicketCreate", "TicketEditItem", "TicketList", @@ -60,9 +63,11 @@ const WORKSPACE_AUTHORING_BASE_TOOL_NAMES: [&str; 10] = [ "TicketClose", "TicketDependencyCheck", "TicketDoctor", + "TicketRelationRecord", + "TicketRelationQuery", ]; -const INTAKE_BASE_TOOL_NAMES: [&str; 8] = [ +const INTAKE_TOOL_NAMES: [&str; 10] = [ "TicketCreate", "TicketEditItem", "TicketList", @@ -71,9 +76,11 @@ const INTAKE_BASE_TOOL_NAMES: [&str; 8] = [ "TicketIntakeReady", "TicketDependencyCheck", "TicketDoctor", + "TicketRelationRecord", + "TicketRelationQuery", ]; -const ORCHESTRATION_CONTROL_BASE_TOOL_NAMES: [&str; 8] = [ +const ORCHESTRATION_CONTROL_TOOL_NAMES: [&str; 12] = [ "TicketList", "TicketShow", "TicketComment", @@ -82,47 +89,42 @@ const ORCHESTRATION_CONTROL_BASE_TOOL_NAMES: [&str; 8] = [ "TicketClose", "TicketDependencyCheck", "TicketDoctor", + "TicketRelationRecord", + "TicketRelationQuery", + "TicketOrchestrationPlanRecord", + "TicketOrchestrationPlanQuery", ]; -const WORK_REPORT_BASE_TOOL_NAMES: [&str; 5] = [ +const WORK_REPORT_TOOL_NAMES: [&str; 7] = [ "TicketList", "TicketShow", "TicketComment", "TicketDependencyCheck", "TicketDoctor", + "TicketRelationQuery", + "TicketOrchestrationPlanQuery", ]; -const REVIEW_BASE_TOOL_NAMES: [&str; 6] = [ +const REVIEW_TOOL_NAMES: [&str; 8] = [ "TicketList", "TicketShow", "TicketComment", "TicketReview", "TicketDependencyCheck", "TicketDoctor", + "TicketRelationQuery", + "TicketOrchestrationPlanQuery", ]; -const RELATION_WRITE_TOOL_NAMES: [&str; 2] = ["TicketRelationRecord", "TicketRelationQuery"]; - impl TicketFeatureAccess { - pub fn base_tool_names(self) -> &'static [&'static str] { + pub fn tool_names(self) -> &'static [&'static str] { match self { - Self::ReadOnly => &TICKET_BASE_READ_ONLY_TOOL_NAMES, - Self::WorkspaceAuthoring => &WORKSPACE_AUTHORING_BASE_TOOL_NAMES, - Self::Intake => &INTAKE_BASE_TOOL_NAMES, - Self::OrchestrationControl => &ORCHESTRATION_CONTROL_BASE_TOOL_NAMES, - Self::WorkReport => &WORK_REPORT_BASE_TOOL_NAMES, - Self::Review => &REVIEW_BASE_TOOL_NAMES, - Self::Lifecycle => &TICKET_BASE_TOOL_NAMES, - } - } - - pub fn orchestration_tool_names(self) -> &'static [&'static str] { - match self { - Self::ReadOnly | Self::WorkReport | Self::Review => { - &TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES - } - Self::WorkspaceAuthoring | Self::Intake => &RELATION_WRITE_TOOL_NAMES, - Self::OrchestrationControl | Self::Lifecycle => &TICKET_ORCHESTRATION_TOOL_NAMES, + Self::ReadOnly => &READ_ONLY_TOOL_NAMES, + Self::WorkspaceAuthoring => &WORKSPACE_AUTHORING_TOOL_NAMES, + Self::Intake => &INTAKE_TOOL_NAMES, + Self::OrchestrationControl => &ORCHESTRATION_CONTROL_TOOL_NAMES, + Self::WorkReport => &WORK_REPORT_TOOL_NAMES, + Self::Review => &REVIEW_TOOL_NAMES, } } } @@ -167,94 +169,59 @@ pub struct TicketFeature { record_language: Option, config_error: Option, access: TicketFeatureAccess, - include_base_tools: bool, - include_orchestration_tools: bool, } impl TicketFeature { pub fn new(backend_root: impl Into) -> Self { - Self::new_with_access(backend_root, TicketFeatureAccess::Lifecycle) + Self::new_with_access(backend_root, TicketFeatureAccess::WorkspaceAuthoring) } pub fn new_with_access(backend_root: impl Into, access: TicketFeatureAccess) -> Self { - Self::new_with_options(backend_root, Some(access), true) - } - - pub fn new_with_options( - backend_root: impl Into, - access: Option, - include_orchestration_tools: bool, - ) -> Self { Self::with_backend( TicketFeatureBackend::Local { root: backend_root.into(), }, access, - include_orchestration_tools, ) } - pub fn with_backend( - backend: TicketFeatureBackend, - access: Option, - include_orchestration_tools: bool, - ) -> Self { + pub fn with_backend(backend: TicketFeatureBackend, access: TicketFeatureAccess) -> Self { if let TicketFeatureBackend::LocalWorkspace { workspace_root } = backend { - return Self::for_workspace_with_options( - workspace_root, - access, - include_orchestration_tools, - ); + return Self::for_workspace_with_access(workspace_root, access); } Self { backend, record_language: None, config_error: None, - access: access.unwrap_or(TicketFeatureAccess::Lifecycle), - include_base_tools: access.is_some(), - include_orchestration_tools, + access, } } pub fn for_workspace(workspace: impl AsRef) -> Self { - Self::for_workspace_with_access(workspace, TicketFeatureAccess::Lifecycle) + Self::for_workspace_with_access(workspace, TicketFeatureAccess::WorkspaceAuthoring) } pub fn for_workspace_with_access( workspace: impl AsRef, access: TicketFeatureAccess, - ) -> Self { - Self::for_workspace_with_options(workspace, Some(access), true) - } - - pub fn for_workspace_with_options( - workspace: impl AsRef, - access: Option, - include_orchestration_tools: bool, ) -> Self { let workspace = workspace.as_ref(); match TicketConfig::load_workspace(workspace) { Ok(config) => { let backend_root = config.backend_root().to_path_buf(); let record_language = config.ticket_record_language().map(str::to_string); - let mut feature = - Self::new_with_options(backend_root, access, include_orchestration_tools); + let mut feature = Self::new_with_access(backend_root, access); feature.record_language = record_language; feature } - Err(error) => { - let access_value = access.unwrap_or(TicketFeatureAccess::Lifecycle); - Self { - backend: TicketFeatureBackend::Local { - root: workspace.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH), - }, - record_language: None, - config_error: Some(error.to_string()), - access: access_value, - include_base_tools: access.is_some(), - include_orchestration_tools, - } - } + Err(error) => Self { + backend: TicketFeatureBackend::Local { + root: workspace.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH), + }, + record_language: None, + config_error: Some(error.to_string()), + access, + }, } } @@ -270,15 +237,8 @@ impl TicketFeature { self.access } - fn enabled_tool_names(&self) -> Vec<&'static str> { - let mut names = Vec::new(); - if self.include_base_tools { - names.extend_from_slice(self.access.base_tool_names()); - } - if self.include_orchestration_tools { - names.extend_from_slice(self.access.orchestration_tool_names()); - } - names + fn enabled_tool_names(&self) -> &'static [&'static str] { + self.access.tool_names() } fn usable_backend_root(&self) -> Result { @@ -336,7 +296,7 @@ impl FeatureModule for TicketFeature { let mut descriptor = FeatureDescriptor::builtin(FEATURE_ID, FEATURE_NAME) .with_description(FEATURE_DESCRIPTION); let enabled_tool_names = self.enabled_tool_names(); - for name in &enabled_tool_names { + for name in enabled_tool_names { descriptor = descriptor.with_tool(ToolDeclaration::new( *name, ticket_tool_description(name, self.record_language.as_deref()), @@ -680,12 +640,11 @@ pub fn ticket_tools_feature_with_access( TicketFeature::for_workspace_with_access(workspace, access) } -pub fn ticket_tools_feature_with_options( +pub fn ticket_tools_feature_with_backend( backend: impl Into, - access: Option, - include_orchestration_tools: bool, + access: TicketFeatureAccess, ) -> TicketFeature { - TicketFeature::with_backend(backend.into(), access, include_orchestration_tools) + TicketFeature::with_backend(backend.into(), access) } #[cfg(test)] @@ -697,10 +656,6 @@ mod tests { use std::net::TcpListener; use std::thread; use tempfile::TempDir; - use ticket::tool::{ - TICKET_BASE_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES, TICKET_READ_ONLY_TOOL_NAMES, - TICKET_TOOL_NAMES, - }; fn make_ticket_root(root: &Path) { std::fs::create_dir_all(root).unwrap(); @@ -732,14 +687,14 @@ mod tests { let descriptor = feature.descriptor(); assert_eq!(descriptor.id.to_string(), "builtin:ticket"); assert_eq!(descriptor.runtime, FeatureRuntimeKind::Builtin); - assert_eq!(descriptor.tools.len(), TICKET_TOOL_NAMES.len()); + assert_eq!(descriptor.tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len()); assert_eq!( descriptor .tools .iter() .map(|tool| tool.name.as_str()) .collect::>(), - TICKET_TOOL_NAMES + WORKSPACE_AUTHORING_TOOL_NAMES ); } @@ -749,48 +704,33 @@ mod tests { let feature = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::ReadOnly); let descriptor = feature.descriptor(); assert_eq!(feature.access(), TicketFeatureAccess::ReadOnly); - assert_eq!(descriptor.tools.len(), TICKET_READ_ONLY_TOOL_NAMES.len()); + assert_eq!(descriptor.tools.len(), READ_ONLY_TOOL_NAMES.len()); assert_eq!( descriptor .tools .iter() .map(|tool| tool.name.as_str()) .collect::>(), - TICKET_READ_ONLY_TOOL_NAMES + READ_ONLY_TOOL_NAMES ); } #[test] - fn descriptor_can_expose_base_ticket_without_orchestration_tools() { + fn orchestration_control_descriptor_declares_orchestration_tools() { let temp = TempDir::new().unwrap(); - let feature = ticket_tools_feature_with_options( + let feature = ticket_tools_feature_with_access( temp.path(), - Some(TicketFeatureAccess::Lifecycle), - false, + TicketFeatureAccess::OrchestrationControl, ); let descriptor = feature.descriptor(); + assert_eq!(feature.access(), TicketFeatureAccess::OrchestrationControl); assert_eq!( descriptor .tools .iter() .map(|tool| tool.name.as_str()) .collect::>(), - TICKET_BASE_TOOL_NAMES - ); - } - - #[test] - fn descriptor_can_expose_orchestration_only_tools() { - let temp = TempDir::new().unwrap(); - let feature = ticket_tools_feature_with_options(temp.path(), None, true); - let descriptor = feature.descriptor(); - assert_eq!( - descriptor - .tools - .iter() - .map(|tool| tool.name.as_str()) - .collect::>(), - TICKET_ORCHESTRATION_TOOL_NAMES + ORCHESTRATION_CONTROL_TOOL_NAMES ); } @@ -798,11 +738,8 @@ mod tests { fn semantic_access_presets_expose_role_capability_surfaces() { let temp = TempDir::new().unwrap(); - let workspace_authoring = ticket_tools_feature_with_options( - temp.path(), - Some(TicketFeatureAccess::WorkspaceAuthoring), - false, - ); + let workspace_authoring = + ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::WorkspaceAuthoring); let workspace_descriptor = workspace_authoring.descriptor(); let workspace_tools = workspace_descriptor .tools @@ -814,10 +751,9 @@ mod tests { assert!(workspace_tools.contains(&"TicketQueue")); assert!(!workspace_tools.contains(&"TicketWorkflowState")); - let orchestration = ticket_tools_feature_with_options( + let orchestration = ticket_tools_feature_with_access( temp.path(), - Some(TicketFeatureAccess::OrchestrationControl), - true, + TicketFeatureAccess::OrchestrationControl, ); let orchestration_descriptor = orchestration.descriptor(); let orchestration_tools = orchestration_descriptor @@ -832,11 +768,8 @@ mod tests { assert!(!orchestration_tools.contains(&"TicketEditItem")); assert!(!orchestration_tools.contains(&"TicketQueue")); - let work_report = ticket_tools_feature_with_options( - temp.path(), - Some(TicketFeatureAccess::WorkReport), - false, - ); + let work_report = + ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::WorkReport); let work_report_descriptor = work_report.descriptor(); let work_report_tools = work_report_descriptor .tools @@ -847,11 +780,7 @@ mod tests { assert!(!work_report_tools.contains(&"TicketReview")); assert!(!work_report_tools.contains(&"TicketWorkflowState")); - let review = ticket_tools_feature_with_options( - temp.path(), - Some(TicketFeatureAccess::Review), - false, - ); + let review = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::Review); let review_descriptor = review.descriptor(); let review_tools = review_descriptor .tools @@ -875,16 +804,13 @@ mod tests { )) .install_into_pending(&mut pending_tools, &mut hooks); - assert_eq!(pending_tools.len(), TICKET_READ_ONLY_TOOL_NAMES.len()); - assert_eq!( - report.reports[0].installed_tools, - TICKET_READ_ONLY_TOOL_NAMES - ); + assert_eq!(pending_tools.len(), READ_ONLY_TOOL_NAMES.len()); + assert_eq!(report.reports[0].installed_tools, READ_ONLY_TOOL_NAMES); let pending_names = pending_tools .iter() .map(|definition| definition().0.name) .collect::>(); - assert_eq!(pending_names, TICKET_READ_ONLY_TOOL_NAMES); + assert_eq!(pending_names, READ_ONLY_TOOL_NAMES); for name in ticket::tool::TICKET_MUTATING_TOOL_NAMES { assert!( !report.reports[0] @@ -924,11 +850,8 @@ language = "Japanese" .with_module(feature) .install_into_pending(&mut pending_tools, &mut hooks); - assert_eq!(pending_tools.len(), TICKET_READ_ONLY_TOOL_NAMES.len()); - assert_eq!( - report.reports[0].installed_tools, - TICKET_READ_ONLY_TOOL_NAMES - ); + assert_eq!(pending_tools.len(), READ_ONLY_TOOL_NAMES.len()); + assert_eq!(report.reports[0].installed_tools, READ_ONLY_TOOL_NAMES); let description = pending_tool_description(&pending_tools, "TicketShow"); assert!(description.contains("Ticket record language: Japanese")); assert!(description.contains("distinct from worker.language")); @@ -936,7 +859,7 @@ language = "Japanese" } #[test] - fn lifecycle_installation_exposes_lifecycle_tools() { + fn workspace_authoring_installation_exposes_authoring_tools() { let temp = TempDir::new().unwrap(); make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)); let mut pending_tools = Vec::new(); @@ -944,36 +867,31 @@ language = "Japanese" let report = FeatureRegistryBuilder::new() .with_module(ticket_tools_feature_with_access( temp.path(), - TicketFeatureAccess::Lifecycle, + TicketFeatureAccess::WorkspaceAuthoring, )) .install_into_pending(&mut pending_tools, &mut hooks); - assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len()); - assert_eq!(report.reports[0].installed_tools, TICKET_TOOL_NAMES); - for name in ticket::tool::TICKET_MUTATING_TOOL_NAMES { - assert!( - report.reports[0] - .installed_tools - .iter() - .any(|tool| tool == name) - ); - } + assert_eq!(pending_tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len()); + let installed = report.reports[0] + .installed_tools + .iter() + .map(String::as_str) + .collect::>(); + assert_eq!(installed, WORKSPACE_AUTHORING_TOOL_NAMES); + assert!(installed.iter().any(|tool| *tool == "TicketCreate")); + assert!(installed.iter().any(|tool| *tool == "TicketEditItem")); + assert!(installed.iter().any(|tool| *tool == "TicketQueue")); + assert!(!installed.iter().any(|tool| *tool == "TicketIntakeReady")); + assert!(!installed.iter().any(|tool| *tool == "TicketWorkflowState")); assert!( - report.reports[0] - .installed_tools + !installed .iter() - .any(|tool| tool == "TicketIntakeReady") - ); - assert!( - report.reports[0] - .installed_tools - .iter() - .any(|tool| tool == "TicketWorkflowState") + .any(|tool| *tool == "TicketOrchestrationPlanRecord") ); } #[test] - fn lifecycle_ticket_role_style_context_exposes_ticket_language_guidance() { + fn workspace_authoring_context_exposes_ticket_language_guidance() { let temp = TempDir::new().unwrap(); write_ticket_config( temp.path(), @@ -988,12 +906,15 @@ language = "Japanese" let report = FeatureRegistryBuilder::new() .with_module(ticket_tools_feature_with_access( temp.path(), - TicketFeatureAccess::Lifecycle, + TicketFeatureAccess::WorkspaceAuthoring, )) .install_into_pending(&mut pending_tools, &mut hooks); - assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len()); - assert_eq!(report.reports[0].installed_tools, TICKET_TOOL_NAMES); + assert_eq!(pending_tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len()); + assert_eq!( + report.reports[0].installed_tools, + WORKSPACE_AUTHORING_TOOL_NAMES + ); let description = pending_tool_description(&pending_tools, "TicketComment"); assert!(description.contains("Ticket record language: Japanese")); assert!(description.contains("durable Ticket record and Ticket tool body text")); @@ -1011,10 +932,13 @@ language = "Japanese" .with_module(ticket_tools_feature(temp.path())) .install_into_pending(&mut pending_tools, &mut hooks); - assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len()); + assert_eq!(pending_tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len()); assert_eq!(report.reports.len(), 1); assert!(report.reports[0].installed); - assert_eq!(report.reports[0].installed_tools, TICKET_TOOL_NAMES); + assert_eq!( + report.reports[0].installed_tools, + WORKSPACE_AUTHORING_TOOL_NAMES + ); assert!(report.reports[0].skipped.is_empty()); } @@ -1046,7 +970,7 @@ profile = "project:coder" .with_module(feature) .install_into_pending(&mut pending_tools, &mut hooks); - assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len()); + assert_eq!(pending_tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len()); assert!(report.reports[0].diagnostics.is_empty()); } @@ -1132,8 +1056,11 @@ provider = "github" .with_module(ticket_tools_feature(temp.path())) .install_into_pending(&mut pending_tools, &mut hooks); - assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len()); - assert_eq!(report.reports[0].installed_tools, TICKET_TOOL_NAMES); + assert_eq!(pending_tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len()); + assert_eq!( + report.reports[0].installed_tools, + WORKSPACE_AUTHORING_TOOL_NAMES + ); assert!(report.reports[0].diagnostics.is_empty()); assert!(!root.join("open").exists()); assert!(!root.join("pending").exists()); diff --git a/resources/profiles/coder.dcdl b/resources/profiles/coder.dcdl index ea53295d..7a6a4e75 100644 --- a/resources/profiles/coder.dcdl +++ b/resources/profiles/coder.dcdl @@ -8,6 +8,6 @@ import "./default.dcdl" // { memory = { enabled = true; }; web = { enabled = true; }; workers = { enabled = false; }; - ticket_orchestration = { enabled = false; }; + ticket = { enabled = true; preset = "work_report"; }; }; } diff --git a/resources/profiles/companion.dcdl b/resources/profiles/companion.dcdl index 2c1c3851..cb78855e 100644 --- a/resources/profiles/companion.dcdl +++ b/resources/profiles/companion.dcdl @@ -8,6 +8,6 @@ import "./default.dcdl" // { memory = { enabled = true; }; web = { enabled = true; }; workers = { enabled = true; }; - ticket_orchestration = { enabled = false; }; + ticket = { enabled = true; preset = "workspace_authoring"; }; }; } diff --git a/resources/profiles/default.dcdl b/resources/profiles/default.dcdl index 81d06f66..9b261b41 100644 --- a/resources/profiles/default.dcdl +++ b/resources/profiles/default.dcdl @@ -26,8 +26,7 @@ feature = { memory = { enabled = true; }; web = { enabled = true; }; workers = { enabled = true; }; - ticket = { enabled = true; access = "lifecycle"; }; - ticket_orchestration = { enabled = false; }; + ticket = { enabled = true; preset = "workspace_authoring"; }; }; memory = { diff --git a/resources/profiles/intake.dcdl b/resources/profiles/intake.dcdl index 91e98b5e..dc0be16b 100644 --- a/resources/profiles/intake.dcdl +++ b/resources/profiles/intake.dcdl @@ -8,6 +8,6 @@ import "./default.dcdl" // { memory = { enabled = true; }; web = { enabled = true; }; workers = { enabled = false; }; - ticket_orchestration = { enabled = false; }; + ticket = { enabled = true; preset = "intake"; }; }; } diff --git a/resources/profiles/orchestrator.dcdl b/resources/profiles/orchestrator.dcdl index a502b999..951bdef3 100644 --- a/resources/profiles/orchestrator.dcdl +++ b/resources/profiles/orchestrator.dcdl @@ -8,6 +8,6 @@ import "./default.dcdl" // { memory = { enabled = true; }; web = { enabled = true; }; workers = { enabled = true; }; - ticket_orchestration = { enabled = true; }; + ticket = { enabled = true; preset = "orchestration_control"; }; }; } diff --git a/resources/profiles/reviewer.dcdl b/resources/profiles/reviewer.dcdl index 9f714f86..41c32401 100644 --- a/resources/profiles/reviewer.dcdl +++ b/resources/profiles/reviewer.dcdl @@ -8,6 +8,6 @@ import "./default.dcdl" // { memory = { enabled = true; }; web = { enabled = true; }; workers = { enabled = false; }; - ticket_orchestration = { enabled = false; }; + ticket = { enabled = true; preset = "review"; }; }; }