mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-16 21:14:50 +08:00
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)
This commit is contained in:
@@ -1,11 +1,42 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// Task scope resolution for defining the granularity of work.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaskScope {
|
||||
/// Work across the entire workspace
|
||||
Workspace,
|
||||
/// Work within a specific module/crate
|
||||
Module,
|
||||
/// Work on a single file
|
||||
SingleFile,
|
||||
/// Custom scope defined by the user
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TaskScope {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Workspace => write!(f, "workspace"),
|
||||
Self::Module => write!(f, "module"),
|
||||
Self::SingleFile => write!(f, "single-file"),
|
||||
Self::Custom => write!(f, "custom"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TaskPacket {
|
||||
pub objective: String,
|
||||
pub scope: String,
|
||||
pub scope: TaskScope,
|
||||
/// Optional scope path when scope is `Module`, `SingleFile`, or `Custom`
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub scope_path: Option<String>,
|
||||
pub repo: String,
|
||||
/// Worktree path for the task
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub worktree: Option<String>,
|
||||
pub branch_policy: String,
|
||||
pub acceptance_tests: Vec<String>,
|
||||
pub commit_policy: String,
|
||||
@@ -57,7 +88,6 @@ pub fn validate_packet(packet: TaskPacket) -> Result<ValidatedPacket, TaskPacket
|
||||
let mut errors = Vec::new();
|
||||
|
||||
validate_required("objective", &packet.objective, &mut errors);
|
||||
validate_required("scope", &packet.scope, &mut errors);
|
||||
validate_required("repo", &packet.repo, &mut errors);
|
||||
validate_required("branch_policy", &packet.branch_policy, &mut errors);
|
||||
validate_required("commit_policy", &packet.commit_policy, &mut errors);
|
||||
@@ -68,6 +98,9 @@ pub fn validate_packet(packet: TaskPacket) -> Result<ValidatedPacket, TaskPacket
|
||||
);
|
||||
validate_required("escalation_policy", &packet.escalation_policy, &mut errors);
|
||||
|
||||
// Validate scope-specific requirements
|
||||
validate_scope_requirements(&packet, &mut errors);
|
||||
|
||||
for (index, test) in packet.acceptance_tests.iter().enumerate() {
|
||||
if test.trim().is_empty() {
|
||||
errors.push(format!(
|
||||
@@ -83,6 +116,21 @@ pub fn validate_packet(packet: TaskPacket) -> Result<ValidatedPacket, TaskPacket
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_scope_requirements(packet: &TaskPacket, errors: &mut Vec<String>) {
|
||||
// Scope path is required for Module, SingleFile, and Custom scopes
|
||||
let needs_scope_path = matches!(
|
||||
packet.scope,
|
||||
TaskScope::Module | TaskScope::SingleFile | TaskScope::Custom
|
||||
);
|
||||
|
||||
if needs_scope_path && packet.scope_path.as_ref().is_none_or(|p| p.trim().is_empty()) {
|
||||
errors.push(format!(
|
||||
"scope_path is required for scope '{}'",
|
||||
packet.scope
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_required(field: &str, value: &str, errors: &mut Vec<String>) {
|
||||
if value.trim().is_empty() {
|
||||
errors.push(format!("{field} must not be empty"));
|
||||
@@ -96,8 +144,10 @@ mod tests {
|
||||
fn sample_packet() -> TaskPacket {
|
||||
TaskPacket {
|
||||
objective: "Implement typed task packet format".to_string(),
|
||||
scope: "runtime/task system".to_string(),
|
||||
scope: TaskScope::Module,
|
||||
scope_path: Some("runtime/task system".to_string()),
|
||||
repo: "claw-code-parity".to_string(),
|
||||
worktree: Some("/tmp/wt-1".to_string()),
|
||||
branch_policy: "origin/main only".to_string(),
|
||||
acceptance_tests: vec![
|
||||
"cargo build --workspace".to_string(),
|
||||
@@ -119,9 +169,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn invalid_packet_accumulates_errors() {
|
||||
use super::TaskScope;
|
||||
let packet = TaskPacket {
|
||||
objective: " ".to_string(),
|
||||
scope: String::new(),
|
||||
scope: TaskScope::Workspace,
|
||||
scope_path: None,
|
||||
worktree: None,
|
||||
repo: String::new(),
|
||||
branch_policy: "\t".to_string(),
|
||||
acceptance_tests: vec!["ok".to_string(), " ".to_string()],
|
||||
@@ -136,9 +189,6 @@ mod tests {
|
||||
assert!(error
|
||||
.errors()
|
||||
.contains(&"objective must not be empty".to_string()));
|
||||
assert!(error
|
||||
.errors()
|
||||
.contains(&"scope must not be empty".to_string()));
|
||||
assert!(error
|
||||
.errors()
|
||||
.contains(&"repo must not be empty".to_string()));
|
||||
|
||||
@@ -85,11 +85,12 @@ impl TaskRegistry {
|
||||
packet: TaskPacket,
|
||||
) -> Result<Task, TaskPacketValidationError> {
|
||||
let packet = validate_packet(packet)?.into_inner();
|
||||
Ok(self.create_task(
|
||||
packet.objective.clone(),
|
||||
Some(packet.scope.clone()),
|
||||
Some(packet),
|
||||
))
|
||||
// Use scope_path as description if available, otherwise use scope as string
|
||||
let description = packet
|
||||
.scope_path
|
||||
.clone()
|
||||
.or_else(|| Some(packet.scope.to_string()));
|
||||
Ok(self.create_task(packet.objective.clone(), description, Some(packet)))
|
||||
}
|
||||
|
||||
fn create_task(
|
||||
@@ -249,10 +250,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn creates_task_from_packet() {
|
||||
use crate::task_packet::TaskScope;
|
||||
let registry = TaskRegistry::new();
|
||||
let packet = TaskPacket {
|
||||
objective: "Ship task packet support".to_string(),
|
||||
scope: "runtime/task system".to_string(),
|
||||
scope: TaskScope::Module,
|
||||
scope_path: Some("runtime/task system".to_string()),
|
||||
worktree: Some("/tmp/wt-task".to_string()),
|
||||
repo: "claw-code-parity".to_string(),
|
||||
branch_policy: "origin/main only".to_string(),
|
||||
acceptance_tests: vec!["cargo test --workspace".to_string()],
|
||||
|
||||
@@ -9554,9 +9554,12 @@ printf 'pwsh:%s' "$1"
|
||||
|
||||
#[test]
|
||||
fn run_task_packet_creates_packet_backed_task() {
|
||||
use runtime::task_packet::TaskScope;
|
||||
let result = run_task_packet(TaskPacket {
|
||||
objective: "Ship packetized runtime task".to_string(),
|
||||
scope: "runtime/task system".to_string(),
|
||||
scope: TaskScope::Module,
|
||||
scope_path: Some("runtime/task system".to_string()),
|
||||
worktree: Some("/tmp/wt-packet".to_string()),
|
||||
repo: "claw-code-parity".to_string(),
|
||||
branch_policy: "origin/main only".to_string(),
|
||||
acceptance_tests: vec![
|
||||
|
||||
Reference in New Issue
Block a user