feat(agentic): add issue update and archive CLI commands

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 12:06:10 +00:00
parent 524810cbda
commit 3883466cc4
2 changed files with 147 additions and 0 deletions

View file

@ -100,6 +100,8 @@ func (s *PrepSubsystem) registerForgeCommands() {
c.Command("issue/list", core.Command{Description: "List Forge issues for a repo", Action: s.cmdIssueList})
c.Command("issue/comment", core.Command{Description: "Comment on a Forge issue", Action: s.cmdIssueComment})
c.Command("issue/create", core.Command{Description: "Create a Forge issue", Action: s.cmdIssueCreate})
c.Command("issue/update", core.Command{Description: "Update a tracked platform issue", Action: s.cmdIssueUpdate})
c.Command("issue/archive", core.Command{Description: "Archive a tracked platform issue", Action: s.cmdIssueArchive})
c.Command("pr/get", core.Command{Description: "Get a Forge PR", Action: s.cmdPRGet})
c.Command("pr/list", core.Command{Description: "List Forge PRs for a repo", Action: s.cmdPRList})
c.Command("pr/merge", core.Command{Description: "Merge a Forge PR", Action: s.cmdPRMerge})
@ -228,6 +230,72 @@ func (s *PrepSubsystem) cmdIssueCreate(options core.Options) core.Result {
return core.Result{Value: issue.Index, OK: true}
}
func (s *PrepSubsystem) cmdIssueUpdate(options core.Options) core.Result {
ctx := context.Background()
id := optionStringValue(options, "id", "slug", "_arg")
if id == "" {
core.Print(nil, "usage: core-agent issue update <slug> [--title=\"...\"] [--description=\"...\"] [--type=bug] [--status=open] [--priority=high] [--labels=a,b] [--sprint-id=7|--sprint-slug=phase-1]")
return core.Result{Value: core.E("agentic.cmdIssueUpdate", "slug or id is required", nil), OK: false}
}
result := s.handleIssueRecordUpdate(ctx, core.NewOptions(
core.Option{Key: "slug", Value: id},
core.Option{Key: "title", Value: options.String("title")},
core.Option{Key: "description", Value: options.String("description")},
core.Option{Key: "type", Value: options.String("type")},
core.Option{Key: "status", Value: options.String("status")},
core.Option{Key: "priority", Value: options.String("priority")},
core.Option{Key: "labels", Value: options.String("labels")},
core.Option{Key: "sprint_id", Value: options.String("sprint-id")},
core.Option{Key: "sprint_slug", Value: options.String("sprint-slug")},
))
if !result.OK {
err := commandResultError("agentic.cmdIssueUpdate", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(IssueOutput)
if !ok {
err := core.E("agentic.cmdIssueUpdate", "invalid issue update output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
core.Print(nil, "%s", output.Issue.Slug)
core.Print(nil, " status: %s", output.Issue.Status)
core.Print(nil, " title: %s", output.Issue.Title)
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) cmdIssueArchive(options core.Options) core.Result {
ctx := context.Background()
id := optionStringValue(options, "id", "slug", "_arg")
if id == "" {
core.Print(nil, "usage: core-agent issue archive <slug>")
return core.Result{Value: core.E("agentic.cmdIssueArchive", "slug or id is required", nil), OK: false}
}
result := s.handleIssueRecordArchive(ctx, core.NewOptions(
core.Option{Key: "slug", Value: id},
))
if !result.OK {
err := commandResultError("agentic.cmdIssueArchive", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(IssueArchiveOutput)
if !ok {
err := core.E("agentic.cmdIssueArchive", "invalid issue archive output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
core.Print(nil, "archived: %s", output.Archived)
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) cmdPRGet(options core.Options) core.Result {
ctx := context.Background()
org, repo, num := parseForgeArgs(options)

View file

@ -10,6 +10,7 @@ import (
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- parseForgeArgs ---
@ -145,6 +146,82 @@ func TestCommandsforge_CmdIssueCreate_Ugly(t *testing.T) {
assert.False(t, r.OK)
}
func TestCommandsforge_CmdIssueUpdate_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, http.MethodPatch, 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, "Fix auth middleware", payload["title"])
require.Equal(t, "in_progress", payload["status"])
_, _ = w.Write([]byte(`{"data":{"issue":{"slug":"fix-auth","title":"Fix auth middleware","status":"in_progress","priority":"high","labels":["auth","backend"]}}}`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.cmdIssueUpdate(core.NewOptions(
core.Option{Key: "_arg", Value: "fix-auth"},
core.Option{Key: "title", Value: "Fix auth middleware"},
core.Option{Key: "status", Value: "in_progress"},
core.Option{Key: "priority", Value: "high"},
core.Option{Key: "labels", Value: "auth,backend"},
))
require.True(t, result.OK)
output, ok := result.Value.(IssueOutput)
require.True(t, ok)
assert.Equal(t, "fix-auth", output.Issue.Slug)
assert.Equal(t, "in_progress", output.Issue.Status)
assert.Equal(t, []string{"auth", "backend"}, output.Issue.Labels)
}
func TestCommandsforge_CmdIssueUpdate_Bad_MissingSlug(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
result := subsystem.cmdIssueUpdate(core.NewOptions())
assert.False(t, result.OK)
assert.EqualError(t, result.Value.(error), "agentic.cmdIssueUpdate: slug or id is required")
}
func TestCommandsforge_CmdIssueArchive_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, http.MethodDelete, r.Method)
_, _ = w.Write([]byte(`{"data":{"result":{"slug":"fix-auth","success":true}}}`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.cmdIssueArchive(core.NewOptions(
core.Option{Key: "slug", Value: "fix-auth"},
))
require.True(t, result.OK)
output, ok := result.Value.(IssueArchiveOutput)
require.True(t, ok)
assert.True(t, output.Success)
assert.Equal(t, "fix-auth", output.Archived)
}
func TestCommandsforge_CmdIssueArchive_Ugly_ServerError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.cmdIssueArchive(core.NewOptions(
core.Option{Key: "_arg", Value: "fix-auth"},
))
assert.False(t, result.OK)
}
func TestCommandsforge_CmdPRGet_Ugly(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) }))
t.Cleanup(srv.Close)
@ -244,4 +321,6 @@ func TestCommandsforge_RegisterForgeCommands_Good_RepoSyncRegistered(t *testing.
s, c := testPrepWithCore(t, nil)
s.registerForgeCommands()
assert.Contains(t, c.Commands(), "repo/sync")
assert.Contains(t, c.Commands(), "issue/update")
assert.Contains(t, c.Commands(), "issue/archive")
}