// 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 }