Skip to content

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

Description

@alphacrack

User story

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:

  1. 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.
  2. 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).

Explicitly out of scope:

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:
    dependencies = [
        "typer>=0.12",
        "rich>=13.7",
        "pydantic>=2.7",
        "jinja2>=3.1",
        "anthropic>=0.34",
    ]
  • pyproject.toml:46-52 — same style in the extras; gemini and openai are single-SDK extras with floors:
    dev = ["pytest>=8.0", "pytest-cov>=5.0", "pytest-mock>=3.14", "ruff>=0.6"]
    docs = ["mkdocs-material>=9.5", "mkdocs-minify-plugin>=0.8"]
    gemini = ["google-genai>=1.0"]
    openai = ["openai>=1.50"]
    Note mypy is absent from dev.
  • .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:253import anthropic # imported lazily so unit tests don't need the package configured, inside _complete_api.
  • .github/workflows/docs.yml:36pip 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.
  • python -m pytest tests/ -q green; ruff check src/ tests/ clean; mypy src/readme2demo clean.

Notes for contributors

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 read images/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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:ciGitHub workflows, release, packagingdependenciesDependency updatesdocumentationDocs, README, docs-sitehelp wantedMaintainer would welcome a PRtech-debtRefactor / cleanup, no behavior change intended

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions