Build Greptile

KINDAreplaces $30/mosaves $360/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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 weekendthe files for this build are in the project pack

RuntimePython 3.12 with FastAPI, one process plus one indexer commandStorageSQLite with a vector extension, one file, on the machine that holds the cloneIdentityA GitHub App, with an installation token minted per job and never written to diskModel accessOne adapter with a local option, every call passing through the egress ledgerOutputOne review per push carrying a capped number of findings, each with a stable fingerprint

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the indexer and the webhook service.

    Get it python.org or your version manager. open ↗

    Verify python3 --version prints 3.12 or higher

  2. 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. open ↗

    Verify The app mints an installation token and reads the repository

  3. API keypay per token for a hosted model; free but slower locally

    Why This is the only part that costs money and the only part that sees your source, which is why the choice is a prerequisite rather than a detail.

    Get it A provider key with a spend limit set in their console, or a local model served on the same machine.

    Verify One chunk embeds and one review call returns valid structured output

  4. decidefree

    Why Deciding this after the bot is running means discovering the answer from somebody else, loudly.

    Get it Read the provider's data policy, note the retention period and the date, and confirm the repository owner accepts it in writing.

    Verify The README states the policy, the date and who agreed

  5. have readythe price of a small instance, or free on hardware you already run

    Why Webhooks arrive when they arrive, and the index has to live beside the code it indexes.

    Get it Any always-on machine, or a tunnel to your workstation for testing only.

    Verify A pull request event reaches the service within seconds

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- files(path, blob_sha, language, bytes, indexed_at, excluded, exclusion_rule) · excluded files keep a row so you can prove what was skipped
- chunks(id, path, start_line, end_line, blob_sha, text, token_estimate)
- embeddings(chunk_id, model_id, created_at, vector) · the model identifier is part of the key, since two models are not comparable
- reviews(id, pr_number, head_sha, base_sha, state, started_at, finished_at, findings_posted, cost_minor)
- findings(id, review_id, path, line, side, severity, body, fingerprint, comment_id, outcome) · outcome is posted, suppressed, dropped or rejected
- egress(id, at, destination, model_id, purpose, paths_json, line_ranges_json, bytes_sent, review_id) · written before the request, not after
- muted(fingerprint, reason, muted_at) · a finding dismissed once never comes back on the next force push
- There is no table holding a raw model response keyed to nothing. Every byte that left has a row saying why.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
GITHUB_APP_IDrequired123456Identifies the app when minting an installation token.
GITHUB_PRIVATE_KEY_PATHsecretrequired./secrets/app.pemThe app key. This file is the whole credential, so treat it as such.
GITHUB_WEBHOOK_SECRETsecretrequiredwhsec-...redactedVerifies every incoming event before it is queued.
REPO_FULL_NAMErequiredyour-org/your-repoThe one repository this instance serves.
MODEL_ENDPOINTrequiredhttps://api.example.com/v1Recorded in the egress ledger on every row.
MODEL_API_KEYsecretoptionalsk-...redactedOmitted when running a local model. Set a spend limit provider side too.
MONTHLY_COST_CEILINGrequired20.00Reviews stop at this figure and the service keeps answering.
EGRESS_DISABLEDrequiredfalseThe off switch. When true, no source leaves the machine for any reason.

The build, in order

  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.

    1. Deciding what is off limits after you have an index is deciding it too late.

      Files exclude.tomlindex.py

    2. Without the hash you cannot reindex incrementally and without lines you cannot comment.

      Files index.pychunker.py

    3. The excluded list is evidence, and evidence is the point of this build.

      Files index.pytests/test_exclusions.py

    done when · tick each as it passes
    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.
  2. The ledger, then the request

    Nothing containing source code leaves the machine without a row written first.

    1. If the row cannot be written the request must not happen.

      Files egress.pytests/test_egress.py

    2. The diff path does not go through the indexer, and that is where secrets arrive.

      Files review.pyexclude.py

    3. It expires after an hour regardless, so storing it buys nothing and costs a lot.

      Files github_auth.pytests/test_no_token_persisted.py

    done when · tick each as it passes
    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.
  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.

    1. You need it to validate the answer, so build it first.

      Files diffmap.pytests/test_diffmap.py

    2. A rising drop count is the earliest sign the prompt has drifted.

      Files review.py

    3. The webhook deadline is short and a model call is not.

      Files server.pytests/test_signature.py

    done when · tick each as it passes
    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.
  4. Silence is the feature

    The bot says less on the second push than on the first, and a dismissed finding stays dismissed.

    1. A rebase must not resurrect a comment somebody already answered.

      Files fingerprint.pytests/test_fingerprint.py

    2. The suppressed pile is how you tell whether the cap is set right.

      Files review.py

    3. You will change it weekly for the first month.

      Files config.py

    done when · tick each as it passes
    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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more dev tools.