summaryrefslogtreecommitdiff
path: root/internal/task
diff options
context:
space:
mode:
Diffstat (limited to 'internal/task')
-rw-r--r--internal/task/project.go11
-rw-r--r--internal/task/story.go41
-rw-r--r--internal/task/story_test.go42
-rw-r--r--internal/task/task.go12
-rw-r--r--internal/task/task_test.go28
-rw-r--r--internal/task/validator.go3
-rw-r--r--internal/task/validator_test.go2
7 files changed, 136 insertions, 3 deletions
diff --git a/internal/task/project.go b/internal/task/project.go
new file mode 100644
index 0000000..bd8a4fb
--- /dev/null
+++ b/internal/task/project.go
@@ -0,0 +1,11 @@
+package task
+
+// Project represents a registered codebase that agents can operate on.
+type Project struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ RemoteURL string `json:"remote_url"`
+ LocalPath string `json:"local_path"`
+ Type string `json:"type"` // "web" | "android"
+ DeployScript string `json:"deploy_script"` // optional path or command
+}
diff --git a/internal/task/story.go b/internal/task/story.go
new file mode 100644
index 0000000..536bda1
--- /dev/null
+++ b/internal/task/story.go
@@ -0,0 +1,41 @@
+package task
+
+import "time"
+
+type StoryState string
+
+const (
+ StoryPending StoryState = "PENDING"
+ StoryInProgress StoryState = "IN_PROGRESS"
+ StoryShippable StoryState = "SHIPPABLE"
+ StoryDeployed StoryState = "DEPLOYED"
+ StoryValidating StoryState = "VALIDATING"
+ StoryReviewReady StoryState = "REVIEW_READY"
+ StoryNeedsFix StoryState = "NEEDS_FIX"
+)
+
+type Story struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ ProjectID string `json:"project_id"`
+ BranchName string `json:"branch_name"`
+ DeployConfig string `json:"deploy_config"`
+ ValidationJSON string `json:"validation_json"`
+ Status StoryState `json:"status"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+var validStoryTransitions = map[StoryState]map[StoryState]bool{
+ StoryPending: {StoryInProgress: true},
+ StoryInProgress: {StoryShippable: true, StoryNeedsFix: true},
+ StoryShippable: {StoryDeployed: true},
+ StoryDeployed: {StoryValidating: true},
+ StoryValidating: {StoryReviewReady: true, StoryNeedsFix: true},
+ StoryReviewReady: {},
+ StoryNeedsFix: {StoryInProgress: true},
+}
+
+func ValidStoryTransition(from, to StoryState) bool {
+ return validStoryTransitions[from][to]
+}
diff --git a/internal/task/story_test.go b/internal/task/story_test.go
new file mode 100644
index 0000000..38d0290
--- /dev/null
+++ b/internal/task/story_test.go
@@ -0,0 +1,42 @@
+package task
+
+import "testing"
+
+func TestValidStoryTransition_Valid(t *testing.T) {
+ cases := []struct {
+ from StoryState
+ to StoryState
+ }{
+ {StoryPending, StoryInProgress},
+ {StoryInProgress, StoryShippable},
+ {StoryInProgress, StoryNeedsFix},
+ {StoryNeedsFix, StoryInProgress},
+ {StoryShippable, StoryDeployed},
+ {StoryDeployed, StoryValidating},
+ {StoryValidating, StoryReviewReady},
+ {StoryValidating, StoryNeedsFix},
+ }
+ for _, tc := range cases {
+ if !ValidStoryTransition(tc.from, tc.to) {
+ t.Errorf("expected valid transition %s → %s", tc.from, tc.to)
+ }
+ }
+}
+
+func TestValidStoryTransition_Invalid(t *testing.T) {
+ cases := []struct {
+ from StoryState
+ to StoryState
+ }{
+ {StoryPending, StoryDeployed},
+ {StoryReviewReady, StoryPending},
+ {StoryReviewReady, StoryInProgress},
+ {StoryReviewReady, StoryShippable},
+ {StoryShippable, StoryPending},
+ }
+ for _, tc := range cases {
+ if ValidStoryTransition(tc.from, tc.to) {
+ t.Errorf("expected invalid transition %s → %s", tc.from, tc.to)
+ }
+ }
+}
diff --git a/internal/task/task.go b/internal/task/task.go
index fd1dde6..935a238 100644
--- a/internal/task/task.go
+++ b/internal/task/task.go
@@ -32,13 +32,14 @@ type AgentConfig struct {
Model string `yaml:"model" json:"model"`
ContextFiles []string `yaml:"context_files" json:"context_files"`
Instructions string `yaml:"instructions" json:"instructions"`
- ProjectDir string `yaml:"project_dir" json:"project_dir"`
+ ContainerImage string `yaml:"container_image" json:"container_image"`
MaxBudgetUSD float64 `yaml:"max_budget_usd" json:"max_budget_usd"`
PermissionMode string `yaml:"permission_mode" json:"permission_mode"`
AllowedTools []string `yaml:"allowed_tools" json:"allowed_tools"`
DisallowedTools []string `yaml:"disallowed_tools" json:"disallowed_tools"`
SystemPromptAppend string `yaml:"system_prompt_append" json:"system_prompt_append"`
AdditionalArgs []string `yaml:"additional_args" json:"additional_args"`
+ ProjectDir string `yaml:"project_dir" json:"project_dir,omitempty"`
SkipPlanning bool `yaml:"skip_planning" json:"skip_planning"`
// Local-runner sampling controls. Pointer for Temperature so a 0 value can
@@ -79,12 +80,19 @@ type Task struct {
ParentTaskID string `yaml:"parent_task_id" json:"parent_task_id"`
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
+ Project string `yaml:"project" json:"project"` // Human-readable project name
+ RepositoryURL string `yaml:"repository_url" json:"repository_url"`
Agent AgentConfig `yaml:"agent" json:"agent"`
Timeout Duration `yaml:"timeout" json:"timeout"`
Retry RetryConfig `yaml:"retry" json:"retry"`
Priority Priority `yaml:"priority" json:"priority"`
Tags []string `yaml:"tags" json:"tags"`
DependsOn []string `yaml:"depends_on" json:"depends_on"`
+ StoryID string `yaml:"-" json:"story_id,omitempty"`
+ BranchName string `yaml:"-" json:"branch_name,omitempty"`
+ AcceptanceCriteria string `yaml:"-" json:"acceptance_criteria,omitempty"`
+ CheckerForTaskID string `yaml:"-" json:"checker_for_task_id,omitempty"`
+ CheckerReport string `yaml:"-" json:"checker_report,omitempty"`
State State `yaml:"-" json:"state"`
RejectionComment string `yaml:"-" json:"rejection_comment,omitempty"`
QuestionJSON string `yaml:"-" json:"question,omitempty"`
@@ -130,7 +138,7 @@ type BatchFile struct {
// BLOCKED may advance to READY when all subtasks complete, or back to QUEUED on user answer.
var validTransitions = map[State]map[State]bool{
StatePending: {StateQueued: true, StateCancelled: true},
- StateQueued: {StateRunning: true, StateCancelled: true},
+ StateQueued: {StateRunning: true, StateCancelled: true, StateReady: true}, // READY: parent task completed via subtask delegation
StateRunning: {StateReady: true, StateCompleted: true, StateFailed: true, StateTimedOut: true, StateCancelled: true, StateBudgetExceeded: true, StateBlocked: true},
StateReady: {StateCompleted: true, StatePending: true},
StateFailed: {StateQueued: true}, // retry
diff --git a/internal/task/task_test.go b/internal/task/task_test.go
index 15ba019..e6a17b8 100644
--- a/internal/task/task_test.go
+++ b/internal/task/task_test.go
@@ -100,3 +100,31 @@ func TestDuration_MarshalYAML(t *testing.T) {
t.Errorf("expected '15m0s', got %v", v)
}
}
+
+func TestTask_ProjectField(t *testing.T) {
+ t.Run("struct assignment", func(t *testing.T) {
+ task := Task{Project: "my-project"}
+ if task.Project != "my-project" {
+ t.Errorf("expected Project 'my-project', got %q", task.Project)
+ }
+ })
+
+ t.Run("yaml parsing", func(t *testing.T) {
+ yaml := `
+name: "Test Task"
+project: my-project
+agent:
+ instructions: "Do something"
+`
+ tasks, err := Parse([]byte(yaml))
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(tasks) != 1 {
+ t.Fatalf("expected 1 task, got %d", len(tasks))
+ }
+ if tasks[0].Project != "my-project" {
+ t.Errorf("expected Project 'my-project', got %q", tasks[0].Project)
+ }
+ })
+}
diff --git a/internal/task/validator.go b/internal/task/validator.go
index 003fab9..43e482e 100644
--- a/internal/task/validator.go
+++ b/internal/task/validator.go
@@ -29,6 +29,9 @@ func Validate(t *Task) error {
if t.Name == "" {
ve.Add("name is required")
}
+ if t.RepositoryURL == "" {
+ ve.Add("repository_url is required")
+ }
if t.Agent.Instructions == "" {
ve.Add("agent.instructions is required")
}
diff --git a/internal/task/validator_test.go b/internal/task/validator_test.go
index 657d93f..2c6735c 100644
--- a/internal/task/validator_test.go
+++ b/internal/task/validator_test.go
@@ -9,10 +9,10 @@ func validTask() *Task {
return &Task{
ID: "test-id",
Name: "Valid Task",
+ RepositoryURL: "https://github.com/user/repo",
Agent: AgentConfig{
Type: "claude",
Instructions: "do something",
- ProjectDir: "/tmp",
},
Priority: PriorityNormal,
Retry: RetryConfig{MaxAttempts: 1, Backoff: "exponential"},