| 117 | } |
| 118 | |
| 119 | func parseReviewFlags(args []string) (reviewOptions, error) { |
| 120 | a := newOcrFlagSet("ocr review") |
| 121 | |
| 122 | opts := reviewOptions{} |
| 123 | |
| 124 | a.StringVar(&opts.toolConfigPath, "tools", "", "path to JSON tools config file (default: embedded)") |
| 125 | a.StringVar(&opts.rulePath, "rule", "", "path to JSON file with system review rules") |
| 126 | a.StringVar(&opts.repoDir, "repo", "", "root directory of the git repository (default: current dir)") |
| 127 | a.StringVar(&opts.from, "from", "", "source ref to start diff from (e.g., 'main')") |
| 128 | a.StringVar(&opts.to, "to", "", "target ref to end diff at (e.g., 'feature-branch')") |
| 129 | a.StringVarP(&opts.commit, "commit", "c", "", "single commit hash or tag to review (vs its parent)") |
| 130 | a.StringVar(&opts.resume, "resume", "", "resume from a previous review session id") |
| 131 | a.StringVar(&opts.excludes, "exclude", "", "comma-separated gitignore-style patterns to exclude; merged with rule.json excludes") |
| 132 | a.StringVarP(&opts.outputFormat, "format", "f", "text", "output format: text or json") |
| 133 | a.IntVar(&opts.concurrency, "concurrency", 8, "max concurrent file reviews") |
| 134 | a.IntVar(&opts.perFileTimeout, "timeout", 10, "concurrent task timeout in minutes") |
| 135 | a.StringVar(&opts.audience, "audience", "human", "output audience: human (show progress) or agent (summary only)") |
| 136 | a.StringVarP(&opts.background, "background", "b", "", "optional requirement/business context for the review") |
| 137 | a.StringVarP(&opts.backgroundFile, "background-file", "B", "", "optional requirement/business context from a Markdown file (combined with --background; inline value appears first when both are set)") |
| 138 | a.StringVar(&opts.model, "model", "", "override LLM model for this review (e.g., claude-opus-4-6)") |
| 139 | a.IntVar(&opts.maxTools, "max-tools", 0, "max tool call rounds per file (0 = template default; min 10)") |
| 140 | a.IntVar(&opts.maxGitProcs, "max-git-procs", 16, "max concurrent git subprocesses") |
| 141 | a.BoolVarP(&opts.preview, "preview", "p", false, "preview which files will be reviewed without running the LLM") |
| 142 | |
| 143 | if err := a.Parse(args); err != nil { |
| 144 | return opts, fmt.Errorf("parse flags: %w", err) |
| 145 | } |
| 146 | |
| 147 | opts.showHelp = a.showHelp |
| 148 | if opts.showHelp { |
| 149 | return opts, nil |
| 150 | } |
| 151 | |
| 152 | modeCount := 0 |
| 153 | if opts.from != "" || opts.to != "" { |
| 154 | modeCount++ |
| 155 | } |
| 156 | if opts.commit != "" { |
| 157 | modeCount++ |
| 158 | } |
| 159 | // modeCount == 0 → workspace mode (no error, allowed) |
| 160 | if modeCount > 1 { |
| 161 | return opts, fmt.Errorf("only one review mode allowed (--from/--to or --commit)") |
| 162 | } |
| 163 | if opts.from != "" && opts.to == "" { |
| 164 | return opts, fmt.Errorf("--to is required when --from is specified") |
| 165 | } |
| 166 | if opts.to != "" && opts.from == "" { |
| 167 | return opts, fmt.Errorf("--from is required when --to is specified") |
| 168 | } |
| 169 | if opts.preview && opts.resume != "" { |
| 170 | return opts, fmt.Errorf("--preview and --resume cannot be used together") |
| 171 | } |
| 172 | |
| 173 | switch opts.audience { |
| 174 | case "human", "agent": |
| 175 | default: |
| 176 | return opts, fmt.Errorf("invalid --audience value %q: must be 'human' or 'agent'", opts.audience) |