Skip to content

headwater is designed, not built. Its design book holds the design whole.

How it works

headwater answers one question about a repository: which releases of what it depends on are newer than what it runs. It reads, it asks, it reports, and it keeps only what it needs to answer again. Numbers in parentheses cite clauses of the statement.

headwater reads every declaration the repository holds and gives each dependency one identity, a Package URL such as pkg:npm/react, whichever file names it (2). It asks each ecosystem’s own package manager first, read-only, in the repository’s own shell, so it sees the registries the repository is configured for, private ones among them: npm view and npm outdated, go list -m -versions, mise ls-remote, and the configured registry’s index for Python and Cargo. GitHub answers for the tools no package manager names, fifty repositories to a query through your own gh sign-in. deps.dev and ecosyste.ms answer where no package manager runs or none exists, and OSV answers which advisories name each pin, every pin in one query. A registry or a lookup learns the identities and versions headwater asks about, and nothing else of the repository (37).

Forty-seven dependencies from five kinds of declaration, three lookups. A dependency whose source does not answer comes home unread, with the reason.

A release is newer than a pin only by the dependency’s own version order: Semantic Versioning for npm, Cargo and GitHub tags that parse, PEP 440 for PyPI, Go’s module rules, and calendar versions field by field where a watch list says so (6). headwater never orders two versions by their dates or their spelling. A release it cannot place against the pin is reported as undecided, beside the newer ones (7).

headwater reads no record of what it reported before. Every scan reports a release until the pin reaches it, so a bump that lands is the only thing that clears one, and no one has to mark it done (9).

A declaration names a range as well as a pin: ^1.4, >=1.4,<2. headwater marks each newer release inside or outside it. The package manager takes a release inside the range by changing only the lockfile. A release outside it changes the manifest, as a major release needs. headwater reports both, so a major release is never hidden.

Inside the range or outside it: both are reportedpackage.json declares react ^18.3.0, which takes 18.3.0 and later releases below 19.0.0, and the lockfile pins 18.3.1. The releases in version order are 18.2.0, 18.3.1, 18.3.2, 19.0.0, 19.1.0 and 19.2.1. 18.3.2 is newer and inside the range: only the lockfile changes, and the brief names npm update react. 19.0.0, 19.1.0 and 19.2.1 are newer and outside it: the manifest changes, as a major release needs, and the brief names npm install react@19.2.1. headwater reports both, so a major release is never hidden, and whoever makes the change runs the command; headwater never does.package.json"react": "^18.3.0"pin 18.3.1package-lock.json18.2.018.3.118.3.219.0.019.1.019.2.1pin^18.3.0 takes >=18.3.0 <19.0.0past the rangeINSIDE THE RANGE18.3.2: only the lockfile changesnpm update reactOUTSIDE THE RANGE19.2.1: the manifest changes, as a major needsnpm install react@19.2.1headwater reports both, so a major release is never hidden.Whoever makes the change runs the command; headwater never does.
Inside the range or outside it: both are reportedpackage.json declares react ^18.3.0, which takes 18.3.0 and later releases below 19.0.0, and the lockfile pins 18.3.1. The releases in version order are 18.2.0, 18.3.1, 18.3.2, 19.0.0, 19.1.0 and 19.2.1. 18.3.2 is newer and inside the range: only the lockfile changes, and the brief names npm update react. 19.0.0, 19.1.0 and 19.2.1 are newer and outside it: the manifest changes, as a major release needs, and the brief names npm install react@19.2.1. headwater reports both, so a major release is never hidden, and whoever makes the change runs the command; headwater never does.package.json"react": "^18.3.0"pin 18.3.1package-lock.json18.2.018.3.118.3.219.0.019.1.019.2.1pin^18.3.0 takes <19.0.0past the rangeINSIDE THE RANGE18.3.2: only the lockfile changesnpm update reactOUTSIDE THE RANGE19.2.1: the manifest changesnpm install react@19.2.1headwater reports both, so a major release is neverhidden. Whoever makes the change runs the command;headwater never does.

A repository names tiers of its dependencies in .config/headwater.toml, each with a cadence: the longest the tier may go without a complete scan. A repository with no such file has one tier, scanned every seven days. When a session starts, headwater scans the tiers that are due and reaches only their sources (15). Asked again before a tier falls due, it answers from what it holds and reaches no source (17). A scan that cannot reach a source reports what it could read, names the rest unread, and leaves the tier due (16).

Tiers and cadence: a scan reaches the sources only of tiers that are dueOver eight days a repository’s sessions start seven times. The favorites tier has a cadence of one day and the default tier of seven. At the first start both tiers are due and both scans reach the sources. Six hours later nothing is due, and the answer comes from what headwater holds, reaching no source. Each day the favorites tier falls due again and is scanned; on day three a source does not answer, the scan is incomplete, the favorites stay due, and the next start scans them again. On day eight the default tier is due again and both are scanned.THE SOURCESWHAT HEADWATER HOLDSday 1day 2day 3day 4day 5day 6day 7day 8favoritescadence 1 daydueduedueduedefaultcadence 7 daysduefirst start: both duesix hours on: nothing duea source failed: still dueseven days on: both duescanned, sources reachednot due: answered from what is heldunread: a source did not answer
Tiers and cadence: a scan reaches the sources only of tiers that are dueOver eight days a repository’s sessions start seven times. The favorites tier has a cadence of one day and the default tier of seven. At the first start both tiers are due and both scans reach the sources. Six hours later nothing is due, and the answer comes from what headwater holds, reaching no source. Each day the favorites tier falls due again and is scanned; on day three a source does not answer, the scan is incomplete, the favorites stay due, and the next start scans them again. On day eight the default tier is due again and both are scanned.THE SOURCESWHAT HEADWATER HOLDS12345678favoritescadence 1 daydueduedueduedefaultcadence 7 daysduescanned, sources reachednot due: answered from what is heldunread: a source did not answer

OSV answers which advisories name each pin. headwater reports them apart from the notes, and never infers one from what a release says (19). A person or an acting session may dismiss a release, or skip it until a newer one is published, and the release leaves the report (21). A dismissal never touches an advisory: a dependency whose pin an advisory names stays marked affected (22). When a bump moves the pin out of the advisory’s range, the advisory no longer names it.

Advisories beside releases: an acknowledgement never hides oneacme-http is pinned at 2.3.1. Three newer releases are reported, 2.4.1, 2.4.0 and 2.3.2, each with its notes as upstream wrote them. Apart from them, an OSV advisory of high severity names versions 2.0.0 to 2.3.1, so the dependency is marked affected. A person dismisses 2.4.0, and it leaves the report; the advisory and the affected mark stay. A fixer moves the pin to 2.4.1 and sluice lands it: no release is newer, the advisory no longer names the pin, and both leave the report.acme-httppkg:pypi/acme-httppin 2.3.1pin 2.4.1affectedRELEASES NEWER THAN THE PIN2.4.12026-10-14“Fixes a regression in 2.4.0’s retry loop.”2.4.02026-10-08“Adds HTTP/3 behind a flag; the default is unchanged.”2.3.22026-09-30“Rejects folded header lines, which 2.3.1 accepted.”dismissed by a person2.4.0 · dismissed by a person, left out of the reportnothing newer than 2.4.1ADVISORIES THAT NAME THE PINOSV · severity highaffects 2.0.0 to 2.3.1fixed in 2.3.2severity as the advisory gives itno longer names the pinno advisory names 2.4.1a fixer moves the pin to 2.4.1sluice lands it1Notes and advisoriesarrive apart.2A dismissed release leaves;the advisory stays.3The pin moves past it,and the advisory goes.
Advisories beside releases: an acknowledgement never hides oneacme-http is pinned at 2.3.1. Three newer releases are reported, 2.4.1, 2.4.0 and 2.3.2, each with its notes as upstream wrote them. Apart from them, an OSV advisory of high severity names versions 2.0.0 to 2.3.1, so the dependency is marked affected. A person dismisses 2.4.0, and it leaves the report; the advisory and the affected mark stay. A fixer moves the pin to 2.4.1 and sluice lands it: no release is newer, the advisory no longer names the pin, and both leave the report.acme-httppkg:pypi/acme-httppin 2.3.1pin 2.4.1affectedRELEASES NEWER THAN THE PIN2.4.12026-10-14“Fixes a regression in 2.4.0’s retry loop.”2.4.02026-10-08“Adds HTTP/3 behind a flag; the default is unchanged.”2.3.22026-09-30“Rejects folded header lines, which 2.3.1 accepted.”dismissed by a person2.4.0 · dismissed by a person, left out of the reportnothing newer than 2.4.1ADVISORIES THAT NAME THE PINOSV · severity highaffects 2.0.0 to 2.3.1fixed in 2.3.2severity as the advisory gives itno longer names the pinno advisory names 2.4.1a fixer moves the pin to 2.4.1sluice lands it1Notes and advisories arrive apart.2A dismissed release leaves; the advisory stays.3The pin moves past it, and the advisory goes.

For any release a report holds, headwater gives a brief: the release, its notes whole, its advisories, and every declaration of the dependency with the pin each gives, so a fixer can move every one (26). The brief names the ecosystem’s own command for the move: npm update or npm install <name>@<version>, uv lock --upgrade-package, cargo update -p, go get, mise use. Whoever makes the change runs it, and headwater never does. Beside the notes, a brief can carry the published package’s own diff between the pin and the release (npm diff), so a README or CHANGELOG change shows as upstream shipped it.

The fixer, a coding agent or a person, decides whether to make the change, and the repository’s own gate decides whether it lands.

headwater keeps its state in the repository’s git directory, so every worktree of one repository shares it and nothing lands in the repository’s content (28). Everything but acknowledgements can be thrown away: the next scan rebuilds it and reports the same releases at a new time (29). Two scans at once leave the state as one of them would have, whole (30).