Bump the kajson dependency
kajson is this engine's serialization layer: the universal encoder/decoder,
the ClassRegistry that resolves a payload's __class__ back to a live type,
and the KajsonManager singleton that owns that registry for the process. It is
a repo we own, in the sibling kajson/ checkout.
Two facts decide how this bump differs from bumping an ordinary dependency, and they pull in opposite directions:
- kajson's output is a wire format, not just an API. What it encodes is what
crosses the Temporal transport, what a working memory rehydrates from, and
what
__kajson_class_source__carries. A change to the format is a compatibility question between two processes, which a type checker cannot see and a single-process test suite will not fail on. - kajson does not reach this engine's public surface. Nothing in
pipelex/**re-exports a kajson symbol through an__all__, so an upstream rename is an internal adaptation here, not a breaking change to pipelex's API. (This is exactly wherebump-mthdsdiffers — verify rather than assume it, by grepping__all__for kajson symbols, since a future re-export would change the answer.)
The job is to land the new pin in a state a human can read and commit: pin moved, lock regenerated, source adapted, workarounds retired, checks green, changelog written, ledger squared. Stop before committing — the user stages and commits.
What the bump touches
| File | Why it moves |
|---|---|
pyproject.toml |
The pin itself, one line in [project].dependencies — kajson==X.Y.Z |
uv.lock |
Regenerated by make li |
pipelex/** |
Wherever the release renamed something this engine imports, or made a local workaround unnecessary |
tests/** |
Same, plus any test asserting on a serialized shape that moved |
CHANGELOG.md |
An entry under ## [Unreleased] — always at least the pin line |
.badges/tests.json, .test_durations |
Only if the adaptation added, removed or renamed tests |
Do not touch [project].version. That is this package's own version and it
moves only at release time, via the release skill.
The pin is exact, and this one has always been
The pin is kajson==X.Y.Z, not >=. Unlike the mthds pin — which was a floor
until recently — kajson has been pinned exactly since v0.1.5, and the changelog
recorded the reason at the time: to tolerate breaking changes from a dependency
we ourselves move quickly.
That reason still holds, and the wire format sharpens it. Two processes
exchanging a kajson payload must agree on the codec, and an installed-range
resolve is exactly how they end up not agreeing. Never relax the == to >=
while bumping, and never widen it to get a resolve unstuck.
The standing policy is to move the pin to the latest release, every time, even when the release carries nothing this engine uses.
The cascade is narrow — say so rather than hunting for it
pipelex/ is the only repo in this workspace that names kajson in a
pyproject.toml. Everyone else — pipelex-sdk-python, every pipelex-server
member, every user install — receives kajson transitively, through this exact
pin. Confirm it rather than trusting this sentence, because a new direct
dependent would change the conclusion:
cd .. && grep -rn "kajson" --include="pyproject.toml" . | grep -v "^./kajson/" | grep -v "^./pipelex/"
Two consequences, and the first is the pleasant one:
- No sibling repo has to move in step. There is no second pin to make unsatisfiable, so unlike an mthds bump this one files no "bump your pin too" item — unless the grep above finds a new direct dependent.
- But everyone still inherits it. "pipelex now requires kajson X.Y.Z" is the user-visible change when the bump is otherwise uneventful, and a consumer that imports kajson directly (without depending on it) is now reading a different version's classes. Say so in the changelog.
kajson is ours — so read the notes for what you can now delete
This is the habit that most distinguishes a kajson bump from a third-party one.
Because we file bugs into kajson and it fixes them, a release frequently
retires something in this repo rather than breaking it: a guard at a call
site, a defensive try, a locally declared dependency, a test asserting the old
broken behaviour.
Two examples from real releases, so the pattern is recognisable:
- v0.7.0 declared
tzdataas its own runtime dependency, and its notes said outright that consumers who had addedtzdatato work around the decode failure could drop it. This repo did. - v0.7.1 made
ClassRegistry.register_classes_dict({})a no-op instead of raisingIndexError, and its notes said callers guarding the call site could drop the guard. Here the same fix removes a latent crash at the two unguarded seeding sites (pipelex/runtime_bridge/primitives/rehydration.pyand the integration fixture that mirrors it) rather than freeing a guard.
So read each release's notes twice: once for "what must I adapt to", once for "what can I now delete". The second pass is the one nobody remembers, and a workaround that outlives its cause is the kind of code that confuses the next reader for years.
Workflow
1. Orient
Read the pin from pyproject.toml, not from the virtualenv:
grep -n '"kajson' pyproject.toml
.venv/bin/python -c "import importlib.metadata as m; print(m.version('kajson'))"
Under an exact pin these two must agree in both directions. Any disagreement
means the venv is stale — someone edited pyproject.toml without re-running
make li, or installed over it. Re-sync with make li before you measure
anything against the venv.
Check git status and note what was already dirty before you start.
Concurrent work in this worktree is normal, and at the end you must separate
your changes from theirs and never stage something that isn't yours.
Then ask the ledger, in both directions — this is a two-way relationship with a repo we control:
ledger list --owner kajson --status open
ledger list --origin kajson --status open
The first is what we asked kajson for: an open item here whose fix ships in
the release you are adopting closes on this bump, with the bump as its evidence.
The second is what kajson filed at us. Claim (ledger claim <id>) anything you
are about to work.
2. Resolve the target version
If the user named a version, use it. Otherwise ask PyPI:
curl -s https://pypi.org/pypi/kajson/json \
| python3 -c "import json,sys; print(json.load(sys.stdin)['info']['version'])"
info.version is the latest non-prerelease, which is what "latest" means for a
pin other people inherit exactly.
Ask PyPI, never the sibling checkout. kajson/pyproject.toml is a working
tree and can be ahead of what is published — a version sitting there under
## [Unreleased] is not installable, and a pin naming it makes uv lock fail.
If the sibling is ahead, that is worth mentioning in your report (it previews
the next bump) but it does not change the target.
If the resolved version equals the current pin, say so and stop: manufacturing
lock churn is worse than reporting "already current". If the venv disagrees,
mention it — make li is the fix there.
3. Digest the upstream changes — before editing anything
.venv/bin/python .claude/skills/bump-kajson/scripts/upstream_notes.py <old> <new>
The helper reads ../kajson/CHANGELOG.md and prints the released sections
strictly after the old pin up to and including the new one. It skips
## [Unreleased], which describes work that is not in the version you are
adopting. If the checkout predates the target release it says so; fall back to:
gh release view v<new> --repo Pipelex/kajson
kajson's changelog is unusually detailed about compatibility — read it for four things specifically:
- Wire-format changes, and in which direction they are compatible. kajson's notes state this explicitly when it matters (v0.7.0's read "In a mixed-version fleet, upgrade consumers before producers"). See step 6.
- Registry and singleton semantics —
ClassRegistry,ClassRegistryAbstract,KajsonManager,MetaSingleton. See step 5. - Workarounds this release retires, per the section above.
- Dependency changes of kajson's own (it declared
tzdataonce already), since those arrive in this repo's lock without any edit here.
4. Move the pin
One line in pyproject.toml. Make it a substring edit — replace ==<old> with
==<new> on that line, keeping the == — and leave every other character
alone. Use your editor rather than a shell one-liner: sed -i takes a separate
empty argument on macOS and an attached suffix on GNU/Linux, so no single
invocation is portable, and the release skill edits this same file the same
way. Confirm with grep -n '"kajson' pyproject.toml.
Then re-lock and install:
make li
That is make lock (uv lock) plus make install (uv sync --all-extras), so
it rewrites uv.lock and puts the new kajson in .venv.
Do not reach for make update. That is uv lock --upgrade, which moves
every dependency at once and turns a reviewable one-package bump into a diff
nobody can reason about. Moving the pin is enough to make plain uv lock
re-resolve kajson.
Verify the lock actually moved — a pin edit that didn't take is silent:
grep -A1 '^name = "kajson"' uv.lock
If uv lock reports the requirement unsatisfiable, the version is probably not
on PyPI yet; re-run step 2. If it genuinely isn't published, stop and tell the
user rather than inventing a git or path source — in uv a source outranks a
version specifier, so adding one quietly makes the pin decorative.
5. Adapt the source, and check the boot seam by hand
Run the type checker first — it is the fastest reader of an API break and costs seconds where the suite costs minutes:
make pyright
Work from the step-3 notes and any ledger item, not from guesswork. Then look at
the one seam the type checker cannot judge: KajsonManager is a
process-global singleton, and this engine's boot and teardown are built around
that fact. pipelex/runtime_boot.py constructs it with the class registry and
tears it down explicitly, and the comments there warn that it is easy to get
backwards — because the singleton silently discards a fresh registry when one
already exists, a missed teardown poisons the next boot rather than raising.
So if the release touches singleton lifetime, teardown, or registry ownership,
re-read runtime_boot.py's construction and teardown sites together and make
sure the invariant the comments describe still holds. The boot/teardown tests
(tests/unit/pipelex/test_runtime_boot_teardown_resilience.py,
test_hub_lifecycle.py, test_class_registry_scoping.py) are the ones that
would catch a regression here — run them early rather than at the end.
Adopt the new API where it moved; a bump whose failures were papered over is worse than no bump.
6. Ask the wire-format question explicitly
The suite serializes and deserializes in one process, with one kajson. That proves round-tripping and proves nothing about two processes on different versions — which is the shape of a distributed run, where a payload encoded by one pipelex is decoded by another.
If step 3 turned up any encode/decode change, answer both directions in your report rather than leaving it implied:
- Can a payload produced by the new kajson be read by the old one? (Additive fields usually yes; a changed representation usually no.)
- Can a payload produced by the old kajson still be read by the new one? (kajson has been careful about this — its notes say when legacy payloads still decode — but confirm it from the notes rather than assuming.)
Where the answer is asymmetric, the upgrade has an ordering requirement, and that belongs in the changelog in plain words. If nothing in the release touches the format, say that in one sentence and move on.
7. Run the checks
make agent-check
make agent-test
agent-check includes drift-check, which reads the git index, not the
working tree — so git add your changes before you trust its verdict. If it
reports an open contract, resolve it with the drift-review skill rather than
acking it blind.
If the adaptation added, removed or renamed tests, two committed artifacts go stale and neither is caught by a local aggregate:
make store-test-durations # .test_durations — keeps the CI shards balanced
make check-test-badge # .badges/tests.json — runs only in CI, so check it here
8. Write the changelog entry
Add to CHANGELOG.md under ## [Unreleased], creating that heading right after
# Changelog if it isn't there. Do not add a ## [vX.Y.Z] heading yourself,
and do not bump [project].version.
Every bump gets at least the pin line, naming the release by what it carries:
- **`kajson` version**: The pin moves to `kajson==X.Y.Z`, <the release that
carries / the release fixing …>. Everyone downstream inherits that exact
version.
Three things earn their own entry beyond the pin line, because a reader deciding whether to upgrade cannot get them anywhere else:
- A wire-format change, with the compatibility direction from step 6.
- A workaround this repo dropped, with what it was working around — that is a real behaviour change here, even though the cause was upstream.
- A fix that reaches users through this engine (a payload that used to fail to decode, a crash that is now a no-op). Write it as the user experiences it, not as the upstream diff.
If the bump is genuinely uneventful, say so in one sentence and stop. Padding a quiet bump with upstream detail that doesn't affect this repo makes the loud ones harder to spot.
9. Square the ledger
- Close what the release actually fixed. An item we filed against kajson
(
ledger list --owner kajson) whose fix ships in this version closes here, with evidence: the version that carries it and the check that went green.Closes <id>goes in the PR body when the user opens one. - File anything the bump revealed, in the right direction: a kajson defect
is
ledger new --owner kajson …naming the version and the reproducer; work this repo must do later is filed here. Theledgerskill has the gesture. - Only file a cascade item if the step-3 grep found a new direct dependent. Normally there is none.
ledger validate, thenledger commit. Nothing else pushes the ledger.
10. Report and stop
Show the user:
- The pin move, old → new, and whether
uv.lockactually followed. - What the release fixed for users of this engine, and what it let you delete — the two things a kajson bump is most often actually about.
- The wire-format verdict from step 6, in both directions, or "format untouched".
- Every file you changed, separated from what was already dirty when you started.
- The check results, honestly — if
agent-testfailed, say so with the output rather than reporting a bump as done. - Anything left for a human: an upstream change whose adaptation is a judgment call, or a version the sibling checkout has that PyPI doesn't yet.
Then stop. Do not commit, branch, or push unless the user asks. If they do,
stage the files explicitly by path — never git add -A. A working branch
here is chore/<Topic> (spelled out, from the closed prefix set) and PRs target
dev.
Traps worth remembering
- Nothing in this repo enforces the
==. No check fails if the pin is relaxed to>=;pyproject.tomlis the only place the policy lives, so a widened pin ships silently. - The pin is not the lock. Editing
pyproject.tomlwithout re-locking leavesuv.lockon the old version — which is what the venv and the suite actually use, giving a green run against the version you meant to leave behind. That is why step 4 verifies the lock moved. - The suite cannot see a wire-format break. One process, one kajson: it round-trips against itself and passes. Step 6 is the only thing standing between a format change and a cross-version decode failure.
KajsonManagerfails silently, not loudly. It is a singleton that discards a fresh registry rather than complaining, so a lifetime regression shows up as a later boot behaving oddly, far from the change.- The sibling checkout can be ahead of PyPI. Read
kajson's changelog for understanding, PyPI for the target version. ## [Unreleased]upstream is not in the version you adopted. Never quote it as part of the release; the helper script skips it for you.make updateis notmake li. The former upgrades the whole tree.drift-checkreads the git index. Unstaged changes give a false green.check-test-badgeruns in no local aggregate. Green here, red in CI, on any commit that changes the test count.[project].versionis pipelex's own version. Bumping it is thereleaseskill's job, not this one's.