feat(agentic): add issue assign and report commands

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 18:16:42 +00:00
parent ce3039544e
commit 2df0b73b1e
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/assign", core.Command{Description: "Assign a Forge issue", Action: s.cmdIssueAssign})
c.Command("issue/report", core.Command{Description: "Post a structured report to a Forge issue", Action: s.cmdIssueReport})
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})
@ -268,6 +270,69 @@ func (s *PrepSubsystem) cmdIssueUpdate(options core.Options) core.Result {
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) cmdIssueAssign(options core.Options) core.Result {
ctx := context.Background()
id := optionStringValue(options, "id", "slug", "_arg")
if id == "" || optionStringValue(options, "assignee", "agent", "agent_type") == "" {
core.Print(nil, "usage: core-agent issue assign <slug> --assignee=codex [--org=core]")
return core.Result{Value: core.E("agentic.cmdIssueAssign", "slug or id and assignee are required", nil), OK: false}
}
result := s.handleIssueRecordAssign(ctx, core.NewOptions(
core.Option{Key: "slug", Value: id},
core.Option{Key: "assignee", Value: optionStringValue(options, "assignee", "agent", "agent_type")},
))
if !result.OK {
err := commandResultError("agentic.cmdIssueAssign", 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.cmdIssueAssign", "invalid issue assign 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, " assignee: %s", output.Issue.Assignee)
core.Print(nil, " status: %s", output.Issue.Status)
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) cmdIssueReport(options core.Options) core.Result {
ctx := context.Background()
id := optionStringValue(options, "id", "slug", "_arg")
if id == "" {
core.Print(nil, "usage: core-agent issue report <slug> --report=\"...\" [--org=core]")
return core.Result{Value: core.E("agentic.cmdIssueReport", "slug or id is required", nil), OK: false}
}
result := s.handleIssueRecordReport(ctx, core.NewOptions(
core.Option{Key: "slug", Value: id},
core.Option{Key: "report", Value: optionAnyValue(options, "report", "body")},
core.Option{Key: "author", Value: options.String("author")},
))
if !result.OK {
err := commandResultError("agentic.cmdIssueReport", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(IssueReportOutput)
if !ok {
err := core.E("agentic.cmdIssueReport", "invalid issue report output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
core.Print(nil, "comment: %d", output.Comment.ID)
core.Print(nil, " author: %s", output.Comment.Author)
core.Print(nil, " body: %s", output.Comment.Body)
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")

View file

@ -188,6 +188,86 @@ func TestCommandsforge_CmdIssueUpdate_Bad_MissingSlug(t *testing.T) {
assert.EqualError(t, result.Value.(error), "agentic.cmdIssueUpdate: slug or id is required")
}
func TestCommandsforge_CmdIssueAssign_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, "codex", payload["assignee"])
_, _ = w.Write([]byte(`{"data":{"issue":{"slug":"fix-auth","title":"Fix auth middleware","status":"open","assignee":"codex"}}}`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.cmdIssueAssign(core.NewOptions(
core.Option{Key: "slug", Value: "fix-auth"},
core.Option{Key: "assignee", Value: "codex"},
))
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, "codex", output.Issue.Assignee)
}
func TestCommandsforge_CmdIssueAssign_Bad_MissingAssignee(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
result := subsystem.cmdIssueAssign(core.NewOptions(core.Option{Key: "slug", Value: "fix-auth"}))
assert.False(t, result.OK)
assert.EqualError(t, result.Value.(error), "agentic.cmdIssueAssign: slug or id and assignee are required")
}
func TestCommandsforge_CmdIssueReport_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/v1/issues/fix-auth/comments", 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)
reportBody := core.JSONMarshalString(map[string]any{
"summary": "Build failed",
})
require.Equal(t, "codex", payload["author"])
require.Equal(t, core.Concat("```json\n", reportBody, "\n```"), payload["body"])
_, _ = w.Write([]byte("{\"data\":{\"comment\":{\"id\":7,\"author\":\"codex\",\"body\":\"report received\"}}}"))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.cmdIssueReport(core.NewOptions(
core.Option{Key: "slug", Value: "fix-auth"},
core.Option{Key: "report", Value: map[string]any{"summary": "Build failed"}},
core.Option{Key: "author", Value: "codex"},
))
require.True(t, result.OK)
output, ok := result.Value.(IssueReportOutput)
require.True(t, ok)
assert.Equal(t, 7, output.Comment.ID)
assert.Equal(t, "codex", output.Comment.Author)
}
func TestCommandsforge_CmdIssueReport_Bad_MissingSlug(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
result := subsystem.cmdIssueReport(core.NewOptions())
assert.False(t, result.OK)
assert.EqualError(t, result.Value.(error), "agentic.cmdIssueReport: 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)
@ -321,6 +401,8 @@ 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/assign")
assert.Contains(t, c.Commands(), "issue/report")
assert.Contains(t, c.Commands(), "issue/update")
assert.Contains(t, c.Commands(), "issue/archive")
}