From 7f7d2fe7fc0987b58a384b2a0b87c342e232396b Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 21 Jul 2026 06:13:26 +0900 Subject: [PATCH] ticket: use boolean ticket capabilities --- crates/manifest/src/config.rs | 58 ++++-- crates/manifest/src/lib.rs | 36 +--- crates/manifest/src/profile.rs | 98 ++++----- crates/tui/src/setup_model.rs | 5 +- crates/worker/src/controller.rs | 29 +-- crates/worker/src/feature/builtin/ticket.rs | 209 ++++++++++++-------- resources/profiles/coder.dcdl | 2 +- resources/profiles/companion.dcdl | 2 +- resources/profiles/default.dcdl | 2 +- resources/profiles/intake.dcdl | 2 +- resources/profiles/orchestrator.dcdl | 2 +- resources/profiles/reviewer.dcdl | 2 +- 12 files changed, 239 insertions(+), 208 deletions(-) diff --git a/crates/manifest/src/config.rs b/crates/manifest/src/config.rs index 6ff27827..73534508 100644 --- a/crates/manifest/src/config.rs +++ b/crates/manifest/src/config.rs @@ -19,8 +19,8 @@ use crate::plugin::PluginConfig; use crate::{ CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits, McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConfig, ScopeConfig, SessionConfig, - SkillsConfig, TicketFeatureAccessConfig, TicketFeatureConfig, ToolOutputLimits, - ToolPermissionConfig, ToolPermissionRule, WebConfig, WorkerManifest, WorkerMeta, + SkillsConfig, TicketFeatureConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, + WebConfig, WorkerManifest, WorkerMeta, }; /// Partial-form Worker manifest. Every field is optional; one or more @@ -117,19 +117,24 @@ impl FeatureFlagConfigPartial { } } -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(default, deny_unknown_fields)] pub struct TicketFeatureConfigPartial { - #[serde(default)] pub enabled: Option, - #[serde(default)] - pub preset: Option, + pub authoring: Option, + pub thread: Option, + pub intake: Option, + pub orchestration_control: Option, } impl TicketFeatureConfigPartial { fn merge(self, other: Self) -> Self { Self { enabled: other.enabled.or(self.enabled), - preset: other.preset.or(self.preset), + authoring: other.authoring.or(self.authoring), + thread: other.thread.or(self.thread), + intake: other.intake.or(self.intake), + orchestration_control: other.orchestration_control.or(self.orchestration_control), } } } @@ -179,7 +184,10 @@ impl From for TicketFeatureConfig { fn from(value: TicketFeatureConfigPartial) -> Self { Self { enabled: value.enabled.unwrap_or_default(), - preset: value.preset.unwrap_or_default(), + authoring: value.authoring.unwrap_or_default(), + thread: value.thread.unwrap_or_default(), + intake: value.intake.unwrap_or_default(), + orchestration_control: value.orchestration_control.unwrap_or_default(), } } } @@ -188,7 +196,10 @@ impl From for TicketFeatureConfigPartial { fn from(value: TicketFeatureConfig) -> Self { Self { enabled: Some(value.enabled), - preset: Some(value.preset), + authoring: Some(value.authoring), + thread: Some(value.thread), + intake: Some(value.intake), + orchestration_control: Some(value.orchestration_control), } } } @@ -1757,7 +1768,10 @@ enabled = true [feature.ticket] enabled = true -preset = "read_only" +authoring = false +thread = false +intake = false +orchestration_control = false "#, ) .unwrap(); @@ -1787,10 +1801,10 @@ preset = "read_only" .unwrap(); assert!(manifest.feature.task.enabled); assert!(manifest.feature.ticket.enabled); - assert_eq!( - manifest.feature.ticket.preset, - TicketFeatureAccessConfig::ReadOnly - ); + assert!(!manifest.feature.ticket.authoring); + assert!(!manifest.feature.ticket.thread); + assert!(!manifest.feature.ticket.intake); + assert!(!manifest.feature.ticket.orchestration_control); assert!(!manifest.feature.memory.enabled); } @@ -1803,14 +1817,18 @@ enabled = true [feature.ticket] enabled = true -preset = "read_only" +authoring = false +thread = false +intake = false +orchestration_control = false "#, ) .unwrap(); let upper = WorkerManifestConfig::from_toml( r#" [feature.ticket] -preset = "orchestration_control" +thread = true +orchestration_control = true [feature.web] enabled = true @@ -1844,10 +1862,10 @@ enabled = true .unwrap(); assert!(manifest.feature.memory.enabled); assert!(manifest.feature.ticket.enabled); - assert_eq!( - manifest.feature.ticket.preset, - TicketFeatureAccessConfig::OrchestrationControl - ); + assert!(!manifest.feature.ticket.authoring); + assert!(manifest.feature.ticket.thread); + assert!(!manifest.feature.ticket.intake); + assert!(manifest.feature.ticket.orchestration_control); 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 41f17280..d4855059 100644 --- a/crates/manifest/src/lib.rs +++ b/crates/manifest/src/lib.rs @@ -153,38 +153,18 @@ impl Default for FeatureFlagConfig { } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct TicketFeatureConfig { #[serde(default)] pub enabled: bool, #[serde(default)] - pub preset: TicketFeatureAccessConfig, -} - -impl Default for TicketFeatureConfig { - fn default() -> Self { - Self { - enabled: false, - preset: TicketFeatureAccessConfig::default(), - } - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum TicketFeatureAccessConfig { - ReadOnly, - WorkspaceAuthoring, - Intake, - OrchestrationControl, - WorkReport, - Review, -} - -impl Default for TicketFeatureAccessConfig { - fn default() -> Self { - Self::WorkspaceAuthoring - } + pub authoring: bool, + #[serde(default)] + pub thread: bool, + #[serde(default)] + pub intake: bool, + #[serde(default)] + pub orchestration_control: bool, } /// External Agent Skills (`SKILL.md`) ingest configuration. Skills are diff --git a/crates/manifest/src/profile.rs b/crates/manifest/src/profile.rs index 63a4d59a..139f1ee3 100644 --- a/crates/manifest/src/profile.rs +++ b/crates/manifest/src/profile.rs @@ -16,8 +16,8 @@ use crate::model::{AuthRef, ModelManifest}; use crate::plugin::PluginConfig; use crate::{ EngineManifestConfig, McpConfig, McpStdioCwdPolicy, MemoryConfig, Permission, ResolveError, - ScopeConfig, ScopeRule, SkillsConfig, TicketFeatureAccessConfig, WebConfig, WorkerManifest, - WorkerManifestConfig, WorkerMetaConfig, paths, + ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifest, WorkerManifestConfig, + WorkerMetaConfig, paths, }; const PROFILE_FORMAT_V1: &str = "yoi.profile.v1"; @@ -971,7 +971,7 @@ fn builtin_default_profile_artifact() -> serde_json::Value { "memory": { "enabled": true }, "web": { "enabled": true }, "workers": { "enabled": true }, - "ticket": { "enabled": true, "preset": "workspace_authoring" } + "ticket": { "enabled": true, "authoring": true, "thread": true } }, "memory": { "extract_threshold": 50000, @@ -1005,18 +1005,19 @@ fn apply_role_profile( value["feature"]["memory"] = serde_json::json!({ "enabled": memory }); value["feature"]["web"] = serde_json::json!({ "enabled": web }); value["feature"]["workers"] = serde_json::json!({ "enabled": workers }); - let ticket_access = match slug { - "companion" => TicketFeatureAccessConfig::WorkspaceAuthoring, - "intake" => TicketFeatureAccessConfig::Intake, - "orchestrator" => TicketFeatureAccessConfig::OrchestrationControl, - "coder" => TicketFeatureAccessConfig::WorkReport, - "reviewer" => TicketFeatureAccessConfig::Review, - _ => TicketFeatureAccessConfig::WorkspaceAuthoring, + let ticket = match slug { + "companion" => serde_json::json!({ "enabled": true, "authoring": true, "thread": true }), + "intake" => { + serde_json::json!({ "enabled": true, "authoring": true, "thread": true, "intake": true }) + } + "orchestrator" => { + serde_json::json!({ "enabled": true, "thread": true, "orchestration_control": true }) + } + "coder" => serde_json::json!({ "enabled": true, "thread": true }), + "reviewer" => serde_json::json!({ "enabled": true, "thread": true }), + _ => serde_json::json!({ "enabled": true, "authoring": true, "thread": true }), }; - value["feature"]["ticket"] = serde_json::json!({ - "enabled": true, - "preset": ticket_access, - }); + value["feature"]["ticket"] = ticket; } fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), ProfileError> { @@ -1442,10 +1443,10 @@ mod tests { assert_eq!(companion.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); assert!(companion.web.is_some()); assert!(companion.feature.ticket.enabled); - assert_eq!( - companion.feature.ticket.preset, - TicketFeatureAccessConfig::WorkspaceAuthoring - ); + assert!(companion.feature.ticket.authoring); + assert!(companion.feature.ticket.thread); + assert!(!companion.feature.ticket.intake); + assert!(!companion.feature.ticket.orchestration_control); assert_eq!( companion.compaction.as_ref().unwrap().threshold, Some(240000) @@ -1467,10 +1468,11 @@ mod tests { assert!(intake.feature.task.enabled); assert!(!intake.feature.workers.enabled); assert!(intake.feature.ticket.enabled); - assert_eq!( - intake.feature.ticket.preset, - TicketFeatureAccessConfig::Intake - ); + assert!(intake.feature.ticket.enabled); + assert!(intake.feature.ticket.authoring); + assert!(intake.feature.ticket.thread); + assert!(intake.feature.ticket.intake); + assert!(!intake.feature.ticket.orchestration_control); assert!(intake.scope.allow.is_empty()); assert!(intake.delegation_scope.allow.is_empty()); assert_eq!(intake.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); @@ -1481,10 +1483,11 @@ mod tests { assert!(orchestrator.feature.task.enabled); assert!(orchestrator.feature.workers.enabled); assert!(orchestrator.feature.ticket.enabled); - assert_eq!( - orchestrator.feature.ticket.preset, - TicketFeatureAccessConfig::OrchestrationControl - ); + assert!(orchestrator.feature.ticket.enabled); + assert!(!orchestrator.feature.ticket.authoring); + assert!(orchestrator.feature.ticket.thread); + assert!(!orchestrator.feature.ticket.intake); + assert!(orchestrator.feature.ticket.orchestration_control); assert!(orchestrator.scope.allow.is_empty()); assert!(orchestrator.delegation_scope.allow.is_empty()); assert_eq!( @@ -1503,19 +1506,20 @@ mod tests { assert!(coder.web.is_some()); assert!(coder.compaction.is_some()); assert!(coder.feature.ticket.enabled); - assert_eq!( - coder.feature.ticket.preset, - TicketFeatureAccessConfig::WorkReport - ); - + assert!(coder.feature.ticket.enabled); + assert!(!coder.feature.ticket.authoring); + assert!(coder.feature.ticket.thread); + assert!(!coder.feature.ticket.intake); + assert!(!coder.feature.ticket.orchestration_control); let reviewer = resolve("reviewer"); assert!(reviewer.feature.task.enabled); assert!(!reviewer.feature.workers.enabled); assert!(reviewer.feature.ticket.enabled); - assert_eq!( - reviewer.feature.ticket.preset, - TicketFeatureAccessConfig::Review - ); + assert!(reviewer.feature.ticket.enabled); + assert!(!reviewer.feature.ticket.authoring); + assert!(reviewer.feature.ticket.thread); + assert!(!reviewer.feature.ticket.intake); + assert!(!reviewer.feature.ticket.orchestration_control); 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")); @@ -1664,7 +1668,10 @@ enabled = true [feature.ticket] enabled = true -preset = "read_only" +authoring = false +thread = false +intake = false +orchestration_control = false "#, ); let workspace = tmp.path().join("workspace"); @@ -1682,10 +1689,10 @@ preset = "read_only" assert!(resolved.manifest.feature.web.enabled); assert!(resolved.manifest.feature.workers.enabled); assert!(resolved.manifest.feature.ticket.enabled); - assert_eq!( - resolved.manifest.feature.ticket.preset, - crate::TicketFeatureAccessConfig::ReadOnly - ); + assert!(!resolved.manifest.feature.ticket.authoring); + assert!(!resolved.manifest.feature.ticket.thread); + assert!(!resolved.manifest.feature.ticket.intake); + assert!(!resolved.manifest.feature.ticket.orchestration_control); assert_eq!( resolved.manifest.delegation_scope.allow[0].target, workspace @@ -1768,14 +1775,11 @@ worker_context_max_tokens = 68000 resolved.manifest.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5") ); - assert!(resolved.manifest.scope.allow.is_empty()); - assert!(resolved.manifest.delegation_scope.allow.is_empty()); - assert!(resolved.manifest.session.record_event_trace); assert!(resolved.manifest.feature.ticket.enabled); - assert_eq!( - resolved.manifest.feature.ticket.preset, - crate::TicketFeatureAccessConfig::WorkspaceAuthoring - ); + assert!(resolved.manifest.feature.ticket.authoring); + assert!(resolved.manifest.feature.ticket.thread); + assert!(!resolved.manifest.feature.ticket.intake); + assert!(!resolved.manifest.feature.ticket.orchestration_control); 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 0c4d6230..b554dd0c 100644 --- a/crates/tui/src/setup_model.rs +++ b/crates/tui/src/setup_model.rs @@ -238,7 +238,8 @@ enabled = false [feature.ticket] enabled = true -preset = "workspace_authoring" +authoring = true +thread = true [memory] extract_threshold = 50000 @@ -296,7 +297,7 @@ mod tests { assert!(profile.contains("ref = \"codex-oauth/gpt-5.5\"")); assert!(profile.contains("scope = \"workspace_write\"")); assert!( - profile.contains("[feature.ticket]\nenabled = true\npreset = \"workspace_authoring\"") + profile.contains("[feature.ticket]\nenabled = true\nauthoring = true\nthread = true") ); } diff --git a/crates/worker/src/controller.rs b/crates/worker/src/controller.rs index cc131c6c..181a7041 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -4,7 +4,6 @@ use std::sync::atomic::Ordering; use llm_engine::EngineError; use llm_engine::llm_client::client::LlmClient; -use manifest::TicketFeatureAccessConfig; use session_store::WorkerMetadataStore; use session_store::{LogEntry, Store}; use ticket::LocalTicketBackend; @@ -547,9 +546,7 @@ fn install_ticket_event_companion_notify_hook( } let ticket_feature = &worker.manifest().feature.ticket; - if !ticket_feature.enabled - || ticket_feature.preset != TicketFeatureAccessConfig::OrchestrationControl - { + if !ticket_feature.enabled || !ticket_feature.orchestration_control { return; } @@ -675,25 +672,11 @@ where feature_registry.add_module(task_feature); } if feature_config.ticket.enabled { - let ticket_access = match feature_config.ticket.preset { - TicketFeatureAccessConfig::ReadOnly => { - crate::feature::builtin::ticket::TicketFeatureAccess::ReadOnly - } - TicketFeatureAccessConfig::WorkspaceAuthoring => { - crate::feature::builtin::ticket::TicketFeatureAccess::WorkspaceAuthoring - } - TicketFeatureAccessConfig::Intake => { - crate::feature::builtin::ticket::TicketFeatureAccess::Intake - } - TicketFeatureAccessConfig::OrchestrationControl => { - crate::feature::builtin::ticket::TicketFeatureAccess::OrchestrationControl - } - TicketFeatureAccessConfig::WorkReport => { - crate::feature::builtin::ticket::TicketFeatureAccess::WorkReport - } - TicketFeatureAccessConfig::Review => { - crate::feature::builtin::ticket::TicketFeatureAccess::Review - } + let ticket_access = crate::feature::builtin::ticket::TicketFeatureAccess { + authoring: feature_config.ticket.authoring, + thread: feature_config.ticket.thread, + intake: feature_config.ticket.intake, + orchestration_control: feature_config.ticket.orchestration_control, }; // Ticket tools are typed operations over the current workspace Ticket backend. // Runtime-hosted Workers prefer the workspace API URI carried by the diff --git a/crates/worker/src/feature/builtin/ticket.rs b/crates/worker/src/feature/builtin/ticket.rs index 2d046a05..2a086cbd 100644 --- a/crates/worker/src/feature/builtin/ticket.rs +++ b/crates/worker/src/feature/builtin/ticket.rs @@ -14,7 +14,7 @@ use ticket::{ TicketIntakeSummary, TicketListQuery, TicketRef, TicketRelation, TicketRelationKind, TicketRelationView, TicketReview, TicketStateChange, TicketSummary, config::{DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TicketConfig}, - tool::{TicketToolBackend, ticket_tool_description, ticket_tools}, + tool::{TICKET_TOOL_NAMES, TicketToolBackend, ticket_tool_description, ticket_tools}, }; use crate::feature::{ @@ -27,23 +27,88 @@ const FEATURE_NAME: &str = "Ticket tools"; const FEATURE_DESCRIPTION: &str = "Typed local Ticket work-item operations over a bounded backend root. \ The tools operate through the ticket crate backend and do not grant generic filesystem write scope."; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum TicketFeatureAccess { - /// Status/diagnostic access for views that must not mutate Tickets. - ReadOnly, - /// User/Companion authoring access for shaping current Ticket specs and queueing work. - WorkspaceAuthoring, - /// Intake access for creating/refining planning Tickets and marking them ready. - Intake, - /// Orchestrator control access for state/thread/relation/orchestration operations. - OrchestrationControl, - /// Coder-style access for reading Tickets and writing implementation reports/comments. - WorkReport, - /// Reviewer-style access for reading Tickets and writing review/comment thread events. - Review, +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct TicketFeatureAccess { + pub authoring: bool, + pub thread: bool, + pub intake: bool, + pub orchestration_control: bool, } -const READ_ONLY_TOOL_NAMES: [&str; 6] = [ +impl TicketFeatureAccess { + pub const fn read_only() -> Self { + Self { + authoring: false, + thread: false, + intake: false, + orchestration_control: false, + } + } + + pub const fn workspace_authoring() -> Self { + Self { + authoring: true, + thread: true, + intake: false, + orchestration_control: false, + } + } + + pub const fn intake() -> Self { + Self { + authoring: true, + thread: true, + intake: true, + orchestration_control: false, + } + } + + pub const fn orchestration_control() -> Self { + Self { + authoring: false, + thread: true, + intake: false, + orchestration_control: true, + } + } + + pub const fn work_report() -> Self { + Self { + authoring: false, + thread: true, + intake: false, + orchestration_control: false, + } + } + + pub const fn review() -> Self { + Self { + authoring: false, + thread: true, + intake: false, + orchestration_control: false, + } + } + + pub fn tool_names(self) -> Vec<&'static str> { + TICKET_TOOL_NAMES + .iter() + .copied() + .filter(|name| self.allows_tool(name)) + .collect() + } + + fn allows_tool(self, name: &str) -> bool { + READ_ONLY_TOOL_NAMES.contains(&name) + || (self.authoring && AUTHORING_TOOL_NAMES.contains(&name)) + || (self.thread && THREAD_TOOL_NAMES.contains(&name)) + || (self.intake && INTAKE_TOOL_NAMES.contains(&name)) + || (self.orchestration_control + && ORCHESTRATION_CONTROL_ADDITIONAL_TOOL_NAMES.contains(&name)) + } +} + +const READ_ONLY_TOOL_NAMES: &[&str] = &[ "TicketList", "TicketShow", "TicketDependencyCheck", @@ -52,7 +117,19 @@ const READ_ONLY_TOOL_NAMES: [&str; 6] = [ "TicketOrchestrationPlanQuery", ]; -const WORKSPACE_AUTHORING_TOOL_NAMES: [&str; 12] = [ +const AUTHORING_TOOL_NAMES: &[&str] = &[ + "TicketCreate", + "TicketEditItem", + "TicketQueue", + "TicketClose", + "TicketRelationRecord", +]; + +const THREAD_TOOL_NAMES: &[&str] = &["TicketComment", "TicketReview"]; + +const INTAKE_TOOL_NAMES: &[&str] = &["TicketIntakeReady"]; + +const WORKSPACE_AUTHORING_TOOL_NAMES: &[&str] = &[ "TicketCreate", "TicketEditItem", "TicketList", @@ -65,22 +142,10 @@ const WORKSPACE_AUTHORING_TOOL_NAMES: [&str; 12] = [ "TicketDoctor", "TicketRelationRecord", "TicketRelationQuery", + "TicketOrchestrationPlanQuery", ]; -const INTAKE_TOOL_NAMES: [&str; 10] = [ - "TicketCreate", - "TicketEditItem", - "TicketList", - "TicketShow", - "TicketComment", - "TicketIntakeReady", - "TicketDependencyCheck", - "TicketDoctor", - "TicketRelationRecord", - "TicketRelationQuery", -]; - -const ORCHESTRATION_CONTROL_TOOL_NAMES: [&str; 12] = [ +const ORCHESTRATION_CONTROL_TOOL_NAMES: &[&str] = &[ "TicketList", "TicketShow", "TicketComment", @@ -95,40 +160,13 @@ const ORCHESTRATION_CONTROL_TOOL_NAMES: [&str; 12] = [ "TicketOrchestrationPlanQuery", ]; -const WORK_REPORT_TOOL_NAMES: [&str; 7] = [ - "TicketList", - "TicketShow", - "TicketComment", - "TicketDependencyCheck", - "TicketDoctor", - "TicketRelationQuery", - "TicketOrchestrationPlanQuery", +const ORCHESTRATION_CONTROL_ADDITIONAL_TOOL_NAMES: &[&str] = &[ + "TicketWorkflowState", + "TicketClose", + "TicketRelationRecord", + "TicketOrchestrationPlanRecord", ]; -const REVIEW_TOOL_NAMES: [&str; 8] = [ - "TicketList", - "TicketShow", - "TicketComment", - "TicketReview", - "TicketDependencyCheck", - "TicketDoctor", - "TicketRelationQuery", - "TicketOrchestrationPlanQuery", -]; - -impl TicketFeatureAccess { - pub fn tool_names(self) -> &'static [&'static str] { - match self { - 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, - } - } -} - #[derive(Clone, Debug)] pub enum TicketFeatureBackend { Local { @@ -173,7 +211,7 @@ pub struct TicketFeature { impl TicketFeature { pub fn new(backend_root: impl Into) -> Self { - Self::new_with_access(backend_root, TicketFeatureAccess::WorkspaceAuthoring) + Self::new_with_access(backend_root, TicketFeatureAccess::workspace_authoring()) } pub fn new_with_access(backend_root: impl Into, access: TicketFeatureAccess) -> Self { @@ -198,7 +236,7 @@ impl TicketFeature { } pub fn for_workspace(workspace: impl AsRef) -> Self { - Self::for_workspace_with_access(workspace, TicketFeatureAccess::WorkspaceAuthoring) + Self::for_workspace_with_access(workspace, TicketFeatureAccess::workspace_authoring()) } pub fn for_workspace_with_access( @@ -237,7 +275,7 @@ impl TicketFeature { self.access } - fn enabled_tool_names(&self) -> &'static [&'static str] { + fn enabled_tool_names(&self) -> Vec<&'static str> { self.access.tool_names() } @@ -298,7 +336,7 @@ impl FeatureModule for TicketFeature { let enabled_tool_names = self.enabled_tool_names(); for name in enabled_tool_names { descriptor = descriptor.with_tool(ToolDeclaration::new( - *name, + name, ticket_tool_description(name, self.record_language.as_deref()), )); } @@ -701,9 +739,10 @@ mod tests { #[test] fn read_only_descriptor_declares_only_state_tools() { let temp = TempDir::new().unwrap(); - let feature = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::ReadOnly); + let feature = + ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::read_only()); let descriptor = feature.descriptor(); - assert_eq!(feature.access(), TicketFeatureAccess::ReadOnly); + assert_eq!(feature.access(), TicketFeatureAccess::read_only()); assert_eq!(descriptor.tools.len(), READ_ONLY_TOOL_NAMES.len()); assert_eq!( descriptor @@ -720,10 +759,13 @@ mod tests { let temp = TempDir::new().unwrap(); let feature = ticket_tools_feature_with_access( temp.path(), - TicketFeatureAccess::OrchestrationControl, + TicketFeatureAccess::orchestration_control(), ); let descriptor = feature.descriptor(); - assert_eq!(feature.access(), TicketFeatureAccess::OrchestrationControl); + assert_eq!( + feature.access(), + TicketFeatureAccess::orchestration_control() + ); assert_eq!( descriptor .tools @@ -735,11 +777,13 @@ mod tests { } #[test] - fn semantic_access_presets_expose_role_capability_surfaces() { + fn additive_ticket_capabilities_expose_expected_tool_surfaces() { let temp = TempDir::new().unwrap(); - let workspace_authoring = - ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::WorkspaceAuthoring); + let workspace_authoring = ticket_tools_feature_with_access( + temp.path(), + TicketFeatureAccess::workspace_authoring(), + ); let workspace_descriptor = workspace_authoring.descriptor(); let workspace_tools = workspace_descriptor .tools @@ -753,7 +797,7 @@ mod tests { let orchestration = ticket_tools_feature_with_access( temp.path(), - TicketFeatureAccess::OrchestrationControl, + TicketFeatureAccess::orchestration_control(), ); let orchestration_descriptor = orchestration.descriptor(); let orchestration_tools = orchestration_descriptor @@ -769,7 +813,7 @@ mod tests { assert!(!orchestration_tools.contains(&"TicketQueue")); let work_report = - ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::WorkReport); + ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::work_report()); let work_report_descriptor = work_report.descriptor(); let work_report_tools = work_report_descriptor .tools @@ -777,10 +821,10 @@ mod tests { .map(|tool| tool.name.as_str()) .collect::>(); assert!(work_report_tools.contains(&"TicketComment")); - assert!(!work_report_tools.contains(&"TicketReview")); + assert!(work_report_tools.contains(&"TicketReview")); assert!(!work_report_tools.contains(&"TicketWorkflowState")); - let review = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::Review); + let review = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::review()); let review_descriptor = review.descriptor(); let review_tools = review_descriptor .tools @@ -800,7 +844,7 @@ mod tests { let report = FeatureRegistryBuilder::new() .with_module(ticket_tools_feature_with_access( temp.path(), - TicketFeatureAccess::ReadOnly, + TicketFeatureAccess::read_only(), )) .install_into_pending(&mut pending_tools, &mut hooks); @@ -833,7 +877,8 @@ language = "Japanese" "#, ); make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)); - let feature = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::ReadOnly); + let feature = + ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::read_only()); let descriptor = feature.descriptor(); let descriptor_description = descriptor .tools @@ -867,7 +912,7 @@ language = "Japanese" let report = FeatureRegistryBuilder::new() .with_module(ticket_tools_feature_with_access( temp.path(), - TicketFeatureAccess::WorkspaceAuthoring, + TicketFeatureAccess::workspace_authoring(), )) .install_into_pending(&mut pending_tools, &mut hooks); @@ -906,7 +951,7 @@ language = "Japanese" let report = FeatureRegistryBuilder::new() .with_module(ticket_tools_feature_with_access( temp.path(), - TicketFeatureAccess::WorkspaceAuthoring, + TicketFeatureAccess::workspace_authoring(), )) .install_into_pending(&mut pending_tools, &mut hooks); diff --git a/resources/profiles/coder.dcdl b/resources/profiles/coder.dcdl index 7a6a4e75..e04783c5 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 = { enabled = true; preset = "work_report"; }; + ticket = { enabled = true; thread = true; }; }; } diff --git a/resources/profiles/companion.dcdl b/resources/profiles/companion.dcdl index cb78855e..0e84e56e 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 = { enabled = true; preset = "workspace_authoring"; }; + ticket = { enabled = true; authoring = true; thread = true; }; }; } diff --git a/resources/profiles/default.dcdl b/resources/profiles/default.dcdl index 9b261b41..41b3b763 100644 --- a/resources/profiles/default.dcdl +++ b/resources/profiles/default.dcdl @@ -26,7 +26,7 @@ feature = { memory = { enabled = true; }; web = { enabled = true; }; workers = { enabled = true; }; - ticket = { enabled = true; preset = "workspace_authoring"; }; + ticket = { enabled = true; authoring = true; thread = true; }; }; memory = { diff --git a/resources/profiles/intake.dcdl b/resources/profiles/intake.dcdl index dc0be16b..28bea099 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 = { enabled = true; preset = "intake"; }; + ticket = { enabled = true; authoring = true; thread = true; intake = true; }; }; } diff --git a/resources/profiles/orchestrator.dcdl b/resources/profiles/orchestrator.dcdl index 951bdef3..a96df119 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 = { enabled = true; preset = "orchestration_control"; }; + ticket = { enabled = true; thread = true; orchestration_control = true; }; }; } diff --git a/resources/profiles/reviewer.dcdl b/resources/profiles/reviewer.dcdl index 41c32401..1e01c3dd 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 = { enabled = true; preset = "review"; }; + ticket = { enabled = true; thread = true; }; }; }