diff --git a/README.md b/README.md index 54ed8bd..9f8b3ce 100644 --- a/README.md +++ b/README.md @@ -1,174 +1,232 @@ -# ai-git-hooks / push-review +# ai-pushgate -A language-agnostic `pre-push` hook that runs your linters and tests against changed files, then asks Claude to review the diff before every push. +`ai-pushgate` is a local pre-push gate plus CI/PR enforcement workflow. -## How it works +## Product Contract -``` +```text git push │ ▼ ┌─────────────────────────────────────┐ -│ Changed files vs target branch │ -│ (ignore_paths filtering applied) │ +│ Changed files vs project.base_ref │ +│ (include/exclude policy applied) │ └──────────────┬──────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ -│ Run configured tools │ +│ Run deterministic checks │ │ (linters, type checkers, tests) │ -│ ✗ any failure → push blocked │ +│ ✗ blocking failure → push blocked │ └──────────────┬──────────────────────┘ - │ all pass + │ checks pass ▼ ┌─────────────────────────────────────┐ -│ AI review via Claude Code CLI │ +│ AI review via configured provider │ │ (diff sent, findings returned) │ │ BLOCK → push blocked │ │ PASS → push proceeds │ └─────────────────────────────────────┘ ``` -## Install +`ai-pushgate` separates fast local feedback from authoritative enforcement: -```bash -# Default (base template — no tools pre-configured, fully documented) -curl -fsSL https://raw.githubusercontent.com/rootstrap/ai-git-hooks/main/install.sh | bash +| Surface | Purpose | Blocking authority | +|---|---|---| +| Local pre-push | Fast deterministic checks before code leaves a developer machine | Convenience only; always bypassable | +| Local AI | Review changed files near the developer workflow | Blocks by default; always bypassable | +| CI/PR | Repeatable checks, review summaries, and policy enforcement | Source of truth for teams | + +Local hooks help developers catch issues early, but they are not a security or +compliance boundary. Anything that must be enforced for a team belongs in CI/PR +checks combined with repository branch protection. -# Node.js -curl -fsSL https://raw.githubusercontent.com/rootstrap/ai-git-hooks/main/install.sh | bash -s -- --template node +## Defaults -# TypeScript -curl -fsSL https://raw.githubusercontent.com/rootstrap/ai-git-hooks/main/install.sh | bash -s -- --template typescript +- Config file: `.pushgate.yml`. +- Deterministic local checks are the default local gate. +- Local AI defaults to `blocking` when configured and available. +- Blocking AI findings stop the local push until they are fixed or skipped. +- AI payloads default to diff-only context. +- Full-file AI context is opt-in. +- Secret redaction is expected before any source content is sent to an AI + provider. -# Next.js -curl -fsSL https://raw.githubusercontent.com/rootstrap/ai-git-hooks/main/install.sh | bash -s -- --template nextjs +## Install + +Run the installer from the Git repository where pushgate should guard pushes: -# Ruby -curl -fsSL https://raw.githubusercontent.com/rootstrap/ai-git-hooks/main/install.sh | bash -s -- --template ruby +```bash +# Default base template +curl -fsSL https://raw.githubusercontent.com/rootstrap/ai-pushgate/main/install.sh | bash -# Ruby on Rails -curl -fsSL https://raw.githubusercontent.com/rootstrap/ai-git-hooks/main/install.sh | bash -s -- --template rails +# Stack-specific template +curl -fsSL https://raw.githubusercontent.com/rootstrap/ai-pushgate/main/install.sh \ + | bash -s -- --template ``` -The installer: +Available templates are `base`, `node`, `typescript`, `nextjs`, `ruby`, and +`rails`. -1. Downloads and validates `hook/pre-push` → `.git/hooks/pre-push` -2. Backs up any existing `pre-push` hook before overwriting -3. Downloads the template config → `.push-review.yml` (only on first install — never overwrites) -4. Checks for Claude Code CLI and warns about missing runtimes +The installer: -## Requirements +1. Installs or verifies the `pushgate` runner. +2. Installs `.git/hooks/pre-push` as a thin delegator to `pushgate pre-push`. +3. Backs up an existing `pre-push` hook before replacing it. +4. Writes the selected template to `.pushgate.yml` when that config does not + already exist. +5. Checks configured tool and AI provider dependencies. -**Claude Code CLI** (required for AI review): +If `pushgate` is already installed, the CLI can set up a repository directly: ```bash -npm install -g @anthropic-ai/claude-code -claude /login +pushgate install --template ``` -**Runtime dependencies** depend on the tools you configure: - -| Runtime | Required by | -|---------|-------------| -| Node.js | `node`, `typescript`, `nextjs` templates | -| Ruby | `ruby`, `rails` templates | -| Python | Python tools (manual config) | -| Go | Go tools (manual config) | - -The installer checks which runtimes your config requires and warns about any that are missing. If Claude Code CLI is not installed, the hook still runs tool checks — it only skips the AI review step. - ## Configuration -After install, edit `.push-review.yml` in your project root: +The config contract is: ```yaml -agent: - # Claude model used for AI review. Requires Claude Code CLI (claude /login). - model: claude-sonnet-4-20250514 - -review: - target_branch: main # diff base: git diff ...HEAD - context_lines: 10 # surrounding context lines included in the diff - max_lines_for_full_file: 300 # below this threshold, full file contents are sent - # instead of just the diff for richer context - - # Topics the AI reviewer focuses on - focus: - - security - - logic_errors - - test_coverage - - performance - - naming_and_readability - - # Findings in these categories block the push - blocking_categories: - - security - - logic_errors - - # Findings in these categories are printed as warnings but never block - warning_categories: - - test_coverage - - performance - - naming_and_readability - -# Tools to run before AI review — first failure blocks the push immediately -tools: +version: 2 + +project: + base_ref: main + include_paths: + - "**/*" + exclude_paths: + - "*.lock" + - "dist/**" + - "coverage/**" + +local: + fail_fast: true + budget_seconds: 60 + +checks: - name: eslint - command: npx eslint {changed_files} # {changed_files} is replaced at runtime + command: ["npx", "eslint", "{changed_files}"] + mode: blocking + run: changed_files extensions: [".js", ".jsx", ".ts", ".tsx"] + timeout_seconds: 30 + + - name: tests + command: ["npm", "test"] + mode: warning + run: always + timeout_seconds: 60 + +ai: + mode: blocking # off | advisory | blocking + provider: + name: claude + privacy: + send_diff: true + send_full_files: false + redact_secrets: true + +ci: + mirror_blocking_checks: true +``` + +### Config Fields + +`project.base_ref` defines the comparison base used to collect changed files. +`project.include_paths` and `project.exclude_paths` define the shared path +policy for deterministic checks and optional AI. + +`checks[]` defines deterministic commands. Commands are argv arrays, not shell +strings, so changed files can be passed safely as discrete arguments. A check can +run on changed files or the whole project, and can be `blocking` or `warning`. + +`ai.mode` controls local AI behavior: + +| Mode | Behavior | +|---|---| +| `off` | Do not run local AI | +| `advisory` | Run local AI when available, print findings, never block | +| `blocking` | Run local AI and block `BLOCK` findings until fixed or skipped | - - name: brakeman - command: bundle exec brakeman --no-pager --quiet - # no {changed_files} → runs on the whole project +`ci` is reserved for generated or documented CI mirror behavior. CI/PR policy is +where teams should enforce required checks. -# Files and patterns excluded from tool checks and AI review -ignore_paths: - - "*.lock" - - "dist/**" - - "coverage/**" +## Git Workflow Integration + +The default developer workflow should remain regular Git: + +```bash +git push ``` -## Available templates +The installed `.git/hooks/pre-push` hook is a thin delegator that invokes the +versioned runner: -| `--template` | Stack | Tools pre-configured | -|---|---|---| -| `base` | Any | None (fully-documented reference config) | -| `node` | Node.js | ESLint, Prettier, Jest | -| `typescript` | TypeScript | tsc, ESLint, Prettier, Jest | -| `nextjs` | Next.js | tsc, next lint, Prettier, Jest | -| `ruby` | Ruby | RuboCop, Reek, RSpec | -| `rails` | Ruby on Rails | RuboCop, Reek, Brakeman, RSpec | +```bash +pushgate pre-push +``` + +The hook is responsible for passing along Git hook input and exiting with the +runner's exit code. The runner owns config loading, changed-file detection, +deterministic checks, optional local AI, and user-facing output. + +The `pushgate push` wrapper is an ergonomic layer for flags Git cannot pass to +hooks, not a replacement for the normal Git workflow. It should translate +pushgate-specific flags into temporary Git config and then call `git push`. -## Skip checks +## Skip Controls -To bypass the hook for a single push: +Raw `git push` cannot pass arbitrary `--skip-*` flags to a pre-push hook; Git +rejects unknown `git push` flags before the hook runs. For a one-off skip in the +regular Git workflow, pass temporary pushgate config with Git: ```bash -git push --no-verify +git -c pushgate.skip-ai-check=true push +git -c pushgate.skip-all-checks=true push ``` -## Updating +`pushgate.skip-ai-check` skips only local AI while preserving deterministic +checks. `pushgate.skip-all-checks` skips all local pushgate behavior for that +push. -Re-run the installer to update the hook script. Your `.push-review.yml` is **never overwritten** — it stays exactly as you've configured it. +The `pushgate push` wrapper provides shorter equivalents: ```bash -curl -fsSL https://raw.githubusercontent.com/rootstrap/ai-git-hooks/main/install.sh | bash +pushgate push --skip-ai-check +pushgate push --skip-all-checks ``` -To also reset your config to a template, delete it first: +The wrapper should apply the matching temporary Git config before it calls +`git push`. + +Git also keeps its native escape hatch: ```bash -rm .push-review.yml -curl -fsSL https://raw.githubusercontent.com/rootstrap/ai-git-hooks/main/install.sh | bash -s -- --template +git push --no-verify ``` +`--no-verify` skips Git hooks entirely, including pushgate. + +## Privacy Contract + +The default AI payload is changed-file metadata and diff context only. Full-file +context must be enabled explicitly because it can increase latency, cost, and +privacy exposure. + +Before any AI call, pushgate should apply the configured path policy and secret +redaction. If redaction cannot run, local AI should fail closed for privacy by +skipping AI feedback instead of sending unredacted content. + ## Contributing -To add a new template: +All changes should go through a pull request. Release files are managed by +release-please and should not be edited manually. -1. Add `templates/.yml` following the structure of an existing template (e.g. `ruby.yml`) -2. Add a row to the **Available templates** table in this README -3. Open a pull request +Before opening a pull request, verify that shell scripts still parse and that +template YAML remains valid: -Templates should include sensible `ignore_paths` defaults and pre-configured `tools` for the common tools in that stack. The `base.yml` template is the reference for all available config options. +```bash +bash -n hook/pre-push +bash -n install.sh +for f in templates/*.yml; do python3 -c "import yaml; yaml.safe_load(open('$f'))"; done +```