a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
278 lines
9.3 KiB
Go
278 lines
9.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/zzet/gortex/internal/review"
|
|
)
|
|
|
|
var (
|
|
reviewScope string
|
|
reviewBase string
|
|
reviewDiff string
|
|
reviewUseLLM bool
|
|
reviewFormat string
|
|
reviewAudience string
|
|
reviewRepo string
|
|
reviewPost bool
|
|
reviewPR int
|
|
reviewConfirmPublic bool
|
|
reviewDryRun bool
|
|
)
|
|
|
|
// reviewDaemonTool is the daemon-tool relay seam. It is indirected through a
|
|
// package var so a test can stub the daemon call without a running daemon.
|
|
var reviewDaemonTool = requireDaemonTool
|
|
|
|
var reviewCmd = &cobra.Command{
|
|
Use: "review [path]",
|
|
Short: "Review a changeset and print line-anchored inline comments + a verdict",
|
|
Long: `Reviews a changeset against the daemon that owns the repo and prints the
|
|
review verdict (BLOCK / REVIEW / APPROVE), a one-line summary, the per-file risk
|
|
ranking, and the line-anchored inline comments — each anchored to an exact
|
|
file + line so it reads like a PR review.
|
|
|
|
The changeset is selected by --scope (unstaged / staged / all / compare), or by
|
|
--base <ref> (shorthand for comparing against that ref), or from a pasted
|
|
unified diff via --diff <file> (use "-" to read the diff from stdin). The
|
|
deterministic correctness rulepack always runs; pass --use-llm to additionally
|
|
fold in LLM-found findings (requires a configured LLM provider).
|
|
|
|
Requires a running daemon that tracks the repo. Use --format json for the
|
|
structured report.`,
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: runReview,
|
|
}
|
|
|
|
func init() {
|
|
reviewCmd.Flags().StringVarP(&reviewScope, "scope", "s", "unstaged", "changeset scope: unstaged, staged, all, or compare")
|
|
reviewCmd.Flags().StringVarP(&reviewBase, "base", "b", "", "base git ref to compare against (forces compare scope)")
|
|
reviewCmd.Flags().StringVarP(&reviewDiff, "diff", "d", "", "review a pasted unified diff from a file (\"-\" for stdin) instead of git")
|
|
reviewCmd.Flags().BoolVar(&reviewUseLLM, "use-llm", false, "fold in LLM-found findings (requires a configured LLM provider)")
|
|
reviewCmd.Flags().StringVarP(&reviewFormat, "format", "f", "text", "output format: text or json")
|
|
reviewCmd.Flags().StringVar(&reviewAudience, "audience", "human", "text-render audience: human (readable packet) or agent (terse machine-first summary); --format json overrides")
|
|
reviewCmd.Flags().StringVar(&reviewRepo, "repo", "", "repository path the daemon must track (default: current directory)")
|
|
reviewCmd.Flags().BoolVar(&reviewPost, "post", false, "post the findings as inline comments on a PR (requires --pr); secrets are redacted before egress")
|
|
reviewCmd.Flags().IntVar(&reviewPR, "pr", 0, "the PR / MR number to post comments on (with --post)")
|
|
reviewCmd.Flags().BoolVar(&reviewConfirmPublic, "confirm-public", false, "confirm posting to a public / fork PR (world-readable comments)")
|
|
reviewCmd.Flags().BoolVar(&reviewDryRun, "dry-run", false, "with --post, build and print the would-post (already-redacted) payloads without posting")
|
|
rootCmd.AddCommand(reviewCmd)
|
|
}
|
|
|
|
func runReview(cmd *cobra.Command, args []string) error {
|
|
repoPath := "."
|
|
if reviewRepo != "" {
|
|
repoPath = reviewRepo
|
|
} else if len(args) > 0 {
|
|
repoPath = args[0]
|
|
}
|
|
|
|
var diffText string
|
|
if reviewDiff != "" {
|
|
dt, err := readReviewDiff(cmd, reviewDiff)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
diffText = dt
|
|
}
|
|
|
|
// Posting path: relay to the post_review daemon tool, which derives the
|
|
// findings server-side (running the review rulepack over the same changeset),
|
|
// redacts every comment body before egress, and gates a public / fork PR.
|
|
if reviewPost {
|
|
return runReviewPost(cmd, repoPath, diffText)
|
|
}
|
|
|
|
toolArgs := map[string]any{
|
|
"scope": reviewScope,
|
|
"use_llm": reviewUseLLM,
|
|
"format": "json", // the CLI always parses JSON; rendering is local
|
|
}
|
|
if reviewBase != "" {
|
|
toolArgs["base"] = reviewBase
|
|
}
|
|
if reviewRepo != "" {
|
|
toolArgs["repo"] = reviewRepo
|
|
}
|
|
if diffText != "" {
|
|
toolArgs["diff"] = diffText
|
|
}
|
|
|
|
raw, err := reviewDaemonTool(repoPath, "review", toolArgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if reviewFormat == "json" {
|
|
return emitDaemonJSON(cmd, raw)
|
|
}
|
|
return printReview(cmd, raw)
|
|
}
|
|
|
|
// runReviewPost relays a review-and-post request to the post_review daemon tool.
|
|
// The daemon derives the findings from the changeset, redacts every comment body
|
|
// before any payload leaves the machine, and refuses a public / fork PR unless
|
|
// --confirm-public is set.
|
|
func runReviewPost(cmd *cobra.Command, repoPath, diffText string) error {
|
|
if reviewPR <= 0 {
|
|
return fmt.Errorf("--post requires --pr <number> (the PR / MR to comment on)")
|
|
}
|
|
toolArgs := map[string]any{
|
|
"number": reviewPR,
|
|
"scope": reviewScope,
|
|
"confirm_public": reviewConfirmPublic,
|
|
"dry_run": reviewDryRun,
|
|
"format": "json",
|
|
}
|
|
if reviewBase != "" {
|
|
toolArgs["base"] = reviewBase
|
|
}
|
|
if reviewRepo != "" {
|
|
toolArgs["repo"] = reviewRepo
|
|
}
|
|
if diffText != "" {
|
|
toolArgs["diff"] = diffText
|
|
}
|
|
|
|
raw, err := reviewDaemonTool(repoPath, "post_review", toolArgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return emitDaemonJSON(cmd, raw)
|
|
}
|
|
|
|
// readReviewDiff reads the pasted-diff source: "-" reads from stdin, anything
|
|
// else is a file path.
|
|
func readReviewDiff(cmd *cobra.Command, src string) (string, error) {
|
|
if src == "-" {
|
|
data, err := io.ReadAll(cmd.InOrStdin())
|
|
if err != nil {
|
|
return "", fmt.Errorf("reading diff from stdin: %w", err)
|
|
}
|
|
return string(data), nil
|
|
}
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
return "", fmt.Errorf("reading diff file %s: %w", src, err)
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
// reviewPayloadCLI mirrors the review tool's wire shape the CLI renders.
|
|
type reviewPayloadCLI struct {
|
|
Verdict string `json:"verdict"`
|
|
Summary string `json:"summary"`
|
|
Total int `json:"total"`
|
|
Comments []struct {
|
|
File string `json:"file"`
|
|
Line int `json:"line"`
|
|
Severity string `json:"severity"`
|
|
Message string `json:"message"`
|
|
Rule string `json:"rule"`
|
|
Category string `json:"category"`
|
|
Source string `json:"source"`
|
|
} `json:"comments"`
|
|
FileRisk []struct {
|
|
File string `json:"file"`
|
|
Risk string `json:"risk"`
|
|
Findings int `json:"findings"`
|
|
Affected int `json:"affected"`
|
|
Symbols int `json:"symbols"`
|
|
Uncovered int `json:"uncovered"`
|
|
} `json:"file_risk"`
|
|
Depth string `json:"depth"`
|
|
Cost *struct {
|
|
InputTokens int `json:"input_tokens"`
|
|
OutputTokens int `json:"output_tokens"`
|
|
CacheReadTokens int `json:"cache_read_tokens"`
|
|
CacheWriteTokens int `json:"cache_write_tokens"`
|
|
USD float64 `json:"usd"`
|
|
Estimated bool `json:"estimated"`
|
|
ElapsedMs int64 `json:"elapsed_ms"`
|
|
} `json:"cost"`
|
|
// degradation / error shape
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// reviewReportFromPayload reconstructs the canonical review.ReviewReport from the
|
|
// daemon's wire payload so the CLI can drive review.RenderSummary — the single
|
|
// source of truth for both the human and agent renderings. The wire uses
|
|
// `comments` for the findings and `risk` strings for the per-file ranking; this
|
|
// rebuilds the Finding / FileRisk / CostBreakdown shapes the renderer consumes.
|
|
func reviewReportFromPayload(p reviewPayloadCLI) *review.ReviewReport {
|
|
report := &review.ReviewReport{
|
|
Verdict: review.Verdict(p.Verdict),
|
|
Summary: p.Summary,
|
|
Depth: p.Depth,
|
|
}
|
|
if report.Verdict == "" {
|
|
report.Verdict = review.VerdictApprove
|
|
}
|
|
for _, c := range p.Comments {
|
|
report.Findings = append(report.Findings, review.Finding{
|
|
File: c.File,
|
|
Line: c.Line,
|
|
Severity: review.Severity(c.Severity),
|
|
Message: c.Message,
|
|
Rule: c.Rule,
|
|
Category: c.Category,
|
|
Source: c.Source,
|
|
})
|
|
}
|
|
for _, fr := range p.FileRisk {
|
|
report.FileRisk = append(report.FileRisk, review.FileRisk{
|
|
File: fr.File,
|
|
Risk: fr.Risk,
|
|
Findings: fr.Findings,
|
|
Affected: fr.Affected,
|
|
Symbols: fr.Symbols,
|
|
Uncovered: fr.Uncovered,
|
|
})
|
|
}
|
|
if p.Cost != nil {
|
|
report.Cost = &review.CostBreakdown{
|
|
InputTokens: p.Cost.InputTokens,
|
|
OutputTokens: p.Cost.OutputTokens,
|
|
CacheReadTokens: p.Cost.CacheReadTokens,
|
|
CacheWriteTokens: p.Cost.CacheWriteTokens,
|
|
USD: p.Cost.USD,
|
|
Estimated: p.Cost.Estimated,
|
|
ElapsedMs: p.Cost.ElapsedMs,
|
|
}
|
|
}
|
|
return report
|
|
}
|
|
|
|
// printReview renders the verdict, summary, per-file risk, and the line-anchored
|
|
// inline comments grouped by file and ordered by line.
|
|
func printReview(cmd *cobra.Command, raw json.RawMessage) error {
|
|
out := cmd.OutOrStdout()
|
|
var p reviewPayloadCLI
|
|
if err := json.Unmarshal(raw, &p); err != nil {
|
|
// Unknown shape — fall back to pretty JSON rather than fail.
|
|
return emitDaemonJSON(cmd, raw)
|
|
}
|
|
if p.Error != "" {
|
|
fmt.Fprintf(out, "review failed: %s\n", p.Error)
|
|
return nil
|
|
}
|
|
|
|
// Both audiences render through the canonical review renderer so the CLI,
|
|
// the MCP text rendering, and any sub-agent shelling the verb see the
|
|
// same packet — the human path used to keep a hand-rolled duplicate here,
|
|
// which silently dropped every field the renderer learned after it forked
|
|
// (coverage evidence, the rulepack-passed line).
|
|
if strings.EqualFold(reviewAudience, "agent") {
|
|
_, _ = io.WriteString(out, review.RenderSummary(reviewReportFromPayload(p), review.AudienceAgent))
|
|
return nil
|
|
}
|
|
_, _ = io.WriteString(out, review.RenderSummary(reviewReportFromPayload(p), review.AudienceHuman))
|
|
return nil
|
|
}
|