// runRepo + runScrum are Phase B entry points. Phase A leaves them // as compilable stubs that produce the JSON shapes the gates expect // but with zero analyzer findings — letting the pipeline structure // be exercised end-to-end before analyzers land. package cli import ( "context" "flag" "fmt" "os" "path/filepath" "local-review-harness/internal/config" "local-review-harness/internal/git" "local-review-harness/internal/pipeline" ) func runRepo(ctx context.Context, repoPath string, cf commonFlags) int { if _, err := os.Stat(repoPath); err != nil { fmt.Fprintln(os.Stderr, "repo: target path:", err) return 65 } rp, err := config.LoadReviewProfile(cf.reviewProfilePath) if err != nil { fmt.Fprintln(os.Stderr, "config:", err) return 65 } mp, err := config.LoadModelProfile(cf.modelProfilePath) if err != nil { fmt.Fprintln(os.Stderr, "config:", err) return 65 } outDir := resolveOutputDir(&cf, rp, repoPath) res, err := pipeline.RunRepo(ctx, pipeline.Inputs{ RepoPath: repoPath, ReviewProfile: rp, ModelProfile: mp, OutputDir: outDir, EmitScrum: false, EnableLLM: cf.enableLLM, }) if err != nil { fmt.Fprintln(os.Stderr, "pipeline:", err) return 65 } for _, f := range res.OutputFiles { fmt.Println(filepath.Join(outDir, f)) } return res.ExitCode } // Diff is the `review-harness diff ` subcommand entry point // (Phase E). Detects changed files via git (unstaged + staged + vs // branch base), then runs the same pipeline as `repo` but scoped to // just those files. PROMPT.md mode 2: "diff review." func Diff(args []string) int { fs := flag.NewFlagSet("diff", flag.ContinueOnError) var cf commonFlags bindCommonFlags(fs, &cf) if err := fs.Parse(args); err != nil { return 64 } if fs.NArg() < 1 { fmt.Fprintln(os.Stderr, "diff: missing target path") return 64 } repoPath := fs.Arg(0) return runDiff(context.Background(), repoPath, cf) } func runDiff(ctx context.Context, repoPath string, cf commonFlags) int { if _, err := os.Stat(repoPath); err != nil { fmt.Fprintln(os.Stderr, "diff: target path:", err) return 65 } rp, err := config.LoadReviewProfile(cf.reviewProfilePath) if err != nil { fmt.Fprintln(os.Stderr, "config:", err) return 65 } mp, err := config.LoadModelProfile(cf.modelProfilePath) if err != nil { fmt.Fprintln(os.Stderr, "config:", err) return 65 } changed, err := git.ChangedFiles(ctx, repoPath) if err != nil { fmt.Fprintln(os.Stderr, "diff: git probe:", err) return 65 } if len(changed) == 0 { fmt.Fprintln(os.Stderr, "diff: no changed files (clean tree, no commits ahead of main/master) — exit 0") return 0 } fmt.Fprintf(os.Stderr, "diff: scanning %d changed file(s):\n", len(changed)) for _, f := range changed { fmt.Fprintln(os.Stderr, " -", f) } outDir := resolveOutputDir(&cf, rp, repoPath) res, err := pipeline.RunRepo(ctx, pipeline.Inputs{ RepoPath: repoPath, ReviewProfile: rp, ModelProfile: mp, OutputDir: outDir, EmitScrum: true, // diff mode emits the scrum bundle so PR reviewers see it EnableLLM: cf.enableLLM, DiffOnlyFiles: changed, }) if err != nil { fmt.Fprintln(os.Stderr, "pipeline:", err) return 65 } for _, f := range res.OutputFiles { fmt.Println(filepath.Join(outDir, f)) } return res.ExitCode } func runScrum(ctx context.Context, repoPath string, cf commonFlags) int { if _, err := os.Stat(repoPath); err != nil { fmt.Fprintln(os.Stderr, "scrum: target path:", err) return 65 } rp, err := config.LoadReviewProfile(cf.reviewProfilePath) if err != nil { fmt.Fprintln(os.Stderr, "config:", err) return 65 } mp, err := config.LoadModelProfile(cf.modelProfilePath) if err != nil { fmt.Fprintln(os.Stderr, "config:", err) return 65 } outDir := resolveOutputDir(&cf, rp, repoPath) res, err := pipeline.RunRepo(ctx, pipeline.Inputs{ RepoPath: repoPath, ReviewProfile: rp, ModelProfile: mp, OutputDir: outDir, EmitScrum: true, EnableLLM: cf.enableLLM, }) if err != nil { fmt.Fprintln(os.Stderr, "pipeline:", err) return 65 } for _, f := range res.OutputFiles { fmt.Println(filepath.Join(outDir, f)) } return res.ExitCode }