# Bump Kajson

> Move this repo's exact `kajson` dependency pin to the latest release on PyPI (or a version you name), re-lock, adapt the engine to whatever the new release changed, retire any workaround the release makes unnecessary, run the checks, and write the CHANGELOG entry — stopping before the commit. Use whenever the user says "bump kajson", "update kajson", "upgrade kajson", "move to kajson 0.8.0", "get us on the latest kajson", "is our kajson pin stale", "what's the latest kajson", or asks to build against a newly published `kajson` release. Also use when a failure smells like a kajson version mismatch — a `KajsonDecoderError` or `UnijsonEncoderError` on a payload that used to round-trip, a `ClassRegistryNotFoundError` after an upstream change, a pyright `reportAttributeAccessIssue` on something under `kajson.`, or a `uv lock` reporting kajson unsatisfiable. This is the **`kajson` PyPI package** (the universal JSON encoder/decoder from the sibling `kajson` repo, which is the serialization layer this engine is built

- Skill: `pipelex/bump-kajson` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add pipelex/bump-kajson`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pipelex/bump-kajson/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: pipelex (https://skillmd.com/u/pipelex)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pipelex/bump-kajson

---


# 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 where `bump-mthds` differs — 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:

```bash
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 `tzdata` as its own runtime dependency**, and its notes said
  outright that consumers who had added `tzdata` to work around the decode
  failure could drop it. This repo did.
- **v0.7.1 made `ClassRegistry.register_classes_dict({})` a no-op** instead of
  raising `IndexError`, 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.py`
  and 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:

```bash
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:

```bash
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:

```bash
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

```bash
.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:

```bash
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 `tzdata` once 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:

```bash
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:

```bash
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:

```bash
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

```bash
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:

```bash
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*:

```markdown
- **`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. The `ledger` skill has the gesture.
- **Only file a cascade item if the step-3 grep found a new direct dependent.**
  Normally there is none.
- `ledger validate`, then `ledger commit`. Nothing else pushes the ledger.

### 10. Report and stop

Show the user:

- The pin move, old → new, and whether `uv.lock` actually 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-test` failed, 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.toml` is the only place the policy lives, so a
  widened pin ships silently.
- **The pin is not the lock.** Editing `pyproject.toml` without re-locking
  leaves `uv.lock` on 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.
- **`KajsonManager` fails 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 update` is not `make li`.** The former upgrades the whole tree.
- **`drift-check` reads the git index.** Unstaged changes give a false green.
- **`check-test-badge` runs in no local aggregate.** Green here, red in CI, on
  any commit that changes the test count.
- **`[project].version` is pipelex's own version.** Bumping it is the `release`
  skill's job, not this one's.

