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>
92 lines
2.8 KiB
Go
92 lines
2.8 KiB
Go
package sdlc
|
|
|
|
import "time"
|
|
|
|
// Classification is the output of the classifier engine.
|
|
type Classification struct {
|
|
Timestamp time.Time `json:"timestamp" yaml:"timestamp"`
|
|
Feature string `json:"feature" yaml:"feature"`
|
|
CurrentPhase FeaturePhase `json:"current_phase" yaml:"current_phase"`
|
|
RuleMatched string `json:"rule_matched" yaml:"rule_matched"`
|
|
Action ActionType `json:"action" yaml:"action"`
|
|
Message string `json:"message" yaml:"message"`
|
|
NextCommand string `json:"next_command,omitempty" yaml:"next_command,omitempty"`
|
|
OutputPath string `json:"output_path,omitempty" yaml:"output_path,omitempty"`
|
|
TransitionTo FeaturePhase `json:"transition_to,omitempty" yaml:"transition_to,omitempty"`
|
|
TaskID string `json:"task_id,omitempty" yaml:"task_id,omitempty"`
|
|
}
|
|
|
|
// EvalContext provides all the state needed for rule evaluation.
|
|
type EvalContext struct {
|
|
State *State
|
|
Feature *Feature
|
|
Config *Config
|
|
Root string
|
|
}
|
|
|
|
// Rule is a single classifier rule with a condition and resulting action.
|
|
type Rule struct {
|
|
ID string
|
|
Condition func(ctx *EvalContext) bool
|
|
Action ActionType
|
|
Message func(ctx *EvalContext) string
|
|
NextCommand func(ctx *EvalContext) string
|
|
OutputPath func(ctx *EvalContext) string
|
|
TransitionTo FeaturePhase
|
|
TaskID func(ctx *EvalContext) string
|
|
}
|
|
|
|
// Classifier evaluates rules in priority order, returning the first match.
|
|
type Classifier struct {
|
|
rules []Rule
|
|
}
|
|
|
|
// NewClassifier creates a classifier with the default rules.
|
|
func NewClassifier() *Classifier {
|
|
return &Classifier{rules: DefaultRules()}
|
|
}
|
|
|
|
// NewClassifierWithRules creates a classifier with custom rules.
|
|
func NewClassifierWithRules(rules []Rule) *Classifier {
|
|
return &Classifier{rules: rules}
|
|
}
|
|
|
|
// Classify evaluates all rules against the context and returns the first match.
|
|
func (c *Classifier) Classify(ctx *EvalContext) *Classification {
|
|
for _, rule := range c.rules {
|
|
if rule.Condition(ctx) {
|
|
cl := &Classification{
|
|
Timestamp: time.Now().UTC(),
|
|
Feature: ctx.Feature.Slug,
|
|
CurrentPhase: ctx.Feature.Phase,
|
|
RuleMatched: rule.ID,
|
|
Action: rule.Action,
|
|
TransitionTo: rule.TransitionTo,
|
|
}
|
|
if rule.Message != nil {
|
|
cl.Message = rule.Message(ctx)
|
|
}
|
|
if rule.NextCommand != nil {
|
|
cl.NextCommand = rule.NextCommand(ctx)
|
|
}
|
|
if rule.OutputPath != nil {
|
|
cl.OutputPath = rule.OutputPath(ctx)
|
|
}
|
|
if rule.TaskID != nil {
|
|
cl.TaskID = rule.TaskID(ctx)
|
|
}
|
|
return cl
|
|
}
|
|
}
|
|
|
|
// Default: nothing to do
|
|
return &Classification{
|
|
Timestamp: time.Now().UTC(),
|
|
Feature: ctx.Feature.Slug,
|
|
CurrentPhase: ctx.Feature.Phase,
|
|
RuleMatched: "nothing-to-do",
|
|
Action: ActionIdle,
|
|
Message: "No actionable work found",
|
|
}
|
|
}
|