# Bump Mthds

> 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 Pytho

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

---


# 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:

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

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

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

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

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

1. **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.
2. **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.
3. **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.
4. **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

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

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

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

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

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

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

```markdown
- 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.

