- Task struct sa svim poljima (ID, Title, Status, Agent, Model, DependsOn, Description) - LoadTask, ScanTasks, FindTask, NextTask, MoveTask - 17 testova — svi prolaze Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
231 lines
5.4 KiB
Go
231 lines
5.4 KiB
Go
// Package supervisor implements the KAOS supervisor process
|
|
// that orchestrates agent execution and task management.
|
|
package supervisor
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Known task folder names and their order.
|
|
var taskFolders = []string{"backlog", "ready", "active", "review", "done"}
|
|
|
|
// Task represents a parsed KAOS task from a markdown file.
|
|
type Task struct {
|
|
ID string // T01, T02...
|
|
Title string // title from # heading
|
|
Status string // backlog, ready, active, review, done (from folder name)
|
|
Agent string // coder, checker, frontend...
|
|
Model string // Haiku, Sonnet, Opus
|
|
DependsOn []string // list of task IDs
|
|
Description string // full description text
|
|
FilePath string // path to the file
|
|
}
|
|
|
|
// headerRegex matches "# T01: Some Title" format.
|
|
var headerRegex = regexp.MustCompile(`^#\s+(T\d+):\s+(.+)$`)
|
|
|
|
// metaRegex matches "**Key:** Value" format.
|
|
var metaRegex = regexp.MustCompile(`^\*\*(.+?):\*\*\s+(.+)$`)
|
|
|
|
// LoadTask parses a single task markdown file and returns a Task.
|
|
// The Status field is derived from the parent folder name.
|
|
func LoadTask(filePath string) (Task, error) {
|
|
f, err := os.Open(filePath)
|
|
if err != nil {
|
|
return Task{}, fmt.Errorf("open task file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
task := Task{
|
|
FilePath: filePath,
|
|
Status: statusFromPath(filePath),
|
|
}
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
inDescription := false
|
|
var descLines []string
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
// Check for header line
|
|
if matches := headerRegex.FindStringSubmatch(line); matches != nil {
|
|
task.ID = matches[1]
|
|
task.Title = matches[2]
|
|
continue
|
|
}
|
|
|
|
// If we're collecting description, check for end marker
|
|
if inDescription {
|
|
if strings.HasPrefix(line, "## ") || strings.HasPrefix(line, "---") {
|
|
inDescription = false
|
|
continue
|
|
}
|
|
descLines = append(descLines, line)
|
|
continue
|
|
}
|
|
|
|
// Check for description section start
|
|
if strings.HasPrefix(line, "## Opis") {
|
|
inDescription = true
|
|
continue
|
|
}
|
|
|
|
// Check for metadata fields
|
|
if matches := metaRegex.FindStringSubmatch(line); matches != nil {
|
|
key := matches[1]
|
|
value := strings.TrimSpace(matches[2])
|
|
switch key {
|
|
case "Agent":
|
|
task.Agent = value
|
|
case "Model":
|
|
task.Model = value
|
|
case "Zavisi od":
|
|
task.DependsOn = parseDependencies(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return Task{}, fmt.Errorf("read task file: %w", err)
|
|
}
|
|
|
|
if task.ID == "" {
|
|
return Task{}, fmt.Errorf("no task ID found in %s", filePath)
|
|
}
|
|
|
|
task.Description = strings.TrimSpace(strings.Join(descLines, "\n"))
|
|
|
|
return task, nil
|
|
}
|
|
|
|
// ScanTasks reads all task files from all status folders under tasksDir.
|
|
func ScanTasks(tasksDir string) ([]Task, error) {
|
|
var tasks []Task
|
|
|
|
for _, folder := range taskFolders {
|
|
dir := filepath.Join(tasksDir, folder)
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("read folder %s: %w", folder, err)
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
|
|
continue
|
|
}
|
|
|
|
path := filepath.Join(dir, entry.Name())
|
|
task, err := LoadTask(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load task %s: %w", path, err)
|
|
}
|
|
|
|
tasks = append(tasks, task)
|
|
}
|
|
}
|
|
|
|
return tasks, nil
|
|
}
|
|
|
|
// FindTask returns a pointer to the task with the given ID, or nil if not found.
|
|
func FindTask(tasks []Task, id string) *Task {
|
|
for i := range tasks {
|
|
if tasks[i].ID == id {
|
|
return &tasks[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NextTask returns the first task in ready/ whose dependencies are all in done/.
|
|
// Returns nil if no task is ready.
|
|
func NextTask(tasks []Task) *Task {
|
|
// Build a set of done task IDs
|
|
doneSet := make(map[string]bool)
|
|
for _, t := range tasks {
|
|
if t.Status == "done" {
|
|
doneSet[t.ID] = true
|
|
}
|
|
}
|
|
|
|
for i := range tasks {
|
|
if tasks[i].Status != "ready" {
|
|
continue
|
|
}
|
|
|
|
allDone := true
|
|
for _, dep := range tasks[i].DependsOn {
|
|
if !doneSet[dep] {
|
|
allDone = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if allDone {
|
|
return &tasks[i]
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MoveTask moves a task file from one status folder to another.
|
|
func MoveTask(tasksDir, taskID, fromFolder, toFolder string) error {
|
|
filename := taskID + ".md"
|
|
src := filepath.Join(tasksDir, fromFolder, filename)
|
|
dst := filepath.Join(tasksDir, toFolder, filename)
|
|
|
|
// Verify source exists
|
|
if _, err := os.Stat(src); err != nil {
|
|
return fmt.Errorf("source task not found: %w", err)
|
|
}
|
|
|
|
// Ensure destination folder exists
|
|
if err := os.MkdirAll(filepath.Join(tasksDir, toFolder), 0755); err != nil {
|
|
return fmt.Errorf("create destination folder: %w", err)
|
|
}
|
|
|
|
if err := os.Rename(src, dst); err != nil {
|
|
return fmt.Errorf("move task: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// statusFromPath extracts the status folder name from a file path.
|
|
// E.g., "/path/to/TASKS/ready/T01.md" → "ready"
|
|
func statusFromPath(path string) string {
|
|
dir := filepath.Base(filepath.Dir(path))
|
|
for _, f := range taskFolders {
|
|
if dir == f {
|
|
return f
|
|
}
|
|
}
|
|
return dir
|
|
}
|
|
|
|
// parseDependencies parses "T01, T03" or "T01 ✅, T03" into ["T01", "T03"].
|
|
// Returns nil for "—" or empty.
|
|
func parseDependencies(value string) []string {
|
|
if value == "—" || value == "-" || value == "" {
|
|
return nil
|
|
}
|
|
|
|
depRegex := regexp.MustCompile(`T\d+`)
|
|
matches := depRegex.FindAllString(value, -1)
|
|
if len(matches) == 0 {
|
|
return nil
|
|
}
|
|
return matches
|
|
}
|