ADR-0039: Ship the developer-experience pack as package data
Status: Accepted
Date: September 2026
Context
A coding agent is only correct about Protean if it reasons about the version of the framework that is installed. The teaching skills and the agent instruction surface (AGENTS.md) describe an API that changes release to release. When that knowledge lives apart from the framework, the two drift: an agent reads guidance for one version while the code runs another, and the model reasons about an API that is no longer there.
The developer-experience epic ships a body of agent-facing knowledge: an AGENTS.md source and a set of skills. It needs a home that couples it to the framework version by construction, a runtime read path that works whether the package is installed unpacked or zipped, and a guard that the data actually reaches the built wheel.
Two existing data trees set the precedent. The template/ copier tree and the
IR JSON schemas under ir/schema/ already ship as package data inside
protean and are read at runtime. Hatchling includes them with no build
configuration: its wheel target picks up every file under src/protean/,
whatever its git status.
Decision
We ship the pack as package data inside the protean wheel, under
src/protean/dx/pack/. The pack travels in the same distribution as the code,
so its version is the framework version and the two cannot drift.
We read the pack through importlib.resources. The accessor lives in
protean.dx.pack and is re-exported from protean.dx, so
protean.dx.load_agents_source() and protean.dx.pack_files() reach the data.
importlib.resources is the standard read path for package data and keeps
working when protean is installed zipped. The accessor mirrors the shape of
protean.ir: module-level constants (PACK_VERSION, AGENTS_SOURCE,
SKILLS_DIR) and load_* helpers.
We add no build configuration. Hatchling already ships every file under
src/protean/ through its packages target, proven by template/ and
ir/schema/. A CI job builds the wheel, installs it into a clean virtualenv
that cannot see the source tree, and asserts the pack is importable and readable
from the installed package. That job is the guard against silent loss of the
data from the build.
The pack's version is the framework version. PACK_VERSION resolves to
protean.__version__, the framework's single version source. A consumer reads
it to tell which framework version's guidance the pack carries, and
bump-my-version moves it, so it stays in step with the content it labels. A hand-typed version
constant would drift the day someone edits a skill and forgets to bump it.
Consequences
The pack and the framework release together, so an agent reading the pack always sees guidance for the installed code. There is no second distribution to publish and no version pin between them.
Reading through importlib.resources hands consumers a Traversable, which is
the abstract resource interface rather than a filesystem Path. Code that needs
a real path on disk has to materialize one. The file-projection engine that
renders these files into a user's project (ADR-0037) reads text, so a
Traversable is enough.
The data files have to be committed so a clean checkout carries them. CI builds the wheel from a fresh checkout, which holds only committed content, so an uncommitted file drops out of that wheel with no error at build time. The clean-venv CI check catches that, and a deleted or moved file too, because it reads the pack from the installed wheel rather than the source tree.
The wheel grows with the pack. The skills corpus is text, so the cost is small, and the coupling it buys is the point of the epic.
Alternatives Considered
A separate protean-skills distribution. Publishing the pack as its own
package on PyPI reintroduces the split it was meant to close: a user can install
a skills version that does not match their framework version, and the drift is
back. Shipping inside protean couples them by construction.
Reading with Path(__file__).parent. The repo reads ir/schema/ this way
today, and it works for an unpacked install. It breaks when the package is
installed zipped, because there is no file on disk to point Path at.
importlib.resources covers both cases, so the DX layer adopts it and leaves the
older read path where it is.
A force-include or artifacts entry in the build config. Hatchling's
default file selection already ships the pack, so an explicit build-config entry
would add configuration that restates the default. The clean-venv check verifies
the outcome without it.