[agent/codex:gpt-5.3-codex-spark] Update the code against the AX (Agent Experience) design pri... #3
4 changed files with 232 additions and 51 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
|
|
@ -93,17 +94,42 @@ func addLintCommands(root *cli.Command) {
|
|||
lintpkg.WriteText(os.Stdout, allFindings)
|
||||
}
|
||||
|
||||
if len(allFindings) > 0 {
|
||||
if checkFormat == "text" && len(allFindings) > 0 {
|
||||
summary := lintpkg.Summarise(allFindings)
|
||||
fmt.Fprintf(os.Stderr, "\n%d finding(s)", summary.Total)
|
||||
fmt.Fprintf(os.Stdout, "\n%d finding(s)", summary.Total)
|
||||
|
||||
orderedSeverities := []string{"critical", "high", "medium", "low", "info"}
|
||||
seen := map[string]bool{}
|
||||
var parts []string
|
||||
for sev, count := range summary.BySeverity {
|
||||
|
||||
for _, sev := range orderedSeverities {
|
||||
count := summary.BySeverity[sev]
|
||||
if count == 0 {
|
||||
continue
|
||||
}
|
||||
seen[sev] = true
|
||||
parts = append(parts, fmt.Sprintf("%d %s", count, sev))
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
fmt.Fprintf(os.Stderr, " (%s)", strings.Join(parts, ", "))
|
||||
|
||||
var extraSeverities []string
|
||||
for severity := range summary.BySeverity {
|
||||
if seen[severity] {
|
||||
continue
|
||||
}
|
||||
extraSeverities = append(extraSeverities, severity)
|
||||
}
|
||||
fmt.Fprintln(os.Stderr)
|
||||
sort.Strings(extraSeverities)
|
||||
for _, severity := range extraSeverities {
|
||||
count := summary.BySeverity[severity]
|
||||
if count == 0 {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%d %s", count, severity))
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
fmt.Fprintf(os.Stdout, " (%s)", strings.Join(parts, ", "))
|
||||
}
|
||||
fmt.Fprintln(os.Stdout)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
var (
|
||||
healthProblems bool
|
||||
healthRegistry string
|
||||
healthJSON bool
|
||||
)
|
||||
|
||||
// HealthWorkflowRun represents a GitHub Actions workflow run
|
||||
|
|
@ -38,11 +39,29 @@ type HealthWorkflowRun struct {
|
|||
|
||||
// RepoHealth represents the CI health of a single repo
|
||||
type RepoHealth struct {
|
||||
Name string
|
||||
Status string // "passing", "failing", "pending", "no_ci", "disabled"
|
||||
Message string
|
||||
URL string
|
||||
FailingSince string
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"` // "passing", "failing", "pending", "no_ci", "disabled"
|
||||
Message string `json:"message"`
|
||||
URL string `json:"url"`
|
||||
FailingSince string `json:"failing_since"`
|
||||
}
|
||||
|
||||
type HealthSummary struct {
|
||||
TotalRepos int `json:"total_repos"`
|
||||
FilteredRepos int `json:"filtered_repos"`
|
||||
Passing int `json:"passing"`
|
||||
Failing int `json:"failing"`
|
||||
Pending int `json:"pending"`
|
||||
Disabled int `json:"disabled"`
|
||||
NotConfigured int `json:"not_configured"`
|
||||
PassingRate int `json:"passing_rate"`
|
||||
ProblemsOnly bool `json:"problems_only"`
|
||||
ByStatus map[string]int `json:"by_status"`
|
||||
}
|
||||
|
||||
type HealthOutput struct {
|
||||
Summary HealthSummary `json:"summary"`
|
||||
Repos []RepoHealth `json:"repos"`
|
||||
}
|
||||
|
||||
// addHealthCommand adds the 'health' subcommand to qa.
|
||||
|
|
@ -58,6 +77,7 @@ func addHealthCommand(parent *cli.Command) {
|
|||
|
||||
healthCmd.Flags().BoolVarP(&healthProblems, "problems", "p", false, i18n.T("cmd.qa.health.flag.problems"))
|
||||
healthCmd.Flags().StringVar(&healthRegistry, "registry", "", i18n.T("common.flag.registry"))
|
||||
healthCmd.Flags().BoolVar(&healthJSON, "json", false, i18n.T("common.flag.json"))
|
||||
|
||||
parent.AddCommand(healthCmd)
|
||||
}
|
||||
|
|
@ -90,18 +110,22 @@ func runHealth() error {
|
|||
repoList := reg.List()
|
||||
|
||||
for i, repo := range repoList {
|
||||
cli.Print("\033[2K\r%s %d/%d %s",
|
||||
dimStyle.Render(i18n.T("cmd.qa.issues.fetching")),
|
||||
i+1, len(repoList), repo.Name)
|
||||
if !healthJSON {
|
||||
cli.Print("%s %d/%d %s\n",
|
||||
dimStyle.Render(i18n.T("cmd.qa.issues.fetching")),
|
||||
i+1, len(repoList), repo.Name)
|
||||
}
|
||||
|
||||
health := fetchRepoHealth(reg.Org, repo.Name)
|
||||
healthResults = append(healthResults, health)
|
||||
}
|
||||
cli.Print("\033[2K\r") // Clear progress
|
||||
|
||||
// Sort: problems first, then passing
|
||||
slices.SortFunc(healthResults, func(a, b RepoHealth) int {
|
||||
return cmp.Compare(healthPriority(a.Status), healthPriority(b.Status))
|
||||
return cmp.Or(
|
||||
cmp.Compare(healthPriority(a.Status), healthPriority(b.Status)),
|
||||
cmp.Compare(a.Name, b.Name),
|
||||
)
|
||||
})
|
||||
|
||||
// Filter if --problems flag
|
||||
|
|
@ -115,19 +139,16 @@ func runHealth() error {
|
|||
healthResults = problems
|
||||
}
|
||||
|
||||
// Calculate summary
|
||||
passing := 0
|
||||
for _, h := range healthResults {
|
||||
if h.Status == "passing" {
|
||||
passing++
|
||||
}
|
||||
}
|
||||
total := len(repoList)
|
||||
percentage := 0
|
||||
if total > 0 {
|
||||
percentage = (passing * 100) / total
|
||||
summary := summariseHealthResults(len(repoList), healthResults, healthProblems)
|
||||
if healthJSON {
|
||||
return printHealthJSON(summary, healthResults)
|
||||
}
|
||||
|
||||
// Calculate human summary
|
||||
passing := summary.Passing
|
||||
total := summary.TotalRepos
|
||||
percentage := summary.PassingRate
|
||||
|
||||
// Print summary
|
||||
cli.Print("%s: %d/%d repos healthy (%d%%)\n\n",
|
||||
i18n.T("cmd.qa.health.summary"),
|
||||
|
|
@ -258,6 +279,49 @@ func healthPriority(status string) int {
|
|||
}
|
||||
}
|
||||
|
||||
func summariseHealthResults(totalRepos int, results []RepoHealth, problemsOnly bool) HealthSummary {
|
||||
summary := HealthSummary{
|
||||
TotalRepos: totalRepos,
|
||||
FilteredRepos: len(results),
|
||||
ByStatus: make(map[string]int),
|
||||
ProblemsOnly: problemsOnly,
|
||||
}
|
||||
|
||||
for _, health := range results {
|
||||
summary.ByStatus[health.Status]++
|
||||
switch health.Status {
|
||||
case "passing":
|
||||
summary.Passing++
|
||||
case "failing":
|
||||
summary.Failing++
|
||||
case "pending":
|
||||
summary.Pending++
|
||||
case "disabled":
|
||||
summary.Disabled++
|
||||
case "no_ci":
|
||||
summary.NotConfigured++
|
||||
}
|
||||
}
|
||||
|
||||
if summary.TotalRepos > 0 {
|
||||
summary.PassingRate = (summary.Passing * 100) / summary.TotalRepos
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
func printHealthJSON(summary HealthSummary, repos []RepoHealth) error {
|
||||
data, err := json.MarshalIndent(HealthOutput{
|
||||
Summary: summary,
|
||||
Repos: repos,
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.Print("%s\n", string(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func printHealthGroup(status string, repos []RepoHealth, style *cli.AnsiStyle) {
|
||||
if len(repos) == 0 {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ var (
|
|||
issuesMine bool
|
||||
issuesTriage bool
|
||||
issuesBlocked bool
|
||||
issuesJSON bool
|
||||
issuesRegistry string
|
||||
issuesLimit int
|
||||
)
|
||||
|
|
@ -65,10 +66,25 @@ type Issue struct {
|
|||
URL string `json:"url"`
|
||||
|
||||
// Computed fields
|
||||
RepoName string
|
||||
Priority int // Lower = higher priority
|
||||
Category string // "needs_response", "ready", "blocked", "triage"
|
||||
ActionHint string
|
||||
RepoName string `json:"repo_name"`
|
||||
Priority int `json:"priority"` // Lower = higher priority
|
||||
Category string `json:"category"` // "needs_response", "ready", "blocked", "triage"
|
||||
ActionHint string `json:"action_hint"`
|
||||
}
|
||||
|
||||
type IssueCategoryOutput struct {
|
||||
Category string `json:"category"`
|
||||
Count int `json:"count"`
|
||||
Issues []Issue `json:"issues"`
|
||||
}
|
||||
|
||||
type IssuesOutput struct {
|
||||
TotalIssues int `json:"total_issues"`
|
||||
FilteredIssues int `json:"filtered_issues"`
|
||||
ShowingMine bool `json:"showing_mine"`
|
||||
ShowingTriage bool `json:"showing_triage"`
|
||||
ShowingBlocked bool `json:"showing_blocked"`
|
||||
Categories []IssueCategoryOutput `json:"categories"`
|
||||
}
|
||||
|
||||
// addIssuesCommand adds the 'issues' subcommand to qa.
|
||||
|
|
@ -87,6 +103,7 @@ func addIssuesCommand(parent *cli.Command) {
|
|||
issuesCmd.Flags().BoolVarP(&issuesBlocked, "blocked", "b", false, i18n.T("cmd.qa.issues.flag.blocked"))
|
||||
issuesCmd.Flags().StringVar(&issuesRegistry, "registry", "", i18n.T("common.flag.registry"))
|
||||
issuesCmd.Flags().IntVarP(&issuesLimit, "limit", "l", 50, i18n.T("cmd.qa.issues.flag.limit"))
|
||||
issuesCmd.Flags().BoolVar(&issuesJSON, "json", false, i18n.T("common.flag.json"))
|
||||
|
||||
parent.AddCommand(issuesCmd)
|
||||
}
|
||||
|
|
@ -119,9 +136,11 @@ func runQAIssues() error {
|
|||
repoList := reg.List()
|
||||
|
||||
for i, repo := range repoList {
|
||||
cli.Print("\033[2K\r%s %d/%d %s",
|
||||
dimStyle.Render(i18n.T("cmd.qa.issues.fetching")),
|
||||
i+1, len(repoList), repo.Name)
|
||||
if !issuesJSON {
|
||||
cli.Print("%s %d/%d %s\n",
|
||||
dimStyle.Render(i18n.T("cmd.qa.issues.fetching")),
|
||||
i+1, len(repoList), repo.Name)
|
||||
}
|
||||
|
||||
issues, err := fetchQAIssues(reg.Org, repo.Name, issuesLimit)
|
||||
if err != nil {
|
||||
|
|
@ -129,9 +148,18 @@ func runQAIssues() error {
|
|||
}
|
||||
allIssues = append(allIssues, issues...)
|
||||
}
|
||||
cli.Print("\033[2K\r") // Clear progress
|
||||
totalIssues := len(allIssues)
|
||||
|
||||
if len(allIssues) == 0 {
|
||||
emptyCategorised := map[string][]Issue{
|
||||
"needs_response": {},
|
||||
"ready": {},
|
||||
"blocked": {},
|
||||
"triage": {},
|
||||
}
|
||||
if issuesJSON {
|
||||
return printCategorisedIssuesJSON(0, emptyCategorised)
|
||||
}
|
||||
cli.Text(i18n.T("cmd.qa.issues.no_issues"))
|
||||
return nil
|
||||
}
|
||||
|
|
@ -150,6 +178,10 @@ func runQAIssues() error {
|
|||
categorised = filterCategory(categorised, "blocked")
|
||||
}
|
||||
|
||||
if issuesJSON {
|
||||
return printCategorisedIssuesJSON(totalIssues, categorised)
|
||||
}
|
||||
|
||||
// Print categorised issues
|
||||
printCategorisedIssues(categorised)
|
||||
|
||||
|
|
@ -363,6 +395,38 @@ func printCategorisedIssues(categorised map[string][]Issue) {
|
|||
}
|
||||
}
|
||||
|
||||
func printCategorisedIssuesJSON(totalIssues int, categorised map[string][]Issue) error {
|
||||
categories := []string{"needs_response", "ready", "blocked", "triage"}
|
||||
filteredIssues := 0
|
||||
categoryOutput := make([]IssueCategoryOutput, 0, len(categories))
|
||||
|
||||
for _, category := range categories {
|
||||
issues := categorised[category]
|
||||
filteredIssues += len(issues)
|
||||
categoryOutput = append(categoryOutput, IssueCategoryOutput{
|
||||
Category: category,
|
||||
Count: len(issues),
|
||||
Issues: issues,
|
||||
})
|
||||
}
|
||||
|
||||
output := IssuesOutput{
|
||||
TotalIssues: totalIssues,
|
||||
FilteredIssues: filteredIssues,
|
||||
ShowingMine: issuesMine,
|
||||
ShowingTriage: issuesTriage,
|
||||
ShowingBlocked: issuesBlocked,
|
||||
Categories: categoryOutput,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.Print("%s\n", string(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func printTriagedIssue(issue Issue) {
|
||||
// #42 [core-bio] Fix avatar upload
|
||||
num := cli.TitleStyle.Render(cli.Sprintf("#%d", issue.Number))
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ var (
|
|||
reviewMine bool
|
||||
reviewRequested bool
|
||||
reviewRepo string
|
||||
reviewJSON bool
|
||||
)
|
||||
|
||||
// PullRequest represents a GitHub pull request
|
||||
|
|
@ -81,6 +82,12 @@ type Review struct {
|
|||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type ReviewOutput struct {
|
||||
Repository string `json:"repository"`
|
||||
MyPRs []PullRequest `json:"my_pull_requests"`
|
||||
Requested []PullRequest `json:"requested_pull_requests"`
|
||||
}
|
||||
|
||||
// addReviewCommand adds the 'review' subcommand to the qa command.
|
||||
func addReviewCommand(parent *cli.Command) {
|
||||
reviewCmd := &cli.Command{
|
||||
|
|
@ -95,6 +102,7 @@ func addReviewCommand(parent *cli.Command) {
|
|||
reviewCmd.Flags().BoolVarP(&reviewMine, "mine", "m", false, i18n.T("cmd.qa.review.flag.mine"))
|
||||
reviewCmd.Flags().BoolVarP(&reviewRequested, "requested", "r", false, i18n.T("cmd.qa.review.flag.requested"))
|
||||
reviewCmd.Flags().StringVar(&reviewRepo, "repo", "", i18n.T("cmd.qa.review.flag.repo"))
|
||||
reviewCmd.Flags().BoolVar(&reviewJSON, "json", false, i18n.T("common.flag.json"))
|
||||
|
||||
parent.AddCommand(reviewCmd)
|
||||
}
|
||||
|
|
@ -121,9 +129,38 @@ func runReview() error {
|
|||
// Default: show both mine and requested if neither flag is set
|
||||
showMine := reviewMine || (!reviewMine && !reviewRequested)
|
||||
showRequested := reviewRequested || (!reviewMine && !reviewRequested)
|
||||
var myPRs, requestedPRs []PullRequest
|
||||
|
||||
if showMine {
|
||||
if err := showMyPRs(ctx, repoFullName); err != nil {
|
||||
var err error
|
||||
myPRs, err = fetchPRs(ctx, repoFullName, "author:@me")
|
||||
if err != nil {
|
||||
return log.E("qa.review", "failed to fetch your PRs", err)
|
||||
}
|
||||
}
|
||||
if showRequested {
|
||||
var err error
|
||||
requestedPRs, err = fetchPRs(ctx, repoFullName, "review-requested:@me")
|
||||
if err != nil {
|
||||
return log.E("qa.review", "failed to fetch review requests", err)
|
||||
}
|
||||
}
|
||||
|
||||
if reviewJSON {
|
||||
data, err := json.MarshalIndent(ReviewOutput{
|
||||
Repository: repoFullName,
|
||||
MyPRs: myPRs,
|
||||
Requested: requestedPRs,
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.Print("%s\n", string(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
if showMine {
|
||||
if err := printMyPRs(myPRs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -132,7 +169,7 @@ func runReview() error {
|
|||
if showMine {
|
||||
cli.Blank()
|
||||
}
|
||||
if err := showRequestedReviews(ctx, repoFullName); err != nil {
|
||||
if err := printRequestedPRs(requestedPRs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -140,13 +177,8 @@ func runReview() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// showMyPRs shows the user's open PRs with status
|
||||
func showMyPRs(ctx context.Context, repo string) error {
|
||||
prs, err := fetchPRs(ctx, repo, "author:@me")
|
||||
if err != nil {
|
||||
return log.E("qa.review", "failed to fetch your PRs", err)
|
||||
}
|
||||
|
||||
// printMyPRs shows the user's open PRs with status
|
||||
func printMyPRs(prs []PullRequest) error {
|
||||
if len(prs) == 0 {
|
||||
cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.qa.review.no_prs")))
|
||||
return nil
|
||||
|
|
@ -161,13 +193,8 @@ func showMyPRs(ctx context.Context, repo string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// showRequestedReviews shows PRs where user's review is requested
|
||||
func showRequestedReviews(ctx context.Context, repo string) error {
|
||||
prs, err := fetchPRs(ctx, repo, "review-requested:@me")
|
||||
if err != nil {
|
||||
return log.E("qa.review", "failed to fetch review requests", err)
|
||||
}
|
||||
|
||||
// printRequestedPRs shows PRs where user's review is requested
|
||||
func printRequestedPRs(prs []PullRequest) error {
|
||||
if len(prs) == 0 {
|
||||
cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.qa.review.no_reviews")))
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue