Implements deterministic feature lifecycle management for agent-driven
development. Agents use the CLI in pods; operators control via REST API.
Library (internal/sdlc/):
- Feature lifecycle with 10 phases (draft → released)
- Classifier engine with priority-ordered rules
- Artifact tracking with approval workflow
- Task management within features
- YAML-based state persistence
CLI (cmd/sdlc/):
- init, state, next, feature, artifact, task, query commands
- --json flag for machine-readable output
- Runs inside project pods
API (21 endpoints under /projects/{id}/sdlc/):
- State: GET /state, GET /next
- Features: CRUD + transition/block/unblock
- Artifacts: approve/reject per type
- Tasks: add/start/complete/block
- Queries: blocked/ready/needs-approval
Architecture:
- Port: SDLCExecutor interface (internal/port/)
- Adapter: kubectl exec into pods (internal/adapter/kubernetes/)
- Service: pod resolution + logging (internal/service/)
- Handlers: 5 files under 500-line limit (internal/handlers/)
Also includes template upgrades (chassis framework, UI components,
OpenAPI helpers, backend/frontend guides) and component improvements.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
135 lines
3.3 KiB
Go
135 lines
3.3 KiB
Go
package sdlc
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Task represents an implementation task within a feature.
|
|
type Task struct {
|
|
ID string `yaml:"id" json:"id"`
|
|
Title string `yaml:"title" json:"title"`
|
|
Status TaskStatus `yaml:"status" json:"status"`
|
|
Spec string `yaml:"spec,omitempty" json:"spec,omitempty"`
|
|
Files []string `yaml:"files,omitempty" json:"files,omitempty"`
|
|
Patterns []string `yaml:"patterns,omitempty" json:"patterns,omitempty"`
|
|
DependsOn []string `yaml:"depends_on,omitempty" json:"depends_on,omitempty"`
|
|
StartedAt *time.Time `yaml:"started_at,omitempty" json:"started_at,omitempty"`
|
|
DoneAt *time.Time `yaml:"done_at,omitempty" json:"done_at,omitempty"`
|
|
Notes string `yaml:"notes,omitempty" json:"notes,omitempty"`
|
|
}
|
|
|
|
// StartTask marks a task as in-progress.
|
|
func StartTask(tasks []Task, taskID string) ([]Task, error) {
|
|
for i, t := range tasks {
|
|
if t.ID == taskID {
|
|
if t.Status != TaskPending && t.Status != TaskBlocked {
|
|
return tasks, fmt.Errorf("task %s is %s, not startable", taskID, t.Status)
|
|
}
|
|
now := time.Now().UTC()
|
|
tasks[i].Status = TaskInProgress
|
|
tasks[i].StartedAt = &now
|
|
return tasks, nil
|
|
}
|
|
}
|
|
return tasks, ErrTaskNotFound
|
|
}
|
|
|
|
// CompleteTask marks a task as complete.
|
|
func CompleteTask(tasks []Task, taskID string) ([]Task, error) {
|
|
for i, t := range tasks {
|
|
if t.ID == taskID {
|
|
if t.Status != TaskInProgress {
|
|
return tasks, fmt.Errorf("task %s is %s, not completable", taskID, t.Status)
|
|
}
|
|
now := time.Now().UTC()
|
|
tasks[i].Status = TaskComplete
|
|
tasks[i].DoneAt = &now
|
|
return tasks, nil
|
|
}
|
|
}
|
|
return tasks, ErrTaskNotFound
|
|
}
|
|
|
|
// BlockTask marks a task as blocked.
|
|
func BlockTask(tasks []Task, taskID string) ([]Task, error) {
|
|
for i, t := range tasks {
|
|
if t.ID == taskID {
|
|
tasks[i].Status = TaskBlocked
|
|
return tasks, nil
|
|
}
|
|
}
|
|
return tasks, ErrTaskNotFound
|
|
}
|
|
|
|
// AddTask appends a new task with an auto-generated ID.
|
|
func AddTask(tasks []Task, title string) []Task {
|
|
id := fmt.Sprintf("task-%03d", len(tasks)+1)
|
|
return append(tasks, Task{
|
|
ID: id,
|
|
Title: title,
|
|
Status: TaskPending,
|
|
})
|
|
}
|
|
|
|
// PendingTasks returns tasks that are pending or blocked.
|
|
func PendingTasks(tasks []Task) []Task {
|
|
var result []Task
|
|
for _, t := range tasks {
|
|
if t.Status == TaskPending {
|
|
result = append(result, t)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// NextTask returns the first pending task, or nil if none.
|
|
func NextTask(tasks []Task) *Task {
|
|
for i, t := range tasks {
|
|
if t.Status == TaskPending {
|
|
return &tasks[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AllTasksComplete returns true if every task is in the complete state.
|
|
func AllTasksComplete(tasks []Task) bool {
|
|
if len(tasks) == 0 {
|
|
return false
|
|
}
|
|
for _, t := range tasks {
|
|
if t.Status != TaskComplete {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// TaskSummary returns counts by status.
|
|
type TaskSummary struct {
|
|
Total int `json:"total"`
|
|
Completed int `json:"completed"`
|
|
InProgress int `json:"in_progress"`
|
|
Pending int `json:"pending"`
|
|
Blocked int `json:"blocked"`
|
|
}
|
|
|
|
// SummarizeTasks computes a TaskSummary from a task list.
|
|
func SummarizeTasks(tasks []Task) TaskSummary {
|
|
s := TaskSummary{Total: len(tasks)}
|
|
for _, t := range tasks {
|
|
switch t.Status {
|
|
case TaskComplete:
|
|
s.Completed++
|
|
case TaskInProgress:
|
|
s.InProgress++
|
|
case TaskPending:
|
|
s.Pending++
|
|
case TaskBlocked:
|
|
s.Blocked++
|
|
}
|
|
}
|
|
return s
|
|
}
|