Build Greptile
KINDAreplaces $30/mosaves $360/yrback to the verdict
A self-hosted review bot for one repository whose hard problems are accounting honestly for the private source it ships to a model, landing comments on lines that actually exist in the diff, and staying quiet enough that the team does not mute it in week two.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the indexer and the webhook service.
Get it python.org or your version manager. open ↗
Verify
python3 --version prints 3.12 or higher - accountfree
Why An app gets a short-lived installation token scoped to the repository, which is a much smaller thing to lose than a personal token.
Get it Register an app with contents read, pull requests write and metadata read, install it on one repository, and download the private key. open ↗
Verify
The app mints an installation token and reads the repository - API keypay per token for a hosted model; free but slower locally
Why This is the only part that costs money and the only part that sees your source, which is why the choice is a prerequisite rather than a detail.
Get it A provider key with a spend limit set in their console, or a local model served on the same machine.
Verify
One chunk embeds and one review call returns valid structured output - decidefree
Why Deciding this after the bot is running means discovering the answer from somebody else, loudly.
Get it Read the provider's data policy, note the retention period and the date, and confirm the repository owner accepts it in writing.
Verify
The README states the policy, the date and who agreed - have readythe price of a small instance, or free on hardware you already run
Why Webhooks arrive when they arrive, and the index has to live beside the code it indexes.
Get it Any always-on machine, or a tunnel to your workstation for testing only.
Verify
A pull request event reaches the service within seconds
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- files(path, blob_sha, language, bytes, indexed_at, excluded, exclusion_rule) · excluded files keep a row so you can prove what was skipped - chunks(id, path, start_line, end_line, blob_sha, text, token_estimate) - embeddings(chunk_id, model_id, created_at, vector) · the model identifier is part of the key, since two models are not comparable - reviews(id, pr_number, head_sha, base_sha, state, started_at, finished_at, findings_posted, cost_minor) - findings(id, review_id, path, line, side, severity, body, fingerprint, comment_id, outcome) · outcome is posted, suppressed, dropped or rejected - egress(id, at, destination, model_id, purpose, paths_json, line_ranges_json, bytes_sent, review_id) · written before the request, not after - muted(fingerprint, reason, muted_at) · a finding dismissed once never comes back on the next force push - There is no table holding a raw model response keyed to nothing. Every byte that left has a row saying why.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
GITHUB_APP_ID | required | 123456 | Identifies the app when minting an installation token. |
GITHUB_PRIVATE_KEY_PATHsecret | required | ./secrets/app.pem | The app key. This file is the whole credential, so treat it as such. |
GITHUB_WEBHOOK_SECRETsecret | required | whsec-...redacted | Verifies every incoming event before it is queued. |
REPO_FULL_NAME | required | your-org/your-repo | The one repository this instance serves. |
MODEL_ENDPOINT | required | https://api.example.com/v1 | Recorded in the egress ledger on every row. |
MODEL_API_KEYsecret | optional | sk-...redacted | Omitted when running a local model. Set a spend limit provider side too. |
MONTHLY_COST_CEILING | required | 20.00 | Reviews stop at this figure and the service keeps answering. |
EGRESS_DISABLED | required | false | The off switch. When true, no source leaves the machine for any reason. |
The build, in order
An index that knows what it is not allowed to read
The repository is chunked and embedded, with exclusions recorded rather than silently applied.
Deciding what is off limits after you have an index is deciding it too late.
Files
exclude.tomlindex.pyWithout the hash you cannot reindex incrementally and without lines you cannot comment.
Files
index.pychunker.pyThe excluded list is evidence, and evidence is the point of this build.
Files
index.pytests/test_exclusions.py
done when · tick each as it passeswatch out- Skipping excluded files without recording them means you cannot later prove that the vendor directory and the environment file were never sent, and the one time somebody asks is the one time it matters. Write a row for every excluded file naming the rule that excluded it, and make the indexer print the count.
The ledger, then the request
Nothing containing source code leaves the machine without a row written first.
If the row cannot be written the request must not happen.
Files
egress.pytests/test_egress.pyThe diff path does not go through the indexer, and that is where secrets arrive.
Files
review.pyexclude.pyIt expires after an hour regardless, so storing it buys nothing and costs a lot.
Files
github_auth.pytests/test_no_token_persisted.py
done when · tick each as it passeswatch out- Caching a GitHub installation token in the database because minting one on every job feels wasteful leaves a credential with repository write access sitting in a file that the token itself expires out of after an hour anyway. Mint it per job from the app key, keep it in memory, and let it die with the process.
Comments that land on lines that exist
A finding maps to a real line of the diff and is rejected loudly when it does not.
You need it to validate the answer, so build it first.
Files
diffmap.pytests/test_diffmap.pyA rising drop count is the earliest sign the prompt has drifted.
Files
review.pyThe webhook deadline is short and a model call is not.
Files
server.pytests/test_signature.py
done when · tick each as it passeswatch out- The review API places a comment using the line of the blob in the pull request diff, so a model that returns a line number from the whole file, or a line in a part of the file the diff never touched, produces a request that fails validation with a 422 and a bot that posts nothing while looking like it worked. Resolve every line against your own diff map first, drop what does not resolve, and count the drops so you can see the prompt degrading.
Silence is the feature
The bot says less on the second push than on the first, and a dismissed finding stays dismissed.
A rebase must not resurrect a comment somebody already answered.
Files
fingerprint.pytests/test_fingerprint.pyThe suppressed pile is how you tell whether the cap is set right.
Files
review.pyYou will change it weekly for the first month.
Files
config.py
done when · tick each as it passeswatch out- Fingerprinting on path and line number means a force push that shifts everything by three lines makes every previous comment new again, and the team mutes the bot that afternoon. Fingerprint on content and context, and test it by inserting a line at the top of the file and asserting the fingerprints do not change.
Reindex, spend and the off switchproduct builder
The index tracks the default branch cheaply, the bill has a ceiling, and the whole thing can be stopped and proved stopped.
Cost should track the size of the change, not the size of the repository.
Files
index.pytests/test_incremental.pyA review bot with no ceiling is an unbounded invoice.
Files
cost.pyThe day you need it you will not want to read code.
Files
config.pyegress.py
done when · tick each as it passeswatch out- Reindexing the whole repository on every merge is the failure that turns a cheap tool into an expensive one overnight, because the cost is proportional to repository size rather than to change size and nobody notices until the invoice. Compare blob hashes, re-embed only changed chunks, and print added, updated and deleted counts on every run.
That is the whole plan for Greptile. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Other hosting platforms.
- Learning from dismissed comments.
- Automatic fix commits.
- Multi-tenant hosting of anyone else's source.
- Tuned false-positive suppression: their bot has been beaten into silence by thousands of teams, yours has not
- Incremental reindexing and monorepo handling that does not choke on a 500k file tree
- Memory of past reviews so the same nit is not raised on every force push
- Team-level config, custom rule sets, and per-repo style learning from accepted or dismissed comments
- Bitbucket, GitLab, and self-hosted host support, plus SOC 2 paperwork your security team will ask for
- A weekly digest of what the bot sent and what it found, which is the only honest measure of whether it earns its place
- A local embedding model, which removes the index from the egress question entirely
- A dry-run command that prints a review without posting, for tuning the prompt against real pull requests
Need the files? The project pack on the verdict page hands your agent the whole brief · more dev tools.