forgot
Names the files that usually change alongside the ones you staged — before the commit lands.
Zero dependencies. Reads nothing but .git. No network, no index, no API key.
forgot: 2 files usually change with this edit but are not staged.
logger_test.go 82% 22 of 84 commits touching logger.go
docs/doc.md 43% 8 of 84 commits touching logger.go
git add logger_test.go docs/doc.md
Percentages weight recent commits above old ones, so they differ from the raw counts.
Check one: forgot why logger.go logger_test.go
Deliberate? Commit with --no-verify, or add the path to .forgotignore.
That is real output from gin-gonic/gin.
Why
A codebase carries knowledge that is in none of its files: these things change together. Edit the serializer, bump the schema. Touch the public helper, add a changelog line. Change the workflow, change its sibling workflow.
People absorb that by working in a repo for months. A coding agent arrives with none of it, and so its characteristic failure is not broken syntax — it is work that is complete-looking but partial. The patch is right and the commit is short one file. CI finds out; the reviewer finds out; the agent does not.
forgot recovers that knowledge from the one place it is already written down — the commit history — and spends it at the only moment it matters, the commit itself.
Install
As a pre-commit hook, which is where it earns its keep:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/vijay-kapse/forgot
rev: v0.1.0
hooks:
- id: forgot # blocks only on high-confidence misses
# - id: forgot-warn # same check, never blocks
Or standalone:
pip install forgot
forgot # check what is staged
forgot check src/auth.py # ask about specific files
forgot check --json # machine-readable
forgot why src/auth.py CHANGES.rst # show the commits behind a number
forgot eval # backtest the model on this repo
Does it actually work
The nice thing about this problem is that it grades itself. Real commits are the labels.
Protocol. Order every commit by time. Train on the oldest 80%, query the newest 20% the model has never seen. For each held-out commit, reveal one file and ask for the rest. No future information reaches the model.
Two numbers matter, and they trade against each other:
- coverage — the share of edits it says anything at all about. Silence is useless; constant guessing is noise.
- precision@5 — of the files it named, the share that really were in that commit. Measured only over edits where it spoke.
- hit-rate@5 — the share of those edits where at least one named file really was in the commit.
Baselines, both deliberately given their best shot: popularity always names the repo's most-churned files; naming applies test/header conventions (auth.py → test_auth.py, auth.c → auth.h) and matches them against the repo's real layout, in any directory, not just a conventional tests/.
| repo | language | commits | coverage | precision@5 | recall@5 | hit-rate@5 | popularity | naming |
|---|---|---|---|---|---|---|---|---|
requests |
Python | 6,495 | 31% | 72% | 48% | 77% | 50% | 29% |
flask |
Python | 5,557 | 52% | 49% | 27% | 68% | 58% | 18% |
pytest |
Python | 17,787 | 42% | 50% | 24% | 61% | 34% | 32% |
django |
Python | 34,964 | 57% | 40% | 25% | 55% | 16% | 17% |
fastapi |
Python | 7,776 | 40% | 65% | 39% | 76% | 39% | 0% |
prettier |
JS/TS | 11,937 | 64% | 69% | 51% | 77% | 52% | 1% |
gin |
Go | 2,020 | 70% | 66% | 55% | 77% | 46% | 57% |
| mean | 51% | 59% | 38% | 70% | 42% | 22% |
16,398 held-out queries across 7 repositories.
So: it speaks on about half of all edits, and when it speaks it correctly names a genuinely missing file 70% of the time — against 42% for naming the repo's busiest files and 22% for test-naming conventions.
Reproduce the whole table, including the clones:
python bench/benchmark.py
Or on your own repo, which is the number you should actually care about:
forgot eval
How it works
Pairwise co-change from git log, with three guards that do most of the work of keeping it quiet:
Lift, not raw correlation. CHANGES.rst changes in a third of all commits, so it correlates with everything. Dividing a pairing's strength by the file's own base rate drops files that are merely busy rather than genuinely coupled. This is why forgot says nothing when you stage only a README.
Lift is always measured from the single strongest pairing, never from the combined score — otherwise stacking several weak signals would inflate confidence and the lift that is supposed to check it, and popular files would walk straight through.
Recency. A repo's shape changes. Commits decay with a 180-day half-life, so a pairing that held for 200 commits three years ago and none since loses to one that has held all month. This is why the headline percentage and the all-time counts differ, and the report says so.
Thresholds are measured, not chosen. bench/sweep.py sweeps them across all
seven repos. The lift floor is 1.2 because 1.0 through 1.5 score identically on
real repos — their base rates are too low for the guard to bind — while 2.0 and
above starts costing real coverage:
| min_lift | min_confidence | coverage | precision@5 | hit-rate@5 |
|---|---|---|---|---|
| 1.2 | 0.3 | 62% | 53% | 69% |
| 1.2 | 0.4 | 51% | 59% | 71% |
| 1.2 | 0.5 | 41% | 62% | 72% |
| 2.0 | 0.4 | 46% | 55% | 68% |
Taking the low end of that flat range is free on a mature repo and buys back the small or young one, where a single file can appear in most commits and a higher floor would suppress coupling that is perfectly real.
Bulk commits dropped. A reformat, a vendor drop or a licence-header sweep couples everything to everything. Commits above 50 files are the largest single source of noise in a co-change model, and are ignored.
Renames are followed, so history survives a file being moved. The model is cached in .git/forgot/ — invisible to git status, keyed to HEAD, rebuilt when it goes stale.
It fails open, always. No repo, an unborn branch, thin history, a corrupt cache, git behaving unexpectedly — every one of those exits 0 in silence. A tool on the commit path does not get to be the reason you cannot commit.
For agents
--json is the interface:
forgot check --json
{
"staged": ["src/requests/sessions.py"],
"missing": [
{
"path": "src/requests/models.py",
"confidence": 0.4337,
"lift": 4.11,
"because_of": "src/requests/sessions.py",
"co_commits": 65,
"total_commits": 294
}
],
"commits_analyzed": 3737,
"note": "confidence is recency-weighted; co_commits/total_commits are all-time counts for the same pairing"
}
Worth pasting into your repo's AGENTS.md:
Before committing, run `forgot check --json`. For each entry in `missing`,
either change that file too or say in the commit message why you did not.
Confidence is evidence, not instruction — `forgot why <a> <b>` lists the
commits behind any number.
Configuration
Everything is a flag; nothing is required. The defaults are what the benchmark above was measured with.
| flag | default | what it does |
|---|---|---|
--fail-under |
0.75 |
exit 1 only at or above this confidence |
--warn-only |
off | always exit 0 |
--min-confidence |
0.4 |
floor for mentioning a file at all |
--min-lift |
1.2 |
how far above its base rate a file must co-occur |
--min-support |
5 |
commits a file needs before it may suggest anything |
--min-co-count |
3 |
commits a pairing needs |
--top-k |
5 |
most files to name |
--half-life |
180 |
days after which a commit counts half as much |
--max-commits |
5000 |
history depth |
--max-files-per-commit |
50 |
ignore commits larger than this |
--combine |
noisy-or |
how evidence from several staged files is pooled (max, mean) |
Paths listed in .forgotignore (glob patterns, one per line) are never suggested.
Limitations
Stated plainly, because the benchmark above already implies them.
- It is silent about half the time. Coupling that history has not recorded cannot be recovered. A new file, a new subsystem or a repo with a short history gets nothing — by design, rather than a guess.
- It is correlation, not causation.
forgotknows that two files moved together, never why. Thewhysubcommand exists so you can check rather than trust. - Squashed history hides it. A repo that squashes every PR to one commit records far less about which files move together.
- Diffuse test layouts weaken it. Where one source file maps to many small test files (Django's
tests/<app>/), coupling spreads thin and coverage drops — visible as Django's 55% in the table. - Not a correctness check. It tells you a file is usually part of this change. Whether it is this time is your call.
License
MIT
Metadata
Release files for forgot 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| forgot-0.1.0.tar.gz | 24.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| forgot-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 45.2 kB
Release files / forgot-0.1.0.tar.gz
| Download URL | forgot-0.1.0.tar.gz |
|---|---|
| Size | 24.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8326f0ea597bab3d7fb6c1690e3f6698f568f3555cd84644df38f0211360fa1c
|
|
BLAKE2b-256 checksum How to use checksums |
e80b8b6ca7812b951a08f8da6351c289d3270fd96232eaf4abbe8e3cbb806a04
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / forgot-0.1.0-py3-none-any.whl
| Download URL | forgot-0.1.0-py3-none-any.whl |
|---|---|
| Size | 20.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fbff042834397c34687b91a216b2797f25ceca644c3f5ff470e312b8e548b33e
|
|
BLAKE2b-256 checksum How to use checksums |
4012ea1d00b78a8e9596b80ee134b63d7a5239d75301283601e4c83a02a5918e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log