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>
278 lines
6.9 KiB
Go
278 lines
6.9 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(),
|
|
needsBranchRule(),
|
|
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 needsBranchRule() Rule {
|
|
return Rule{
|
|
ID: "needs-branch",
|
|
Condition: func(ctx *EvalContext) bool {
|
|
if ctx.Feature.Phase != PhasePlanned {
|
|
return false
|
|
}
|
|
if ctx.Config == nil || !ctx.Config.Compliance.RequireBranch {
|
|
return false
|
|
}
|
|
return ctx.Feature.Branch == ""
|
|
},
|
|
Action: ActionCreateBranch,
|
|
Message: func(_ *EvalContext) string {
|
|
return "Feature branch required before implementation"
|
|
},
|
|
NextCommand: func(ctx *EvalContext) string {
|
|
return "sdlc branch create " + ctx.Feature.Slug
|
|
},
|
|
}
|
|
}
|
|
|
|
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"
|
|
},
|
|
}
|
|
}
|