Bump the mthds dependency
mthds is the MTHDS standard's Python client, and this SDK is not merely a
caller of it — it is built on its inheritance seam. PipelexAPIClient
subclasses MthdsAPIClient and reuses its transport (_send, _url), its
body-builders (_post_validate), its constants (_API_PREFIX,
_DEFAULT_REQUEST_TIMEOUT_SECONDS) and its degrade helper
(_raise_if_execute_degraded). pipelex_sdk/validation_models.py subclasses
its report and diagnostic models. pipelex_sdk/errors.py re-exports
RunStillRunningError as part of this package's own public surface.
That is what makes this bump different from bumping an ordinary dependency, in two directions at once:
- Upstream can break you without calling it a break. Half the surface this
repo depends on is underscore-prefixed.
mthds-pythonis free to rename_sendin a patch release, because by Python convention that name is private — but here it is a documented protected extension surface (seeCLAUDE.md, "Architecture invariants"). Read every release as if those names were public. - You can break your own consumers without writing the line. A renamed
symbol this package re-exports, or a tightened model this package narrows, is
a breaking change to
pipelex-sdk's API even though the diff is one version string. It has to reach the changelog as one.
The job is to land the new pin in a state a human can read and commit: pin moved, lock regenerated, source adapted, checks green, changelog and docs 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 — mthds==X.Y.Z. Possibly also [tool.ruff.lint.flake8-type-checking].runtime-evaluated-base-classes, which names mthds classes by dotted path (see step 7) |
uv.lock |
Regenerated by make li |
pipelex_sdk/** |
Wherever the new release renamed, split, moved or tightened something this client inherits, imports or re-exports |
tests/** |
Same, plus the hand-written httpx fixtures that encode a protocol model's shape |
CHANGELOG.md |
An entry under ## [Unreleased] — always at least the pin line |
docs/architecture.md |
Only when the inherited surface or the brand boundary moved — that document names both explicitly |
Do not touch [project].version. That is pipelex-sdk's own version and it
moves only at release time, via the release skill.
The pin is exact, and it always tracks latest
The requirement is mthds==X.Y.Z, not >=. That is the opposite of the usual
advice for a published library, so know why before you loosen it: this SDK does
not merely call mthds, it inherits its transport and narrows its protocol
models, and those models are extra="forbid" shapes the standard's client
tightens release by release. A range would let a resolver hand a consumer a
version this repo never tested against, and the break would surface as a parse
refusal inside validate at runtime rather than as a conflict at install time.
The standing policy is to move the pin to the latest release, every time, even when the new release carries nothing this client uses. Two consequences worth naming when you report:
- Every downstream install is forced onto exactly that
mthds. Say so in the changelog when the bump is otherwise uneventful, because for a consumer "pipelex-sdk now requires mthds 0.11.1" is the change. pipelex(the engine) pinsmthdsexactly too, and the two packages routinely land in one environment. Two exact pins on different versions do not resolve at all, so a skew between the repos is an install-time failure for anyone holding both, not a silent drift that surfaces later. Readpipelex/pyproject.tomlas part of this bump and report the skew if you find one; keeping the two moving together is why both repos run this same skill.
Four numbers, none of which is the others
This is the single most common way to get confused here, and two of them appear in the same sentence of an upstream changelog.
| Number | Where it lives | What it means |
|---|---|---|
The mthds package version |
mthds-python/pyproject.toml, PyPI |
The Python client's own release number. This is what you are bumping. |
| The MTHDS standard version | MTHDS_STANDARD_VERSION in mthds.package.manifest.schema |
The version of the standard that client implements. This repo never reads it — it stamps no crates and ships no manifest — so unlike in pipelex, there is nothing here to check when it moves. |
| The spec site's release number | the mthds/ repo's own CHANGELOG |
The documentation site's release. Coincidentally close to the package number; unrelated to it. |
pipelex-sdk's own version |
[project].version here |
This package's release number. Not yours to move — that is the release skill. |
Read every version reference in the upstream notes against this table before repeating it in ours.
Workflow
1. Orient
Read the pin from pyproject.toml, not from the virtualenv:
grep -n '"mthds' pyproject.toml
.venv/bin/python -c "import importlib.metadata as m; print(m.version('mthds'))"
Under an exact pin these two must agree, so any disagreement in either
direction means the same thing: someone edited pyproject.toml without
re-running make li. (That is a change from when the requirement was a floor,
where a venv resolved above the minimum was normal and unremarkable.)
Check git status and note what was already dirty before you start. At the
end you need to separate your changes from theirs and never stage something that
isn't yours.
Then ask the ledger what it already knows. Both mthds-python and pipelex
file items here when they land something this SDK will have to absorb, and those
items usually carry the exact diff — file, line, and the shape of the fix —
which is faster and more reliable than rediscovering it from a pyright error:
ledger inbound
ledger list --origin mthds-python --status open
Read past the rows owned by this repo: a row owned by pipelex or pipelex-sdk-js
is a sibling piece of the same cascade you will be filing into at step 10. Claim
(ledger claim <id>) any item that describes the adaptation you are about to do.
2. Resolve the target version
If the user named a version, use it. Otherwise ask PyPI:
curl -s https://pypi.org/pypi/mthds/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.
Ask PyPI, never the sibling checkout. mthds-python/pyproject.toml is a
working tree and is frequently ahead of what has been published — a version
sitting there under ## [Unreleased] is not installable, and a pin naming it
makes uv lock fail. If you notice the sibling is ahead, mention it in your
report (it is a preview of the next bump) but do not adopt it.
If the resolved version equals the current pin, say so and stop: there is
nothing to do, and manufacturing lock churn is worse than reporting "already
current". If the venv disagrees, mention that — make li is the real fix there.
3. Digest the upstream changes — before editing anything
For most dependencies you bump first and read the notes later. Not this one. Because this SDK builds on mthds' types and inheritance seam rather than merely calling it, the release notes are a work list, and reading them first is what turns a pyright cascade into a set of expected edits:
.venv/bin/python .claude/skills/bump-mthds/scripts/upstream_notes.py 0.9.0 0.11.0
The helper reads ../mthds-python/CHANGELOG.md and prints the released sections
strictly after the old pin up to and including the new one. It skips
## [Unreleased] — that section describes work that is not in the version you
are adopting, and this repo's changelog is read by people deciding whether an
upgrade will break them. If the checkout predates the target release the script
says so; fall back to:
gh release view v0.11.0 --repo mthds-ai/mthds-python
Read the notes for four things specifically, in this order of how often they bite here:
- Anything under
mthds.runners.api.client— the base class this client extends. A changed signature onexecute/start, a member moving from the base into a mixin, a renamed underscore helper. Upstream may not flag any of these as breaking. - Renames, splits, or module moves under
mthds.protocol— imported inclient.py,errors.py,validation_models.py,execute_result.py,runs.py, and in the tests. - Parse-time tightening. mthds' protocol models are
extra="forbid", and this SDK narrows several of them. A model that gains or loses a required field breaks both the narrowing and the hand-written test fixtures. - Anything that becomes redundant here. Duplication between this package
and
mthdsis deliberate and temporary in places (see step 6) — an upstream removal is your cue to delete something local, not to leave it standing.
4. Move the pin
One line in pyproject.toml. Make it a substring edit — replace ==0.11.0 with
==0.11.1 on that line 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 '"mthds' pyproject.toml before moving on. That grep
returns four hits, not one: the pin, plus the three dotted mthds.protocol
paths in the ruff runtime-evaluated-base-classes list. Read them now — if the
step-3 notes moved any of those three classes, that list moves in this same edit
(step 7 explains what happens when it doesn't).
5. 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 mthds in .venv, which everything
downstream of here depends on.
Do not reach for make update. That is uv lock --upgrade and it moves
every dependency in the tree at once — including the pinned dev toolchain
(ruff, pyright, mypy, pylint are == pins in [project.optional-dependencies].dev
precisely so lint findings do not appear out of nowhere). Moving the pin is
already enough to make plain uv lock re-resolve mthds.
Check the lock actually moved — a pin edit that didn't take is silent:
grep -A1 '^name = "mthds"' uv.lock
If uv lock reports the requirement is unsatisfiable, the version is almost
certainly not on PyPI yet. Re-run the query from step 2. If it genuinely isn't
published, stop and tell the user rather than inventing a git or path source:
adding one is a deliberate decision with a real cost (in uv, a source outranks
a version specifier, so the pin you wrote becomes decorative), not a
workaround to apply quietly.
6. Take stock of the seam
Before running anything, print what the base actually offers now. This is cheap, and it is the one reading that tells you whether a local workaround has expired:
.venv/bin/python - <<'PY'
import inspect
from mthds.runners.api.client import MthdsAPIClient
from pipelex_sdk.client import PipelexAPIClient
NOISE = {"_abc_impl", "_is_protocol", "_is_runtime_protocol"} # ABC/Protocol machinery, not a seam
base = {name for name, _ in inspect.getmembers(MthdsAPIClient) if not name.startswith("__")} - NOISE
own = set(PipelexAPIClient.__dict__) - NOISE
print("inherited, protected:", sorted(n for n in base - own if n.startswith("_")))
print("inherited, public: ", sorted(n for n in base - own if not n.startswith("_")))
print("overridden: ", sorted(base & own))
PY
Two things to compare it against:
- The protected list is the extension surface
CLAUDE.mdpins. A name that vanished from it is the break, andclient.pyis where it lands. - The overridden list is where local suppressions live.
docs/architecture.mdrecords that some duplication between this package andmthdsis transitional: the lifecycle models are owned here while the base still declared its own copies, and the narrow# type: ignore[override]onvalidateexists for exactly that divergence. When the base stops declaring a member, the suppression is no longer buying anything and should go, along with the paragraph in the docs that explains it.
7. Adapt the source
Run the type checkers first — they are the fastest and most complete readers of a protocol break, and they cost seconds where the suite costs minutes. Run both; they disagree about different things, and this repo gates on both:
make pyright
make mypy
Work from the step-3 notes and any ledger item, not from guesswork. The adaptations that recur here, in rough order of how often they bite:
- A renamed or moved member of the base class. Fix the call site in
client.py. If the shape changed rather than the name, adopt the new shape rather than reconstructing the old one locally. - A renamed or split type under
mthds.protocol. Fix the import, everyisinstance/matchnarrowing, and — critically — the ruff config. Theruntime-evaluated-base-classeslist inpyproject.tomlnamesmthds.protocol.models.ValidationReport,…InvalidValidationReportand…ValidationDiagnosticby dotted path. Ruff does not error on an entry that no longer resolves; it just silently stops treating those bases as runtime-evaluated, decides themthds.protocolannotations are type-only, and moves them into aTYPE_CHECKINGblock — where pydantic cannot resolve them when it builds the model. The failure is at import time, in the test run, with a message about an unresolvable annotation and nothing pointing at ruff. Greppyproject.tomlfor the old dotted path in the same edit as the import. - A tightened model this package narrows or constructs. mthds' protocol
models are
extra="forbid", so a member that used to ride through now raises at construction. Watch formodel_copy(update={...})— it does not validate, so it will happily leave a model in a state its class forbids, and the type checker is the only thing that notices. - A stale suppression.
reportUnnecessaryTypeIgnoreCommentis"none"in this repo's pyright config, so a# type: ignore[override]or# pyright: ignore[reportIncompatibleVariableOverride]that the bump just made unnecessary will sit there forever without a single warning. Step 6 is how you find them; check each one by hand against the new base. - A new abstract method on something this repo subclasses. Implement it;
don't
raise NotImplementedErrorto get green.
Failures here are the breaking change announcing itself, not incidental breakage. Adopt the new API. A bump whose failures were papered over is worse than no bump.
8. Run the checks
make agent-check
make agent-test
agent-check is fix-unused-imports format lint pyright mypy. Before you call
the bump done, also run the two gates it leaves out, since CI runs them:
make pylint
make check-unused-imports
Read test failures carefully — they come in two kinds and only one is a bug.
Unit tests here mock at the httpx boundary with hand-written JSON, so a
tightened or reshaped upstream model produces a failure that says "your fixture
is stale", not "your code is wrong". Updating the fixture is correct when the
new shape is what the hosted API actually emits; it is a cover-up when the
client should have been the thing that changed. tests/unit/test_validation_contract.py
is the one that pins the nesting of the two strictness regimes (closed imported
artifacts inside an extension-open report envelope) — a failure there is about
the contract, not the fixture, and deserves a real answer.
9. Write the changelog and the docs
Add to CHANGELOG.md under ## [Unreleased], creating that heading right after
# Changelog if it isn't there. Work in progress accumulates there until a
release cuts it into a version heading — do not add a ## [vX.Y.Z] heading
yourself, and do not bump [project].version.
Every bump gets at least the pin line. Follow the house form, which names the release by what it carries:
- Moved the exact `mthds` pin from `==0.11.0` to `==0.11.1`, <the release
that carries / the release fixing …>.
A pin move narrows what every downstream environment may install, so it is breaking for this package's consumers even when the upstream release is not. Mark it so.
Anything that reached pipelex-sdk's own surface gets its own entry under
### Changed, marked breaking, written for an importer deciding whether this
upgrade will cost them work. Name the old symbol and the new one — a reader
hitting an ImportError searches for the name they had. Write "breaking", not
"pre-1.0 breaking". Three things count as this package's surface even though the
symbol belongs to mthds: the re-export in errors.py, the narrowing
subclasses in validation_models.py, and any imported annotation that
appears in a public signature.
If the bump changed the inherited surface, the brand boundary, or retired a
transitional duplication, update docs/architecture.md in the same change — it
describes all three by name, and a bump is exactly what makes those paragraphs
stale. If the bump is genuinely uneventful, say that 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.
10. Square the ledger
- Close what you actually landed, with evidence — the file and line you
changed, and the check that went green.
Closes <id>goes in the PR body when the user opens one. - File the parity item.
pipelex-sdk-jsis this package's twin and consumes themthdsnpm package; when a protocol model moves, it usually moves in both languages. That repo's move is not yours to make from here — file it (ledger new --owner pipelex-sdk-js …) naming the symbol and the version. - File the engine item if the pins have diverged. If
pipelexnames a different exactmthdsafter this, the two packages no longer co-install at all — that is not a latent gap but a live break, and it belongs to that repo. - File the reverse direction if you found an upstream problem. A protocol
model that cannot express what the hosted API emits is an item owned by
mthds-python, with the payload that broke it. ledger validate, thenledger commit. Nothing else pushes the ledger.
11. Report and stop
Show the user:
- The pin move, old → new, and whether
uv.lockactually followed. - Whether
pipelex's exactmthdspin agrees with the one you just wrote. - What changed on the inherited seam (step 6) — that is the part nobody can see from the diff, and the part most likely to matter next time.
- 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. If you updated a test fixture, say which and why it was the fixture that was wrong. - Whether the bump is breaking for
pipelex-sdk's own importers, since that is what decides how the next release is written and who has to move after it. - 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 ask, 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
- Upstream's private is this repo's contract.
_send,_url,_post_validate,_raise_if_execute_degradedare underscore-prefixed upstream and load-bearing here. A patch release can move them. - The ruff
runtime-evaluated-base-classeslist names mthds classes by dotted path. A module move there fails at runtime, in pydantic, with nothing pointing back at the lint config. reportUnnecessaryTypeIgnoreCommentis off. Suppressions here never expire on their own; step 6 is the only thing that finds them.- The pin and
pipelex's pin are one system. Both packages namemthdsexactly, so a version this repo moves to alone makes the pair uninstallable together. Checkpipelex/pyproject.tomlin the same pass, and say what you found even when they agree. - The sibling checkout is routinely ahead of PyPI. Read
mthds-python's changelog for understanding, PyPI for the target version. A pin naming an unpublished version failsuv lock. ## [Unreleased]upstream is not in the version you adopted. Never quote it as part of the release. The helper script skips it for you.model_copy(update=…)does not validate. It is how a model ends up holding a value its class forbids, green at runtime, wrong on the type.make updateis notmake li. The former upgrades the whole tree, including the==-pinned linters.- A stale test fixture and a real break look identical. Both are a red test against a hand-written JSON body. Decide which one you are looking at before editing either side.
MTHDS_STANDARD_VERSIONis not this repo's problem.pipelexstamps it onto crates and has to track it; this SDK never reads it. Don't port that step over from the engine's version of this skill.[project].versionispipelex-sdk's own version. Bumping it is thereleaseskill's job, not this one's.