156 lines
5.3 KiB
Go
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
|
|
}
|