You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
deps: no written pinning policy — every dependency is a bare floor, and the two parser-coupled image pins are invisible to Dependabot (docs/dependencies.md) #201
As a maintainer reviewing a Dependabot PR (or a contributor deciding whether to add a dependency), I want a written pinning policy that says what gets a floor, what gets a ceiling, how the optional extras are treated, and which pins are deliberately excluded from automation, so that I can approve or reject a bump in seconds instead of re-deriving the reasoning — and so nobody ever wires an auto-bumper onto a pin that is coupled to a transcript parser.
What
In scope — one doc plus a small number of mechanical pyproject.toml corrections:
docs/dependencies.md (new page, added to the nav: in mkdocs.yml) covering:
Floors vs ceilings. Today every requirement is a bare >= floor with no upper bound (see Pointers). State the rule the project actually wants — e.g. floor = "oldest version whose API we use", ceiling = only when a known-incompatible major exists — and say plainly that a bare floor is the default so the tail of the policy is short.
Runtime vs dev vs optional.dev and docs extras are tooling and can float freely; gemini / openai are user-installed SDKs where a floor is a compatibility contract the code already enforces at runtime via llm.check_sdk (it distinguishes absent / broken / too-old). Document that raising a floor in [project.optional-dependencies] and changing the sentinel attribute in _BACKEND_SDKS are the same decision.
The two PINNED-IN-IMAGE tools, and why they are exempt from all automation: @anthropic-ai/claude-code@2.0.14 (images/base/Dockerfile:61) and openhands-ai==0.48.0 (images/openhands/Dockerfile:52). CLAUDE.md's rule: "The pinned Claude Code version in images/base/Dockerfile is coupled to the stream-json parser (engines/claude_code.py) — bump them together", and the OpenHands Dockerfile header repeats it: "bump the pin and the engine adapter TOGETHER, exactly like the Claude Code pin in images/base/Dockerfile." The doc should carry an explicit do-not-automate note: do not add a package.json, a docker ecosystem entry for /images/openhands, or a pip entry that would let a bot move either pin without the matching parser change.
What Dependabot covers today (.github/dependabot.yml) and the three gaps below, so the doc is honest about the blind spots rather than implying coverage.
pyproject.toml corrections (each small, each independently arguable — the PR should propose, not silently change):
Add mypy to the dev extra. CI installs it out-of-band (.github/workflows/ci.yml:29: pip install -e ".[dev]" mypy), so it is completely unpinned and invisible to Dependabot, and pip install -e ".[dev]" locally does not reproduce the typecheck job.
Decide and record whether anthropic belongs in core dependencies or in an extra. It is a core dep (pyproject.toml:43) but is imported lazily inside _complete_api (src/readme2demo/llm.py:253) and is only reachable on --llm-backend api; the default path is claude-cli. Either placement is defensible — the deliverable is a recorded decision in the doc, not necessarily a move (moving it is a packaging break for existing api users).
Introducing a lockfile, uv.lock, requirements*.txt, or hash pinning. There is no lockfile today; adopting one is a separate decision with its own CI cost.
Actually bumping either image pin, or touching engines/claude_code.py / engines/openhands.py.
Any change to sandbox hardening flags or to what the base image installs.
Why
There is no dependency policy anywhere in the repo today — CONTRIBUTING.md has no section on it, SECURITY.md mentions pinning only from the consumer side ("Pin a version for reproducibility", SECURITY.md:50), and CLAUDE.md's "bump them together" rule for the Claude Code pin is a single line aimed at maintainers, not at the drive-by contributor opening a version bump.
The concrete incident this protects against is known failure class 14 in CLAUDE.md — an engine runtime mismatch inside the sandbox image, which surfaces as a bare exit 127 with no transcript. The mechanism generalizes: both image pins feed the transcript parsers, and a parser that silently mis-reads a transcript produces a wrong command_log.json.
Grounding implication (explicit). These two pins sit upstream of the grounding path. command_log.json is the record of what the agent actually ran; normalize.py and distill.py's grounding validator have nothing else to check published commands against. If a Claude Code or OpenHands upgrade changes its output format and the parser is not updated in the same change, the failure is not a loud crash — it is a degraded or wrongly-populated command log, and grounding then validates against the wrong ground truth. That is exactly the invariant the repo refuses to weaken, which is why "these pins are never auto-bumped" needs to be written down rather than remembered.
Pointers
All verified by reading the files at main (64a3cbd).
pyproject.toml:38-44 — every runtime dependency is a bare >= floor, no ceilings anywhere:
.github/workflows/ci.yml:29 — - run: pip install -e ".[dev]" mypy. The typecheck job's tool is not declared anywhere in pyproject.toml.
.github/dependabot.yml — three ecosystems, weekly, all labelled dependencies:
pip at / (yes, pip is covered — it reads pyproject.toml), open-pull-requests-limit: 5, commit prefix deps;
github-actions at /, prefix ci;
docker at /images/base, prefix docker.
Gap 1 — the docker entry is close to a no-op.images/base/Dockerfile:14 is FROM ghcr.io/charmbracelet/vhs:latest; a floating latest tag has nothing for Dependabot to bump to.
Gap 2 — /images/openhands has no Dependabot entry at all (only /images/base is listed), and its FROM readme2demo/base:latest (images/openhands/Dockerfile:22) is a locally-built image no registry can resolve.
Gap 3 — both coupled pins are installed inside RUN layers, which no ecosystem parses.images/base/Dockerfile:61 is RUN npm install -g @anthropic-ai/claude-code@2.0.14 (there is no package.json in the repo, so the npm ecosystem sees nothing), and images/openhands/Dockerfile:52 is openhands-ai==0.48.0 inside a uv pip install. This is currently accidentally correct — the pins that must never be auto-bumped happen to be the ones automation cannot see. The doc's job is to convert that accident into a stated rule.
images/base/Dockerfile:58-61 — the comment already says "UPDATE THIS PIN deliberately: the M4 transcript parser depends on this version's stream-json output format".
images/openhands/Dockerfile:9-20 — the longest-standing version rationale in the repo (0.49.0–0.62.0 are x86_64-only wheels so the build fails on Apple Silicon; 0.62.0 pins a pre-release uv refuses; V1 replaced the headless interface). docs/dependencies.md should link here rather than restate it.
src/readme2demo/llm.py:150-195 — _BACKEND_SDKS maps each optional backend to (import name, pip name, sentinel attribute) and check_sdk raises a distinct error for absent / broken / too-old. The docstring notes a real incident: "a --openai run once burned its ingest stage on this". This is the runtime half of the extras' floor policy.
src/readme2demo/llm.py:253 — import anthropic # imported lazily so unit tests don't need the package configured, inside _complete_api.
.github/workflows/docs.yml:36 — pip install -e ".[docs]", the only consumer of the docs extra.
Acceptance criteria
docs/dependencies.md exists and is added to nav: in mkdocs.yml (the docs build runs with --strict, so a page outside nav or exclude_docs fails).
The doc states the floor/ceiling rule for runtime deps and explains why bare >= is the current default.
The doc covers dev / docs (tooling, float freely) separately from gemini / openai (user-installed SDKs), and links the extras' floors to llm._BACKEND_SDKS / check_sdk as the runtime enforcement of the same contract.
The doc names both image pins with their file:line, quotes CLAUDE.md's "bump them together" rule, and carries an explicit do-not-automate list (no package.json, no docker entry for /images/openhands, no pip entry for openhands-ai) with the grounding reason stated in one sentence.
The doc accurately describes current Dependabot coverage (pip, github-actions, docker@/images/base) and names the three gaps above without overstating them.
mypy is added to the dev extra with a floor, and .github/workflows/ci.yml:29 is simplified to pip install -e ".[dev]". The typecheck job still passes.
The anthropic core-vs-extra question is answered in writing in the doc, with the reasoning recorded, whether or not pyproject.toml changes.
CONTRIBUTING.md gains a one-line pointer to the new page (matching how it points at other process docs).
No change to engines/, distill.py, normalize.py, tutorial.py, or prompts/ — this issue must not touch the grounding path.
This is not a good-first-issue, deliberately. The writing is easy; the decisions are not. Choosing whether anthropic stays a core dependency, whether the extras get ceilings, and how firmly to word the do-not-automate rule are maintainer judgment calls with packaging and grounding consequences. Please open the PR with the decisions stated as proposals and expect discussion on them — a PR that only mechanically adds mypy to [dev] and writes down what already exists is a fine, smaller first step, but say so in the description.
Honest prerequisites:
No Docker required. You need to readimages/base/Dockerfile and images/openhands/Dockerfile, not build them.
No API key required. Nothing here calls an LLM.
Mostly Markdown + a few TOML/YAML lines. The only code-adjacent reading is src/readme2demo/llm.py:150-195.
Setup and test loop: pip install -e ".[dev]" && python -m pytest tests/ -q runs in about 1.5s — no Docker, no network, no API key.
To preview the new page: pip install -e ".[docs]" && mkdocs serve.
Repo conventions still apply to any Python touched: type hints + docstrings on public functions, pydantic v2, ruff clean. No regression test is expected here — the repo's rule is that every bug fix carries a """Regression: ...""" docstring test, and this is documentation plus a packaging correction, not a behavior fix. If you do change pyproject.toml in a way a test could assert (e.g. "every extra referenced by a workflow exists"), a test is welcome but not required.
Useful background reading before you start: CLAUDE.md's known-failure-class 14 and the "Repo conventions" section, plus images/openhands/Dockerfile:9-20.
User story
What
In scope — one doc plus a small number of mechanical
pyproject.tomlcorrections:docs/dependencies.md(new page, added to thenav:inmkdocs.yml) covering:>=floor with no upper bound (see Pointers). State the rule the project actually wants — e.g. floor = "oldest version whose API we use", ceiling = only when a known-incompatible major exists — and say plainly that a bare floor is the default so the tail of the policy is short.devanddocsextras are tooling and can float freely;gemini/openaiare user-installed SDKs where a floor is a compatibility contract the code already enforces at runtime viallm.check_sdk(it distinguishes absent / broken / too-old). Document that raising a floor in[project.optional-dependencies]and changing the sentinel attribute in_BACKEND_SDKSare the same decision.@anthropic-ai/claude-code@2.0.14(images/base/Dockerfile:61) andopenhands-ai==0.48.0(images/openhands/Dockerfile:52). CLAUDE.md's rule: "The pinned Claude Code version inimages/base/Dockerfileis coupled to the stream-json parser (engines/claude_code.py) — bump them together", and the OpenHands Dockerfile header repeats it: "bump the pin and the engine adapter TOGETHER, exactly like the Claude Code pin in images/base/Dockerfile." The doc should carry an explicit do-not-automate note: do not add apackage.json, adockerecosystem entry for/images/openhands, or a pip entry that would let a bot move either pin without the matching parser change..github/dependabot.yml) and the three gaps below, so the doc is honest about the blind spots rather than implying coverage.pyproject.tomlcorrections (each small, each independently arguable — the PR should propose, not silently change):mypyto thedevextra. CI installs it out-of-band (.github/workflows/ci.yml:29:pip install -e ".[dev]" mypy), so it is completely unpinned and invisible to Dependabot, andpip install -e ".[dev]"locally does not reproduce the typecheck job.anthropicbelongs in coredependenciesor in an extra. It is a core dep (pyproject.toml:43) but is imported lazily inside_complete_api(src/readme2demo/llm.py:253) and is only reachable on--llm-backend api; the default path isclaude-cli. Either placement is defensible — the deliverable is a recorded decision in the doc, not necessarily a move (moving it is a packaging break for existingapiusers).Explicitly out of scope:
tomli; python_version < "3.11"— that is config: readme2demo.toml is silently ignored on Python 3.10 without tomli #39 (config: readme2demo.toml is silently ignored on Python 3.10 without tomli). This issue may cite config: readme2demo.toml is silently ignored on Python 3.10 without tomli #39 as evidence the policy is missing, and the policy should cover conditional/marker deps generally, but the fix itself lands in config: readme2demo.toml is silently ignored on Python 3.10 without tomli #39.uv.lock,requirements*.txt, or hash pinning. There is no lockfile today; adopting one is a separate decision with its own CI cost.engines/claude_code.py/engines/openhands.py.Why
There is no dependency policy anywhere in the repo today —
CONTRIBUTING.mdhas no section on it,SECURITY.mdmentions pinning only from the consumer side ("Pin a version for reproducibility", SECURITY.md:50), and CLAUDE.md's "bump them together" rule for the Claude Code pin is a single line aimed at maintainers, not at the drive-by contributor opening a version bump.The concrete incident this protects against is known failure class 14 in CLAUDE.md — an engine runtime mismatch inside the sandbox image, which surfaces as a bare exit 127 with no transcript. The mechanism generalizes: both image pins feed the transcript parsers, and a parser that silently mis-reads a transcript produces a wrong
command_log.json.Grounding implication (explicit). These two pins sit upstream of the grounding path.
command_log.jsonis the record of what the agent actually ran;normalize.pyanddistill.py's grounding validator have nothing else to check published commands against. If a Claude Code or OpenHands upgrade changes its output format and the parser is not updated in the same change, the failure is not a loud crash — it is a degraded or wrongly-populated command log, and grounding then validates against the wrong ground truth. That is exactly the invariant the repo refuses to weaken, which is why "these pins are never auto-bumped" needs to be written down rather than remembered.Pointers
All verified by reading the files at
main(64a3cbd).pyproject.toml:38-44— every runtime dependency is a bare>=floor, no ceilings anywhere:pyproject.toml:46-52— same style in the extras;geminiandopenaiare single-SDK extras with floors:mypyis absent fromdev..github/workflows/ci.yml:29—- run: pip install -e ".[dev]" mypy. The typecheck job's tool is not declared anywhere inpyproject.toml..github/dependabot.yml— three ecosystems, weekly, all labelleddependencies:pipat/(yes, pip is covered — it readspyproject.toml),open-pull-requests-limit: 5, commit prefixdeps;github-actionsat/, prefixci;dockerat/images/base, prefixdocker.dockerentry is close to a no-op.images/base/Dockerfile:14isFROM ghcr.io/charmbracelet/vhs:latest; a floatinglatesttag has nothing for Dependabot to bump to./images/openhandshas no Dependabot entry at all (only/images/baseis listed), and itsFROM readme2demo/base:latest(images/openhands/Dockerfile:22) is a locally-built image no registry can resolve.RUNlayers, which no ecosystem parses.images/base/Dockerfile:61isRUN npm install -g @anthropic-ai/claude-code@2.0.14(there is nopackage.jsonin the repo, so thenpmecosystem sees nothing), andimages/openhands/Dockerfile:52isopenhands-ai==0.48.0inside auv pip install. This is currently accidentally correct — the pins that must never be auto-bumped happen to be the ones automation cannot see. The doc's job is to convert that accident into a stated rule.images/base/Dockerfile:58-61— the comment already says "UPDATE THIS PIN deliberately: the M4 transcript parser depends on this version's stream-json output format".images/openhands/Dockerfile:9-20— the longest-standing version rationale in the repo (0.49.0–0.62.0 are x86_64-only wheels so the build fails on Apple Silicon; 0.62.0 pins a pre-releaseuvrefuses; V1 replaced the headless interface).docs/dependencies.mdshould link here rather than restate it.src/readme2demo/llm.py:150-195—_BACKEND_SDKSmaps each optional backend to(import name, pip name, sentinel attribute)andcheck_sdkraises a distinct error for absent / broken / too-old. The docstring notes a real incident: "a --openai run once burned its ingest stage on this". This is the runtime half of the extras' floor policy.src/readme2demo/llm.py:253—import anthropic # imported lazily so unit tests don't need the package configured, inside_complete_api..github/workflows/docs.yml:36—pip install -e ".[docs]", the only consumer of thedocsextra.Acceptance criteria
docs/dependencies.mdexists and is added tonav:inmkdocs.yml(the docs build runs with--strict, so a page outsidenavorexclude_docsfails).>=is the current default.dev/docs(tooling, float freely) separately fromgemini/openai(user-installed SDKs), and links the extras' floors tollm._BACKEND_SDKS/check_sdkas the runtime enforcement of the same contract.package.json, nodockerentry for/images/openhands, no pip entry foropenhands-ai) with the grounding reason stated in one sentence.mypyis added to thedevextra with a floor, and.github/workflows/ci.yml:29is simplified topip install -e ".[dev]". The typecheck job still passes.anthropiccore-vs-extra question is answered in writing in the doc, with the reasoning recorded, whether or notpyproject.tomlchanges.CONTRIBUTING.mdgains a one-line pointer to the new page (matching how it points at other process docs).engines/,distill.py,normalize.py,tutorial.py, orprompts/— this issue must not touch the grounding path.python -m pytest tests/ -qgreen;ruff check src/ tests/clean;mypy src/readme2democlean.Notes for contributors
This is not a good-first-issue, deliberately. The writing is easy; the decisions are not. Choosing whether
anthropicstays a core dependency, whether the extras get ceilings, and how firmly to word the do-not-automate rule are maintainer judgment calls with packaging and grounding consequences. Please open the PR with the decisions stated as proposals and expect discussion on them — a PR that only mechanically addsmypyto[dev]and writes down what already exists is a fine, smaller first step, but say so in the description.Honest prerequisites:
images/base/Dockerfileandimages/openhands/Dockerfile, not build them.src/readme2demo/llm.py:150-195.pip install -e ".[dev]" && python -m pytest tests/ -qruns in about 1.5s — no Docker, no network, no API key.pip install -e ".[docs]" && mkdocs serve."""Regression: ..."""docstring test, and this is documentation plus a packaging correction, not a behavior fix. If you do changepyproject.tomlin a way a test could assert (e.g. "every extra referenced by a workflow exists"), a test is welcome but not required.images/openhands/Dockerfile:9-20.