| 45 | } |
| 46 | |
| 47 | func parseScanFlags(args []string) (scanOptions, error) { |
| 48 | a := newOcrFlagSet("ocr scan") |
| 49 | opts := scanOptions{} |
| 50 | |
| 51 | a.StringVar(&opts.toolConfigPath, "tools", "", "path to JSON tools config file (default: embedded)") |
| 52 | a.StringVar(&opts.rulePath, "rule", "", "path to JSON file with system review rules") |
| 53 | a.StringVar(&opts.repoDir, "repo", "", "root directory of the git repository (default: current dir)") |
| 54 | a.StringVar(&opts.paths, "path", "", "comma-separated repo-relative directories or files to scan (default: whole repo)") |
| 55 | a.StringVar(&opts.excludes, "exclude", "", "comma-separated gitignore-style patterns to exclude; merged with rule.json excludes") |
| 56 | a.StringVarP(&opts.outputFormat, "format", "f", "text", "output format: text or json") |
| 57 | a.IntVar(&opts.concurrency, "concurrency", 8, "max concurrent file scans") |
| 58 | a.IntVar(&opts.perFileTimeout, "timeout", 10, "concurrent task timeout in minutes") |
| 59 | a.StringVar(&opts.audience, "audience", "human", "output audience: human (show progress) or agent (summary only)") |
| 60 | a.StringVarP(&opts.background, "background", "b", "", "optional requirement/business context for the scan") |
| 61 | a.IntVar(&opts.maxTools, "max-tools", 0, "max tool call rounds per file; only takes effect when greater than template default") |
| 62 | a.IntVar(&opts.maxGitProcs, "max-git-procs", 16, "max concurrent git subprocesses") |
| 63 | a.BoolVarP(&opts.preview, "preview", "p", false, "preview which files will be scanned without running the LLM") |
| 64 | a.BoolVar(&opts.noPlan, "no-plan", false, "skip the per-file PLAN_TASK pre-pass (one fewer LLM call per file; may reduce review focus)") |
| 65 | a.BoolVar(&opts.noDedup, "no-dedup", false, "skip the per-batch DEDUP_TASK (keeps raw comments; one fewer LLM call per batch)") |
| 66 | a.BoolVar(&opts.noSummary, "no-summary", false, "skip the post-run PROJECT_SUMMARY_TASK (no project-level markdown summary)") |
| 67 | a.StringVar(&opts.batch, "batch", "", "override BATCH_STRATEGY from scan template: none | by-language | by-directory") |
| 68 | a.IntVar(&opts.maxTokensBudget, "max-tokens-budget", 0, "cap total token usage (input+output); dispatch stops once exceeded (0 = unlimited)") |
| 69 | a.StringVar(&opts.model, "model", "", "override LLM model for this scan (e.g., claude-opus-4-6)") |
| 70 | |
| 71 | if err := a.Parse(args); err != nil { |
| 72 | return opts, fmt.Errorf("parse flags: %w", err) |
| 73 | } |
| 74 | |
| 75 | opts.showHelp = a.showHelp |
| 76 | if opts.showHelp { |
| 77 | return opts, nil |
| 78 | } |
| 79 | |
| 80 | switch opts.audience { |
| 81 | case "human", "agent": |
| 82 | default: |
| 83 | return opts, fmt.Errorf("invalid --audience value %q: must be 'human' or 'agent'", opts.audience) |
| 84 | } |
| 85 | |
| 86 | if opts.maxTools < 0 { |
| 87 | return opts, fmt.Errorf("--max-tools must be a non-negative integer (0 means use template default)") |
| 88 | } |
| 89 | if opts.maxGitProcs < 0 { |
| 90 | return opts, fmt.Errorf("--max-git-procs must be a non-negative integer (0 means use default 16)") |
| 91 | } |
| 92 | if opts.maxTokensBudget < 0 { |
| 93 | return opts, fmt.Errorf("--max-tokens-budget must be a non-negative integer (0 means unlimited)") |
| 94 | } |
| 95 | return opts, nil |
| 96 | } |
| 97 | |
| 98 | func splitPaths(raw string) []string { |
| 99 | if raw == "" { |