(args: Args)
| 420 | // Conforms to the #253 replacement spec: clean checkout, unique run dir, no GitHub |
| 421 | // token, and reject unsupported repos (never fall back to a foreign profile). |
| 422 | function localReviewCommand(args: Args): void { |
| 423 | const targetDir = resolve(argString(args, "target_dir", ".")); |
| 424 | const baseBranch = argString(args, "base", "main"); |
| 425 | const reportDir = resolve( |
| 426 | argString(args, "report_dir", join(homedir(), ".clawsweeper-local-reviews")), |
| 427 | ); |
| 428 | |
| 429 | // Spec: genuinely offline — withhold every GitHub credential from the review engine. |
| 430 | for (const tokenVar of LOCAL_REVIEW_SCRUBBED_TOKEN_ENV) { |
| 431 | delete process.env[tokenVar]; |
| 432 | } |
| 433 | |
| 434 | // Spec: committed-range review requires a clean checkout (no hidden staged/untracked work). |
| 435 | const dirtyTree = run("git", ["status", "--porcelain"], { cwd: targetDir }).trim(); |
| 436 | if (dirtyTree) { |
| 437 | console.error(`[local-review] working tree not clean — commit or stash first:\n${dirtyTree}`); |
| 438 | process.exit(1); |
| 439 | } |
| 440 | |
| 441 | const targetRepo = |
| 442 | argString(args, "target_repo", "") || |
| 443 | run("git", ["remote", "get-url", "origin"], { cwd: targetDir }) |
| 444 | .replace(/.*github\.com[:/]/, "") |
| 445 | .replace(/\.git\s*$/, "") |
| 446 | .trim(); |
| 447 | |
| 448 | // Spec: reject unsupported repos — never silently fall back to a foreign profile. |
| 449 | const profile = configuredRepositoryProfileFor(targetRepo); |
| 450 | if (!profile) { |
| 451 | console.error( |
| 452 | `[local-review] no review profile for '${targetRepo}'. Add a repository profile, or pass --target-repo <known-repo>.`, |
| 453 | ); |
| 454 | process.exit(1); |
| 455 | } |
| 456 | const profileSlug = profile.slug; |
| 457 | |
| 458 | // Range = merge-base(base, HEAD)..HEAD — the whole branch, reviewed as one unit. |
| 459 | const headSha = run("git", ["rev-parse", "HEAD"], { cwd: targetDir }).trim(); |
| 460 | const baseSha = run("git", ["merge-base", baseBranch, "HEAD"], { cwd: targetDir }).trim(); |
| 461 | if (!baseSha || baseSha === headSha) { |
| 462 | console.error(`[local-review] no commits on HEAD beyond ${baseBranch} — nothing to review.`); |
| 463 | process.exit(1); |
| 464 | } |
| 465 | |
| 466 | const metadata = commitMetadata(targetDir, targetRepo, headSha, true); |
| 467 | |
| 468 | // Spec: unique per-run dir so concurrent runs never collide on result paths. |
| 469 | const runDir = join(reportDir, `run-${headSha.slice(0, 8)}-${Date.now()}-${process.pid}`); |
| 470 | ensureDir(runDir); |
| 471 | |
| 472 | // Spec: hard-enforce no GitHub access. The review prompt suggests `gh` for issue |
| 473 | // refs, and `gh` uses its own configured auth (token-env deletion can't stop it), |
| 474 | // so point it at an empty config dir — any `gh` the spawned reviewer runs finds |
| 475 | // no cached credentials. Belt-and-suspenders with Codex's read-only sandbox. |
| 476 | const ghEmptyConfig = join(runDir, ".gh-empty"); |
| 477 | ensureDir(ghEmptyConfig); |
| 478 | process.env.GH_CONFIG_DIR = ghEmptyConfig; |
| 479 |
no test coverage detected