- Add auth.RequireScope() to all handler routes for proper authorization - Add SDLC OpenAPI endpoint documentation (state, features, tasks, branches, merge, archive, orchestrator) - Add SDLC documentation guides (getting-started, cli-reference, api-reference, command-catalog) - Add artifact_test.go for SDLC artifact coverage - Add CLAUDE.md rules: auth scopes requirement, error wrapping with %w - Fix error wrapping to use %w instead of %v throughout codebase - Improve CLI merge command with conflict detection and resolution - Fix handler tests to include auth middleware for RequireScope - Add cookbook tree runner scripts for automated testing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
171 lines
4.0 KiB
Go
171 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/orchard9/rdev/internal/sdlc"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
nextForFeature string
|
|
nextExecute bool
|
|
)
|
|
|
|
var nextCmd = &cobra.Command{
|
|
Use: "next",
|
|
Short: "Run classifier and show next required action",
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
root := mustResolveRoot()
|
|
|
|
state, err := sdlc.LoadState(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg, err := sdlc.LoadConfig(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
classifier := sdlc.NewClassifier()
|
|
|
|
// If a specific feature is requested
|
|
if nextForFeature != "" {
|
|
return classifyFeature(root, state, cfg, classifier, nextForFeature)
|
|
}
|
|
|
|
// Classify all active features, return first actionable
|
|
if len(state.ActiveWork.Features) == 0 {
|
|
if jsonOutput {
|
|
return printJSON(map[string]string{"action": "IDLE", "message": "No active features"})
|
|
}
|
|
fmt.Println("No active features. Create one: sdlc feature create <slug>")
|
|
return nil
|
|
}
|
|
|
|
for _, af := range state.ActiveWork.Features {
|
|
f, err := sdlc.LoadFeature(root, af.Slug)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
cl := classifier.Classify(&sdlc.EvalContext{
|
|
State: state,
|
|
Feature: f,
|
|
Config: cfg,
|
|
Root: root,
|
|
})
|
|
|
|
if cl.Action != sdlc.ActionIdle {
|
|
return printClassification(cl, f)
|
|
}
|
|
}
|
|
|
|
if jsonOutput {
|
|
return printJSON(map[string]string{"action": "IDLE", "message": "No actionable work found"})
|
|
}
|
|
|
|
fmt.Println("No actionable work found across active features.")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func classifyFeature(root string, state *sdlc.State, cfg *sdlc.Config, classifier *sdlc.Classifier, slug string) error {
|
|
f, err := sdlc.LoadFeature(root, slug)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cl := classifier.Classify(&sdlc.EvalContext{
|
|
State: state,
|
|
Feature: f,
|
|
Config: cfg,
|
|
Root: root,
|
|
})
|
|
|
|
if nextExecute && cl.Action == sdlc.ActionTransition && cl.TransitionTo != "" {
|
|
if err := f.Transition(cl.TransitionTo); err != nil {
|
|
return fmt.Errorf("execute transition: %w", err)
|
|
}
|
|
if err := f.Save(root); err != nil {
|
|
return err
|
|
}
|
|
state.UpdateActiveFeature(slug, cl.TransitionTo, f.Branch)
|
|
state.RecordAction("TRANSITION", slug, "cli", "success")
|
|
if err := state.Save(root); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !jsonOutput {
|
|
fmt.Printf("Executed: transition %s -> %s\n\n", f.Slug, cl.TransitionTo)
|
|
}
|
|
|
|
// Re-classify after transition
|
|
f, err = sdlc.LoadFeature(root, slug)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cl = classifier.Classify(&sdlc.EvalContext{
|
|
State: state,
|
|
Feature: f,
|
|
Config: cfg,
|
|
Root: root,
|
|
})
|
|
} else if nextExecute && cl.Action != sdlc.ActionTransition {
|
|
if !jsonOutput && cl.NextCommand != "" {
|
|
fmt.Printf("Cannot auto-execute %s. Run: %s\n\n", cl.Action, cl.NextCommand)
|
|
}
|
|
}
|
|
|
|
return printClassification(cl, f)
|
|
}
|
|
|
|
func printClassification(cl *sdlc.Classification, f *sdlc.Feature) error {
|
|
if jsonOutput {
|
|
return printJSON(cl)
|
|
}
|
|
|
|
fmt.Printf("Feature: %s\n", cl.Feature)
|
|
fmt.Printf("Phase: %s\n", cl.CurrentPhase)
|
|
|
|
if len(f.Tasks) > 0 {
|
|
s := sdlc.SummarizeTasks(f.Tasks)
|
|
fmt.Printf("Tasks: %d/%d complete", s.Completed, s.Total)
|
|
if s.InProgress > 0 {
|
|
fmt.Printf(", %d in-progress", s.InProgress)
|
|
}
|
|
if s.Pending > 0 {
|
|
fmt.Printf(", %d pending", s.Pending)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
fmt.Println()
|
|
|
|
fmt.Printf("NEXT ACTION: %s\n", cl.Action)
|
|
if cl.Message != "" {
|
|
fmt.Printf("Message: %s\n", cl.Message)
|
|
}
|
|
if cl.NextCommand != "" {
|
|
fmt.Printf("Command: %s\n", cl.NextCommand)
|
|
}
|
|
if cl.OutputPath != "" {
|
|
fmt.Printf("Output: %s\n", cl.OutputPath)
|
|
}
|
|
if cl.TransitionTo != "" {
|
|
fmt.Printf("Transition: -> %s\n", cl.TransitionTo)
|
|
}
|
|
if cl.TaskID != "" {
|
|
fmt.Printf("Task: %s\n", cl.TaskID)
|
|
}
|
|
fmt.Printf("Rule: %s\n", cl.RuleMatched)
|
|
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
nextCmd.Flags().StringVar(&nextForFeature, "for", "", "classify specific feature")
|
|
nextCmd.Flags().BoolVar(&nextExecute, "execute", false, "auto-execute transition actions")
|
|
rootCmd.AddCommand(nextCmd)
|
|
}
|