From 405cd44ac31a43ac889e4d76b5c1a0c713155e89 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 15:52:32 +0000 Subject: [PATCH] feat(agentic): extract checklist tasks from issue plans Co-Authored-By: Virgil --- pkg/agentic/plan_from_issue.go | 26 ++++++++++++++++++++++++ pkg/agentic/plan_from_issue_test.go | 31 +++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/pkg/agentic/plan_from_issue.go b/pkg/agentic/plan_from_issue.go index 03edfc7..82f884c 100644 --- a/pkg/agentic/plan_from_issue.go +++ b/pkg/agentic/plan_from_issue.go @@ -66,6 +66,7 @@ func (s *PrepSubsystem) planFromIssue(ctx context.Context, _ *mcp.CallToolReques if objective == "" { objective = issueOutput.Issue.Title } + tasks := planFromIssueTasks(description) phaseTitle := core.Concat("Resolve issue: ", issueOutput.Issue.Title) phaseCriteria := []string{"Issue is closed", "QA passes"} @@ -93,6 +94,7 @@ func (s *PrepSubsystem) planFromIssue(ctx context.Context, _ *mcp.CallToolReques Name: phaseTitle, Description: description, Criteria: phaseCriteria, + Tasks: tasks, }, }, Notes: core.Concat("Created from issue ", identifier), @@ -122,6 +124,30 @@ func (s *PrepSubsystem) planFromIssue(ctx context.Context, _ *mcp.CallToolReques }, nil } +func planFromIssueTasks(body string) []PlanTask { + var tasks []PlanTask + for _, line := range core.Split(body, "\n") { + trimmed := core.Trim(line) + if title, ok := planFromIssueTaskTitle(trimmed); ok { + tasks = append(tasks, PlanTask{Title: title}) + } + } + return tasks +} + +func planFromIssueTaskTitle(line string) (string, bool) { + for _, prefix := range []string{"- [ ] ", "- [x] ", "- [X] ", "* [ ] ", "* [x] ", "* [X] "} { + if core.HasPrefix(line, prefix) { + title := core.Trim(core.TrimPrefix(line, prefix)) + if title != "" { + return title, true + } + return "", false + } + } + return "", false +} + // plan-from-issue fixtures should use issue slugs like `fix-auth`. func planFromIssueSlug(slug string) string { if slug == "" { diff --git a/pkg/agentic/plan_from_issue_test.go b/pkg/agentic/plan_from_issue_test.go index 5711636..97bdb63 100644 --- a/pkg/agentic/plan_from_issue_test.go +++ b/pkg/agentic/plan_from_issue_test.go @@ -21,7 +21,7 @@ func TestPlanFromIssue_PlanFromIssue_Good(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "/v1/issues/fix-auth", r.URL.Path) require.Equal(t, "Bearer secret-token", r.Header.Get("Authorization")) - _, _ = w.Write([]byte(`{"data":{"issue":{"id":17,"slug":"fix-auth","title":"Fix auth middleware","description":"Stop anonymous access to the admin route","type":"bug","status":"open","priority":"high","labels":["security","backend"],"metadata":{"source":"forge"}}}}`)) + _, _ = w.Write([]byte(`{"data":{"issue":{"id":17,"slug":"fix-auth","title":"Fix auth middleware","description":"Stop anonymous access to the admin route\n\n## Checklist\n- [ ] Keep CLI output stable","type":"bug","status":"open","priority":"high","labels":["security","backend"],"metadata":{"source":"forge"}}}}`)) })) defer server.Close() @@ -38,7 +38,7 @@ func TestPlanFromIssue_PlanFromIssue_Good(t *testing.T) { assert.True(t, output.Success) assert.Equal(t, "Fix auth middleware", output.Issue.Title) assert.Equal(t, "issue-fix-auth", output.Plan.Slug) - assert.Equal(t, "Stop anonymous access to the admin route", output.Plan.Objective) + assert.Equal(t, "Stop anonymous access to the admin route\n\n## Checklist\n- [ ] Keep CLI output stable", output.Plan.Objective) assert.NotEmpty(t, output.Path) assert.True(t, fs.Exists(output.Path)) @@ -46,6 +46,9 @@ func TestPlanFromIssue_PlanFromIssue_Good(t *testing.T) { require.NoError(t, err) assert.Equal(t, output.Plan.Slug, plan.Slug) assert.Equal(t, output.Issue.Slug, plan.Context["source_issue_slug"]) + require.Len(t, plan.Phases, 1) + require.Len(t, plan.Phases[0].Tasks, 1) + assert.Equal(t, "Keep CLI output stable", plan.Phases[0].Tasks[0].Title) } func TestPlanFromIssue_PlanFromIssue_Bad_MissingIdentifier(t *testing.T) { @@ -83,6 +86,30 @@ func TestPlanFromIssue_PlanFromIssue_Ugly_FallsBackToTitleObjective(t *testing.T assert.Equal(t, "Refine logging", output.Plan.Title) } +func TestPlanFromIssue_PlanFromIssue_Good_NoChecklistKeepsTasksEmpty(t *testing.T) { + dir := t.TempDir() + t.Setenv("CORE_WORKSPACE", dir) + t.Setenv("CORE_AGENT_API_KEY", "secret-token") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"data":{"issue":{"id":31,"slug":"investigate-latency","title":"Investigate latency","description":"The dashboard is slow. Please investigate."}}}`)) + })) + defer server.Close() + + s := newTestPrep(t) + s.brainURL = server.URL + + result := s.handlePlanFromIssue(context.Background(), core.NewOptions( + core.Option{Key: "slug", Value: "investigate-latency"}, + )) + require.True(t, result.OK) + + output, ok := result.Value.(PlanFromIssueOutput) + require.True(t, ok) + require.Len(t, output.Plan.Phases, 1) + assert.Empty(t, output.Plan.Phases[0].Tasks) +} + func TestPlanFromIssue_CmdPlanFromIssue_Good(t *testing.T) { dir := t.TempDir() t.Setenv("CORE_WORKSPACE", dir)