docir — design documents Graph

Documents / Decisions / adr-a555ee6bc484

Upgrading the package: re-exec, and only where docir owns its environment

How docir self upgrade installs a new docir, which installs it refuses to touch, and why the release check is opt-in.

adr-a555ee6bc484decisionaccepted#agents#cli#daemon
View as Markdown◉ View in graph

Context

adr-31aa7aa60d11 built docir self upgrade and left the package install out of it, saying the omission was a separate decision: the process running the installer is the code being replaced, so every step after that call is the old build's work — starting with the rebuild that stamps which version built the index. That left the one thing an agent cannot do for itself. It can run every docir command; it cannot know that a newer docir exists, and it cannot install it without being told the right incantation for an environment it did not create.

Decision

The package step runs first and then re-executes docir. os.execv replaces the process with python -m docir from the upgraded environment, carrying a hidden --upgraded-from so the report still names the version that was there. -m docir rather than sys.argv[0]: the console script is a generated shebang wrapper, and the interpreter is the one thing that certainly belongs to the environment that was just upgraded. The flag is also the loop guard — a process that already carries it never runs the installer again.

An installer runs only where docir owns its environment. A uv tool install (uv-receipt.toml at the root of the venv), a pipx install (pipx_metadata.json), or a virtualenv (pyvenv.cfg, upgraded through sys.executable -m pip). Every other case returns no command and a reason:

  • a checkout or path install (PEP 610 direct_url.json naming a file: URL) belongs to a project whose lockfile decides its version — upgrading it from underneath would leave that lockfile describing something that is no longer installed;
  • an ephemeral uvx environment (under uv's cache) has nothing to upgrade; it is resolved per run and discarded;
  • an unrecognised layout gets no guess at all. Running the wrong installer against the wrong environment is worse than doing nothing, and "nothing happened, here is why" is a complete answer.

Every marker read is one an installer wrote for its own bookkeeping, so this is reading a fact rather than inferring one.

A failed install stops the command. The local steps would otherwise resync a store against a docir the user believes they no longer have.

The release check is opt-in, daily, and cached. docir self status reads the last answer and reaches PyPI only on --refresh, skipping even that when the answer is already from today. DOCIR_UPDATE_CHECK=1 puts the fetch in the daemon — the one process nobody is waiting on — and lets every command print a one-line stderr notice from the cached file. Off by default on the argument DOCIR_SCHEMA_NOTICE already makes about repeated notices, and one more: this is the only network call docir makes in its life, and a documentation tool that phones home unasked is not one people keep installed.

latest is three-valued. A version, or None meaning nobody has checked or the check failed — never "up to date". Every network failure collapses to None, so being offline is not news about docir.

Version ordering is PEP 440, from packaging. Hand-rolling it is how 0.9.0 ends up newer than 0.10.0, and how a release candidate becomes unorderable. The dependency already arrived transitively; declaring it is the rule this project applied to watchfiles and markdown-it-py.

Consequences

  • A new leaf module, release — it looks at the installation, owns no index or database state, and depends only on platform.clock. Same shape as agents (adr-3a2d5ee7bc84), and like it, in-process rather than through the dispatcher.
  • One more file in the store: release-check.json, the last answer and its date. Outside the index, because it is a fact about the installation and reindex must not be able to lose it.
  • The test suite is structurally safe from self-upgrading: it runs from a checkout, which detects as project, and a test asserts exactly that.
  • Nothing about the release check is exposed over MCP. An MCP server is a docir process a client is talking to, and the command that replaces it is not a tool it should be able to call.

To amend: Re-verify: