Skip to content

Commit 1d5748f

Browse files
committed
US-005: Typed task packet format with TaskScope enum
- Add TaskScope enum with Workspace, Module, SingleFile, Custom variants - Update TaskPacket struct with scope_path and worktree fields - Add validation for scope-specific requirements - Fix tests in task_packet.rs, task_registry.rs, and tools/src/lib.rs - Export TaskScope from runtime crate Closes US-005 (Phase 4)
1 parent 77fb62a commit 1d5748f

3 files changed

Lines changed: 71 additions & 14 deletions

File tree

rust/crates/runtime/src/task_packet.rs

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
11
use serde::{Deserialize, Serialize};
22
use 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)]
530
pub 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+
86134
fn 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()));

rust/crates/runtime/src/task_registry.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,12 @@ impl TaskRegistry {
8585
packet: TaskPacket,
8686
) -> Result<Task, TaskPacketValidationError> {
8787
let packet = validate_packet(packet)?.into_inner();
88-
Ok(self.create_task(
89-
packet.objective.clone(),
90-
Some(packet.scope.clone()),
91-
Some(packet),
92-
))
88+
// Use scope_path as description if available, otherwise use scope as string
89+
let description = packet
90+
.scope_path
91+
.clone()
92+
.or_else(|| Some(packet.scope.to_string()));
93+
Ok(self.create_task(packet.objective.clone(), description, Some(packet)))
9394
}
9495

9596
fn create_task(
@@ -249,10 +250,13 @@ mod tests {
249250

250251
#[test]
251252
fn creates_task_from_packet() {
253+
use crate::task_packet::TaskScope;
252254
let registry = TaskRegistry::new();
253255
let packet = TaskPacket {
254256
objective: "Ship task packet support".to_string(),
255-
scope: "runtime/task system".to_string(),
257+
scope: TaskScope::Module,
258+
scope_path: Some("runtime/task system".to_string()),
259+
worktree: Some("/tmp/wt-task".to_string()),
256260
repo: "claw-code-parity".to_string(),
257261
branch_policy: "origin/main only".to_string(),
258262
acceptance_tests: vec!["cargo test --workspace".to_string()],

rust/crates/tools/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9554,9 +9554,12 @@ printf 'pwsh:%s' "$1"
95549554

95559555
#[test]
95569556
fn run_task_packet_creates_packet_backed_task() {
9557+
use runtime::task_packet::TaskScope;
95579558
let result = run_task_packet(TaskPacket {
95589559
objective: "Ship packetized runtime task".to_string(),
9559-
scope: "runtime/task system".to_string(),
9560+
scope: TaskScope::Module,
9561+
scope_path: Some("runtime/task system".to_string()),
9562+
worktree: Some("/tmp/wt-packet".to_string()),
95609563
repo: "claw-code-parity".to_string(),
95619564
branch_policy: "origin/main only".to_string(),
95629565
acceptance_tests: vec![

0 commit comments

Comments
 (0)