Files
ai-workflow-skill/packages/orch-runtime/internal/cli/orch/task.go
T

156 lines
5.3 KiB
Go

package orch
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"strings"
"ai-workflow-skill/packages/coord-core/protocol"
"ai-workflow-skill/packages/coord-core/store"
"github.com/spf13/cobra"
)
type taskAddOptions struct {
runID string
taskID string
title string
summary string
defaultTo string
acceptanceJSON string
priority string
specFile string
specSHA string
checkProfile string
requiredChecks []string
allowedPaths []string
blockedPaths []string
metadataJSON string
}
func newTaskCmd(root *rootOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "task",
Short: "Task management commands",
Long: helpLong(
"Use task commands to define schedulable work inside one run.",
"Tasks should be small enough to inspect, dispatch, retry, and reconcile independently.",
),
Example: ` orch --db .agents/coord.db task add --run blog_mvp_001 --task T1 --title "Implement backend" --default-to backend-worker`,
}
cmd.AddCommand(newTaskAddCmd(root))
return cmd
}
func newTaskAddCmd(root *rootOptions) *cobra.Command {
opts := &taskAddOptions{}
cmd := &cobra.Command{
Use: "add",
Short: "Add a task to a run",
Long: helpLong(
"Use task add to register one schedulable task inside a run.",
"Tasks may include a default worker target, priority, optional acceptance JSON, and a task-spec snapshot with verification policy.",
"A task must belong to an existing run before it can become ready or be dispatched.",
),
Example: ` orch --db .agents/coord.db task add --run blog_mvp_001 --task T1 --title "Implement backend" --summary "Ship the first API slice" --default-to backend-worker --priority high
orch --db .agents/coord.db task add --run blog_mvp_001 --task T2 --title "Polish release flow" --spec-file ./tasks/t2.md --check-profile cadence_component --required-check lint --required-check test:e2e`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
sqlDB, err := openOrchDB(ctx, root.dbPath)
if err != nil {
return err
}
defer sqlDB.Close()
specBody, computedSpecSHA, err := loadTaskSpecSnapshot(opts.specFile, opts.specSHA)
if err != nil {
return err
}
task, err := store.NewOrchStore(sqlDB).AddTask(ctx, store.AddTaskInput{
RunID: opts.runID,
TaskID: opts.taskID,
Title: opts.title,
Summary: opts.summary,
DefaultTo: opts.defaultTo,
AcceptanceJSON: opts.acceptanceJSON,
Priority: opts.priority,
SpecFile: strings.TrimSpace(opts.specFile),
SpecSHA: computedSpecSHA,
SpecBody: specBody,
CheckProfile: strings.TrimSpace(opts.checkProfile),
RequiredChecks: opts.requiredChecks,
AllowedPaths: opts.allowedPaths,
BlockedPaths: opts.blockedPaths,
MetadataJSON: opts.metadataJSON,
})
if err != nil {
return err
}
resp := protocol.Success{
OK: true,
Command: "task add",
Data: map[string]any{
"task": task,
},
}
if root.json {
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
}
_, err = fmt.Fprintf(cmd.OutOrStdout(), "added task %s to run %s\n", task.TaskID, task.RunID)
return err
},
}
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
cmd.Flags().StringVar(&opts.taskID, "task", "", "Task ID")
cmd.Flags().StringVar(&opts.title, "title", "", "Task title")
cmd.Flags().StringVar(&opts.summary, "summary", "", "Task summary")
cmd.Flags().StringVar(&opts.defaultTo, "default-to", "", "Default worker agent")
cmd.Flags().StringVar(&opts.acceptanceJSON, "acceptance-json", "", "Acceptance criteria JSON")
cmd.Flags().StringVar(&opts.priority, "priority", "normal", "Task priority")
cmd.Flags().StringVar(&opts.specFile, "spec-file", "", "Path to the task spec file to snapshot")
cmd.Flags().StringVar(&opts.specSHA, "spec-sha", "", "Optional expected SHA256 for --spec-file")
cmd.Flags().StringVar(&opts.checkProfile, "check-profile", "", "Verification check profile name")
cmd.Flags().StringSliceVar(&opts.requiredChecks, "required-check", nil, "Required verification check name; repeat for multiple checks")
cmd.Flags().StringSliceVar(&opts.allowedPaths, "allowed-path", nil, "Allowed path prefix for task scope; repeat for multiple paths")
cmd.Flags().StringSliceVar(&opts.blockedPaths, "blocked-path", nil, "Blocked path prefix for task scope; repeat for multiple paths")
cmd.Flags().StringVar(&opts.metadataJSON, "metadata-json", "", "Structured metadata JSON for task policy")
_ = cmd.MarkFlagRequired("run")
_ = cmd.MarkFlagRequired("task")
_ = cmd.MarkFlagRequired("title")
return cmd
}
func loadTaskSpecSnapshot(specFile, expectedSHA string) (string, string, error) {
specFile = strings.TrimSpace(specFile)
expectedSHA = strings.TrimSpace(expectedSHA)
if specFile == "" {
if expectedSHA != "" {
return "", "", fmt.Errorf("%w: spec-sha requires spec-file", store.ErrInvalidInput)
}
return "", "", nil
}
body, err := os.ReadFile(specFile)
if err != nil {
return "", "", protocol.InvalidInput("failed to read spec-file", err)
}
sum := sha256.Sum256(body)
computed := hex.EncodeToString(sum[:])
if expectedSHA != "" && !strings.EqualFold(expectedSHA, computed) {
return "", "", fmt.Errorf("%w: spec-sha does not match spec-file contents", store.ErrInvalidInput)
}
return string(body), computed, nil
}