A missing uv is a refusal that names the fix, not a traceback - #311
Merged
Conversation
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A missing
uvis a refusal that names the fix, not a tracebackCloses #310.
The skill wrappers shell into
uv runto install and run the engine, and did sowith no guard, byte-identically in all three. On a machine without
uvonPATHthe first run ended here:
Nothing said
uvwas required either: not the plugin manifest, not the skillfrontmatter, not the install sections of either README. The documented Claude Code
path is two
/plugincommands and then "the skills appear and auto-trigger", so auser who followed it exactly could land on a stack trace, and the person most
likely to be standing there is a Claude Code user rather than a Python developer.
dex collects no telemetry by design, which is what makes this worth fixing
pre-emptively rather than on a report. Every user who hit it churned invisibly, and
the report was never going to arrive.
Two failure surfaces, and only one of them is catchable
The wrapper executed directly (
python3 .../run.py, theAGENTS.mdany-agentpath, any host that calls the script) runs our code, so it can refuse. It now does:
uv run .../run.py, which is what everySKILL.mdtells the agent to run,fails in the shell with
uv: command not foundbefore a line of ours executes. Noguard inside the script can catch that, so the fix there is documentation aimed at
the agent rather than at the user: each
SKILL.mdnow says what that message means,what to tell the user to install, and not to work around it. That last clause is
the load-bearing one. An agent that cannot run the wrapper will otherwise reach for
raw Python or a database CLI, and every guardrail lives in the engine, so any other
path is unguarded. The three skills each name their own version of the wrong move
(raw SQL, hand-editing the dbt project, eyeballing drift) rather than sharing one
generic warning.
The envelope shape is the engine's, not the issue's sketch
The issue body sketches
{"ok": false, "error": {"code": "uv_not_found", ...}},which is not a shape dex has ever emitted; its prose asks for "the same sanitized
JSON envelope shape the engine emits", and that is the one above. Confirmed before
building. A caller reads this exactly like any other refusal instead of learning a
second format for one case.
reason: prerequisiteis not a new classification either. It is what the enginealready assigns to a missing dependency the user installs and retries, the same
reason
DemoDependencyErrorandDialectDependencyErrorcarry throughenvelope._reason_overrides.This is the one envelope in the product built by hand rather than through
exmergo_dex_core.envelope, because the engine that would build it is preciselywhat has not been installed yet. That is a real drift risk, so a test holds the two
in step: it constructs
Envelope(status=ERROR, errors=[...], reason=PREREQUISITE)and asserts the wrapper's literal equals its
model_dump(mode="json"). It sitsbehind
importorskip, so it skips in CI's engine-lessuvx pytest evalsjob andruns everywhere the engine is present. Verified passing with the engine importable
rather than left to skip silently.
Tests
In
evals/tests/test_wrapper.py, the established home for wrapper tests._SKILLS, not left to the existing byte-identity assertion. This guard is exactlythe kind of thing a refactor drops from one copy, which the issue calls out.
subprocess.callis patched to a raiser rather than a stub, so a guard that letexecution through fails loudly here instead of quietly shelling out to a
uvthatis not there.
main()returns 1 (the engine's own exit code for an error envelope), and themessage names
uvand an install command, since a user who is not a Pythondeveloper has to be able to act on it without reading our source.
uvpresent the guard does not fire, theargv still begins
["uv", "run", ...], and nothing of the wrapper's own reachesthe single-envelope stdout the engine owns.
Files
Engine. None. The engine is untouched.
Wrappers.
skills/{explore,transform,maintain}/scripts/run.py:jsonandshutiladded to the stdlib imports, the guard inline at the top ofmain(), anda docstring paragraph stating the prerequisite. Edited once and copied, so the three
stay byte-identical. The wrapper's constraint is that it runs before the engine is
installed, not that it stays minimal, so two more stdlib imports cost nothing.
Tests.
evals/tests/test_wrapper.py, five tests and a shared driver.Docs. Each
SKILL.mdunder "How to drive it";README.mdgains a## Prerequisite: uvsection covering both install paths, sincenpx skills addneeds it as much as the plugin does;
AGENTS.mdstates it for the any-agentcontract, which documented
uv runtwice and never said so;references/command-contract.mdrecords that the envelope contract holds even on the one path that runs before the
engine exists;
CHANGELOG.mdunder[Unreleased] / Fixed.Not changed.
.claude-plugin/plugin.jsonhas no prerequisites field.scripts/prepare_release.shand.github/workflows/release.ymlsedandgrepDEX_CORE_VERSION, which this does not move. The.cursorand.windsurfrulefiles document
uv run python -m exmergo_dex_coreand defer toAGENTS.md.Out of scope, per the issue
Falling back to
python -m exmergo_dex_corewhen the engine is already importableand
uvis absent. It carries its own questions (which interpreter, which extras,how the pinned version is honoured), and a clear refusal that names what to install
is a complete fix for the reported failure.
Also considered and dropped: changing the documented invocation from
uv runtopython3, which would make the guard reachable on the documented path. It trades amissing-
uvfailure for a missing-python3one (a bare macOS without the Xcodecommand line tools has only a stub), so it widens the blast radius to fix a message.
Verification
exit=1.uvx pytest evals -q: 33 passed, 1 skipped (the engine cross-check). The samefile under the engine: 28 passed, 0 skipped, so the cross-check ran and passed.
packages/dex-core: 2178 passed, 65 skipped, unaffected.check_no_em_dashes.pyclean.democreatesdex_demo.duckdband.dex/config.yml,explore inventory --rankreturns 7objects. No regression on a machine that has
uv.md5 skills/*/scripts/run.pyidentical across the three.