diff --git a/pkg/agentic/commands_task.go b/pkg/agentic/commands_task.go index 2431c2b..245a2ea 100644 --- a/pkg/agentic/commands_task.go +++ b/pkg/agentic/commands_task.go @@ -24,13 +24,13 @@ func (s *PrepSubsystem) cmdTask(options core.Options) core.Result { case "update": return s.cmdTaskUpdate(options) case "": - core.Print(nil, "usage: core-agent task update --phase=1 --task=1 [--status=completed] [--notes=\"Done\"]") - core.Print(nil, " core-agent task create --phase=1 --title=\"Review RFC\" [--description=\"...\"] [--status=pending] [--notes=\"...\"]") + core.Print(nil, "usage: core-agent task update --phase=1 --task=1 [--status=completed] [--notes=\"Done\"] [--file=pkg/agentic/task.go] [--line=42]") + core.Print(nil, " core-agent task create --phase=1 --title=\"Review RFC\" [--description=\"...\"] [--status=pending] [--notes=\"...\"] [--file=pkg/agentic/task.go] [--line=42]") core.Print(nil, " core-agent task toggle --phase=1 --task=1") return core.Result{OK: true} default: - core.Print(nil, "usage: core-agent task update --phase=1 --task=1 [--status=completed] [--notes=\"Done\"]") - core.Print(nil, " core-agent task create --phase=1 --title=\"Review RFC\" [--description=\"...\"] [--status=pending] [--notes=\"...\"]") + core.Print(nil, "usage: core-agent task update --phase=1 --task=1 [--status=completed] [--notes=\"Done\"] [--file=pkg/agentic/task.go] [--line=42]") + core.Print(nil, " core-agent task create --phase=1 --title=\"Review RFC\" [--description=\"...\"] [--status=pending] [--notes=\"...\"] [--file=pkg/agentic/task.go] [--line=42]") core.Print(nil, " core-agent task toggle --phase=1 --task=1") return core.Result{Value: core.E("agentic.cmdTask", core.Concat("unknown task command: ", action), nil), OK: false} } @@ -42,7 +42,7 @@ func (s *PrepSubsystem) cmdTaskCreate(options core.Options) core.Result { title := optionStringValue(options, "title", "task") if planSlug == "" || phaseOrder == 0 || title == "" { - core.Print(nil, "usage: core-agent task create --phase=1 --title=\"Review RFC\" [--description=\"...\"] [--status=pending] [--notes=\"...\"]") + core.Print(nil, "usage: core-agent task create --phase=1 --title=\"Review RFC\" [--description=\"...\"] [--status=pending] [--notes=\"...\"] [--file=pkg/agentic/task.go] [--line=42]") return core.Result{Value: core.E("agentic.cmdTaskCreate", "plan_slug, phase_order, and title are required", nil), OK: false} } @@ -53,6 +53,8 @@ func (s *PrepSubsystem) cmdTaskCreate(options core.Options) core.Result { core.Option{Key: "description", Value: optionStringValue(options, "description")}, core.Option{Key: "status", Value: optionStringValue(options, "status")}, core.Option{Key: "notes", Value: optionStringValue(options, "notes")}, + core.Option{Key: "file", Value: optionStringValue(options, "file")}, + core.Option{Key: "line", Value: optionIntValue(options, "line")}, )) if !result.OK { err := commandResultError("agentic.cmdTaskCreate", result) @@ -69,6 +71,12 @@ func (s *PrepSubsystem) cmdTaskCreate(options core.Options) core.Result { core.Print(nil, "task: %s", output.Task.Title) core.Print(nil, "status: %s", output.Task.Status) + if output.Task.File != "" { + core.Print(nil, "file: %s", output.Task.File) + } + if output.Task.Line > 0 { + core.Print(nil, "line: %d", output.Task.Line) + } return core.Result{Value: output, OK: true} } @@ -78,7 +86,7 @@ func (s *PrepSubsystem) cmdTaskUpdate(options core.Options) core.Result { taskIdentifier := optionAnyValue(options, "task_identifier", "task") if planSlug == "" || phaseOrder == 0 || taskIdentifierValue(taskIdentifier) == "" { - core.Print(nil, "usage: core-agent task update --phase=1 --task=1 [--status=completed] [--notes=\"Done\"]") + core.Print(nil, "usage: core-agent task update --phase=1 --task=1 [--status=completed] [--notes=\"Done\"] [--file=pkg/agentic/task.go] [--line=42]") return core.Result{Value: core.E("agentic.cmdTaskUpdate", "plan_slug, phase_order, and task_identifier are required", nil), OK: false} } @@ -88,6 +96,8 @@ func (s *PrepSubsystem) cmdTaskUpdate(options core.Options) core.Result { core.Option{Key: "task_identifier", Value: taskIdentifier}, core.Option{Key: "status", Value: optionStringValue(options, "status")}, core.Option{Key: "notes", Value: optionStringValue(options, "notes")}, + core.Option{Key: "file", Value: optionStringValue(options, "file")}, + core.Option{Key: "line", Value: optionIntValue(options, "line")}, )) if !result.OK { err := commandResultError("agentic.cmdTaskUpdate", result) @@ -107,6 +117,12 @@ func (s *PrepSubsystem) cmdTaskUpdate(options core.Options) core.Result { if output.Task.Notes != "" { core.Print(nil, "notes: %s", output.Task.Notes) } + if output.Task.File != "" { + core.Print(nil, "file: %s", output.Task.File) + } + if output.Task.Line > 0 { + core.Print(nil, "line: %d", output.Task.Line) + } return core.Result{Value: output, OK: true} } diff --git a/pkg/agentic/commands_task_test.go b/pkg/agentic/commands_task_test.go index e684b0b..9a4c5ec 100644 --- a/pkg/agentic/commands_task_test.go +++ b/pkg/agentic/commands_task_test.go @@ -34,6 +34,8 @@ func TestCommands_TaskCommand_Good_Update(t *testing.T) { core.Option{Key: "task_identifier", Value: "1"}, core.Option{Key: "status", Value: "completed"}, core.Option{Key: "notes", Value: "Done"}, + core.Option{Key: "file", Value: "pkg/agentic/task.go"}, + core.Option{Key: "line", Value: 128}, )) require.True(t, r.OK) @@ -41,6 +43,8 @@ func TestCommands_TaskCommand_Good_Update(t *testing.T) { require.True(t, ok) assert.Equal(t, "completed", output.Task.Status) assert.Equal(t, "Done", output.Task.Notes) + assert.Equal(t, "pkg/agentic/task.go", output.Task.File) + assert.Equal(t, 128, output.Task.Line) } func TestCommands_TaskCommand_Good_Create(t *testing.T) { @@ -67,6 +71,8 @@ func TestCommands_TaskCommand_Good_Create(t *testing.T) { core.Option{Key: "description", Value: "Update the implementation"}, core.Option{Key: "status", Value: "pending"}, core.Option{Key: "notes", Value: "Do this first"}, + core.Option{Key: "file", Value: "pkg/agentic/task.go"}, + core.Option{Key: "line", Value: 153}, )) require.True(t, r.OK) @@ -75,6 +81,8 @@ func TestCommands_TaskCommand_Good_Create(t *testing.T) { assert.Equal(t, "Patch code", output.Task.Title) assert.Equal(t, "pending", output.Task.Status) assert.Equal(t, "Do this first", output.Task.Notes) + assert.Equal(t, "pkg/agentic/task.go", output.Task.File) + assert.Equal(t, 153, output.Task.Line) } func TestCommands_TaskCommand_Bad_MissingRequiredFields(t *testing.T) { diff --git a/pkg/agentic/plan.go b/pkg/agentic/plan.go index 1455102..931406d 100644 --- a/pkg/agentic/plan.go +++ b/pkg/agentic/plan.go @@ -43,13 +43,15 @@ type Phase struct { Notes string `json:"notes,omitempty"` } -// task := agentic.PlanTask{ID: "1", Title: "Review imports", Status: "pending"} +// task := agentic.PlanTask{ID: "1", Title: "Review imports", Status: "pending", File: "pkg/agentic/plan.go", Line: 46} type PlanTask struct { ID string `json:"id,omitempty"` Title string `json:"title"` Description string `json:"description,omitempty"` Status string `json:"status,omitempty"` Notes string `json:"notes,omitempty"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` } // checkpoint := agentic.PhaseCheckpoint{Note: "Build passes", CreatedAt: "2026-03-31T00:00:00Z"} @@ -658,6 +660,8 @@ func planTaskValue(value any) (PlanTask, bool) { Description: stringValue(typed["description"]), Status: stringValue(typed["status"]), Notes: stringValue(typed["notes"]), + File: stringValue(typed["file"]), + Line: intValue(typed["line"]), }, title != "" case map[string]string: return planTaskValue(anyMapValue(typed)) diff --git a/pkg/agentic/plan_test.go b/pkg/agentic/plan_test.go index 941fb9f..e1ed6da 100644 --- a/pkg/agentic/plan_test.go +++ b/pkg/agentic/plan_test.go @@ -70,7 +70,7 @@ func TestPlan_ReadPlan_Good(t *testing.T) { Org: "core", Objective: "Verify plan reading works", Phases: []Phase{ - {Number: 1, Name: "Setup", Status: "done"}, + {Number: 1, Name: "Setup", Status: "done", Tasks: []PlanTask{{ID: "1", Title: "Review imports", File: "pkg/agentic/plan.go", Line: 46}}}, {Number: 2, Name: "Implement", Status: "pending"}, }, Notes: "Some notes", @@ -92,6 +92,10 @@ func TestPlan_ReadPlan_Good(t *testing.T) { assert.Len(t, read.Phases, 2) assert.Equal(t, "Setup", read.Phases[0].Name) assert.Equal(t, "done", read.Phases[0].Status) + require.Len(t, read.Phases[0].Tasks, 1) + assert.Equal(t, "Review imports", read.Phases[0].Tasks[0].Title) + assert.Equal(t, "pkg/agentic/plan.go", read.Phases[0].Tasks[0].File) + assert.Equal(t, 46, read.Phases[0].Tasks[0].Line) assert.Equal(t, "Implement", read.Phases[1].Name) assert.Equal(t, "pending", read.Phases[1].Status) assert.Equal(t, "Some notes", read.Notes) @@ -123,7 +127,7 @@ func TestPlan_WriteRead_Good_Roundtrip(t *testing.T) { Org: "core", Objective: "Ensure write-read roundtrip works", Phases: []Phase{ - {Number: 1, Name: "Phase One", Status: "done", Criteria: []string{"tests pass", "coverage > 80%"}, Tests: 5}, + {Number: 1, Name: "Phase One", Status: "done", Criteria: []string{"tests pass", "coverage > 80%"}, Tests: 5, Tasks: []PlanTask{{ID: "1", Title: "tests pass", File: "pkg/agentic/plan_test.go", Line: 100}}}, {Number: 2, Name: "Phase Two", Status: "in_progress", Notes: "Working on it"}, {Number: 3, Name: "Phase Three", Status: "pending"}, }, @@ -142,6 +146,9 @@ func TestPlan_WriteRead_Good_Roundtrip(t *testing.T) { assert.Len(t, read.Phases, 3) assert.Equal(t, []string{"tests pass", "coverage > 80%"}, read.Phases[0].Criteria) assert.Equal(t, 5, read.Phases[0].Tests) + require.Len(t, read.Phases[0].Tasks, 1) + assert.Equal(t, "pkg/agentic/plan_test.go", read.Phases[0].Tasks[0].File) + assert.Equal(t, 100, read.Phases[0].Tasks[0].Line) assert.Equal(t, "Working on it", read.Phases[1].Notes) } diff --git a/pkg/agentic/task.go b/pkg/agentic/task.go index 0f786b6..59f439a 100644 --- a/pkg/agentic/task.go +++ b/pkg/agentic/task.go @@ -10,13 +10,15 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) -// input := agentic.TaskUpdateInput{PlanSlug: "my-plan-abc123", PhaseOrder: 1, TaskIdentifier: "1"} +// input := agentic.TaskUpdateInput{PlanSlug: "my-plan-abc123", PhaseOrder: 1, TaskIdentifier: "1", File: "pkg/agentic/task.go", Line: 128} type TaskUpdateInput struct { PlanSlug string `json:"plan_slug"` PhaseOrder int `json:"phase_order"` TaskIdentifier any `json:"task_identifier"` Status string `json:"status,omitempty"` Notes string `json:"notes,omitempty"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` } // input := agentic.TaskToggleInput{PlanSlug: "my-plan-abc123", PhaseOrder: 1, TaskIdentifier: 1} @@ -26,7 +28,7 @@ type TaskToggleInput struct { TaskIdentifier any `json:"task_identifier"` } -// input := agentic.TaskCreateInput{PlanSlug: "my-plan-abc123", PhaseOrder: 1, Title: "Review imports"} +// input := agentic.TaskCreateInput{PlanSlug: "my-plan-abc123", PhaseOrder: 1, Title: "Review imports", File: "pkg/agentic/task.go", Line: 153} type TaskCreateInput struct { PlanSlug string `json:"plan_slug"` PhaseOrder int `json:"phase_order"` @@ -34,15 +36,17 @@ type TaskCreateInput struct { Description string `json:"description,omitempty"` Status string `json:"status,omitempty"` Notes string `json:"notes,omitempty"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` } -// out := agentic.TaskOutput{Success: true, Task: agentic.PlanTask{ID: "1", Title: "Review imports"}} +// out := agentic.TaskOutput{Success: true, Task: agentic.PlanTask{ID: "1", Title: "Review imports", File: "pkg/agentic/task.go", Line: 128}} type TaskOutput struct { Success bool `json:"success"` Task PlanTask `json:"task"` } -// out := agentic.TaskCreateOutput{Success: true, Task: agentic.PlanTask{ID: "3", Title: "Review imports"}} +// out := agentic.TaskCreateOutput{Success: true, Task: agentic.PlanTask{ID: "3", Title: "Review imports", File: "pkg/agentic/task.go", Line: 153}} type TaskCreateOutput struct { Success bool `json:"success"` Task PlanTask `json:"task"` @@ -63,6 +67,8 @@ func (s *PrepSubsystem) handleTaskCreate(ctx context.Context, options core.Optio Description: optionStringValue(options, "description"), Status: optionStringValue(options, "status"), Notes: optionStringValue(options, "notes"), + File: optionStringValue(options, "file"), + Line: optionIntValue(options, "line"), }) if err != nil { return core.Result{Value: err, OK: false} @@ -78,6 +84,8 @@ func (s *PrepSubsystem) handleTaskUpdate(ctx context.Context, options core.Optio TaskIdentifier: optionAnyValue(options, "task_identifier", "task"), Status: optionStringValue(options, "status"), Notes: optionStringValue(options, "notes"), + File: optionStringValue(options, "file"), + Line: optionIntValue(options, "line"), }) if err != nil { return core.Result{Value: err, OK: false} @@ -134,6 +142,12 @@ func (s *PrepSubsystem) taskUpdate(_ context.Context, _ *mcp.CallToolRequest, in if notes := core.Trim(input.Notes); notes != "" { plan.Phases[phaseIndex].Tasks[taskIndex].Notes = notes } + if file := core.Trim(input.File); file != "" { + plan.Phases[phaseIndex].Tasks[taskIndex].File = file + } + if input.Line > 0 { + plan.Phases[phaseIndex].Tasks[taskIndex].Line = input.Line + } plan.UpdatedAt = time.Now() if result := writePlanResult(PlansRoot(), plan); !result.OK { @@ -173,6 +187,8 @@ func (s *PrepSubsystem) taskCreate(_ context.Context, _ *mcp.CallToolRequest, in Description: core.Trim(input.Description), Status: input.Status, Notes: core.Trim(input.Notes), + File: core.Trim(input.File), + Line: input.Line, } newTask = normalisePlanTask(newTask, nextIndex) diff --git a/pkg/agentic/task_test.go b/pkg/agentic/task_test.go index 6f60a6c..5f5efa6 100644 --- a/pkg/agentic/task_test.go +++ b/pkg/agentic/task_test.go @@ -33,11 +33,15 @@ func TestTask_TaskUpdate_Good(t *testing.T) { TaskIdentifier: "1", Status: "completed", Notes: "Done", + File: "pkg/agentic/task.go", + Line: 128, }) require.NoError(t, err) assert.True(t, output.Success) assert.Equal(t, "completed", output.Task.Status) assert.Equal(t, "Done", output.Task.Notes) + assert.Equal(t, "pkg/agentic/task.go", output.Task.File) + assert.Equal(t, 128, output.Task.Line) } func TestTask_TaskCreate_Good(t *testing.T) { @@ -64,12 +68,16 @@ func TestTask_TaskCreate_Good(t *testing.T) { Description: "Update the implementation", Status: "pending", Notes: "Do this first", + File: "pkg/agentic/task.go", + Line: 153, }) require.NoError(t, err) assert.True(t, output.Success) assert.Equal(t, "Patch code", output.Task.Title) assert.Equal(t, "pending", output.Task.Status) assert.Equal(t, "Do this first", output.Task.Notes) + assert.Equal(t, "pkg/agentic/task.go", output.Task.File) + assert.Equal(t, 153, output.Task.Line) } func TestTask_TaskCreate_Bad_MissingTitle(t *testing.T) { @@ -152,4 +160,6 @@ func TestTask_TaskCreate_Ugly_CriteriaFallback(t *testing.T) { require.Len(t, updated.Phases[0].Tasks, 2) assert.Equal(t, "Review RFC", updated.Phases[0].Tasks[0].Title) assert.Equal(t, "Patch code", updated.Phases[0].Tasks[1].Title) + assert.Empty(t, updated.Phases[0].Tasks[1].File) + assert.Zero(t, updated.Phases[0].Tasks[1].Line) } diff --git a/pkg/agentic/template.go b/pkg/agentic/template.go index f09bfb0..6f3301d 100644 --- a/pkg/agentic/template.go +++ b/pkg/agentic/template.go @@ -432,6 +432,8 @@ func templatePlanTask(item any, number int) PlanTask { Description: stringValue(value["description"]), Status: status, Notes: stringValue(value["notes"]), + File: stringValue(value["file"]), + Line: intValue(value["line"]), } } return PlanTask{} diff --git a/pkg/agentic/template_test.go b/pkg/agentic/template_test.go index ac9b5db..9abd71f 100644 --- a/pkg/agentic/template_test.go +++ b/pkg/agentic/template_test.go @@ -70,6 +70,20 @@ func TestTemplate_HandleTemplatePreview_Ugly_MissingVariables(t *testing.T) { assert.Contains(t, output.Preview, "{{ feature_name }}") } +func TestTemplate_TemplatePlanTask_Good_FileLineReference(t *testing.T) { + task := templatePlanTask(map[string]any{ + "title": "Review RFC", + "status": "pending", + "file": "pkg/agentic/template.go", + "line": 411, + }, 1) + + assert.Equal(t, "Review RFC", task.Title) + assert.Equal(t, "pending", task.Status) + assert.Equal(t, "pkg/agentic/template.go", task.File) + assert.Equal(t, 411, task.Line) +} + func TestTemplate_HandleTemplateCreatePlan_Good(t *testing.T) { subsystem := testPrepWithPlatformServer(t, nil, "") result := subsystem.handleTemplateCreatePlan(context.Background(), core.NewOptions(