feat(agentic): support completion_criteria phase alias

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 03:16:27 +00:00
parent ea469bb2ec
commit 60906f8286
3 changed files with 97 additions and 23 deletions

View file

@ -36,16 +36,17 @@ type AgentPlan = Plan
// phase := agentic.Phase{Number: 1, Name: "Migrate strings", Status: "in_progress"}
type Phase struct {
Number int `json:"number"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Status string `json:"status"`
Criteria []string `json:"criteria,omitempty"`
Dependencies []string `json:"dependencies,omitempty"`
Tasks []PlanTask `json:"tasks,omitempty"`
Checkpoints []PhaseCheckpoint `json:"checkpoints,omitempty"`
Tests int `json:"tests,omitempty"`
Notes string `json:"notes,omitempty"`
Number int `json:"number"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Status string `json:"status"`
Criteria []string `json:"criteria,omitempty"`
CompletionCriteria []string `json:"completion_criteria,omitempty"`
Dependencies []string `json:"dependencies,omitempty"`
Tasks []PlanTask `json:"tasks,omitempty"`
Checkpoints []PhaseCheckpoint `json:"checkpoints,omitempty"`
Tests int `json:"tests,omitempty"`
Notes string `json:"notes,omitempty"`
}
// task := agentic.PlanTask{ID: "1", Title: "Review imports", Status: "pending", File: "pkg/agentic/plan.go", Line: 46}
@ -589,17 +590,26 @@ func phaseValue(value any) (Phase, bool) {
case Phase:
return typed, true
case map[string]any:
criteria := stringSliceValue(typed["criteria"])
completionCriteria := phaseCriteriaValue(typed["completion_criteria"], typed["completion-criteria"])
if len(criteria) == 0 {
criteria = completionCriteria
}
if len(completionCriteria) == 0 {
completionCriteria = criteria
}
return Phase{
Number: intValue(typed["number"]),
Name: stringValue(typed["name"]),
Description: stringValue(typed["description"]),
Status: stringValue(typed["status"]),
Criteria: stringSliceValue(typed["criteria"]),
Dependencies: phaseDependenciesValue(typed["dependencies"]),
Tasks: planTaskSliceValue(typed["tasks"]),
Checkpoints: phaseCheckpointSliceValue(typed["checkpoints"]),
Tests: intValue(typed["tests"]),
Notes: stringValue(typed["notes"]),
Number: intValue(typed["number"]),
Name: stringValue(typed["name"]),
Description: stringValue(typed["description"]),
Status: stringValue(typed["status"]),
Criteria: criteria,
CompletionCriteria: completionCriteria,
Dependencies: phaseDependenciesValue(typed["dependencies"]),
Tasks: planTaskSliceValue(typed["tasks"]),
Checkpoints: phaseCheckpointSliceValue(typed["checkpoints"]),
Tests: intValue(typed["tests"]),
Notes: stringValue(typed["notes"]),
}, true
case map[string]string:
return phaseValue(anyMapValue(typed))
@ -652,6 +662,16 @@ func phaseDependenciesValue(value any) []string {
return nil
}
func phaseCriteriaValue(values ...any) []string {
for _, value := range values {
criteria := stringSliceValue(value)
if len(criteria) > 0 {
return criteria
}
}
return nil
}
func planTaskSliceValue(value any) []PlanTask {
switch typed := value.(type) {
case []PlanTask:
@ -821,6 +841,36 @@ func phaseCheckpointValue(value any) (PhaseCheckpoint, bool) {
return PhaseCheckpoint{}, false
}
func phaseCriteriaList(phase Phase) []string {
criteria := cleanStrings(phase.Criteria)
completionCriteria := cleanStrings(phase.CompletionCriteria)
if len(criteria) == 0 {
return completionCriteria
}
if len(completionCriteria) == 0 {
return criteria
}
merged := make([]string, 0, len(criteria)+len(completionCriteria))
seen := map[string]bool{}
for _, value := range criteria {
if seen[value] {
continue
}
seen[value] = true
merged = append(merged, value)
}
for _, value := range completionCriteria {
if seen[value] {
continue
}
seen[value] = true
merged = append(merged, value)
}
return merged
}
// result := readPlanResult(PlansRoot(), "plan-id")
// if result.OK { plan := result.Value.(*Plan) }
func readPlanResult(dir, id string) core.Result {
@ -934,6 +984,9 @@ func normalisePhase(phase Phase, number int) Phase {
if phase.Status == "" {
phase.Status = "pending"
}
criteria := phaseCriteriaList(phase)
phase.Criteria = criteria
phase.CompletionCriteria = criteria
for i := range phase.Tasks {
phase.Tasks[i] = normalisePlanTask(phase.Tasks[i], i+1)
}

View file

@ -303,12 +303,13 @@ func phaseTaskList(phase Phase) []PlanTask {
return tasks
}
if len(phase.Criteria) == 0 {
criteria := phaseCriteriaList(phase)
if len(criteria) == 0 {
return nil
}
tasks := make([]PlanTask, 0, len(phase.Criteria))
for index, criterion := range cleanStrings(phase.Criteria) {
tasks := make([]PlanTask, 0, len(criteria))
for index, criterion := range criteria {
tasks = append(tasks, normalisePlanTask(PlanTask{Title: criterion}, index+1))
}
return tasks

View file

@ -173,3 +173,23 @@ func TestPlan_ReadPlan_Ugly_EmptyFileLogic(t *testing.T) {
_, err := readPlan(dir, "empty")
assert.Error(t, err)
}
func TestPlan_PhaseValue_Good_CompletionCriteriaAlias(t *testing.T) {
phase, ok := phaseValue(map[string]any{
"name": "Setup",
"completion_criteria": []any{"repo cloned", "dependencies installed"},
})
require.True(t, ok)
assert.Equal(t, []string{"repo cloned", "dependencies installed"}, phase.Criteria)
assert.Equal(t, []string{"repo cloned", "dependencies installed"}, phase.CompletionCriteria)
normalised := normalisePhase(phase, 1)
assert.Equal(t, []string{"repo cloned", "dependencies installed"}, normalised.Criteria)
assert.Equal(t, []string{"repo cloned", "dependencies installed"}, normalised.CompletionCriteria)
tasks := phaseTaskList(normalised)
require.Len(t, tasks, 2)
assert.Equal(t, "repo cloned", tasks[0].Title)
assert.Equal(t, "dependencies installed", tasks[1].Title)
}