From 60906f828667eefe234bb17c0953dec8d0fe2acc Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 03:16:27 +0000 Subject: [PATCH] feat(agentic): support completion_criteria phase alias Co-Authored-By: Virgil --- pkg/agentic/plan.go | 93 ++++++++++++++++++++++++++-------- pkg/agentic/plan_compat.go | 7 +-- pkg/agentic/plan_logic_test.go | 20 ++++++++ 3 files changed, 97 insertions(+), 23 deletions(-) diff --git a/pkg/agentic/plan.go b/pkg/agentic/plan.go index 78812ea..65ecbdb 100644 --- a/pkg/agentic/plan.go +++ b/pkg/agentic/plan.go @@ -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) } diff --git a/pkg/agentic/plan_compat.go b/pkg/agentic/plan_compat.go index e162573..da1d030 100644 --- a/pkg/agentic/plan_compat.go +++ b/pkg/agentic/plan_compat.go @@ -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 diff --git a/pkg/agentic/plan_logic_test.go b/pkg/agentic/plan_logic_test.go index f091e7c..b4efb6c 100644 --- a/pkg/agentic/plan_logic_test.go +++ b/pkg/agentic/plan_logic_test.go @@ -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) +}