From 9f9e42768d8b7f91b57f5fd912e8d5edc147efca Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 07:59:58 +0000 Subject: [PATCH] fix(agentic): add task aliases and session model normalization Co-Authored-By: Virgil --- pkg/agentic/commands_session.go | 6 ++-- pkg/agentic/commands_session_test.go | 33 +++++++++++++++++- pkg/agentic/commands_task.go | 5 ++- pkg/agentic/commands_task_test.go | 3 ++ pkg/agentic/session.go | 51 +++++++++++++++++++++++----- pkg/agentic/session_test.go | 35 ++++++++++++++++++- 6 files changed, 119 insertions(+), 14 deletions(-) diff --git a/pkg/agentic/commands_session.go b/pkg/agentic/commands_session.go index fc4023a..b8c7386 100644 --- a/pkg/agentic/commands_session.go +++ b/pkg/agentic/commands_session.go @@ -123,16 +123,16 @@ func (s *PrepSubsystem) cmdSessionList(options core.Options) core.Result { return core.Result{Value: output, OK: true} } -// core-agent session start ax-follow-up --agent-type=codex +// core-agent session start ax-follow-up --agent-type=claude:opus func (s *PrepSubsystem) cmdSessionStart(options core.Options) core.Result { planSlug := optionStringValue(options, "plan_slug", "plan", "_arg") agentType := optionStringValue(options, "agent_type", "agent") if planSlug == "" { - core.Print(nil, "usage: core-agent session start --agent-type=codex [--context='{\"repo\":\"go-io\"}']") + core.Print(nil, "usage: core-agent session start --agent-type=claude:opus [--context='{\"repo\":\"go-io\"}']") return core.Result{Value: core.E("agentic.cmdSessionStart", "plan_slug is required", nil), OK: false} } if agentType == "" { - core.Print(nil, "usage: core-agent session start --agent-type=codex [--context='{\"repo\":\"go-io\"}']") + core.Print(nil, "usage: core-agent session start --agent-type=claude:opus [--context='{\"repo\":\"go-io\"}']") return core.Result{Value: core.E("agentic.cmdSessionStart", "agent_type is required", nil), OK: false} } diff --git a/pkg/agentic/commands_session_test.go b/pkg/agentic/commands_session_test.go index c7deae5..1180078 100644 --- a/pkg/agentic/commands_session_test.go +++ b/pkg/agentic/commands_session_test.go @@ -123,6 +123,37 @@ func TestCommandsSession_CmdSessionStart_Good(t *testing.T) { assert.Equal(t, "opus", output.Session.AgentType) } +func TestCommandsSession_CmdSessionStart_Good_CanonicalAlias(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/v1/sessions", r.URL.Path) + require.Equal(t, http.MethodPost, r.Method) + + bodyResult := core.ReadAll(r.Body) + require.True(t, bodyResult.OK) + + var payload map[string]any + parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload) + require.True(t, parseResult.OK) + assert.Equal(t, "opus", payload["agent_type"]) + + _, _ = w.Write([]byte(`{"data":{"session_id":"ses-start","plan_slug":"ax-follow-up","agent_type":"opus","status":"active"}}`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.cmdSessionStart(core.NewOptions( + core.Option{Key: "_arg", Value: "ax-follow-up"}, + core.Option{Key: "agent_type", Value: "claude:opus"}, + )) + require.True(t, result.OK) + + output, ok := result.Value.(SessionOutput) + require.True(t, ok) + assert.Equal(t, "ses-start", output.Session.SessionID) + assert.Equal(t, "ax-follow-up", output.Session.PlanSlug) + assert.Equal(t, "opus", output.Session.AgentType) +} + func TestCommandsSession_CmdSessionStart_Bad_MissingPlanSlug(t *testing.T) { subsystem := testPrepWithPlatformServer(t, nil, "secret-token") @@ -143,7 +174,7 @@ func TestCommandsSession_CmdSessionStart_Bad_InvalidAgentType(t *testing.T) { assert.False(t, result.OK) require.Error(t, result.Value.(error)) - assert.Contains(t, result.Value.(error).Error(), "opus, sonnet, or haiku") + assert.Contains(t, result.Value.(error).Error(), "claude:opus") } func TestCommandsSession_CmdSessionStart_Ugly_InvalidResponse(t *testing.T) { diff --git a/pkg/agentic/commands_task.go b/pkg/agentic/commands_task.go index 79ac27f..35706b7 100644 --- a/pkg/agentic/commands_task.go +++ b/pkg/agentic/commands_task.go @@ -11,12 +11,15 @@ func (s *PrepSubsystem) registerTaskCommands() { c.Command("task", core.Command{Description: "Manage plan tasks", Action: s.cmdTask}) c.Command("agentic:task", core.Command{Description: "Manage plan tasks", Action: s.cmdTask}) c.Command("task/create", core.Command{Description: "Create a task in a plan phase", Action: s.cmdTaskCreate}) + c.Command("agentic:task/create", core.Command{Description: "Create a task in a plan phase", Action: s.cmdTaskCreate}) c.Command("task/update", core.Command{Description: "Update a plan task status, notes, priority, or category", Action: s.cmdTaskUpdate}) + c.Command("agentic:task/update", core.Command{Description: "Update a plan task status, notes, priority, or category", Action: s.cmdTaskUpdate}) c.Command("task/toggle", core.Command{Description: "Toggle a plan task between pending and completed", Action: s.cmdTaskToggle}) + c.Command("agentic:task/toggle", core.Command{Description: "Toggle a plan task between pending and completed", Action: s.cmdTaskToggle}) } func (s *PrepSubsystem) cmdTask(options core.Options) core.Result { - action := optionStringValue(options, "action") + action := optionStringValue(options, "action", "_arg") switch action { case "create": return s.cmdTaskCreate(options) diff --git a/pkg/agentic/commands_task_test.go b/pkg/agentic/commands_task_test.go index 0984b2a..dd8b46d 100644 --- a/pkg/agentic/commands_task_test.go +++ b/pkg/agentic/commands_task_test.go @@ -58,6 +58,9 @@ func TestCommands_TaskCommand_Good_SpecAliasRegistered(t *testing.T) { s.registerTaskCommands() assert.Contains(t, c.Commands(), "agentic:task") + assert.Contains(t, c.Commands(), "agentic:task/create") + assert.Contains(t, c.Commands(), "agentic:task/update") + assert.Contains(t, c.Commands(), "agentic:task/toggle") } func TestCommands_TaskCommand_Good_Create(t *testing.T) { diff --git a/pkg/agentic/session.go b/pkg/agentic/session.go index a4f1eb4..5ce8b68 100644 --- a/pkg/agentic/session.go +++ b/pkg/agentic/session.go @@ -408,12 +408,13 @@ func (s *PrepSubsystem) sessionStart(ctx context.Context, _ *mcp.CallToolRequest if input.AgentType == "" { return nil, SessionOutput{}, core.E("sessionStart", "agent_type is required", nil) } - if !validSessionAgentType(input.AgentType) { - return nil, SessionOutput{}, core.E("sessionStart", "agent_type must be opus, sonnet, or haiku", nil) + normalisedAgentType, ok := normaliseSessionAgentType(input.AgentType) + if !ok { + return nil, SessionOutput{}, core.E("sessionStart", "agent_type must be opus, sonnet, haiku, or claude:opus|claude:sonnet|claude:haiku", nil) } body := map[string]any{ - "agent_type": input.AgentType, + "agent_type": normalisedAgentType, } if input.PlanSlug != "" { body["plan_slug"] = input.PlanSlug @@ -453,7 +454,11 @@ func (s *PrepSubsystem) sessionGet(ctx context.Context, _ *mcp.CallToolRequest, func (s *PrepSubsystem) sessionList(ctx context.Context, _ *mcp.CallToolRequest, input SessionListInput) (*mcp.CallToolResult, SessionListOutput, error) { path := "/v1/sessions" path = appendQueryParam(path, "plan_slug", input.PlanSlug) - path = appendQueryParam(path, "agent_type", input.AgentType) + if agentType, ok := normaliseSessionAgentType(input.AgentType); ok { + path = appendQueryParam(path, "agent_type", agentType) + } else { + path = appendQueryParam(path, "agent_type", input.AgentType) + } path = appendQueryParam(path, "status", input.Status) if input.Limit > 0 { path = appendQueryParam(path, "limit", core.Sprint(input.Limit)) @@ -478,7 +483,9 @@ func (s *PrepSubsystem) sessionContinue(ctx context.Context, _ *mcp.CallToolRequ } body := map[string]any{} - if input.AgentType != "" { + if agentType, ok := normaliseSessionAgentType(input.AgentType); ok { + body["agent_type"] = agentType + } else if input.AgentType != "" { body["agent_type"] = input.AgentType } if len(input.WorkLog) > 0 { @@ -1186,10 +1193,38 @@ func resultErrorValue(action string, result core.Result) error { } func validSessionAgentType(agentType string) bool { - switch core.Lower(core.Trim(agentType)) { + _, ok := normaliseSessionAgentType(agentType) + return ok +} + +func normaliseSessionAgentType(agentType string) (string, bool) { + trimmed := core.Lower(core.Trim(agentType)) + if trimmed == "" { + return "", false + } + + switch trimmed { + case "claude": + return "opus", true case "opus", "sonnet", "haiku": - return true + return trimmed, true + } + + parts := core.SplitN(trimmed, ":", 2) + if len(parts) != 2 { + return "", false + } + if parts[0] != "claude" { + return "", false + } + switch core.Lower(core.Trim(agentType)) { + case "claude:opus": + return "opus", true + case "claude:sonnet": + return "sonnet", true + case "claude:haiku": + return "haiku", true default: - return false + return "", false } } diff --git a/pkg/agentic/session_test.go b/pkg/agentic/session_test.go index feb4d2b..e6a58a3 100644 --- a/pkg/agentic/session_test.go +++ b/pkg/agentic/session_test.go @@ -47,6 +47,39 @@ func TestSession_HandleSessionStart_Good(t *testing.T) { assert.Equal(t, "opus", output.Session.AgentType) } +func TestSession_HandleSessionStart_Good_CanonicalAlias(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/v1/sessions", r.URL.Path) + require.Equal(t, http.MethodPost, r.Method) + + bodyResult := core.ReadAll(r.Body) + require.True(t, bodyResult.OK) + + var payload map[string]any + parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload) + require.True(t, parseResult.OK) + require.Equal(t, "opus", payload["agent_type"]) + require.Equal(t, "ax-follow-up", payload["plan_slug"]) + + _, _ = w.Write([]byte(`{"data":{"id":1,"session_id":"ses_abc123","plan_slug":"ax-follow-up","agent_type":"opus","status":"active","context_summary":{"repo":"core/go"}}}`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.handleSessionStart(context.Background(), core.NewOptions( + core.Option{Key: "agent_type", Value: "claude:opus"}, + core.Option{Key: "plan_slug", Value: "ax-follow-up"}, + core.Option{Key: "context", Value: `{"repo":"core/go"}`}, + )) + require.True(t, result.OK) + + output, ok := result.Value.(SessionOutput) + require.True(t, ok) + assert.Equal(t, "ses_abc123", output.Session.SessionID) + assert.Equal(t, "active", output.Session.Status) + assert.Equal(t, "opus", output.Session.AgentType) +} + func TestSession_HandleSessionStart_Bad(t *testing.T) { subsystem := testPrepWithPlatformServer(t, nil, "secret-token") @@ -61,7 +94,7 @@ func TestSession_HandleSessionStart_Bad_InvalidAgentType(t *testing.T) { core.Option{Key: "agent_type", Value: "codex"}, )) assert.False(t, result.OK) - require.Contains(t, result.Value.(error).Error(), "opus, sonnet, or haiku") + require.Contains(t, result.Value.(error).Error(), "claude:opus") } func TestSession_HandleSessionStart_Ugly(t *testing.T) {