rdev/internal/sdlc/rules.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

479 lines
12 KiB
Go

package sdlc
import (
"fmt"
"strings"
)
// DefaultRules returns all classifier rules in priority order.
// First matching rule wins.
func DefaultRules() []Rule {
return []Rule{
blockedDependencyRule(),
needsSpecRule(),
specNeedsApprovalRule(),
specApprovedRule(),
needsDesignRule(),
designNeedsApprovalRule(),
needsTasksRule(),
tasksNeedApprovalRule(),
needsQAPlanRule(),
qaPlanNeedsApprovalRule(),
planningCompleteRule(),
readyToImplementRule(),
implementNextTaskRule(),
implementationCompleteRule(),
needsReviewRule(),
reviewHasIssuesRule(),
reviewPassedRule(),
needsAuditRule(),
auditHasIssuesRule(),
auditPassedRule(),
needsQARule(),
qaHasFailuresRule(),
qaPassedRule(),
needsMergeRule(),
archiveFeatureRule(),
}
}
func blockedDependencyRule() Rule {
return Rule{
ID: "blocked-dependency",
Condition: func(ctx *EvalContext) bool {
return ctx.Feature.IsBlocked()
},
Action: ActionBlocked,
Message: func(ctx *EvalContext) string {
return fmt.Sprintf("Feature blocked by: %s", strings.Join(ctx.Feature.Blockers, ", "))
},
}
}
func needsSpecRule() Rule {
return Rule{
ID: "needs-spec",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseDraft {
return false
}
art := ctx.Feature.GetArtifact(ArtifactSpec)
return art == nil || art.Status == StatusPending
},
Action: ActionCreateSpec,
NextCommand: func(ctx *EvalContext) string {
return "/spec-feature " + ctx.Feature.Slug
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/spec.md"
},
}
}
func specNeedsApprovalRule() Rule {
return Rule{
ID: "spec-needs-approval",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseDraft {
return false
}
art := ctx.Feature.GetArtifact(ArtifactSpec)
return art != nil && art.Status == StatusDraft
},
Action: ActionAwaitApproval,
Message: func(_ *EvalContext) string {
return "Specification requires user approval"
},
}
}
func specApprovedRule() Rule {
return Rule{
ID: "spec-approved",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseDraft {
return false
}
art := ctx.Feature.GetArtifact(ArtifactSpec)
return art != nil && art.Status == StatusApproved
},
Action: ActionTransition,
TransitionTo: PhaseSpecified,
}
}
func needsDesignRule() Rule {
return Rule{
ID: "needs-design",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
art := ctx.Feature.GetArtifact(ArtifactDesign)
return art == nil || art.Status == StatusPending
},
Action: ActionCreateDesign,
NextCommand: func(ctx *EvalContext) string {
return "/design-feature " + ctx.Feature.Slug
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/design.md"
},
}
}
func designNeedsApprovalRule() Rule {
return Rule{
ID: "design-needs-approval",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
art := ctx.Feature.GetArtifact(ArtifactDesign)
return art != nil && (art.Status == StatusDraft || art.Status == StatusRejected)
},
Action: ActionAwaitApproval,
Message: func(_ *EvalContext) string {
return "Design document requires user approval"
},
}
}
func needsTasksRule() Rule {
return Rule{
ID: "needs-tasks",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
design := ctx.Feature.GetArtifact(ArtifactDesign)
if design == nil || design.Status != StatusApproved {
return false
}
tasks := ctx.Feature.GetArtifact(ArtifactTasks)
return tasks == nil || tasks.Status == StatusPending
},
Action: ActionCreateTasks,
NextCommand: func(ctx *EvalContext) string {
return "/breakdown-feature " + ctx.Feature.Slug
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/tasks.md"
},
}
}
func tasksNeedApprovalRule() Rule {
return Rule{
ID: "tasks-need-approval",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
tasks := ctx.Feature.GetArtifact(ArtifactTasks)
return tasks != nil && (tasks.Status == StatusDraft || tasks.Status == StatusRejected)
},
Action: ActionAwaitApproval,
Message: func(_ *EvalContext) string {
return "Task breakdown requires user approval"
},
}
}
func needsQAPlanRule() Rule {
return Rule{
ID: "needs-qa-plan",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
tasks := ctx.Feature.GetArtifact(ArtifactTasks)
if tasks == nil || tasks.Status != StatusApproved {
return false
}
qa := ctx.Feature.GetArtifact(ArtifactQAPlan)
return qa == nil || qa.Status == StatusPending
},
Action: ActionCreateQAPlan,
NextCommand: func(ctx *EvalContext) string {
return "/create-qa-plan " + ctx.Feature.Slug
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/qa-plan.md"
},
}
}
func qaPlanNeedsApprovalRule() Rule {
return Rule{
ID: "qa-plan-needs-approval",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
qa := ctx.Feature.GetArtifact(ArtifactQAPlan)
return qa != nil && (qa.Status == StatusDraft || qa.Status == StatusRejected)
},
Action: ActionAwaitApproval,
Message: func(_ *EvalContext) string {
return "QA plan requires user approval"
},
}
}
func planningCompleteRule() Rule {
return Rule{
ID: "planning-complete",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseSpecified {
return false
}
qa := ctx.Feature.GetArtifact(ArtifactQAPlan)
return qa != nil && qa.Status == StatusApproved
},
Action: ActionTransition,
TransitionTo: PhasePlanned,
}
}
func readyToImplementRule() Rule {
return Rule{
ID: "ready-to-implement",
Condition: func(ctx *EvalContext) bool {
return ctx.Feature.Phase == PhasePlanned || ctx.Feature.Phase == PhaseReady
},
Action: ActionTransition,
TransitionTo: PhaseReady,
Message: func(ctx *EvalContext) string {
if ctx.Feature.Phase == PhasePlanned {
return "Ready for implementation: transition to ready, then implementation"
}
return "Ready for implementation"
},
}
}
func implementNextTaskRule() Rule {
return Rule{
ID: "implement-next-task",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseImplementation {
return false
}
next := NextTask(ctx.Feature.Tasks)
return next != nil
},
Action: ActionImplementTask,
NextCommand: func(ctx *EvalContext) string {
next := NextTask(ctx.Feature.Tasks)
if next == nil {
return ""
}
return fmt.Sprintf("/implement-task %s %s", ctx.Feature.Slug, next.ID)
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/tasks.md"
},
TaskID: func(ctx *EvalContext) string {
next := NextTask(ctx.Feature.Tasks)
if next == nil {
return ""
}
return next.ID
},
}
}
func implementationCompleteRule() Rule {
return Rule{
ID: "implementation-complete",
Condition: func(ctx *EvalContext) bool {
return ctx.Feature.Phase == PhaseImplementation && AllTasksComplete(ctx.Feature.Tasks)
},
Action: ActionTransition,
TransitionTo: PhaseReview,
}
}
func needsReviewRule() Rule {
return Rule{
ID: "needs-review",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseReview {
return false
}
art := ctx.Feature.GetArtifact(ArtifactReview)
return art == nil || art.Status == StatusPending
},
Action: ActionReviewCode,
NextCommand: func(ctx *EvalContext) string {
return "/review-feature " + ctx.Feature.Slug
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/review.md"
},
}
}
func reviewHasIssuesRule() Rule {
return Rule{
ID: "review-has-issues",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseReview {
return false
}
art := ctx.Feature.GetArtifact(ArtifactReview)
return art != nil && art.Status == StatusNeedsFix
},
Action: ActionFixReviewIssues,
NextCommand: func(ctx *EvalContext) string {
return "/fix-review-issues " + ctx.Feature.Slug
},
}
}
func reviewPassedRule() Rule {
return Rule{
ID: "review-passed",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseReview {
return false
}
art := ctx.Feature.GetArtifact(ArtifactReview)
return art != nil && art.Status == StatusPassed
},
Action: ActionTransition,
TransitionTo: PhaseAudit,
}
}
func needsAuditRule() Rule {
return Rule{
ID: "needs-audit",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseAudit {
return false
}
art := ctx.Feature.GetArtifact(ArtifactAudit)
return art == nil || art.Status == StatusPending
},
Action: ActionAuditCode,
NextCommand: func(ctx *EvalContext) string {
return "/audit-feature " + ctx.Feature.Slug
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/audit.md"
},
}
}
func auditHasIssuesRule() Rule {
return Rule{
ID: "audit-has-issues",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseAudit {
return false
}
art := ctx.Feature.GetArtifact(ArtifactAudit)
return art != nil && art.Status == StatusNeedsFix
},
Action: ActionRemediateAudit,
NextCommand: func(ctx *EvalContext) string {
return "/remediate-audit " + ctx.Feature.Slug
},
}
}
func auditPassedRule() Rule {
return Rule{
ID: "audit-passed",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseAudit {
return false
}
art := ctx.Feature.GetArtifact(ArtifactAudit)
return art != nil && art.Status == StatusPassed
},
Action: ActionTransition,
TransitionTo: PhaseQA,
}
}
func needsQARule() Rule {
return Rule{
ID: "needs-qa",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseQA {
return false
}
art := ctx.Feature.GetArtifact(ArtifactQAResults)
return art == nil || art.Status == StatusPending
},
Action: ActionRunQA,
NextCommand: func(ctx *EvalContext) string {
return "/run-qa " + ctx.Feature.Slug
},
OutputPath: func(ctx *EvalContext) string {
return ".sdlc/features/" + ctx.Feature.Slug + "/qa-results.md"
},
}
}
func qaHasFailuresRule() Rule {
return Rule{
ID: "qa-has-failures",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseQA {
return false
}
art := ctx.Feature.GetArtifact(ArtifactQAResults)
return art != nil && art.Status == StatusFailed
},
Action: ActionFixQAFailures,
NextCommand: func(ctx *EvalContext) string {
return "/fix-qa-failures " + ctx.Feature.Slug
},
}
}
func qaPassedRule() Rule {
return Rule{
ID: "qa-passed",
Condition: func(ctx *EvalContext) bool {
if ctx.Feature.Phase != PhaseQA {
return false
}
art := ctx.Feature.GetArtifact(ArtifactQAResults)
return art != nil && art.Status == StatusPassed
},
Action: ActionTransition,
TransitionTo: PhaseMerge,
}
}
func needsMergeRule() Rule {
return Rule{
ID: "needs-merge",
Condition: func(ctx *EvalContext) bool {
return ctx.Feature.Phase == PhaseMerge
},
Action: ActionMergeFeature,
NextCommand: func(ctx *EvalContext) string {
return "/merge-feature " + ctx.Feature.Slug
},
}
}
func archiveFeatureRule() Rule {
return Rule{
ID: "archive-feature",
Condition: func(ctx *EvalContext) bool {
return ctx.Feature.Phase == PhaseReleased
},
Action: ActionArchive,
NextCommand: func(ctx *EvalContext) string {
return "/archive-feature " + ctx.Feature.Slug
},
}
}