From bf10d16f49075a5efce4c8c562160bdf174d02ec Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 25 Apr 2026 14:55:18 +0100 Subject: [PATCH] =?UTF-8?q?feat(agent):=20batch=20=E2=80=94=20sprint=20MCP?= =?UTF-8?q?=20tools=20+=20cmd=20cleanup=20(#142=20#225=20#226=20#227)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex 5.5 batch lane processed 26 open Mantis tickets. 13 stale-fixed, 4 implemented, 9 deferred. Tickets implemented: - #142 — agentic_sprint_start + agentic_sprint_complete MCP tools wired to /v1/sprints/{id}/{start,complete} platform endpoints with tests - #225 — cmd/core-agent/commands.go: removed raw flag parsing; startupArgs() uses Core arg filtering + local log-level strip - #226 — cmd/core-agent/main.go: syscall.Exit(1) → core.Exit(1) - #227 — pkg/agentic/dispatch.go: runtime.GOOS → Core environment-backed OS detection Tickets stale-fixed: #161, #162, #163, #166, #167, #168, #171, #172, #223, #224, #230, #231, #232, #233 Tickets deferred: #160, #164, #165, #173, #222, #228, #229, #234 Co-authored-by: Codex Closes tasks.lthn.sh/view.php?id=142 Closes tasks.lthn.sh/view.php?id=225 Closes tasks.lthn.sh/view.php?id=226 Closes tasks.lthn.sh/view.php?id=227 --- cmd/core-agent/commands.go | 30 ++-------------------------- cmd/core-agent/main.go | 3 +-- pkg/agentic/dispatch.go | 5 ++--- pkg/agentic/prep_test.go | 2 ++ pkg/agentic/sprint.go | 41 ++++++++++++++++++++++++++++++++++++++ pkg/agentic/sprint_test.go | 32 +++++++++++++++++++++++++++++ 6 files changed, 80 insertions(+), 33 deletions(-) diff --git a/cmd/core-agent/commands.go b/cmd/core-agent/commands.go index d1be230..4d105d6 100644 --- a/cmd/core-agent/commands.go +++ b/cmd/core-agent/commands.go @@ -3,7 +3,7 @@ package main import ( - "flag" + "os" "dappco.re/go/agent/pkg/agentic" "dappco.re/go/core" @@ -16,33 +16,7 @@ type applicationCommandSet struct { // args := startupArgs() // _ = c.Cli().Run("version") func startupArgs() []string { - previous := flag.CommandLine - commandLine := flag.NewFlagSet("core-agent", flag.ContinueOnError) - commandLine.SetOutput(core.NewBuffer()) - commandLine.BoolFunc("quiet", "", func(string) error { - core.SetLevel(core.LevelError) - return nil - }) - commandLine.BoolFunc("q", "", func(string) error { - core.SetLevel(core.LevelError) - return nil - }) - commandLine.BoolFunc("debug", "", func(string) error { - core.SetLevel(core.LevelDebug) - return nil - }) - commandLine.BoolFunc("d", "", func(string) error { - core.SetLevel(core.LevelDebug) - return nil - }) - - flag.CommandLine = commandLine - defer func() { - flag.CommandLine = previous - }() - - flag.Parse() - return applyLogLevel(commandLine.Args()) + return applyLogLevel(core.FilterArgs(os.Args[1:])) } // args := applyLogLevel([]string{"version", "-q"}) diff --git a/cmd/core-agent/main.go b/cmd/core-agent/main.go index b15f552..d34207c 100644 --- a/cmd/core-agent/main.go +++ b/cmd/core-agent/main.go @@ -4,7 +4,6 @@ package main import ( "context" - "syscall" agentpkg "dappco.re/go/agent" "dappco.re/go/core" @@ -20,7 +19,7 @@ import ( func main() { if err := runCoreAgent(); err != nil { core.Error("core-agent failed", "err", err) - syscall.Exit(1) + core.Exit(1) } } diff --git a/pkg/agentic/dispatch.go b/pkg/agentic/dispatch.go index fee5d4f..d9b73ee 100644 --- a/pkg/agentic/dispatch.go +++ b/pkg/agentic/dispatch.go @@ -4,13 +4,12 @@ package agentic import ( "context" - "runtime" "time" "dappco.re/go/agent/pkg/messages" core "dappco.re/go/core" - "dappco.re/go/process" coremcp "dappco.re/go/mcp/pkg/mcp" + "dappco.re/go/process" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -235,7 +234,7 @@ func containerRuntimeBinary(runtime string) string { // goosIsDarwin reports whether the running process is on macOS. Captured at // package init so tests can compare against a fixed value without taking a // dependency on the `runtime` package themselves. -var goosIsDarwin = runtime.GOOS == "darwin" +var goosIsDarwin = core.Lower(core.Trim(envOr("GOOS", core.Env("OS")))) == "darwin" // runtimeAvailable reports whether the runtime's binary is available on PATH // or via known absolute paths. Apple Container additionally requires macOS as diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index fbe59d0..a2c43ac 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -732,6 +732,8 @@ func TestPrep_RegisterTools_Good_RegistersCompletionTool(t *testing.T) { assert.Contains(t, toolNames, "agentic_task_create") assert.Contains(t, toolNames, "agentic_state_set") assert.Contains(t, toolNames, "agentic_sprint_create") + assert.Contains(t, toolNames, "agentic_sprint_start") + assert.Contains(t, toolNames, "agentic_sprint_complete") assert.Contains(t, toolNames, "session_complete") assert.Contains(t, toolNames, "agentic_message_send") assert.Contains(t, toolNames, "agent_send") diff --git a/pkg/agentic/sprint.go b/pkg/agentic/sprint.go index bd6ff88..9a4ee00 100644 --- a/pkg/agentic/sprint.go +++ b/pkg/agentic/sprint.go @@ -65,6 +65,12 @@ type SprintArchiveInput struct { Slug string `json:"slug,omitempty"` } +// input := agentic.SprintTransitionInput{Slug: "ax-follow-up"} +type SprintTransitionInput struct { + ID string `json:"id,omitempty"` + Slug string `json:"slug,omitempty"` +} + // out := agentic.SprintOutput{Success: true, Sprint: agentic.Sprint{Slug: "ax-follow-up"}} type SprintOutput struct { Success bool `json:"success"` @@ -191,6 +197,16 @@ func (s *PrepSubsystem) registerSprintTools(svc *coremcp.Service) { Description: "Update fields on a tracked platform sprint by slug.", }, s.sprintUpdate) + coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{ + Name: "agentic_sprint_start", + Description: "Start a tracked platform sprint by slug or ID.", + }, s.sprintStart) + + coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{ + Name: "agentic_sprint_complete", + Description: "Complete a tracked platform sprint by slug or ID.", + }, s.sprintComplete) + coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{ Name: "sprint_archive", Description: "Archive a tracked platform sprint by slug.", @@ -308,6 +324,14 @@ func (s *PrepSubsystem) sprintUpdate(ctx context.Context, _ *mcp.CallToolRequest }, nil } +func (s *PrepSubsystem) sprintStart(ctx context.Context, _ *mcp.CallToolRequest, input SprintTransitionInput) (*mcp.CallToolResult, SprintOutput, error) { + return s.sprintTransition(ctx, "sprint.start", "start", input) +} + +func (s *PrepSubsystem) sprintComplete(ctx context.Context, _ *mcp.CallToolRequest, input SprintTransitionInput) (*mcp.CallToolResult, SprintOutput, error) { + return s.sprintTransition(ctx, "sprint.complete", "complete", input) +} + func (s *PrepSubsystem) sprintArchive(ctx context.Context, _ *mcp.CallToolRequest, input SprintArchiveInput) (*mcp.CallToolResult, SprintArchiveOutput, error) { identifier := sprintIdentifier(input.Slug, input.ID) if identifier == "" { @@ -334,6 +358,23 @@ func (s *PrepSubsystem) sprintArchive(ctx context.Context, _ *mcp.CallToolReques return nil, output, nil } +func (s *PrepSubsystem) sprintTransition(ctx context.Context, action, transition string, input SprintTransitionInput) (*mcp.CallToolResult, SprintOutput, error) { + identifier := sprintIdentifier(input.Slug, input.ID) + if identifier == "" { + return nil, SprintOutput{}, core.E("sprintTransition", "id or slug is required", nil) + } + + result := s.platformPayload(ctx, action, "POST", core.Concat("/v1/sprints/", identifier, "/", transition), nil) + if !result.OK { + return nil, SprintOutput{}, resultErrorValue(action, result) + } + + return nil, SprintOutput{ + Success: true, + Sprint: parseSprint(payloadResourceMap(result.Value.(map[string]any), "sprint")), + }, nil +} + func sprintIdentifier(values ...string) string { for _, value := range values { if trimmed := core.Trim(value); trimmed != "" { diff --git a/pkg/agentic/sprint_test.go b/pkg/agentic/sprint_test.go index e3d5409..602a157 100644 --- a/pkg/agentic/sprint_test.go +++ b/pkg/agentic/sprint_test.go @@ -93,3 +93,35 @@ func TestSprint_HandleSprintList_Ugly_NestedEnvelope(t *testing.T) { assert.Equal(t, 2, output.Sprints[0].WorkspaceID) assert.Equal(t, "Finish RFC parity", output.Sprints[0].Goal) } + +func TestSprint_SprintStart_Good(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/v1/sprints/ax-follow-up/start", r.URL.Path) + require.Equal(t, http.MethodPost, r.Method) + _, _ = w.Write([]byte(`{"data":{"sprint":{"slug":"ax-follow-up","title":"AX Follow-up","status":"active"}}}`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + _, output, err := subsystem.sprintStart(context.Background(), nil, SprintTransitionInput{Slug: "ax-follow-up"}) + require.NoError(t, err) + assert.True(t, output.Success) + assert.Equal(t, "ax-follow-up", output.Sprint.Slug) + assert.Equal(t, "active", output.Sprint.Status) +} + +func TestSprint_SprintComplete_Good(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/v1/sprints/7/complete", r.URL.Path) + require.Equal(t, http.MethodPost, r.Method) + _, _ = w.Write([]byte(`{"data":{"sprint":{"id":7,"slug":"ax-follow-up","title":"AX Follow-up","status":"completed"}}}`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + _, output, err := subsystem.sprintComplete(context.Background(), nil, SprintTransitionInput{ID: "7"}) + require.NoError(t, err) + assert.True(t, output.Success) + assert.Equal(t, 7, output.Sprint.ID) + assert.Equal(t, "completed", output.Sprint.Status) +}