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 ") 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) }