A command-line tool that uses AI models to perform semantic guard reviews on git diffs.
Semantic guards are plain-English constraints about architectural and behavioral intent. Unlike tests or linters, they evaluate whether a code change respects the design rationale of your system.
This document explains the concept and how hrev implements it. Whether you use hrev or build your own guard pipeline, the same principles apply.
AI-assisted coding (Copilot, Cursor, Devin, etc.) expands the solution domain of your codebase. These tools generate code that compiles, passes tests, and looks idiomatic — but can silently violate your architecture because they were never trained on your design intent.
Without explicit constraints, every AI-assisted PR is a drift risk.
Examples of architectural drift that tests won't catch:
- A new endpoint bypasses the audit log because the developer (or AI) didn't know it existed.
- A utility function duplicates existing logic because the AI didn't search the codebase.
- A circuit breaker is removed because the diff looked clean but the rationale was lost.
- A feature is built as a web UI first because the AI defaulted to what it sees in training data, not your "MCP tool first" policy.
Tests verify correctness. Linters enforce style and structure. Neither captures intent.
npm install -g @the-harness-dev/hrevRequires Node ≥ 20.
Create hrev.yml in your repo root.
model: gpt-4o
rules:
- id: no-direct-aggregate-insert
description: "Never insert directly into aggregate tables. Always create source documents first and use edge methods."
severity: blocker
path: "src/services/"
- id: mcp-first-design
description: "New operations must be accessible via MCP server. Design as MCP tool first, web UI second."
severity: general
path: "src/"Model resolution (in order):
rule.model— per-rule overrideconfig.model— project defaultHREV_MODEL— environment variable- ❌ Error — no model specified
Works with any OpenAI-compatible endpoint: OpenAI, Anthropic (via proxy), Ollama, Groq, etc.
HREV_API_URL=https://api.openai.com/v1
HREV_API_KEY=sk-...Use AI rules for intent and behavior that a unit test cannot easily assert. Do not use AI rules for deterministic logic that a unit test or linter covers more reliably, faster, and at zero API cost.
Use AI Rules (hrev.yml) |
Use Unit Tests or Lint Instead |
|---|---|
| "Every public API must have a usage example in the docstring" | ✅ Import restrictions (no-restricted-imports) |
| "Do not add utility functions that duplicate existing ones" | ✅ Priority logic like model resolution hierarchy |
| "New features need both human-readable reasoning and a pass/fail verdict" | ✅ Aggregator pass/fail logic based on severity levels |
| "Agent nodes must log their prompts, tool calls, and results" | ✅ Path-constraint skip logic (startsWith checks) |
| "Error messages must explain why, not just what" | ✅ API key auto-detection branching (zero/one/multiple keys) |
-
If you can write a unit test for it, do not make it an AI rule. AI review costs tokens and time. A Jest or
node:testassertion runs in milliseconds for free. Examples: priority chains, conditional branches, return-code logic. -
If a linter enforces it, do not duplicate it in
hrev.yml.hrevevaluates plain-English descriptions. A custom ESLint rule with an autofix is more precise and requires no model context window. Examples: restricted imports, code style, naming conventions. -
If the rule is about semantic correctness or cross-file relationships, use AI. These require understanding intent across context a linter cannot see. Examples: architectural patterns, business rules, design rationale.
-
Keep descriptions short. Dense prompts bloat context windows and slow reviews. One or two sentences plus a concrete example per rule is ideal. If you find yourself writing a 200-line prompt, split it into multiple focused rules or move the logic into unit tests.
-
Use severity levels honestly. Reserve
blockerfor things that must fail CI. Usegeneralfor patterns that need human attention but should not block merging. Usenitfor style and readability suggestions.
# Quick setup in any project
hrev init
# Review staged changes
hrev
# Review a specific commit range
hrev --base main --head feature-branch
# Review a specific diff file
hrev --diff path/to/diff.patch
# See per-rule reasoning
hrev --verbose
# Machine-readable JSON output
hrev --jsonAdd code review to any existing workflow in one line.
- Create
hrev.ymlin your repo (semantic rules for your project) - Add the action to
.github/workflows/ci.yml:
name: CI
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required for git diff
- run: npm ci
- run: npm test
# One-line addition to any workflow
- uses: the-harness-dev/hrev@v1
with:
api-key: ${{ secrets.HREV_API_KEY }}- Add your API key as a repo secret:
Settings → Secrets → HREV_API_KEY
If you have GitHub Copilot, you already have access to GitHub Models:
- uses: the-harness-dev/hrev@v1
with:
api-url: https://models.github.ai
api-key: ${{ secrets.HREV_API_KEY }}
model: openai/gpt-4.1Create a fine-grained personal access token with the models:read scope at github.com/settings/tokens.
- uses: the-harness-dev/hrev@v1
with:
api-key: ${{ secrets.HREV_API_KEY }}
config-file: .github/hrev.yml
verbose: trueIf exactly one of these env vars is set, hrev will use it automatically:
# Zero-config if you already have OPENAI_API_KEY
- uses: the-harness-dev/hrev@v1Supported auto-detected variables: HREV_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GROQ_API_KEY
| Input | Default | Description |
|---|---|---|
api-url |
https://models.github.ai |
Model API endpoint |
api-key |
auto-detected | API key (GitHub Secret) |
model |
openai/gpt-4.1 |
Model identifier |
config-file |
hrev.yml |
Path to rules config |
base-ref |
PR base SHA | Git ref for diff comparison |
verbose |
false |
Show per-rule details in logs |
comment |
true |
Post review summary as PR comment |
status |
true |
Set commit status check (pass/fail) |
A codebase is protected by three layers of guards. hrev is the third. You need all three.
Unit tests, type checkers, linters, and static analysis. They verify what happens in a controlled, repeatable way.
- Precision: Exact — a test either passes or fails.
- Scope: One function, one module, one assertion.
- Cost: Near-zero (CPU time).
- Limitation: Cannot assert intent, design rationale, or cross-file architectural patterns.
Always use these first. Before you define a semantic guard, ask: "Can a unit test assert this?" If yes, write the test. If no, proceed.
Examples of deterministic guards:
- Import restrictions (
no-restricted-importsin ESLint) - Return types and type safety (TypeScript, mypy)
- Priority chains (unit test the function, not the intent)
- Branch coverage and conditional logic
- Path-constraint matching (
startsWithchecks)
Module boundaries, package rules, dependency injection, and directory conventions. They enforce what can depend on what, and where code can live.
- Precision: Exact — a module either imports from the right layer or it doesn't.
- Scope: One edge in the dependency graph.
- Cost: Near-zero (build time).
- Limitation: Cannot assert why a dependency exists, only that it does.
Examples of structural guards:
- Package-level import restrictions (e.g.,
src/domain/cannot import fromsrc/ui/) - Cyclic dependency detection
- Directory-based conventions (feature folders, Clean Architecture layers)
- API contract validation (OpenAPI, Protocol Buffers)
Plain-English rules that constrain intent. They evaluate whether a code change respects the design rationale that exists in the heads of your senior engineers but in no compiler, linter, or type system.
- Precision: Probabilistic — an LLM reads the diff and the rule, then reasons about alignment.
- Scope: Cross-file, cross-module, whole-system.
- Cost: Token cost and latency (expensive).
- Strength: Can assert things no other layer can.
When and only when to use semantic guards:
- The rule is about architectural or behavioral intent.
- The rule spans multiple files and requires contextual reasoning.
- No unit test or linter can practically assert it.
- A human code reviewer would need to be present to catch it.
Examples of semantic guards:
- "Every public API must have a usage example in the docstring."
- "Debits must equal credits before persisting a financial transaction."
- "New features must be accessible via MCP server before they get a web UI."
- "Do not silently ignore exceptions — every catch block must log, transform, or rethrow."
- "Adding a new utility function? Search the codebase first for an equivalent."
Do not use semantic guards for what deterministic guards do better, faster, and at zero token cost.
| Use Deterministic or Structural | Do Not Use Semantic Guards For |
|---|---|
| Import restrictions | "Only use LangChain providers" (use no-restricted-imports) |
| Return-code logic | "Fail the review if any blocker fails" (unit-test the aggregator) |
| Priority chains | "Resolve model in order: rule > project > env" (unit-test resolveModel) |
| Path matching | "Skip rules when diff doesn't touch the path" (unit-test startsWith logic) |
| Branching logic | "Auto-detect exactly one API key" (unit-test the conditional branches) |
| API contract shape | "All integrations must be OpenAI-compatible" (type-check the interface) |
Rule of thumb: If you can write a unit test or linter rule for it, doing so is more precise, faster, and eliminates LLM hallucination risk.
Whether you use hrev, a custom pipeline, or manual review, follow these guidelines.
Don't pack multiple concerns into a single description. Each rule should constrain exactly one behavior.
Bad: "New code should be well-tested, follow our design patterns, and include docstrings."
Good:
- "New features must include a test demonstrating the pre-fix failure."
- "Every new endpoint must create a source document before persisting to an aggregate table."
blocker: CI fails. The merge is blocked. Use only for architectural or security invariants.general: Visible to human reviewers, but doesn't block the merge. Use for patterns that need attention.nit: Optional. Use for style and readability suggestions.
Bad: Every rule is blocker.
Good: A rule is blocker because violating it breaks a system invariant. Not because it's important, but because it's load-bearing.
Only run a rule against diffs that touch the files it cares about. A rule about GitHub Actions shouldn't evaluate a change to your domain models.
Example:
- id: prompt-injection-safety
description: "Workflows passing untrusted user input to LLM prompts must sanitize or quote the input."
severity: blocker
path: .github/workflows/Dense prompts bloat context windows and slow reviews. One or two sentences plus a concrete example per rule is ideal. If you find yourself writing a 200-line specification, split it into multiple focused rules or move the logic into unit tests.
Good:
- id: circuit-breaker-required
description: |
Outbound HTTP calls must use a circuit breaker. Do not add naive
`fetch()` or `axios.get()` without timeout, retry-with-backoff, or
a circuit breaker abstraction.
severity: blocker
path: src/services/Bad: A 20-point checklist covering every edge case of circuit breaker behavior. That's a library implementation spec, not a guard intent.
A single concrete example of a violation is more useful than an abstract definition.
Good:
- id: no-direct-aggregate-insert
description: |
Never insert directly into aggregate tables. Create source
documents first and use edge methods.
BAD: db.insertInto("order_totals").values(data)
GOOD: order.createSourceDocument(data)Rules are evaluated in parallel via LangGraph. Each node gets:
- The full git diff
- One rule (plain English description + severity)
- Optional path constraint (skips if diff doesn't touch that path)
Results feed into an aggregator that:
- Fails the review if any
blockerrule fails - Reports
generalandnitviolations for human review - Summarizes with per-rule reasoning
Formal specs are precise but brittle. They require learning a DSL and maintenance as the spec language itself evolves. Plain English is understood by every engineer and AI model today. The tradeoff is probabilistic precision — which is acceptable for the cross-file, intent-driven problems semantic guards solve.
If the behavior is deterministic and testable in a single module, write a unit test. If the behavior requires reasoning across files, understanding design rationale, or evaluating tradeoffs that aren't encoded in code, use a semantic guard.
The concepts in this document apply regardless of tooling. You can implement the same idea with:
- A custom CI step that sends the diff and rules to OpenAI, Anthropic, or any other LLM API
- A manual code review checklist that reviewers follow for every PR
- A combination of the above, with the checklist automated where possible
The output format (pass/fail + reasoning) is what matters, not the specific implementation.
Start with 3–5 high-value guards. More rules mean longer reviews and higher token costs. Remove or downgrade rules that produce false positives. A few sharp guards beat a hundred vague ones.
Yes. Built-in detectors catch classes of AI-generated risks (prompt injection, CI gaming, hallucinated correctness) that are easy to miss in custom rule sets. They require no configuration and run automatically.
MIT