Defaults.pkl imports Config.pkl/Builtins.pkl from a version-pinned hk release, and consumers separately amends their own pinned hk Config.pkl. That's two pins for the same Pkl module moved by two different clocks: renovate raises the consumer's pin to the newest hk release, while hk-config's pin is frozen in whatever the last hk-config release shipped. Whenever hk publishes ahead of tooling, the graph is skewed and Pkl rejects it on module identity.
This broke gtbuchanan/dotfiles#64, where the consumer amended hk 1.54.1 while hk-config@0.2.3 imported 1.54.0:
Module version conflict: Expected value of type `hk.Config#Hook` defined in module
`…hk@1.54.1#/Config.pkl`, but got type `hk.Config#Hook` defined in module `…hk@1.54.0#/Config.pkl`.
Grouping the two bumps into one PR (which the shared Renovate config already does) makes them land together but cannot make them agree — hk-config's pin lives inside a published artifact.
Status: blocked upstream
The end state is right — the pin should exist in exactly one place — but every mechanism for getting there is blocked by hk, not by Pkl. Details below. Blocked rather than rejected: a fix to hk's module resolver makes this implementable immediately.
Rejected: publish packages/hk-config/Config.pkl for consumers to amend
The original proposal here was to add a Config.pkl that amends hk's Config.pkl and holds the sole hk pin, so consumers amend that instead of naming an hk version at all. It was implemented and does not work.
Built hk-config with Config.pkl plus a Builtins.pkl re-export (needed too — builtin steps are typed by the Config.pkl that hk's own Builtins.pkl imports, so importing hk's builtins directly reintroduces the same pin), injected the package into Pkl's cache, and ran four consumers against the same hk-config@0.2.3 cache entry with hk 1.54.1 and Pkl 0.32.1:
Consumer hk.pkl |
hk validate |
amends hk directly, imports the package's Defaults.pkl (status quo) |
valid |
amends the package's Defaults.pkl (a module with no nested package amends) |
valid |
amends the package's Config.pkl (the proposal) |
IO error reading …\pklr-pkg-0-…\Config.pkl |
full proposed usage (Config + Defaults + Builtins) |
same error |
Same package, same cache entry, same shell — only the imported module differs. hk cannot resolve a package module whose own header is amends "package://…". It extracts the package to a temp directory and then fails to read the file back out. Deterministic across repeated runs.
Every one of those consumers evaluates correctly under real pkl eval, including the full facade. This is hk's Pkl evaluation path, not Pkl semantics and not a flaw in the pattern. It also explains the pre-filing verification that suggested this would work: the chained amends does render fully under pkl, and does not under hk.
Rejected: Pkl project dependencies (minimum version selection)
The obvious alternative, and the one Pkl actually designed for this: MVS over project dependencies would unify a consumer on 1.54.1 with a preset on 1.54.0 automatically and make the facade unnecessary. Unavailable for two independent reasons:
- hk's Pkl evaluation path isn't project-aware. With a valid
PklProject and resolved PklProject.deps.json beside it, hk validate fails with Import not found: …\@hk/Builtins.pkl — it resolves @hk/… as a literal relative path. The identical file renders under pkl eval --project-dir.
- Even with that fixed, MVS still wouldn't unify hk's pins. hk's package URI embeds the version in the release download path, so the resolver keys
…/download/v1.54.1/hk@1 and …/download/v1.54.0/hk@1 as two unrelated packages. Resolving a consumer on 1.54.1 alongside a preset on 1.54.0 emits two live hk entries rather than one.
A version-free baseUri upstream fixes the second half. Our own hk-config is already shaped correctly for this (package://github.com/gtbuchanan/tooling/hk-config@0).
Untested
An extends-based facade instead of amends is untested, not ruled out. The offline cache-injection harness stopped honoring freshly injected entries partway through, and a control using amends on the same fresh entry failed identically — so that run says nothing about extends. Confirming it needs a real published prerelease.
Separately, the hk binary tolerates trailing the Pkl module version: hk validate passes with a binary several minors ahead of the module graph, and the drift direction is safe by construction, since renovate keeps the binary at latest and hk-config can only pin an already-published hk. That still holds and is not affected by the above.
Next
- A
gtb verify hk guard comparing the consumer's amends version against the hk-config release's pin, turning a Module version conflict deep in Pkl into a clear "bump hk to X". Doesn't remove the double pin, removes the baffling failure. Actionable now, independent of upstream — tracked separately.
- Report the nested-package
amends failure upstream. jdx/hk has issues disabled, and discussion #1008 — someone hitting this from the other direction, trying to publish a package exporting Config.Step — has been unanswered since June. A minimal deterministic reproducer of a resolver bug is a stronger thing to post than a design question, and it's the blocker for the pattern two people arrived at independently.
- The Renovate hk group rule stays the mitigation meanwhile; it carries more weight than previously credited.
Defaults.pklimportsConfig.pkl/Builtins.pklfrom a version-pinned hk release, and consumers separatelyamendstheir own pinned hkConfig.pkl. That's two pins for the same Pkl module moved by two different clocks: renovate raises the consumer's pin to the newest hk release, while hk-config's pin is frozen in whatever the last hk-config release shipped. Whenever hk publishes ahead of tooling, the graph is skewed and Pkl rejects it on module identity.This broke gtbuchanan/dotfiles#64, where the consumer amended hk 1.54.1 while hk-config@0.2.3 imported 1.54.0:
Grouping the two bumps into one PR (which the shared Renovate config already does) makes them land together but cannot make them agree — hk-config's pin lives inside a published artifact.
Status: blocked upstream
The end state is right — the pin should exist in exactly one place — but every mechanism for getting there is blocked by hk, not by Pkl. Details below. Blocked rather than rejected: a fix to hk's module resolver makes this implementable immediately.
Rejected: publish
packages/hk-config/Config.pklfor consumers to amendThe original proposal here was to add a
Config.pklthat amends hk'sConfig.pkland holds the sole hk pin, so consumers amend that instead of naming an hk version at all. It was implemented and does not work.Built
hk-configwithConfig.pklplus aBuiltins.pklre-export (needed too — builtin steps are typed by theConfig.pklthat hk's ownBuiltins.pklimports, so importing hk's builtins directly reintroduces the same pin), injected the package into Pkl's cache, and ran four consumers against the samehk-config@0.2.3cache entry with hk 1.54.1 and Pkl 0.32.1:hk.pklhk validateDefaults.pkl(status quo)Defaults.pkl(a module with no nested package amends)Config.pkl(the proposal)IO error reading …\pklr-pkg-0-…\Config.pklConfig+Defaults+Builtins)Same package, same cache entry, same shell — only the imported module differs. hk cannot resolve a package module whose own header is
amends "package://…". It extracts the package to a temp directory and then fails to read the file back out. Deterministic across repeated runs.Every one of those consumers evaluates correctly under real
pkl eval, including the full facade. This is hk's Pkl evaluation path, not Pkl semantics and not a flaw in the pattern. It also explains the pre-filing verification that suggested this would work: the chained amends does render fully underpkl, and does not underhk.Rejected: Pkl project dependencies (minimum version selection)
The obvious alternative, and the one Pkl actually designed for this: MVS over project dependencies would unify a consumer on 1.54.1 with a preset on 1.54.0 automatically and make the facade unnecessary. Unavailable for two independent reasons:
PklProjectand resolvedPklProject.deps.jsonbeside it,hk validatefails withImport not found: …\@hk/Builtins.pkl— it resolves@hk/…as a literal relative path. The identical file renders underpkl eval --project-dir.…/download/v1.54.1/hk@1and…/download/v1.54.0/hk@1as two unrelated packages. Resolving a consumer on 1.54.1 alongside a preset on 1.54.0 emits two live hk entries rather than one.A version-free
baseUriupstream fixes the second half. Our own hk-config is already shaped correctly for this (package://github.com/gtbuchanan/tooling/hk-config@0).Untested
An
extends-based facade instead ofamendsis untested, not ruled out. The offline cache-injection harness stopped honoring freshly injected entries partway through, and a control usingamendson the same fresh entry failed identically — so that run says nothing aboutextends. Confirming it needs a real published prerelease.Separately, the hk binary tolerates trailing the Pkl module version:
hk validatepasses with a binary several minors ahead of the module graph, and the drift direction is safe by construction, since renovate keeps the binary at latest and hk-config can only pin an already-published hk. That still holds and is not affected by the above.Next
gtb verify hkguard comparing the consumer'samendsversion against the hk-config release's pin, turning aModule version conflictdeep in Pkl into a clear "bump hk to X". Doesn't remove the double pin, removes the baffling failure. Actionable now, independent of upstream — tracked separately.amendsfailure upstream. jdx/hk has issues disabled, and discussion #1008 — someone hitting this from the other direction, trying to publish a package exportingConfig.Step— has been unanswered since June. A minimal deterministic reproducer of a resolver bug is a stronger thing to post than a design question, and it's the blocker for the pattern two people arrived at independently.