From 332a464cf660899c7c42e3c4472627e67e0f499d Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 20:08:41 +0000 Subject: [PATCH] feat(agentic): add task file ref aliases Co-Authored-By: Virgil --- pkg/agentic/commands_task.go | 12 ++++++--- pkg/agentic/commands_task_test.go | 34 ++++++++++++++++++++++++ pkg/agentic/plan.go | 28 ++++++++++++++++++-- pkg/agentic/task.go | 20 ++++++++++++++ pkg/agentic/task_test.go | 44 +++++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 6 deletions(-) diff --git a/pkg/agentic/commands_task.go b/pkg/agentic/commands_task.go index 3c6ce0c..54f3d4e 100644 --- a/pkg/agentic/commands_task.go +++ b/pkg/agentic/commands_task.go @@ -25,13 +25,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\"] [--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, "usage: core-agent task update --phase=1 --task=1 [--status=completed] [--notes=\"Done\"] [--file=pkg/agentic/task.go|--file-ref=pkg/agentic/task.go] [--line=42|--line-ref=42]") + core.Print(nil, " core-agent task create --phase=1 --title=\"Review RFC\" [--description=\"...\"] [--status=pending] [--notes=\"...\"] [--file=pkg/agentic/task.go|--file-ref=pkg/agentic/task.go] [--line=42|--line-ref=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\"] [--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, "usage: core-agent task update --phase=1 --task=1 [--status=completed] [--notes=\"Done\"] [--file=pkg/agentic/task.go|--file-ref=pkg/agentic/task.go] [--line=42|--line-ref=42]") + core.Print(nil, " core-agent task create --phase=1 --title=\"Review RFC\" [--description=\"...\"] [--status=pending] [--notes=\"...\"] [--file=pkg/agentic/task.go|--file-ref=pkg/agentic/task.go] [--line=42|--line-ref=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} } @@ -56,6 +56,8 @@ func (s *PrepSubsystem) cmdTaskCreate(options core.Options) core.Result { core.Option{Key: "notes", Value: optionStringValue(options, "notes")}, core.Option{Key: "file", Value: optionStringValue(options, "file")}, core.Option{Key: "line", Value: optionIntValue(options, "line")}, + core.Option{Key: "file_ref", Value: optionStringValue(options, "file_ref", "file-ref")}, + core.Option{Key: "line_ref", Value: optionIntValue(options, "line_ref", "line-ref")}, )) if !result.OK { err := commandResultError("agentic.cmdTaskCreate", result) @@ -99,6 +101,8 @@ func (s *PrepSubsystem) cmdTaskUpdate(options core.Options) core.Result { core.Option{Key: "notes", Value: optionStringValue(options, "notes")}, core.Option{Key: "file", Value: optionStringValue(options, "file")}, core.Option{Key: "line", Value: optionIntValue(options, "line")}, + core.Option{Key: "file_ref", Value: optionStringValue(options, "file_ref", "file-ref")}, + core.Option{Key: "line_ref", Value: optionIntValue(options, "line_ref", "line-ref")}, )) if !result.OK { err := commandResultError("agentic.cmdTaskUpdate", result) diff --git a/pkg/agentic/commands_task_test.go b/pkg/agentic/commands_task_test.go index 4a47c70..01c4f43 100644 --- a/pkg/agentic/commands_task_test.go +++ b/pkg/agentic/commands_task_test.go @@ -94,6 +94,40 @@ func TestCommands_TaskCommand_Good_Create(t *testing.T) { assert.Equal(t, 153, output.Task.Line) } +func TestCommands_TaskCommand_Good_CreateFileRefAliases(t *testing.T) { + dir := t.TempDir() + t.Setenv("CORE_WORKSPACE", dir) + + s := newTestPrep(t) + _, created, err := s.planCreate(context.Background(), nil, PlanCreateInput{ + Title: "Task Command File Ref", + Description: "Create task through CLI command with RFC aliases", + Phases: []Phase{ + {Name: "Setup"}, + }, + }) + require.NoError(t, err) + + plan, err := readPlan(PlansRoot(), created.ID) + require.NoError(t, err) + + r := s.cmdTaskCreate(core.NewOptions( + core.Option{Key: "plan_slug", Value: plan.Slug}, + core.Option{Key: "phase_order", Value: 1}, + core.Option{Key: "title", Value: "Patch code"}, + core.Option{Key: "file_ref", Value: "pkg/agentic/task.go"}, + core.Option{Key: "line_ref", Value: 153}, + )) + require.True(t, r.OK) + + output, ok := r.Value.(TaskCreateOutput) + require.True(t, ok) + assert.Equal(t, "pkg/agentic/task.go", output.Task.FileRef) + assert.Equal(t, 153, output.Task.LineRef) + 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) { s := newTestPrep(t) diff --git a/pkg/agentic/plan.go b/pkg/agentic/plan.go index d0e9b10..6e454d4 100644 --- a/pkg/agentic/plan.go +++ b/pkg/agentic/plan.go @@ -54,6 +54,8 @@ type PlanTask struct { Notes string `json:"notes,omitempty"` File string `json:"file,omitempty"` Line int `json:"line,omitempty"` + FileRef string `json:"file_ref,omitempty"` + LineRef int `json:"line_ref,omitempty"` } // checkpoint := agentic.PhaseCheckpoint{Note: "Build passes", CreatedAt: "2026-03-31T00:00:00Z"} @@ -706,14 +708,24 @@ func planTaskValue(value any) (PlanTask, bool) { if title == "" { title = stringValue(typed["name"]) } + file := stringValue(typed["file"]) + if file == "" { + file = stringValue(typed["file_ref"]) + } + line := intValue(typed["line"]) + if line == 0 { + line = intValue(typed["line_ref"]) + } return PlanTask{ ID: stringValue(typed["id"]), Title: title, Description: stringValue(typed["description"]), Status: stringValue(typed["status"]), Notes: stringValue(typed["notes"]), - File: stringValue(typed["file"]), - Line: intValue(typed["line"]), + File: file, + Line: line, + FileRef: file, + LineRef: line, }, title != "" case map[string]string: return planTaskValue(anyMapValue(typed)) @@ -936,6 +948,18 @@ func normalisePlanTask(task PlanTask, index int) PlanTask { if task.Title == "" { task.Title = task.Description } + if task.File == "" { + task.File = task.FileRef + } + if task.FileRef == "" { + task.FileRef = task.File + } + if task.Line == 0 { + task.Line = task.LineRef + } + if task.LineRef == 0 { + task.LineRef = task.Line + } return task } diff --git a/pkg/agentic/task.go b/pkg/agentic/task.go index 59f439a..68c1743 100644 --- a/pkg/agentic/task.go +++ b/pkg/agentic/task.go @@ -19,6 +19,8 @@ type TaskUpdateInput struct { Notes string `json:"notes,omitempty"` File string `json:"file,omitempty"` Line int `json:"line,omitempty"` + FileRef string `json:"file_ref,omitempty"` + LineRef int `json:"line_ref,omitempty"` } // input := agentic.TaskToggleInput{PlanSlug: "my-plan-abc123", PhaseOrder: 1, TaskIdentifier: 1} @@ -38,6 +40,8 @@ type TaskCreateInput struct { Notes string `json:"notes,omitempty"` File string `json:"file,omitempty"` Line int `json:"line,omitempty"` + FileRef string `json:"file_ref,omitempty"` + LineRef int `json:"line_ref,omitempty"` } // out := agentic.TaskOutput{Success: true, Task: agentic.PlanTask{ID: "1", Title: "Review imports", File: "pkg/agentic/task.go", Line: 128}} @@ -69,6 +73,8 @@ func (s *PrepSubsystem) handleTaskCreate(ctx context.Context, options core.Optio Notes: optionStringValue(options, "notes"), File: optionStringValue(options, "file"), Line: optionIntValue(options, "line"), + FileRef: optionStringValue(options, "file_ref", "file-ref"), + LineRef: optionIntValue(options, "line_ref", "line-ref"), }) if err != nil { return core.Result{Value: err, OK: false} @@ -86,6 +92,8 @@ func (s *PrepSubsystem) handleTaskUpdate(ctx context.Context, options core.Optio Notes: optionStringValue(options, "notes"), File: optionStringValue(options, "file"), Line: optionIntValue(options, "line"), + FileRef: optionStringValue(options, "file_ref", "file-ref"), + LineRef: optionIntValue(options, "line_ref", "line-ref"), }) if err != nil { return core.Result{Value: err, OK: false} @@ -144,9 +152,19 @@ func (s *PrepSubsystem) taskUpdate(_ context.Context, _ *mcp.CallToolRequest, in } if file := core.Trim(input.File); file != "" { plan.Phases[phaseIndex].Tasks[taskIndex].File = file + plan.Phases[phaseIndex].Tasks[taskIndex].FileRef = file + } + if fileRef := core.Trim(input.FileRef); fileRef != "" { + plan.Phases[phaseIndex].Tasks[taskIndex].FileRef = fileRef + plan.Phases[phaseIndex].Tasks[taskIndex].File = fileRef } if input.Line > 0 { plan.Phases[phaseIndex].Tasks[taskIndex].Line = input.Line + plan.Phases[phaseIndex].Tasks[taskIndex].LineRef = input.Line + } + if input.LineRef > 0 { + plan.Phases[phaseIndex].Tasks[taskIndex].LineRef = input.LineRef + plan.Phases[phaseIndex].Tasks[taskIndex].Line = input.LineRef } plan.UpdatedAt = time.Now() @@ -189,6 +207,8 @@ func (s *PrepSubsystem) taskCreate(_ context.Context, _ *mcp.CallToolRequest, in Notes: core.Trim(input.Notes), File: core.Trim(input.File), Line: input.Line, + FileRef: core.Trim(input.FileRef), + LineRef: input.LineRef, } newTask = normalisePlanTask(newTask, nextIndex) diff --git a/pkg/agentic/task_test.go b/pkg/agentic/task_test.go index 5f5efa6..909fe17 100644 --- a/pkg/agentic/task_test.go +++ b/pkg/agentic/task_test.go @@ -163,3 +163,47 @@ func TestTask_TaskCreate_Ugly_CriteriaFallback(t *testing.T) { assert.Empty(t, updated.Phases[0].Tasks[1].File) assert.Zero(t, updated.Phases[0].Tasks[1].Line) } + +func TestTask_TaskFileRefAliases_Good(t *testing.T) { + dir := t.TempDir() + t.Setenv("CORE_WORKSPACE", dir) + + s := newTestPrep(t) + _, created, err := s.planCreate(context.Background(), nil, PlanCreateInput{ + Title: "Task File Ref Aliases", + Description: "Accept RFC task file reference names", + Phases: []Phase{ + {Name: "Setup", Tasks: []PlanTask{{ID: "1", Title: "Review RFC"}}}, + }, + }) + require.NoError(t, err) + + plan, err := readPlan(PlansRoot(), created.ID) + require.NoError(t, err) + + _, createdOutput, err := s.taskCreate(context.Background(), nil, TaskCreateInput{ + PlanSlug: plan.Slug, + PhaseOrder: 1, + Title: "Patch code", + FileRef: "pkg/agentic/task.go", + LineRef: 153, + }) + require.NoError(t, err) + assert.Equal(t, "pkg/agentic/task.go", createdOutput.Task.FileRef) + assert.Equal(t, 153, createdOutput.Task.LineRef) + assert.Equal(t, "pkg/agentic/task.go", createdOutput.Task.File) + assert.Equal(t, 153, createdOutput.Task.Line) + + _, updatedOutput, err := s.taskUpdate(context.Background(), nil, TaskUpdateInput{ + PlanSlug: plan.Slug, + PhaseOrder: 1, + TaskIdentifier: createdOutput.Task.ID, + FileRef: "pkg/agentic/task.go", + LineRef: 171, + }) + require.NoError(t, err) + assert.Equal(t, "pkg/agentic/task.go", updatedOutput.Task.FileRef) + assert.Equal(t, 171, updatedOutput.Task.LineRef) + assert.Equal(t, "pkg/agentic/task.go", updatedOutput.Task.File) + assert.Equal(t, 171, updatedOutput.Task.Line) +}