feat(agentic): extract checklist tasks from issue plans

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 15:52:32 +00:00
parent 6eadd70aef
commit 405cd44ac3
2 changed files with 55 additions and 2 deletions

View file

@ -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 == "" {

View file

@ -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)