11use serde:: { Deserialize , Serialize } ;
22use std:: fmt:: { Display , Formatter } ;
33
4+ /// Task scope resolution for defining the granularity of work.
5+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Serialize , Deserialize ) ]
6+ #[ serde( rename_all = "snake_case" ) ]
7+ pub enum TaskScope {
8+ /// Work across the entire workspace
9+ Workspace ,
10+ /// Work within a specific module/crate
11+ Module ,
12+ /// Work on a single file
13+ SingleFile ,
14+ /// Custom scope defined by the user
15+ Custom ,
16+ }
17+
18+ impl std:: fmt:: Display for TaskScope {
19+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
20+ match self {
21+ Self :: Workspace => write ! ( f, "workspace" ) ,
22+ Self :: Module => write ! ( f, "module" ) ,
23+ Self :: SingleFile => write ! ( f, "single-file" ) ,
24+ Self :: Custom => write ! ( f, "custom" ) ,
25+ }
26+ }
27+ }
28+
429#[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
530pub struct TaskPacket {
631 pub objective : String ,
7- pub scope : String ,
32+ pub scope : TaskScope ,
33+ /// Optional scope path when scope is `Module`, `SingleFile`, or `Custom`
34+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
35+ pub scope_path : Option < String > ,
836 pub repo : String ,
37+ /// Worktree path for the task
38+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
39+ pub worktree : Option < String > ,
940 pub branch_policy : String ,
1041 pub acceptance_tests : Vec < String > ,
1142 pub commit_policy : String ,
@@ -57,7 +88,6 @@ pub fn validate_packet(packet: TaskPacket) -> Result<ValidatedPacket, TaskPacket
5788 let mut errors = Vec :: new ( ) ;
5889
5990 validate_required ( "objective" , & packet. objective , & mut errors) ;
60- validate_required ( "scope" , & packet. scope , & mut errors) ;
6191 validate_required ( "repo" , & packet. repo , & mut errors) ;
6292 validate_required ( "branch_policy" , & packet. branch_policy , & mut errors) ;
6393 validate_required ( "commit_policy" , & packet. commit_policy , & mut errors) ;
@@ -68,6 +98,9 @@ pub fn validate_packet(packet: TaskPacket) -> Result<ValidatedPacket, TaskPacket
6898 ) ;
6999 validate_required ( "escalation_policy" , & packet. escalation_policy , & mut errors) ;
70100
101+ // Validate scope-specific requirements
102+ validate_scope_requirements ( & packet, & mut errors) ;
103+
71104 for ( index, test) in packet. acceptance_tests . iter ( ) . enumerate ( ) {
72105 if test. trim ( ) . is_empty ( ) {
73106 errors. push ( format ! (
@@ -83,6 +116,21 @@ pub fn validate_packet(packet: TaskPacket) -> Result<ValidatedPacket, TaskPacket
83116 }
84117}
85118
119+ fn validate_scope_requirements ( packet : & TaskPacket , errors : & mut Vec < String > ) {
120+ // Scope path is required for Module, SingleFile, and Custom scopes
121+ let needs_scope_path = matches ! (
122+ packet. scope,
123+ TaskScope :: Module | TaskScope :: SingleFile | TaskScope :: Custom
124+ ) ;
125+
126+ if needs_scope_path && packet. scope_path . as_ref ( ) . is_none_or ( |p| p. trim ( ) . is_empty ( ) ) {
127+ errors. push ( format ! (
128+ "scope_path is required for scope '{}'" ,
129+ packet. scope
130+ ) ) ;
131+ }
132+ }
133+
86134fn validate_required ( field : & str , value : & str , errors : & mut Vec < String > ) {
87135 if value. trim ( ) . is_empty ( ) {
88136 errors. push ( format ! ( "{field} must not be empty" ) ) ;
@@ -96,8 +144,10 @@ mod tests {
96144 fn sample_packet ( ) -> TaskPacket {
97145 TaskPacket {
98146 objective : "Implement typed task packet format" . to_string ( ) ,
99- scope : "runtime/task system" . to_string ( ) ,
147+ scope : TaskScope :: Module ,
148+ scope_path : Some ( "runtime/task system" . to_string ( ) ) ,
100149 repo : "claw-code-parity" . to_string ( ) ,
150+ worktree : Some ( "/tmp/wt-1" . to_string ( ) ) ,
101151 branch_policy : "origin/main only" . to_string ( ) ,
102152 acceptance_tests : vec ! [
103153 "cargo build --workspace" . to_string( ) ,
@@ -119,9 +169,12 @@ mod tests {
119169
120170 #[ test]
121171 fn invalid_packet_accumulates_errors ( ) {
172+ use super :: TaskScope ;
122173 let packet = TaskPacket {
123174 objective : " " . to_string ( ) ,
124- scope : String :: new ( ) ,
175+ scope : TaskScope :: Workspace ,
176+ scope_path : None ,
177+ worktree : None ,
125178 repo : String :: new ( ) ,
126179 branch_policy : "\t " . to_string ( ) ,
127180 acceptance_tests : vec ! [ "ok" . to_string( ) , " " . to_string( ) ] ,
@@ -136,9 +189,6 @@ mod tests {
136189 assert ! ( error
137190 . errors( )
138191 . contains( & "objective must not be empty" . to_string( ) ) ) ;
139- assert ! ( error
140- . errors( )
141- . contains( & "scope must not be empty" . to_string( ) ) ) ;
142192 assert ! ( error
143193 . errors( )
144194 . contains( & "repo must not be empty" . to_string( ) ) ) ;
0 commit comments