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>
138 lines
2.9 KiB
Go
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)
|
|
}
|