1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
package task
import (
"encoding/json"
"time"
)
type State string
const (
StatePending State = "PENDING"
StateQueued State = "QUEUED"
StateRunning State = "RUNNING"
StateReady State = "READY"
StateCompleted State = "COMPLETED"
StateFailed State = "FAILED"
StateTimedOut State = "TIMED_OUT"
StateCancelled State = "CANCELLED"
StateBudgetExceeded State = "BUDGET_EXCEEDED"
StateBlocked State = "BLOCKED"
)
type Priority string
const (
PriorityHigh Priority = "high"
PriorityNormal Priority = "normal"
PriorityLow Priority = "low"
)
type ClaudeConfig 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"`
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"`
SkipPlanning bool `yaml:"skip_planning" json:"skip_planning"`
}
// UnmarshalJSON reads project_dir with fallback to legacy working_dir.
func (c *ClaudeConfig) UnmarshalJSON(data []byte) error {
type Alias ClaudeConfig
aux := &struct {
ProjectDir string `json:"project_dir"`
WorkingDir string `json:"working_dir"` // legacy
*Alias
}{Alias: (*Alias)(c)}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
if aux.ProjectDir != "" {
c.ProjectDir = aux.ProjectDir
} else {
c.ProjectDir = aux.WorkingDir
}
return nil
}
type RetryConfig struct {
MaxAttempts int `yaml:"max_attempts" json:"max_attempts"`
Backoff string `yaml:"backoff" json:"backoff"` // "linear", "exponential"
}
type Task struct {
ID string `yaml:"id" json:"id"`
ParentTaskID string `yaml:"parent_task_id" json:"parent_task_id"`
Name string `yaml:"name" json:"name"`
Description string `yaml:"description" json:"description"`
Claude ClaudeConfig `yaml:"claude" json:"claude"`
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"`
State State `yaml:"-" json:"state"`
RejectionComment string `yaml:"-" json:"rejection_comment,omitempty"`
QuestionJSON string `yaml:"-" json:"question,omitempty"`
CreatedAt time.Time `yaml:"-" json:"created_at"`
UpdatedAt time.Time `yaml:"-" json:"updated_at"`
}
// Duration wraps time.Duration for YAML unmarshaling from strings like "30m".
type Duration struct {
time.Duration
}
func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err
}
dur, err := time.ParseDuration(s)
if err != nil {
return err
}
d.Duration = dur
return nil
}
func (d Duration) MarshalYAML() (interface{}, error) {
return d.Duration.String(), nil
}
// BatchFile represents a YAML file containing multiple tasks.
type BatchFile struct {
Tasks []Task `yaml:"tasks"`
}
// ValidTransition returns true if moving from the current state to next is allowed.
func ValidTransition(from, to State) bool {
transitions := map[State][]State{
StatePending: {StateQueued, StateCancelled},
StateQueued: {StateRunning, StateCancelled},
StateRunning: {StateReady, StateCompleted, StateFailed, StateTimedOut, StateCancelled, StateBudgetExceeded, StateBlocked},
StateReady: {StateCompleted, StatePending},
StateFailed: {StateQueued}, // retry
StateTimedOut: {StateQueued}, // retry
StateCancelled: {StateQueued}, // restart
StateBlocked: {StateQueued}, // answer received → re-queue as resume execution
}
for _, allowed := range transitions[from] {
if allowed == to {
return true
}
}
return false
}
|