rdev/cmd/sdlc/cmd_next.go
jordan 425ef0f806 feat: add SDLC orchestration - library, CLI, and API integration
Implements deterministic feature lifecycle management for agent-driven
development. Agents use the CLI in pods; operators control via REST API.

Library (internal/sdlc/):
- Feature lifecycle with 10 phases (draft → released)
- Classifier engine with priority-ordered rules
- Artifact tracking with approval workflow
- Task management within features
- YAML-based state persistence

CLI (cmd/sdlc/):
- init, state, next, feature, artifact, task, query commands
- --json flag for machine-readable output
- Runs inside project pods

API (21 endpoints under /projects/{id}/sdlc/):
- State: GET /state, GET /next
- Features: CRUD + transition/block/unblock
- Artifacts: approve/reject per type
- Tasks: add/start/complete/block
- Queries: blocked/ready/needs-approval

Architecture:
- Port: SDLCExecutor interface (internal/port/)
- Adapter: kubectl exec into pods (internal/adapter/kubernetes/)
- Service: pod resolution + logging (internal/service/)
- Handlers: 5 files under 500-line limit (internal/handlers/)

Also includes template upgrades (chassis framework, UI components,
OpenAPI helpers, backend/frontend guides) and component improvements.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 09:57:05 -07:00

138 lines
2.9 KiB
Go

package main
import (
"fmt"
"github.com/orchard9/rdev/internal/sdlc"
"github.com/spf13/cobra"
)
var (
nextForFeature string
)
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,
})
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")
rootCmd.AddCommand(nextCmd)
}