| name | bump-mthds |
| description | Move this repo's exact `mthds` dependency pin to the latest release on PyPI (or a version you name), re-lock, adapt the client to whatever the new release broke, run the checks, and write the CHANGELOG entry — stopping before the commit. Use whenever the user says "bump mthds", "update mthds", "upgrade mthds", "move to mthds 0.11.0", "get us on the latest mthds", "is our mthds floor stale", "what's the latest mthds", or asks to build this SDK against a newly published `mthds` release. Also use when a failure looks like an mthds version mismatch — a pyright `reportAttributeAccessIssue` on an inherited `MthdsAPIClient` member (`_send`, `_url`, `_post_validate`), a `reportIncompatibleMethodOverride` or `reportIncompatibleVariableOverride` on one of the validate narrowings, an ImportError for a name that used to be under `mthds.protocol`, a pydantic `Extra inputs are not permitted` from a protocol model, or a `uv lock` saying mthds is unsatisfiable. This is the **`mthds` PyPI package** (the MTHDS standard's Python client, from the sibling `mthds-python` repo) — not the MTHDS spec pages, not the `mthds` npm package, and not releasing `pipelex-sdk` itself, which is the `release` skill.
|
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-python is free to rename
_send in a patch release, because by Python convention that name is private —
but here it is a documented protected extension surface (see CLAUDE.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) pins mthds exactly 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. Read
pipelex/pyproject.toml as 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 on execute / 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 in
client.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
mthds is 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"}
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.md pins. A name that
vanished from it is the break, and client.py is where it lands.
- The overridden list is where local suppressions live.
docs/architecture.md
records that some duplication between this package and mthds is transitional:
the lifecycle models are owned here while the base still declared its own
copies, and the narrow # type: ignore[override] on validate exists 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, every
isinstance / match narrowing, and — critically — the ruff config. The
runtime-evaluated-base-classes list in pyproject.toml names
mthds.protocol.models.ValidationReport, …InvalidValidationReport and
…ValidationDiagnostic by dotted path. Ruff does not error on an entry
that no longer resolves; it just silently stops treating those bases as
runtime-evaluated, decides the mthds.protocol annotations are type-only, and
moves them into a TYPE_CHECKING block — 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.
Grep pyproject.toml for 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 for model_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.
reportUnnecessaryTypeIgnoreComment is "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 NotImplementedError to 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-js is this package's twin and consumes
the mthds npm 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
pipelex names a
different exact mthds after 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, then ledger commit. Nothing else pushes the ledger.
11. Report and stop
Show the user:
- The pin move, old → new, and whether
uv.lock actually followed.
- Whether
pipelex's exact mthds pin 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-test failed, 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_degraded are underscore-prefixed
upstream and load-bearing here. A patch release can move them.
- The ruff
runtime-evaluated-base-classes list names mthds classes by dotted
path. A module move there fails at runtime, in pydantic, with nothing
pointing back at the lint config.
reportUnnecessaryTypeIgnoreComment is 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 name mthds
exactly, so a version this repo moves to alone makes the pair uninstallable
together. Check pipelex/pyproject.toml in 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 fails uv 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 update is not make 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_VERSION is not this repo's problem. pipelex stamps 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].version is pipelex-sdk's own version. Bumping it is the
release skill's job, not this one's.