Vibecode Greptile
track this build4 phases, 12 steps, beginner friendly0%The mechanical core is genuinely a weekend project: a GitHub App that catches pull request webhooks, pulls the diff, retrieves related code from an embedding index of the repo, and posts inline comments from an LLM. You can get to first useful comment in an afternoon. What you will not get in one sitting is signal quality, which is the entire product: knowing when to shut up, not re-flagging the same nit on every push, understanding a monorepo without blowing the context window, and keeping the index fresh without a full reindex on every merge. Expect a bot that is impressive on day one and muted by the team on day nine. Worth building if you own the repo and enjoy tuning prompts; not worth building to save a per-seat fee across a real engineering org.
You are building a lean indie version of Greptile. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Greptile · indie build 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. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI, one process plus one indexer command | webhooks need an always-on listener and nothing here justifies more moving parts | | Storage | SQLite with a vector extension, one file, on the machine that holds the clone | the index is a derivative of private source and belongs where the source already is | | Identity | A GitHub App, with an installation token minted per job and never written to disk | installation access tokens expire after one hour, so anything long-lived you store is just a liability | | Model access | One adapter with a local option, every call passing through the egress ledger | swapping providers must not be the moment somebody forgets to record what went out | | Output | One review per push carrying a capped number of findings, each with a stable fingerprint | a bot that comments forty times is muted, and a muted bot is worth nothing | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the indexer and the webhook service. - Get it: python.org or your version manager. - Verify: python3 --version prints 3.12 or higher - [ ] **A GitHub App registered on one repository** · free - 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. - Verify: The app mints an installation token and reads the repository - [ ] **A model endpoint, hosted or local** · pay 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 - [ ] **What your employer allows to leave the repository** · free - 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 - [ ] **A machine that stays on with the clone on it** · the 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Greptile, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Greptile The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a pull request reviewer for one private repository that keeps a written record of every line of your code it sent to a model to replace Greptile. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool reads a private repository and hands pieces of it to somebody else's computer, which is the central fact of the build. So the first thing you write is not the reviewer, it is the egress ledger. Every outbound request that carries source code records the file paths, the line ranges, the byte count, the destination, the model identifier and the time, before the request is made. If the ledger write fails, the request does not happen. A human must be able to answer 'what did this thing send on Tuesday' from a table, not from a log file that rotated. Second, an exclusion list enforced twice: once when indexing and again immediately before any send. Environment files, key material, certificates and anything matching a secret pattern are never chunked, embedded or put in a prompt. Enforcing only at index time fails the day somebody pastes a key into a diff, because the diff goes out on a different path from the index. Third, read your provider's current data policy and put what it says in the README with the date. OpenAI's published API data usage policy states that data sent to the API is not used to train its models by default, that inputs and outputs may be retained for up to thirty days for abuse monitoring, and that zero data retention is available for eligible endpoints on request. Other providers differ and terms change. If that is not acceptable, run a local model and say so. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI, one process plus one indexer command · webhooks need an always-on listener and nothing here justifies more moving parts - Storage: SQLite with a vector extension, one file, on the machine that holds the clone · the index is a derivative of private source and belongs where the source already is - Identity: A GitHub App, with an installation token minted per job and never written to disk · installation access tokens expire after one hour, so anything long-lived you store is just a liability - Model access: One adapter with a local option, every call passing through the egress ledger · swapping providers must not be the moment somebody forgets to record what went out - Output: One review per push carrying a capped number of findings, each with a stable fingerprint · a bot that comments forty times is muted, and a muted bot is worth nothing ### Data model (create this before Phase 1) - 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. ### Phase 1 · An index that knows what it is not allowed to read Build: Walk the clone, honour the repository's ignore rules, and apply an exclusion file you write by hand for secrets, key material, lockfiles, vendored trees and anything over a size you choose. Chunk source by function or class where a parser exists and by overlapping line windows otherwise. Key every chunk to a file path, a line range and the blob hash it came from. Trap: 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. Done when: Every file in the tree has a row, either indexed with a blob hash or excluded with the rule name that excluded it, verified by counting against the working tree. An environment file placed in the repository is excluded by rule and appears in the excluded list, tested. Chunks carry a path, a start line, an end line and the blob hash, and re-running the indexer with no changes embeds nothing new. The whole index lives in one file on disk with no network call except the embedding request. Do not build yet: No webhook, no reviewing, no posting. ### Phase 2 · The ledger, then the request Build: Route every provider call through one function that writes an egress row and only then makes the request. Re-apply the exclusion rules to the assembled prompt, not just to the index. Mint a GitHub installation token per job, hold it in memory, and never write it anywhere. Add a command that prints the ledger for a date range with byte totals per destination. Trap: 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. Done when: No code path reaches a provider except through the ledgered client, asserted by a test that patches the transport and fails if it is called directly. A secret pattern planted in a diff is stripped before the prompt is assembled and the removal is recorded, tested. The installation token is never persisted and never logged, verified by grepping the database and the log output in a test. The ledger command prints paths, line ranges and byte totals for any date range. Do not build yet: No diff handling or comment posting yet. ### Phase 3 · Comments that land on lines that exist Build: On a pull request event, verify the webhook signature, queue the job and return immediately. Fetch the diff, build a map from file and line to the side of the diff that line appears on, and require every finding to resolve against that map before posting. Post one review holding all surviving findings rather than a stream of individual comments. Trap: 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. Done when: A finding whose line is outside the diff is dropped before any API call and counted, tested with a crafted response. Webhook requests with a bad signature are rejected and never queued, tested. The endpoint returns before any model call, so a slow provider never causes a delivery timeout. All findings for one push go out as a single review and the identifiers of the created comments are stored. Do not build yet: No deduplication, muting or budgeting yet. ### Phase 4 · Silence is the feature Build: Fingerprint every finding from the file path, the normalised body and the surrounding code rather than the line number, so the same complaint survives a rebase. Never post a fingerprint that is muted or already posted on this pull request. Cap findings per review and per file, and drop everything below a severity floor you can change without redeploying. Trap: 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. Done when: Inserting a line at the top of a changed file leaves every fingerprint unchanged, tested directly. A finding muted once is never posted again on any later push, tested across two runs. A review never exceeds the configured cap per review or per file, and the excess is stored with outcome suppressed rather than discarded. Lowering the severity floor changes what is posted without a code change. Do not build yet: No incremental reindex, cost ceiling or export yet. ### Phase 5 · Reindex, spend and the off switch Build: Reindex incrementally on merge by comparing blob hashes and re-embedding only what changed. Track spend per review against a monthly ceiling, and stop reviewing rather than stopping the service when it is reached. Add a single switch that disables all outbound model calls while leaving the webhook answering, and export findings, ledger and mutes as CSV. Trap: 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. Done when: A merge touching one file re-embeds only that file's chunks and prints the counts, tested against a known commit pair. Reaching the monthly ceiling stops reviews with a clear comment on the next pull request and leaves the service answering webhooks. The disable switch stops every outbound model call, verified by the ledger recording nothing while it is on. Findings, ledger and mutes export as CSV and reimport into an empty database. Do not build yet: Nothing after this. ### Out of scope (and why) - Other hosting platforms, because each one has a different diff model and comment API and one of them is enough work. - Learning from dismissed comments, which needs far more dismissals than one repository produces before it is anything but noise. - Automatic fix suggestions, which turn a wrong comment into a wrong commit. - A multi-tenant service, because then you are holding other people's private source and the whole egress argument becomes somebody else's to audit. ### README must contain - Exactly what leaves the machine, in which direction, and how to read the egress ledger for any date - The provider's current data retention and training policy in its own words, with the date you read it and the link - That the exclusion rules are applied at index time and again before every send, and how to add one - That installation tokens are minted per job and never stored, and what that means if the key file leaks - How to run the whole thing against a local model, and what it costs in review quality ===== AGENTS.md ===== # Agent instructions · Greptile indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with FastAPI, one process plus one indexer command, SQLite with a vector extension, one file, on the machine that holds the clone, A GitHub App, with an installation token minted per job and never written to disk, One adapter with a local option, every call passing through the egress ledger, One review per push carrying a capped number of findings, each with a stable fingerprint. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Greptile 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · An index that knows what it is not allowed to read The repository is chunked and embedded, with exclusions recorded rather than silently applied. ### Steps 1. Write the exclusion file before the walker Deciding what is off limits after you have an index is deciding it too late. Files: `exclude.toml`, `index.py` 2. Key chunks to path, line range and blob hash Without the hash you cannot reindex incrementally and without lines you cannot comment. Files: `index.py`, `chunker.py` 3. Record every skipped file with its rule The excluded list is evidence, and evidence is the point of this build. Files: `index.py`, `tests/test_exclusions.py` ### Done when - [ ] Every file is either indexed or excluded with a named rule - [ ] A planted environment file is excluded and listed - [ ] Chunks carry path, line range and blob hash - [ ] Re-indexing an unchanged tree embeds nothing ### Watch 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. ## Phase 2 · The ledger, then the request Nothing containing source code leaves the machine without a row written first. ### Steps 1. One client, ledger write before send If the row cannot be written the request must not happen. Files: `egress.py`, `tests/test_egress.py` 2. Re-apply exclusions to the assembled prompt The diff path does not go through the indexer, and that is where secrets arrive. Files: `review.py`, `exclude.py` 3. Mint the installation token per job It expires after an hour regardless, so storing it buys nothing and costs a lot. Files: `github_auth.py`, `tests/test_no_token_persisted.py` ### Done when - [ ] Every provider call goes through the ledgered client - [ ] A secret in a diff is stripped before assembly and recorded - [ ] No token reaches the database or the logs - [ ] The ledger prints byte totals per destination ### Watch 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. ## Phase 3 · 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. ### Steps 1. Build a diff map before asking the model anything You need it to validate the answer, so build it first. Files: `diffmap.py`, `tests/test_diffmap.py` 2. Drop unresolvable findings and count them A rising drop count is the earliest sign the prompt has drifted. Files: `review.py` 3. Verify the signature, queue, return The webhook deadline is short and a model call is not. Files: `server.py`, `tests/test_signature.py` ### Done when - [ ] A finding outside the diff is dropped and counted - [ ] A bad signature is rejected before queueing - [ ] The endpoint responds before any model call - [ ] One review per push, with comment identifiers stored ### Watch 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. ## Phase 4 · Silence is the feature The bot says less on the second push than on the first, and a dismissed finding stays dismissed. ### Steps 1. Fingerprint on content, never on line numbers A rebase must not resurrect a comment somebody already answered. Files: `fingerprint.py`, `tests/test_fingerprint.py` 2. Store suppressed findings instead of dropping them The suppressed pile is how you tell whether the cap is set right. Files: `review.py` 3. Put the severity floor in configuration You will change it weekly for the first month. Files: `config.py` ### Done when - [ ] Fingerprints survive a line inserted above - [ ] A muted fingerprint never posts again - [ ] Caps hold per review and per file - [ ] The severity floor is configurable without a deploy ### Watch 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. ## Not in this build - Other hosting platforms. - Learning from dismissed comments. - Automatic fix commits. - Multi-tenant hosting of anyone else's source. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Identifies the app when minting an installation token. GITHUB_APP_ID=123456 # Required · secret. The app key. This file is the whole credential, so treat it as such. GITHUB_PRIVATE_KEY_PATH=./secrets/app.pem # Required · secret. Verifies every incoming event before it is queued. GITHUB_WEBHOOK_SECRET=whsec-...redacted # Required. The one repository this instance serves. REPO_FULL_NAME=your-org/your-repo # Required. Recorded in the egress ledger on every row. MODEL_ENDPOINT=https://api.example.com/v1 # Optional · secret. Omitted when running a local model. Set a spend limit provider side too. MODEL_API_KEY=sk-...redacted # Required. Reviews stop at this figure and the service keeps answering. MONTHLY_COST_CEILING=20.00 # Required. The off switch. When true, no source leaves the machine for any reason. EGRESS_DISABLED=false
You are building a lean indie version of Greptile. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Greptile · indie build 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. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI, one process plus one indexer command | webhooks need an always-on listener and nothing here justifies more moving parts | | Storage | SQLite with a vector extension, one file, on the machine that holds the clone | the index is a derivative of private source and belongs where the source already is | | Identity | A GitHub App, with an installation token minted per job and never written to disk | installation access tokens expire after one hour, so anything long-lived you store is just a liability | | Model access | One adapter with a local option, every call passing through the egress ledger | swapping providers must not be the moment somebody forgets to record what went out | | Output | One review per push carrying a capped number of findings, each with a stable fingerprint | a bot that comments forty times is muted, and a muted bot is worth nothing | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the indexer and the webhook service. - Get it: python.org or your version manager. - Verify: python3 --version prints 3.12 or higher - [ ] **A GitHub App registered on one repository** · free - 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. - Verify: The app mints an installation token and reads the repository - [ ] **A model endpoint, hosted or local** · pay 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 - [ ] **What your employer allows to leave the repository** · free - 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 - [ ] **A machine that stays on with the clone on it** · the 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Greptile, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Greptile The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a pull request reviewer for one private repository that keeps a written record of every line of your code it sent to a model to replace Greptile. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool reads a private repository and hands pieces of it to somebody else's computer, which is the central fact of the build. So the first thing you write is not the reviewer, it is the egress ledger. Every outbound request that carries source code records the file paths, the line ranges, the byte count, the destination, the model identifier and the time, before the request is made. If the ledger write fails, the request does not happen. A human must be able to answer 'what did this thing send on Tuesday' from a table, not from a log file that rotated. Second, an exclusion list enforced twice: once when indexing and again immediately before any send. Environment files, key material, certificates and anything matching a secret pattern are never chunked, embedded or put in a prompt. Enforcing only at index time fails the day somebody pastes a key into a diff, because the diff goes out on a different path from the index. Third, read your provider's current data policy and put what it says in the README with the date. OpenAI's published API data usage policy states that data sent to the API is not used to train its models by default, that inputs and outputs may be retained for up to thirty days for abuse monitoring, and that zero data retention is available for eligible endpoints on request. Other providers differ and terms change. If that is not acceptable, run a local model and say so. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI, one process plus one indexer command · webhooks need an always-on listener and nothing here justifies more moving parts - Storage: SQLite with a vector extension, one file, on the machine that holds the clone · the index is a derivative of private source and belongs where the source already is - Identity: A GitHub App, with an installation token minted per job and never written to disk · installation access tokens expire after one hour, so anything long-lived you store is just a liability - Model access: One adapter with a local option, every call passing through the egress ledger · swapping providers must not be the moment somebody forgets to record what went out - Output: One review per push carrying a capped number of findings, each with a stable fingerprint · a bot that comments forty times is muted, and a muted bot is worth nothing ### Data model (create this before Phase 1) - 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. ### Phase 1 · An index that knows what it is not allowed to read Build: Walk the clone, honour the repository's ignore rules, and apply an exclusion file you write by hand for secrets, key material, lockfiles, vendored trees and anything over a size you choose. Chunk source by function or class where a parser exists and by overlapping line windows otherwise. Key every chunk to a file path, a line range and the blob hash it came from. Trap: 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. Done when: Every file in the tree has a row, either indexed with a blob hash or excluded with the rule name that excluded it, verified by counting against the working tree. An environment file placed in the repository is excluded by rule and appears in the excluded list, tested. Chunks carry a path, a start line, an end line and the blob hash, and re-running the indexer with no changes embeds nothing new. The whole index lives in one file on disk with no network call except the embedding request. Do not build yet: No webhook, no reviewing, no posting. ### Phase 2 · The ledger, then the request Build: Route every provider call through one function that writes an egress row and only then makes the request. Re-apply the exclusion rules to the assembled prompt, not just to the index. Mint a GitHub installation token per job, hold it in memory, and never write it anywhere. Add a command that prints the ledger for a date range with byte totals per destination. Trap: 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. Done when: No code path reaches a provider except through the ledgered client, asserted by a test that patches the transport and fails if it is called directly. A secret pattern planted in a diff is stripped before the prompt is assembled and the removal is recorded, tested. The installation token is never persisted and never logged, verified by grepping the database and the log output in a test. The ledger command prints paths, line ranges and byte totals for any date range. Do not build yet: No diff handling or comment posting yet. ### Phase 3 · Comments that land on lines that exist Build: On a pull request event, verify the webhook signature, queue the job and return immediately. Fetch the diff, build a map from file and line to the side of the diff that line appears on, and require every finding to resolve against that map before posting. Post one review holding all surviving findings rather than a stream of individual comments. Trap: 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. Done when: A finding whose line is outside the diff is dropped before any API call and counted, tested with a crafted response. Webhook requests with a bad signature are rejected and never queued, tested. The endpoint returns before any model call, so a slow provider never causes a delivery timeout. All findings for one push go out as a single review and the identifiers of the created comments are stored. Do not build yet: No deduplication, muting or budgeting yet. ### Phase 4 · Silence is the feature Build: Fingerprint every finding from the file path, the normalised body and the surrounding code rather than the line number, so the same complaint survives a rebase. Never post a fingerprint that is muted or already posted on this pull request. Cap findings per review and per file, and drop everything below a severity floor you can change without redeploying. Trap: 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. Done when: Inserting a line at the top of a changed file leaves every fingerprint unchanged, tested directly. A finding muted once is never posted again on any later push, tested across two runs. A review never exceeds the configured cap per review or per file, and the excess is stored with outcome suppressed rather than discarded. Lowering the severity floor changes what is posted without a code change. Do not build yet: No incremental reindex, cost ceiling or export yet. ### Phase 5 · Reindex, spend and the off switch Build: Reindex incrementally on merge by comparing blob hashes and re-embedding only what changed. Track spend per review against a monthly ceiling, and stop reviewing rather than stopping the service when it is reached. Add a single switch that disables all outbound model calls while leaving the webhook answering, and export findings, ledger and mutes as CSV. Trap: 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. Done when: A merge touching one file re-embeds only that file's chunks and prints the counts, tested against a known commit pair. Reaching the monthly ceiling stops reviews with a clear comment on the next pull request and leaves the service answering webhooks. The disable switch stops every outbound model call, verified by the ledger recording nothing while it is on. Findings, ledger and mutes export as CSV and reimport into an empty database. Do not build yet: Nothing after this. ### Out of scope (and why) - Other hosting platforms, because each one has a different diff model and comment API and one of them is enough work. - Learning from dismissed comments, which needs far more dismissals than one repository produces before it is anything but noise. - Automatic fix suggestions, which turn a wrong comment into a wrong commit. - A multi-tenant service, because then you are holding other people's private source and the whole egress argument becomes somebody else's to audit. ### README must contain - Exactly what leaves the machine, in which direction, and how to read the egress ledger for any date - The provider's current data retention and training policy in its own words, with the date you read it and the link - That the exclusion rules are applied at index time and again before every send, and how to add one - That installation tokens are minted per job and never stored, and what that means if the key file leaks - How to run the whole thing against a local model, and what it costs in review quality ===== AGENTS.md ===== # Agent instructions · Greptile indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with FastAPI, one process plus one indexer command, SQLite with a vector extension, one file, on the machine that holds the clone, A GitHub App, with an installation token minted per job and never written to disk, One adapter with a local option, every call passing through the egress ledger, One review per push carrying a capped number of findings, each with a stable fingerprint. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Greptile 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · An index that knows what it is not allowed to read The repository is chunked and embedded, with exclusions recorded rather than silently applied. ### Steps 1. Write the exclusion file before the walker Deciding what is off limits after you have an index is deciding it too late. Files: `exclude.toml`, `index.py` 2. Key chunks to path, line range and blob hash Without the hash you cannot reindex incrementally and without lines you cannot comment. Files: `index.py`, `chunker.py` 3. Record every skipped file with its rule The excluded list is evidence, and evidence is the point of this build. Files: `index.py`, `tests/test_exclusions.py` ### Done when - [ ] Every file is either indexed or excluded with a named rule - [ ] A planted environment file is excluded and listed - [ ] Chunks carry path, line range and blob hash - [ ] Re-indexing an unchanged tree embeds nothing ### Watch 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. ## Phase 2 · The ledger, then the request Nothing containing source code leaves the machine without a row written first. ### Steps 1. One client, ledger write before send If the row cannot be written the request must not happen. Files: `egress.py`, `tests/test_egress.py` 2. Re-apply exclusions to the assembled prompt The diff path does not go through the indexer, and that is where secrets arrive. Files: `review.py`, `exclude.py` 3. Mint the installation token per job It expires after an hour regardless, so storing it buys nothing and costs a lot. Files: `github_auth.py`, `tests/test_no_token_persisted.py` ### Done when - [ ] Every provider call goes through the ledgered client - [ ] A secret in a diff is stripped before assembly and recorded - [ ] No token reaches the database or the logs - [ ] The ledger prints byte totals per destination ### Watch 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. ## Phase 3 · 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. ### Steps 1. Build a diff map before asking the model anything You need it to validate the answer, so build it first. Files: `diffmap.py`, `tests/test_diffmap.py` 2. Drop unresolvable findings and count them A rising drop count is the earliest sign the prompt has drifted. Files: `review.py` 3. Verify the signature, queue, return The webhook deadline is short and a model call is not. Files: `server.py`, `tests/test_signature.py` ### Done when - [ ] A finding outside the diff is dropped and counted - [ ] A bad signature is rejected before queueing - [ ] The endpoint responds before any model call - [ ] One review per push, with comment identifiers stored ### Watch 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. ## Phase 4 · Silence is the feature The bot says less on the second push than on the first, and a dismissed finding stays dismissed. ### Steps 1. Fingerprint on content, never on line numbers A rebase must not resurrect a comment somebody already answered. Files: `fingerprint.py`, `tests/test_fingerprint.py` 2. Store suppressed findings instead of dropping them The suppressed pile is how you tell whether the cap is set right. Files: `review.py` 3. Put the severity floor in configuration You will change it weekly for the first month. Files: `config.py` ### Done when - [ ] Fingerprints survive a line inserted above - [ ] A muted fingerprint never posts again - [ ] Caps hold per review and per file - [ ] The severity floor is configurable without a deploy ### Watch 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. ## Not in this build - Other hosting platforms. - Learning from dismissed comments. - Automatic fix commits. - Multi-tenant hosting of anyone else's source. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Identifies the app when minting an installation token. GITHUB_APP_ID=123456 # Required · secret. The app key. This file is the whole credential, so treat it as such. GITHUB_PRIVATE_KEY_PATH=./secrets/app.pem # Required · secret. Verifies every incoming event before it is queued. GITHUB_WEBHOOK_SECRET=whsec-...redacted # Required. The one repository this instance serves. REPO_FULL_NAME=your-org/your-repo # Required. Recorded in the egress ledger on every row. MODEL_ENDPOINT=https://api.example.com/v1 # Optional · secret. Omitted when running a local model. Set a spend limit provider side too. MODEL_API_KEY=sk-...redacted # Required. Reviews stop at this figure and the service keeps answering. MONTHLY_COST_CEILING=20.00 # Required. The off switch. When true, no source leaves the machine for any reason. EGRESS_DISABLED=false
You are building a production product version of Greptile. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Greptile · product brief ## Problem The mechanical core is genuinely a weekend project: a GitHub App that catches pull request webhooks, pulls the diff, retrieves related code from an embedding index of the repo, and posts inline comments from an LLM. You can get to first useful comment in an afternoon. What you will not get in one sitting is signal quality, which is the entire product: knowing when to shut up, not re-flagging the same nit on every push, understanding a monorepo without blowing the context window, and keeping the index fresh without a full reindex on every merge. Expect a bot that is impressive on day one and muted by the team on day nine. Worth building if you own the repo and enjoy tuning prompts; not worth building to save a per-seat fee across a real engineering org. ## Product outcome A review bot for one repository that can account for every byte of private source it sent, posts comments that land on real lines, gets quieter rather than louder over a week, and can be stopped with one switch. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - GitHub App registration with webhook secret and private key - An LLM API key (embeddings plus a strong reasoning model) - A small always-on host or tunnel to receive webhooks - Local disk for the vector index, roughly proportional to repo size - Willingness to iterate on the review prompt for weeks ## Explicit non-goals for v1 - 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 ## Success criteria - No provider call is confirmed possible outside the ledgered client - A planted secret is confirmed stripped from a diff before assembly - The installation token is confirmed absent from database and logs - A finding outside the diff is confirmed dropped rather than posted - The egress switch is confirmed to produce zero ledger rows ===== BRIEF.md ===== # Build brief · Greptile The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a pull request reviewer for one private repository that keeps a written record of every line of your code it sent to a model to replace Greptile. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool reads a private repository and hands pieces of it to somebody else's computer, which is the central fact of the build. So the first thing you write is not the reviewer, it is the egress ledger. Every outbound request that carries source code records the file paths, the line ranges, the byte count, the destination, the model identifier and the time, before the request is made. If the ledger write fails, the request does not happen. A human must be able to answer 'what did this thing send on Tuesday' from a table, not from a log file that rotated. Second, an exclusion list enforced twice: once when indexing and again immediately before any send. Environment files, key material, certificates and anything matching a secret pattern are never chunked, embedded or put in a prompt. Enforcing only at index time fails the day somebody pastes a key into a diff, because the diff goes out on a different path from the index. Third, read your provider's current data policy and put what it says in the README with the date. OpenAI's published API data usage policy states that data sent to the API is not used to train its models by default, that inputs and outputs may be retained for up to thirty days for abuse monitoring, and that zero data retention is available for eligible endpoints on request. Other providers differ and terms change. If that is not acceptable, run a local model and say so. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI, one process plus one indexer command · webhooks need an always-on listener and nothing here justifies more moving parts - Storage: SQLite with a vector extension, one file, on the machine that holds the clone · the index is a derivative of private source and belongs where the source already is - Identity: A GitHub App, with an installation token minted per job and never written to disk · installation access tokens expire after one hour, so anything long-lived you store is just a liability - Model access: One adapter with a local option, every call passing through the egress ledger · swapping providers must not be the moment somebody forgets to record what went out - Output: One review per push carrying a capped number of findings, each with a stable fingerprint · a bot that comments forty times is muted, and a muted bot is worth nothing ### Data model (create this before Phase 1) - 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. ### Phase 1 · An index that knows what it is not allowed to read Build: Walk the clone, honour the repository's ignore rules, and apply an exclusion file you write by hand for secrets, key material, lockfiles, vendored trees and anything over a size you choose. Chunk source by function or class where a parser exists and by overlapping line windows otherwise. Key every chunk to a file path, a line range and the blob hash it came from. Trap: 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. Done when: Every file in the tree has a row, either indexed with a blob hash or excluded with the rule name that excluded it, verified by counting against the working tree. An environment file placed in the repository is excluded by rule and appears in the excluded list, tested. Chunks carry a path, a start line, an end line and the blob hash, and re-running the indexer with no changes embeds nothing new. The whole index lives in one file on disk with no network call except the embedding request. Do not build yet: No webhook, no reviewing, no posting. ### Phase 2 · The ledger, then the request Build: Route every provider call through one function that writes an egress row and only then makes the request. Re-apply the exclusion rules to the assembled prompt, not just to the index. Mint a GitHub installation token per job, hold it in memory, and never write it anywhere. Add a command that prints the ledger for a date range with byte totals per destination. Trap: 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. Done when: No code path reaches a provider except through the ledgered client, asserted by a test that patches the transport and fails if it is called directly. A secret pattern planted in a diff is stripped before the prompt is assembled and the removal is recorded, tested. The installation token is never persisted and never logged, verified by grepping the database and the log output in a test. The ledger command prints paths, line ranges and byte totals for any date range. Do not build yet: No diff handling or comment posting yet. ### Phase 3 · Comments that land on lines that exist Build: On a pull request event, verify the webhook signature, queue the job and return immediately. Fetch the diff, build a map from file and line to the side of the diff that line appears on, and require every finding to resolve against that map before posting. Post one review holding all surviving findings rather than a stream of individual comments. Trap: 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. Done when: A finding whose line is outside the diff is dropped before any API call and counted, tested with a crafted response. Webhook requests with a bad signature are rejected and never queued, tested. The endpoint returns before any model call, so a slow provider never causes a delivery timeout. All findings for one push go out as a single review and the identifiers of the created comments are stored. Do not build yet: No deduplication, muting or budgeting yet. ### Phase 4 · Silence is the feature Build: Fingerprint every finding from the file path, the normalised body and the surrounding code rather than the line number, so the same complaint survives a rebase. Never post a fingerprint that is muted or already posted on this pull request. Cap findings per review and per file, and drop everything below a severity floor you can change without redeploying. Trap: 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. Done when: Inserting a line at the top of a changed file leaves every fingerprint unchanged, tested directly. A finding muted once is never posted again on any later push, tested across two runs. A review never exceeds the configured cap per review or per file, and the excess is stored with outcome suppressed rather than discarded. Lowering the severity floor changes what is posted without a code change. Do not build yet: No incremental reindex, cost ceiling or export yet. ### Phase 5 · Reindex, spend and the off switch Build: Reindex incrementally on merge by comparing blob hashes and re-embedding only what changed. Track spend per review against a monthly ceiling, and stop reviewing rather than stopping the service when it is reached. Add a single switch that disables all outbound model calls while leaving the webhook answering, and export findings, ledger and mutes as CSV. Trap: 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. Done when: A merge touching one file re-embeds only that file's chunks and prints the counts, tested against a known commit pair. Reaching the monthly ceiling stops reviews with a clear comment on the next pull request and leaves the service answering webhooks. The disable switch stops every outbound model call, verified by the ledger recording nothing while it is on. Findings, ledger and mutes export as CSV and reimport into an empty database. Do not build yet: Nothing after this. ### Out of scope (and why) - Other hosting platforms, because each one has a different diff model and comment API and one of them is enough work. - Learning from dismissed comments, which needs far more dismissals than one repository produces before it is anything but noise. - Automatic fix suggestions, which turn a wrong comment into a wrong commit. - A multi-tenant service, because then you are holding other people's private source and the whole egress argument becomes somebody else's to audit. ### README must contain - Exactly what leaves the machine, in which direction, and how to read the egress ledger for any date - The provider's current data retention and training policy in its own words, with the date you read it and the link - That the exclusion rules are applied at index time and again before every send, and how to add one - That installation tokens are minted per job and never stored, and what that means if the key file leaks - How to run the whole thing against a local model, and what it costs in review quality ===== ARCHITECTURE.md ===== # Architecture · Greptile ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI, one process plus one indexer command | webhooks need an always-on listener and nothing here justifies more moving parts | | Storage | SQLite with a vector extension, one file, on the machine that holds the clone | the index is a derivative of private source and belongs where the source already is | | Identity | A GitHub App, with an installation token minted per job and never written to disk | installation access tokens expire after one hour, so anything long-lived you store is just a liability | | Model access | One adapter with a local option, every call passing through the egress ledger | swapping providers must not be the moment somebody forgets to record what went out | | Output | One review per push carrying a capped number of findings, each with a stable fingerprint | a bot that comments forty times is muted, and a muted bot is worth nothing | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Index | Walking, exclusion, chunking, embedding. | Any chunker, provided chunks keep a path, a line range and a blob hash. | | Egress | The ledger and the only outbound client. | Not swappable for direct provider calls, which is the whole point of the module. | | Review | Diff mapping, prompting, validation. | Any model, provided unresolvable findings are dropped rather than forced. | | Signal | Fingerprints, mutes, caps, severity floor. | Any scheme, provided fingerprints do not contain line numbers. | | Operations | Incremental reindex, spend ceiling, export. | Any scheduler, provided reindex cost tracks change size. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `GITHUB_APP_ID` · required · Identifies the app when minting an installation token. - `GITHUB_PRIVATE_KEY_PATH` · required, secret · The app key. This file is the whole credential, so treat it as such. - `GITHUB_WEBHOOK_SECRET` · required, secret · Verifies every incoming event before it is queued. - `REPO_FULL_NAME` · required · The one repository this instance serves. - `MODEL_ENDPOINT` · required · Recorded in the egress ledger on every row. - `MODEL_API_KEY` · optional, secret · Omitted when running a local model. Set a spend limit provider side too. - `MONTHLY_COST_CEILING` · required · Reviews stop at this figure and the service keeps answering. - `EGRESS_DISABLED` · required · The off switch. When true, no source leaves the machine for any reason. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Greptile product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with FastAPI, one process plus one indexer command, SQLite with a vector extension, one file, on the machine that holds the clone, A GitHub App, with an installation token minted per job and never written to disk, One adapter with a local option, every call passing through the egress ledger, One review per push carrying a capped number of findings, each with a stable fingerprint. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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. ===== MILESTONES.md ===== # Delivery milestones · Greptile Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · An index that knows what it is not allowed to read The repository is chunked and embedded, with exclusions recorded rather than silently applied. ### Steps 1. Write the exclusion file before the walker Deciding what is off limits after you have an index is deciding it too late. Files: `exclude.toml`, `index.py` 2. Key chunks to path, line range and blob hash Without the hash you cannot reindex incrementally and without lines you cannot comment. Files: `index.py`, `chunker.py` 3. Record every skipped file with its rule The excluded list is evidence, and evidence is the point of this build. Files: `index.py`, `tests/test_exclusions.py` ### Done when - [ ] Every file is either indexed or excluded with a named rule - [ ] A planted environment file is excluded and listed - [ ] Chunks carry path, line range and blob hash - [ ] Re-indexing an unchanged tree embeds nothing ### Watch 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. ## M2 · The ledger, then the request Nothing containing source code leaves the machine without a row written first. ### Steps 1. One client, ledger write before send If the row cannot be written the request must not happen. Files: `egress.py`, `tests/test_egress.py` 2. Re-apply exclusions to the assembled prompt The diff path does not go through the indexer, and that is where secrets arrive. Files: `review.py`, `exclude.py` 3. Mint the installation token per job It expires after an hour regardless, so storing it buys nothing and costs a lot. Files: `github_auth.py`, `tests/test_no_token_persisted.py` ### Done when - [ ] Every provider call goes through the ledgered client - [ ] A secret in a diff is stripped before assembly and recorded - [ ] No token reaches the database or the logs - [ ] The ledger prints byte totals per destination ### Watch 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. ## M3 · 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. ### Steps 1. Build a diff map before asking the model anything You need it to validate the answer, so build it first. Files: `diffmap.py`, `tests/test_diffmap.py` 2. Drop unresolvable findings and count them A rising drop count is the earliest sign the prompt has drifted. Files: `review.py` 3. Verify the signature, queue, return The webhook deadline is short and a model call is not. Files: `server.py`, `tests/test_signature.py` ### Done when - [ ] A finding outside the diff is dropped and counted - [ ] A bad signature is rejected before queueing - [ ] The endpoint responds before any model call - [ ] One review per push, with comment identifiers stored ### Watch 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. ## M4 · Silence is the feature The bot says less on the second push than on the first, and a dismissed finding stays dismissed. ### Steps 1. Fingerprint on content, never on line numbers A rebase must not resurrect a comment somebody already answered. Files: `fingerprint.py`, `tests/test_fingerprint.py` 2. Store suppressed findings instead of dropping them The suppressed pile is how you tell whether the cap is set right. Files: `review.py` 3. Put the severity floor in configuration You will change it weekly for the first month. Files: `config.py` ### Done when - [ ] Fingerprints survive a line inserted above - [ ] A muted fingerprint never posts again - [ ] Caps hold per review and per file - [ ] The severity floor is configurable without a deploy ### Watch 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. ## M5 · Reindex, spend and the off switch (production only) The index tracks the default branch cheaply, the bill has a ceiling, and the whole thing can be stopped and proved stopped. ### Steps 1. Re-embed by blob hash difference only Cost should track the size of the change, not the size of the repository. Files: `index.py`, `tests/test_incremental.py` 2. Cap the month and degrade to silence A review bot with no ceiling is an unbounded invoice. Files: `cost.py` 3. One switch that stops all egress The day you need it you will not want to read code. Files: `config.py`, `egress.py` ### Done when - [ ] A one-file merge re-embeds only that file - [ ] The ceiling stops reviews, not the service - [ ] The disable switch produces zero ledger rows - [ ] Findings, ledger and mutes round-trip as CSV ### Watch 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. ===== OPERATIONS.md ===== # Operations · Greptile ## Backup The database holds the index, the ledger and the mutes. The index can be rebuilt from the clone; the ledger and the mutes cannot, so back those up separately and keep them longer than you think you need to. ## Restore Restore the database, confirm the ledger row count and the mute list, then reindex from the current default branch. A restore that loses mutes will make the bot noisy again on the next push, which is how you will notice. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Bytes sent per day, findings dropped for not resolving against the diff, findings suppressed by the cap, and the proportion of posted comments that get a reply. A rising drop rate means the prompt or the model changed under you. ## Incident checklist The incident in this build is not downtime, it is an unexplained outbound request or a secret in a prompt. The ledger is the first thing you read, the excluded file list is the second, and both need to be queryable without shelling into a log directory. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No provider call is confirmed possible outside the ledgered client - [ ] A planted secret is confirmed stripped from a diff before assembly - [ ] The installation token is confirmed absent from database and logs - [ ] A finding outside the diff is confirmed dropped rather than posted - [ ] The egress switch is confirmed to produce zero ledger rows ## Launch constraint Do not market omitted Greptile capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Identifies the app when minting an installation token. GITHUB_APP_ID=123456 # Required · secret. The app key. This file is the whole credential, so treat it as such. GITHUB_PRIVATE_KEY_PATH=./secrets/app.pem # Required · secret. Verifies every incoming event before it is queued. GITHUB_WEBHOOK_SECRET=whsec-...redacted # Required. The one repository this instance serves. REPO_FULL_NAME=your-org/your-repo # Required. Recorded in the egress ledger on every row. MODEL_ENDPOINT=https://api.example.com/v1 # Optional · secret. Omitted when running a local model. Set a spend limit provider side too. MODEL_API_KEY=sk-...redacted # Required. Reviews stop at this figure and the service keeps answering. MONTHLY_COST_CEILING=20.00 # Required. The off switch. When true, no source leaves the machine for any reason. EGRESS_DISABLED=false
# Greptile · indie build 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. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI, one process plus one indexer command | webhooks need an always-on listener and nothing here justifies more moving parts | | Storage | SQLite with a vector extension, one file, on the machine that holds the clone | the index is a derivative of private source and belongs where the source already is | | Identity | A GitHub App, with an installation token minted per job and never written to disk | installation access tokens expire after one hour, so anything long-lived you store is just a liability | | Model access | One adapter with a local option, every call passing through the egress ledger | swapping providers must not be the moment somebody forgets to record what went out | | Output | One review per push carrying a capped number of findings, each with a stable fingerprint | a bot that comments forty times is muted, and a muted bot is worth nothing | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the indexer and the webhook service. - Get it: python.org or your version manager. - Verify: python3 --version prints 3.12 or higher - [ ] **A GitHub App registered on one repository** · free - 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. - Verify: The app mints an installation token and reads the repository - [ ] **A model endpoint, hosted or local** · pay 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 - [ ] **What your employer allows to leave the repository** · free - 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 - [ ] **A machine that stays on with the clone on it** · the 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Greptile, and the README should say so rather than pretend.
# Build brief · Greptile The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a pull request reviewer for one private repository that keeps a written record of every line of your code it sent to a model to replace Greptile. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool reads a private repository and hands pieces of it to somebody else's computer, which is the central fact of the build. So the first thing you write is not the reviewer, it is the egress ledger. Every outbound request that carries source code records the file paths, the line ranges, the byte count, the destination, the model identifier and the time, before the request is made. If the ledger write fails, the request does not happen. A human must be able to answer 'what did this thing send on Tuesday' from a table, not from a log file that rotated. Second, an exclusion list enforced twice: once when indexing and again immediately before any send. Environment files, key material, certificates and anything matching a secret pattern are never chunked, embedded or put in a prompt. Enforcing only at index time fails the day somebody pastes a key into a diff, because the diff goes out on a different path from the index. Third, read your provider's current data policy and put what it says in the README with the date. OpenAI's published API data usage policy states that data sent to the API is not used to train its models by default, that inputs and outputs may be retained for up to thirty days for abuse monitoring, and that zero data retention is available for eligible endpoints on request. Other providers differ and terms change. If that is not acceptable, run a local model and say so. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI, one process plus one indexer command · webhooks need an always-on listener and nothing here justifies more moving parts - Storage: SQLite with a vector extension, one file, on the machine that holds the clone · the index is a derivative of private source and belongs where the source already is - Identity: A GitHub App, with an installation token minted per job and never written to disk · installation access tokens expire after one hour, so anything long-lived you store is just a liability - Model access: One adapter with a local option, every call passing through the egress ledger · swapping providers must not be the moment somebody forgets to record what went out - Output: One review per push carrying a capped number of findings, each with a stable fingerprint · a bot that comments forty times is muted, and a muted bot is worth nothing ### Data model (create this before Phase 1) - 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. ### Phase 1 · An index that knows what it is not allowed to read Build: Walk the clone, honour the repository's ignore rules, and apply an exclusion file you write by hand for secrets, key material, lockfiles, vendored trees and anything over a size you choose. Chunk source by function or class where a parser exists and by overlapping line windows otherwise. Key every chunk to a file path, a line range and the blob hash it came from. Trap: 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. Done when: Every file in the tree has a row, either indexed with a blob hash or excluded with the rule name that excluded it, verified by counting against the working tree. An environment file placed in the repository is excluded by rule and appears in the excluded list, tested. Chunks carry a path, a start line, an end line and the blob hash, and re-running the indexer with no changes embeds nothing new. The whole index lives in one file on disk with no network call except the embedding request. Do not build yet: No webhook, no reviewing, no posting. ### Phase 2 · The ledger, then the request Build: Route every provider call through one function that writes an egress row and only then makes the request. Re-apply the exclusion rules to the assembled prompt, not just to the index. Mint a GitHub installation token per job, hold it in memory, and never write it anywhere. Add a command that prints the ledger for a date range with byte totals per destination. Trap: 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. Done when: No code path reaches a provider except through the ledgered client, asserted by a test that patches the transport and fails if it is called directly. A secret pattern planted in a diff is stripped before the prompt is assembled and the removal is recorded, tested. The installation token is never persisted and never logged, verified by grepping the database and the log output in a test. The ledger command prints paths, line ranges and byte totals for any date range. Do not build yet: No diff handling or comment posting yet. ### Phase 3 · Comments that land on lines that exist Build: On a pull request event, verify the webhook signature, queue the job and return immediately. Fetch the diff, build a map from file and line to the side of the diff that line appears on, and require every finding to resolve against that map before posting. Post one review holding all surviving findings rather than a stream of individual comments. Trap: 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. Done when: A finding whose line is outside the diff is dropped before any API call and counted, tested with a crafted response. Webhook requests with a bad signature are rejected and never queued, tested. The endpoint returns before any model call, so a slow provider never causes a delivery timeout. All findings for one push go out as a single review and the identifiers of the created comments are stored. Do not build yet: No deduplication, muting or budgeting yet. ### Phase 4 · Silence is the feature Build: Fingerprint every finding from the file path, the normalised body and the surrounding code rather than the line number, so the same complaint survives a rebase. Never post a fingerprint that is muted or already posted on this pull request. Cap findings per review and per file, and drop everything below a severity floor you can change without redeploying. Trap: 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. Done when: Inserting a line at the top of a changed file leaves every fingerprint unchanged, tested directly. A finding muted once is never posted again on any later push, tested across two runs. A review never exceeds the configured cap per review or per file, and the excess is stored with outcome suppressed rather than discarded. Lowering the severity floor changes what is posted without a code change. Do not build yet: No incremental reindex, cost ceiling or export yet. ### Phase 5 · Reindex, spend and the off switch Build: Reindex incrementally on merge by comparing blob hashes and re-embedding only what changed. Track spend per review against a monthly ceiling, and stop reviewing rather than stopping the service when it is reached. Add a single switch that disables all outbound model calls while leaving the webhook answering, and export findings, ledger and mutes as CSV. Trap: 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. Done when: A merge touching one file re-embeds only that file's chunks and prints the counts, tested against a known commit pair. Reaching the monthly ceiling stops reviews with a clear comment on the next pull request and leaves the service answering webhooks. The disable switch stops every outbound model call, verified by the ledger recording nothing while it is on. Findings, ledger and mutes export as CSV and reimport into an empty database. Do not build yet: Nothing after this. ### Out of scope (and why) - Other hosting platforms, because each one has a different diff model and comment API and one of them is enough work. - Learning from dismissed comments, which needs far more dismissals than one repository produces before it is anything but noise. - Automatic fix suggestions, which turn a wrong comment into a wrong commit. - A multi-tenant service, because then you are holding other people's private source and the whole egress argument becomes somebody else's to audit. ### README must contain - Exactly what leaves the machine, in which direction, and how to read the egress ledger for any date - The provider's current data retention and training policy in its own words, with the date you read it and the link - That the exclusion rules are applied at index time and again before every send, and how to add one - That installation tokens are minted per job and never stored, and what that means if the key file leaks - How to run the whole thing against a local model, and what it costs in review quality
# Agent instructions · Greptile indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with FastAPI, one process plus one indexer command, SQLite with a vector extension, one file, on the machine that holds the clone, A GitHub App, with an installation token minted per job and never written to disk, One adapter with a local option, every call passing through the egress ledger, One review per push carrying a capped number of findings, each with a stable fingerprint. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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.
# Build plan · Greptile 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · An index that knows what it is not allowed to read The repository is chunked and embedded, with exclusions recorded rather than silently applied. ### Steps 1. Write the exclusion file before the walker Deciding what is off limits after you have an index is deciding it too late. Files: `exclude.toml`, `index.py` 2. Key chunks to path, line range and blob hash Without the hash you cannot reindex incrementally and without lines you cannot comment. Files: `index.py`, `chunker.py` 3. Record every skipped file with its rule The excluded list is evidence, and evidence is the point of this build. Files: `index.py`, `tests/test_exclusions.py` ### Done when - [ ] Every file is either indexed or excluded with a named rule - [ ] A planted environment file is excluded and listed - [ ] Chunks carry path, line range and blob hash - [ ] Re-indexing an unchanged tree embeds nothing ### Watch 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. ## Phase 2 · The ledger, then the request Nothing containing source code leaves the machine without a row written first. ### Steps 1. One client, ledger write before send If the row cannot be written the request must not happen. Files: `egress.py`, `tests/test_egress.py` 2. Re-apply exclusions to the assembled prompt The diff path does not go through the indexer, and that is where secrets arrive. Files: `review.py`, `exclude.py` 3. Mint the installation token per job It expires after an hour regardless, so storing it buys nothing and costs a lot. Files: `github_auth.py`, `tests/test_no_token_persisted.py` ### Done when - [ ] Every provider call goes through the ledgered client - [ ] A secret in a diff is stripped before assembly and recorded - [ ] No token reaches the database or the logs - [ ] The ledger prints byte totals per destination ### Watch 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. ## Phase 3 · 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. ### Steps 1. Build a diff map before asking the model anything You need it to validate the answer, so build it first. Files: `diffmap.py`, `tests/test_diffmap.py` 2. Drop unresolvable findings and count them A rising drop count is the earliest sign the prompt has drifted. Files: `review.py` 3. Verify the signature, queue, return The webhook deadline is short and a model call is not. Files: `server.py`, `tests/test_signature.py` ### Done when - [ ] A finding outside the diff is dropped and counted - [ ] A bad signature is rejected before queueing - [ ] The endpoint responds before any model call - [ ] One review per push, with comment identifiers stored ### Watch 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. ## Phase 4 · Silence is the feature The bot says less on the second push than on the first, and a dismissed finding stays dismissed. ### Steps 1. Fingerprint on content, never on line numbers A rebase must not resurrect a comment somebody already answered. Files: `fingerprint.py`, `tests/test_fingerprint.py` 2. Store suppressed findings instead of dropping them The suppressed pile is how you tell whether the cap is set right. Files: `review.py` 3. Put the severity floor in configuration You will change it weekly for the first month. Files: `config.py` ### Done when - [ ] Fingerprints survive a line inserted above - [ ] A muted fingerprint never posts again - [ ] Caps hold per review and per file - [ ] The severity floor is configurable without a deploy ### Watch 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. ## Not in this build - Other hosting platforms. - Learning from dismissed comments. - Automatic fix commits. - Multi-tenant hosting of anyone else's source. ## After v1, if you want it - 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
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Identifies the app when minting an installation token. GITHUB_APP_ID=123456 # Required · secret. The app key. This file is the whole credential, so treat it as such. GITHUB_PRIVATE_KEY_PATH=./secrets/app.pem # Required · secret. Verifies every incoming event before it is queued. GITHUB_WEBHOOK_SECRET=whsec-...redacted # Required. The one repository this instance serves. REPO_FULL_NAME=your-org/your-repo # Required. Recorded in the egress ledger on every row. MODEL_ENDPOINT=https://api.example.com/v1 # Optional · secret. Omitted when running a local model. Set a spend limit provider side too. MODEL_API_KEY=sk-...redacted # Required. Reviews stop at this figure and the service keeps answering. MONTHLY_COST_CEILING=20.00 # Required. The off switch. When true, no source leaves the machine for any reason. EGRESS_DISABLED=false
# Greptile · product brief ## Problem The mechanical core is genuinely a weekend project: a GitHub App that catches pull request webhooks, pulls the diff, retrieves related code from an embedding index of the repo, and posts inline comments from an LLM. You can get to first useful comment in an afternoon. What you will not get in one sitting is signal quality, which is the entire product: knowing when to shut up, not re-flagging the same nit on every push, understanding a monorepo without blowing the context window, and keeping the index fresh without a full reindex on every merge. Expect a bot that is impressive on day one and muted by the team on day nine. Worth building if you own the repo and enjoy tuning prompts; not worth building to save a per-seat fee across a real engineering org. ## Product outcome A review bot for one repository that can account for every byte of private source it sent, posts comments that land on real lines, gets quieter rather than louder over a week, and can be stopped with one switch. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - GitHub App registration with webhook secret and private key - An LLM API key (embeddings plus a strong reasoning model) - A small always-on host or tunnel to receive webhooks - Local disk for the vector index, roughly proportional to repo size - Willingness to iterate on the review prompt for weeks ## Explicit non-goals for v1 - 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 ## Success criteria - No provider call is confirmed possible outside the ledgered client - A planted secret is confirmed stripped from a diff before assembly - The installation token is confirmed absent from database and logs - A finding outside the diff is confirmed dropped rather than posted - The egress switch is confirmed to produce zero ledger rows
# Build brief · Greptile The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a pull request reviewer for one private repository that keeps a written record of every line of your code it sent to a model to replace Greptile. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool reads a private repository and hands pieces of it to somebody else's computer, which is the central fact of the build. So the first thing you write is not the reviewer, it is the egress ledger. Every outbound request that carries source code records the file paths, the line ranges, the byte count, the destination, the model identifier and the time, before the request is made. If the ledger write fails, the request does not happen. A human must be able to answer 'what did this thing send on Tuesday' from a table, not from a log file that rotated. Second, an exclusion list enforced twice: once when indexing and again immediately before any send. Environment files, key material, certificates and anything matching a secret pattern are never chunked, embedded or put in a prompt. Enforcing only at index time fails the day somebody pastes a key into a diff, because the diff goes out on a different path from the index. Third, read your provider's current data policy and put what it says in the README with the date. OpenAI's published API data usage policy states that data sent to the API is not used to train its models by default, that inputs and outputs may be retained for up to thirty days for abuse monitoring, and that zero data retention is available for eligible endpoints on request. Other providers differ and terms change. If that is not acceptable, run a local model and say so. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI, one process plus one indexer command · webhooks need an always-on listener and nothing here justifies more moving parts - Storage: SQLite with a vector extension, one file, on the machine that holds the clone · the index is a derivative of private source and belongs where the source already is - Identity: A GitHub App, with an installation token minted per job and never written to disk · installation access tokens expire after one hour, so anything long-lived you store is just a liability - Model access: One adapter with a local option, every call passing through the egress ledger · swapping providers must not be the moment somebody forgets to record what went out - Output: One review per push carrying a capped number of findings, each with a stable fingerprint · a bot that comments forty times is muted, and a muted bot is worth nothing ### Data model (create this before Phase 1) - 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. ### Phase 1 · An index that knows what it is not allowed to read Build: Walk the clone, honour the repository's ignore rules, and apply an exclusion file you write by hand for secrets, key material, lockfiles, vendored trees and anything over a size you choose. Chunk source by function or class where a parser exists and by overlapping line windows otherwise. Key every chunk to a file path, a line range and the blob hash it came from. Trap: 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. Done when: Every file in the tree has a row, either indexed with a blob hash or excluded with the rule name that excluded it, verified by counting against the working tree. An environment file placed in the repository is excluded by rule and appears in the excluded list, tested. Chunks carry a path, a start line, an end line and the blob hash, and re-running the indexer with no changes embeds nothing new. The whole index lives in one file on disk with no network call except the embedding request. Do not build yet: No webhook, no reviewing, no posting. ### Phase 2 · The ledger, then the request Build: Route every provider call through one function that writes an egress row and only then makes the request. Re-apply the exclusion rules to the assembled prompt, not just to the index. Mint a GitHub installation token per job, hold it in memory, and never write it anywhere. Add a command that prints the ledger for a date range with byte totals per destination. Trap: 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. Done when: No code path reaches a provider except through the ledgered client, asserted by a test that patches the transport and fails if it is called directly. A secret pattern planted in a diff is stripped before the prompt is assembled and the removal is recorded, tested. The installation token is never persisted and never logged, verified by grepping the database and the log output in a test. The ledger command prints paths, line ranges and byte totals for any date range. Do not build yet: No diff handling or comment posting yet. ### Phase 3 · Comments that land on lines that exist Build: On a pull request event, verify the webhook signature, queue the job and return immediately. Fetch the diff, build a map from file and line to the side of the diff that line appears on, and require every finding to resolve against that map before posting. Post one review holding all surviving findings rather than a stream of individual comments. Trap: 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. Done when: A finding whose line is outside the diff is dropped before any API call and counted, tested with a crafted response. Webhook requests with a bad signature are rejected and never queued, tested. The endpoint returns before any model call, so a slow provider never causes a delivery timeout. All findings for one push go out as a single review and the identifiers of the created comments are stored. Do not build yet: No deduplication, muting or budgeting yet. ### Phase 4 · Silence is the feature Build: Fingerprint every finding from the file path, the normalised body and the surrounding code rather than the line number, so the same complaint survives a rebase. Never post a fingerprint that is muted or already posted on this pull request. Cap findings per review and per file, and drop everything below a severity floor you can change without redeploying. Trap: 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. Done when: Inserting a line at the top of a changed file leaves every fingerprint unchanged, tested directly. A finding muted once is never posted again on any later push, tested across two runs. A review never exceeds the configured cap per review or per file, and the excess is stored with outcome suppressed rather than discarded. Lowering the severity floor changes what is posted without a code change. Do not build yet: No incremental reindex, cost ceiling or export yet. ### Phase 5 · Reindex, spend and the off switch Build: Reindex incrementally on merge by comparing blob hashes and re-embedding only what changed. Track spend per review against a monthly ceiling, and stop reviewing rather than stopping the service when it is reached. Add a single switch that disables all outbound model calls while leaving the webhook answering, and export findings, ledger and mutes as CSV. Trap: 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. Done when: A merge touching one file re-embeds only that file's chunks and prints the counts, tested against a known commit pair. Reaching the monthly ceiling stops reviews with a clear comment on the next pull request and leaves the service answering webhooks. The disable switch stops every outbound model call, verified by the ledger recording nothing while it is on. Findings, ledger and mutes export as CSV and reimport into an empty database. Do not build yet: Nothing after this. ### Out of scope (and why) - Other hosting platforms, because each one has a different diff model and comment API and one of them is enough work. - Learning from dismissed comments, which needs far more dismissals than one repository produces before it is anything but noise. - Automatic fix suggestions, which turn a wrong comment into a wrong commit. - A multi-tenant service, because then you are holding other people's private source and the whole egress argument becomes somebody else's to audit. ### README must contain - Exactly what leaves the machine, in which direction, and how to read the egress ledger for any date - The provider's current data retention and training policy in its own words, with the date you read it and the link - That the exclusion rules are applied at index time and again before every send, and how to add one - That installation tokens are minted per job and never stored, and what that means if the key file leaks - How to run the whole thing against a local model, and what it costs in review quality
# Architecture · Greptile ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI, one process plus one indexer command | webhooks need an always-on listener and nothing here justifies more moving parts | | Storage | SQLite with a vector extension, one file, on the machine that holds the clone | the index is a derivative of private source and belongs where the source already is | | Identity | A GitHub App, with an installation token minted per job and never written to disk | installation access tokens expire after one hour, so anything long-lived you store is just a liability | | Model access | One adapter with a local option, every call passing through the egress ledger | swapping providers must not be the moment somebody forgets to record what went out | | Output | One review per push carrying a capped number of findings, each with a stable fingerprint | a bot that comments forty times is muted, and a muted bot is worth nothing | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Index | Walking, exclusion, chunking, embedding. | Any chunker, provided chunks keep a path, a line range and a blob hash. | | Egress | The ledger and the only outbound client. | Not swappable for direct provider calls, which is the whole point of the module. | | Review | Diff mapping, prompting, validation. | Any model, provided unresolvable findings are dropped rather than forced. | | Signal | Fingerprints, mutes, caps, severity floor. | Any scheme, provided fingerprints do not contain line numbers. | | Operations | Incremental reindex, spend ceiling, export. | Any scheduler, provided reindex cost tracks change size. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `GITHUB_APP_ID` · required · Identifies the app when minting an installation token. - `GITHUB_PRIVATE_KEY_PATH` · required, secret · The app key. This file is the whole credential, so treat it as such. - `GITHUB_WEBHOOK_SECRET` · required, secret · Verifies every incoming event before it is queued. - `REPO_FULL_NAME` · required · The one repository this instance serves. - `MODEL_ENDPOINT` · required · Recorded in the egress ledger on every row. - `MODEL_API_KEY` · optional, secret · Omitted when running a local model. Set a spend limit provider side too. - `MONTHLY_COST_CEILING` · required · Reviews stop at this figure and the service keeps answering. - `EGRESS_DISABLED` · required · The off switch. When true, no source leaves the machine for any reason. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Greptile product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with FastAPI, one process plus one indexer command, SQLite with a vector extension, one file, on the machine that holds the clone, A GitHub App, with an installation token minted per job and never written to disk, One adapter with a local option, every call passing through the egress ledger, One review per push carrying a capped number of findings, each with a stable fingerprint. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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.
# Delivery milestones · Greptile Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · An index that knows what it is not allowed to read The repository is chunked and embedded, with exclusions recorded rather than silently applied. ### Steps 1. Write the exclusion file before the walker Deciding what is off limits after you have an index is deciding it too late. Files: `exclude.toml`, `index.py` 2. Key chunks to path, line range and blob hash Without the hash you cannot reindex incrementally and without lines you cannot comment. Files: `index.py`, `chunker.py` 3. Record every skipped file with its rule The excluded list is evidence, and evidence is the point of this build. Files: `index.py`, `tests/test_exclusions.py` ### Done when - [ ] Every file is either indexed or excluded with a named rule - [ ] A planted environment file is excluded and listed - [ ] Chunks carry path, line range and blob hash - [ ] Re-indexing an unchanged tree embeds nothing ### Watch 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. ## M2 · The ledger, then the request Nothing containing source code leaves the machine without a row written first. ### Steps 1. One client, ledger write before send If the row cannot be written the request must not happen. Files: `egress.py`, `tests/test_egress.py` 2. Re-apply exclusions to the assembled prompt The diff path does not go through the indexer, and that is where secrets arrive. Files: `review.py`, `exclude.py` 3. Mint the installation token per job It expires after an hour regardless, so storing it buys nothing and costs a lot. Files: `github_auth.py`, `tests/test_no_token_persisted.py` ### Done when - [ ] Every provider call goes through the ledgered client - [ ] A secret in a diff is stripped before assembly and recorded - [ ] No token reaches the database or the logs - [ ] The ledger prints byte totals per destination ### Watch 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. ## M3 · 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. ### Steps 1. Build a diff map before asking the model anything You need it to validate the answer, so build it first. Files: `diffmap.py`, `tests/test_diffmap.py` 2. Drop unresolvable findings and count them A rising drop count is the earliest sign the prompt has drifted. Files: `review.py` 3. Verify the signature, queue, return The webhook deadline is short and a model call is not. Files: `server.py`, `tests/test_signature.py` ### Done when - [ ] A finding outside the diff is dropped and counted - [ ] A bad signature is rejected before queueing - [ ] The endpoint responds before any model call - [ ] One review per push, with comment identifiers stored ### Watch 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. ## M4 · Silence is the feature The bot says less on the second push than on the first, and a dismissed finding stays dismissed. ### Steps 1. Fingerprint on content, never on line numbers A rebase must not resurrect a comment somebody already answered. Files: `fingerprint.py`, `tests/test_fingerprint.py` 2. Store suppressed findings instead of dropping them The suppressed pile is how you tell whether the cap is set right. Files: `review.py` 3. Put the severity floor in configuration You will change it weekly for the first month. Files: `config.py` ### Done when - [ ] Fingerprints survive a line inserted above - [ ] A muted fingerprint never posts again - [ ] Caps hold per review and per file - [ ] The severity floor is configurable without a deploy ### Watch 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. ## M5 · Reindex, spend and the off switch (production only) The index tracks the default branch cheaply, the bill has a ceiling, and the whole thing can be stopped and proved stopped. ### Steps 1. Re-embed by blob hash difference only Cost should track the size of the change, not the size of the repository. Files: `index.py`, `tests/test_incremental.py` 2. Cap the month and degrade to silence A review bot with no ceiling is an unbounded invoice. Files: `cost.py` 3. One switch that stops all egress The day you need it you will not want to read code. Files: `config.py`, `egress.py` ### Done when - [ ] A one-file merge re-embeds only that file - [ ] The ceiling stops reviews, not the service - [ ] The disable switch produces zero ledger rows - [ ] Findings, ledger and mutes round-trip as CSV ### Watch 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.
# Operations · Greptile ## Backup The database holds the index, the ledger and the mutes. The index can be rebuilt from the clone; the ledger and the mutes cannot, so back those up separately and keep them longer than you think you need to. ## Restore Restore the database, confirm the ledger row count and the mute list, then reindex from the current default branch. A restore that loses mutes will make the bot noisy again on the next push, which is how you will notice. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Bytes sent per day, findings dropped for not resolving against the diff, findings suppressed by the cap, and the proportion of posted comments that get a reply. A rising drop rate means the prompt or the model changed under you. ## Incident checklist The incident in this build is not downtime, it is an unexplained outbound request or a secret in a prompt. The ledger is the first thing you read, the excluded file list is the second, and both need to be queryable without shelling into a log directory. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No provider call is confirmed possible outside the ledgered client - [ ] A planted secret is confirmed stripped from a diff before assembly - [ ] The installation token is confirmed absent from database and logs - [ ] A finding outside the diff is confirmed dropped rather than posted - [ ] The egress switch is confirmed to produce zero ledger rows ## Launch constraint Do not market omitted Greptile capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Identifies the app when minting an installation token. GITHUB_APP_ID=123456 # Required · secret. The app key. This file is the whole credential, so treat it as such. GITHUB_PRIVATE_KEY_PATH=./secrets/app.pem # Required · secret. Verifies every incoming event before it is queued. GITHUB_WEBHOOK_SECRET=whsec-...redacted # Required. The one repository this instance serves. REPO_FULL_NAME=your-org/your-repo # Required. Recorded in the egress ledger on every row. MODEL_ENDPOINT=https://api.example.com/v1 # Optional · secret. Omitted when running a local model. Set a spend limit provider side too. MODEL_API_KEY=sk-...redacted # Required. Reviews stop at this figure and the service keeps answering. MONTHLY_COST_CEILING=20.00 # Required. The off switch. When true, no source leaves the machine for any reason. EGRESS_DISABLED=false
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because a noisy code reviewer is worse than none, and getting from noisy to useful is a long grind of prompt tuning, retrieval tweaks, and feedback loops you cannot shortcut with one prompt. Teams also want the review bot to be someone else's uptime problem, to work across every repo without a platform engineer babysitting an index, and to arrive with a compliance page attached. A per-developer fee is trivially cheaper than an engineer maintaining an in-house bot that everyone quietly mutes.
xTuned false-positive suppression: their bot has been beaten into silence by thousands of teams, yours has not
xIncremental reindexing and monorepo handling that does not choke on a 500k file tree
xMemory of past reviews so the same nit is not raised on every force push
xTeam-level config, custom rule sets, and per-repo style learning from accepted or dismissed comments
xBitbucket, GitLab, and self-hosted host support, plus SOC 2 paperwork your security team will ask for
Nothing worth pointing at. That's why the prompt exists.
Vibecode Greptile
Kinda. The core of Greptile is buildable in a weekend with the prompt on this page, but there are real gaps: 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. Read the honest list above before committing.
How much does Greptile cost?
Greptile costs about $30/month (Pro, checked 2026-08-18), which is $360 per year.
What do I lose by replacing Greptile?
Honestly: 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. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Greptile?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.