rdev/cmd/sdlc/cmd_next.go
jordan f22b220c6d feat: add SDLC branch management, merge, archive, and orchestrator APIs
Add branch lifecycle commands (branch, merge, archive) to the SDLC CLI.
Introduce orchestrator handler and service for multi-step SDLC workflows.
Expand skeleton template with 15 Claude commands covering the full feature
lifecycle. Extend classifier rules, error types, and executor port for
branch operations. Split rules.go and classifier_test.go to stay within
500-line limit.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 12:30:03 -07:00

174 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 {
printJSON(map[string]string{"action": "IDLE", "message": "No active features"})
return nil
}
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 {
printJSON(map[string]string{"action": "IDLE", "message": "No actionable work found"})
return nil
}
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")
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 {
printJSON(cl)
return nil
}
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)
}