2026-03-16 11:10:33 +00:00
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
|
|
|
|
|
|
package agentic
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-03-30 20:37:23 +00:00
|
|
|
"net/url"
|
2026-03-16 11:10:33 +00:00
|
|
|
|
2026-03-22 03:41:07 +00:00
|
|
|
core "dappco.re/go/core"
|
2026-03-16 11:10:33 +00:00
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ScanInput struct {
|
2026-03-30 20:45:23 +00:00
|
|
|
Org string `json:"org,omitempty"`
|
|
|
|
|
Labels []string `json:"labels,omitempty"`
|
|
|
|
|
Limit int `json:"limit,omitempty"`
|
2026-03-16 11:10:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ScanOutput struct {
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
Count int `json:"count"`
|
|
|
|
|
Issues []ScanIssue `json:"issues"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ScanIssue struct {
|
|
|
|
|
Repo string `json:"repo"`
|
|
|
|
|
Number int `json:"number"`
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
Labels []string `json:"labels"`
|
|
|
|
|
Assignee string `json:"assignee,omitempty"`
|
|
|
|
|
URL string `json:"url"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *PrepSubsystem) scan(ctx context.Context, _ *mcp.CallToolRequest, input ScanInput) (*mcp.CallToolResult, ScanOutput, error) {
|
|
|
|
|
if s.forgeToken == "" {
|
2026-03-22 03:41:07 +00:00
|
|
|
return nil, ScanOutput{}, core.E("scan", "no Forge token configured", nil)
|
2026-03-16 11:10:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if input.Org == "" {
|
|
|
|
|
input.Org = "core"
|
|
|
|
|
}
|
|
|
|
|
if input.Limit == 0 {
|
|
|
|
|
input.Limit = 20
|
|
|
|
|
}
|
|
|
|
|
if len(input.Labels) == 0 {
|
|
|
|
|
input.Labels = []string{"agentic", "help-wanted", "bug"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var allIssues []ScanIssue
|
|
|
|
|
|
|
|
|
|
repos, err := s.listOrgRepos(ctx, input.Org)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, ScanOutput{}, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 20:45:23 +00:00
|
|
|
for _, repoName := range repos {
|
2026-03-16 11:10:33 +00:00
|
|
|
for _, label := range input.Labels {
|
2026-03-30 20:45:23 +00:00
|
|
|
issues, err := s.listRepoIssues(ctx, input.Org, repoName, label)
|
2026-03-16 11:10:33 +00:00
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
allIssues = append(allIssues, issues...)
|
|
|
|
|
|
|
|
|
|
if len(allIssues) >= input.Limit {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(allIssues) >= input.Limit {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seen := make(map[string]bool)
|
|
|
|
|
var unique []ScanIssue
|
|
|
|
|
for _, issue := range allIssues {
|
refactor: migrate core/agent to Core primitives — reference implementation
Phase 1: go-io/go-log → core.Fs{}, core.E(), core.Error/Info/Warn
Phase 2: strings/fmt → core.Contains, core.Sprintf, core.Split etc
Phase 3: embed.FS → core.Mount/core.Embed, core.Extract
Phase 4: cmd/main.go → core.Command(), c.Cli().Run(), no cli package
All packages migrated:
- pkg/lib (Codex): core.Mount, core.Extract, Result returns, AX comments
- pkg/setup (Codex): core.Fs, core.E, fixed missing lib helpers
- pkg/brain (Codex): Core primitives, AX comments
- pkg/monitor (Codex): Core string/logging primitives
- pkg/agentic (Codex): 20 files, Core primitives throughout
- cmd/main.go: pure Core CLI, no fmt/log/filepath/strings/cli
Remaining stdlib: path/filepath (Core doesn't wrap OS paths),
fmt.Sscanf/strings.Map (no Core equivalent).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 06:13:41 +00:00
|
|
|
key := core.Sprintf("%s#%d", issue.Repo, issue.Number)
|
2026-03-16 11:10:33 +00:00
|
|
|
if !seen[key] {
|
|
|
|
|
seen[key] = true
|
|
|
|
|
unique = append(unique, issue)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(unique) > input.Limit {
|
|
|
|
|
unique = unique[:input.Limit]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, ScanOutput{
|
|
|
|
|
Success: true,
|
|
|
|
|
Count: len(unique),
|
|
|
|
|
Issues: unique,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string, error) {
|
2026-03-22 14:15:41 +00:00
|
|
|
repos, err := s.forge.Repos.ListOrgRepos(ctx, org)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, core.E("scan.listOrgRepos", "failed to list repos", err)
|
|
|
|
|
}
|
2026-03-16 11:10:33 +00:00
|
|
|
|
2026-03-22 14:15:41 +00:00
|
|
|
var allNames []string
|
2026-03-30 20:45:23 +00:00
|
|
|
for _, repoInfo := range repos {
|
|
|
|
|
allNames = append(allNames, repoInfo.Name)
|
2026-03-16 11:10:33 +00:00
|
|
|
}
|
2026-03-21 16:53:55 +00:00
|
|
|
return allNames, nil
|
2026-03-16 11:10:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label string) ([]ScanIssue, error) {
|
2026-03-30 20:45:23 +00:00
|
|
|
requestURL := core.Sprintf("%s/api/v1/repos/%s/%s/issues?state=open&limit=10&type=issues",
|
2026-03-21 16:22:18 +00:00
|
|
|
s.forgeURL, org, repo)
|
|
|
|
|
if label != "" {
|
2026-03-30 20:45:23 +00:00
|
|
|
requestURL = core.Concat(requestURL, "&labels=", url.QueryEscape(label))
|
2026-03-21 16:22:18 +00:00
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
httpResult := HTTPGet(ctx, requestURL, s.forgeToken, "token")
|
|
|
|
|
if !httpResult.OK {
|
feat(v0.8.0): full AX migration — ServiceRuntime, Actions, quality gates, transport
go-process:
- Register factory, Result lifecycle, 5 named Action handlers
- Start/Run/StartWithOptions/RunWithOptions all return core.Result
- core.ID() replaces fmt.Sprintf, core.As replaces errors.As
core/agent:
- PrepSubsystem + monitor.Subsystem + setup.Service embed ServiceRuntime[T]
- 22 named Actions + agent.completion Task pipeline in OnStartup
- ChannelNotifier removed — all IPC via c.ACTION(messages.X{})
- proc.go: all methods via s.Core().Process(), returns core.Result
- status.go: WriteAtomic + JSONMarshalString
- paths.go: Fs.NewUnrestricted() replaces unsafe.Pointer
- transport.go: ONE net/http file — HTTPGet/HTTPPost/HTTPDo/MCP transport
- All disallowed imports eliminated from source files (13 quality gates)
- String concat eliminated — core.Concat() throughout
- 1:1 _test.go + _example_test.go for every source file
- Reference docs synced from core/go v0.8.0
- RFC-025 updated with net/http, net/url, io/fs quality gates
- lib.go: io/fs eliminated via Data.ListNames, Array[T].Deduplicate
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:27:46 +00:00
|
|
|
return nil, core.E("scan.listRepoIssues", core.Concat("failed to list issues for ", repo), nil)
|
2026-03-16 11:10:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var issues []struct {
|
|
|
|
|
Number int `json:"number"`
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
Labels []struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
} `json:"labels"`
|
|
|
|
|
Assignee *struct {
|
|
|
|
|
Login string `json:"login"`
|
|
|
|
|
} `json:"assignee"`
|
|
|
|
|
HTMLURL string `json:"html_url"`
|
|
|
|
|
}
|
2026-03-30 20:45:23 +00:00
|
|
|
if parseResult := core.JSONUnmarshalString(httpResult.Value.(string), &issues); !parseResult.OK {
|
|
|
|
|
err, _ := parseResult.Value.(error)
|
2026-03-30 20:37:23 +00:00
|
|
|
return nil, core.E("scan.listRepoIssues", "parse issues response", err)
|
|
|
|
|
}
|
2026-03-16 11:10:33 +00:00
|
|
|
|
|
|
|
|
var result []ScanIssue
|
|
|
|
|
for _, issue := range issues {
|
|
|
|
|
var labels []string
|
2026-03-30 20:45:23 +00:00
|
|
|
for _, labelInfo := range issue.Labels {
|
|
|
|
|
labels = append(labels, labelInfo.Name)
|
2026-03-16 11:10:33 +00:00
|
|
|
}
|
|
|
|
|
assignee := ""
|
|
|
|
|
if issue.Assignee != nil {
|
|
|
|
|
assignee = issue.Assignee.Login
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result = append(result, ScanIssue{
|
|
|
|
|
Repo: repo,
|
|
|
|
|
Number: issue.Number,
|
|
|
|
|
Title: issue.Title,
|
|
|
|
|
Labels: labels,
|
|
|
|
|
Assignee: assignee,
|
refactor(agentic): route file I/O through core.Fs
Replace raw os.* file operations with Core Fs equivalents:
- os.Stat → fs.Exists/fs.IsFile/fs.IsDir (resume, pr, plan, mirror, prep)
- os.ReadDir → fs.List (queue, status, plan, mirror, review_queue)
- os.Remove → fs.Delete (dispatch)
- os.OpenFile(append) → fs.Append (events, review_queue)
- strings.Replace → core.Replace (scan)
Eliminates os import from resume.go, pr.go. Eliminates strings
import from scan.go. Trades os for io in events.go.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 09:08:45 +00:00
|
|
|
URL: core.Replace(issue.HTMLURL, "https://forge.lthn.ai", s.forgeURL),
|
2026-03-16 11:10:33 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|