Orchestrata Ultimate › Gates › worktree-envs
Gate: worktree-envs
At a glance
| Purpose | Refuse a non-primary worktree whose .venv is a symlink into another checkout's virtualenv (the battery check); the standalone script checks three more conditions. |
| Where it runs | The scripts/gates.py battery (registered as worktree-envs, root !always, never cached); scripts/health_check.py. |
| Trigger | Every battery run; a health check. |
| Exact command | Battery: python scripts/check_worktree_envs.py --checks venv-symlink. Standalone: python scripts/check_worktree_envs.py [--fix-wheels] [--json] [--repo-root PATH]. |
| Blocks | Exit 1 on any violation; exit 0 clean, or SKIP with a printed reason when the directory is not a git repository or has no worktrees. |
| Ruling / ADR | RUL-021 — a worktree owns its env. No ADR names this gate. |
| Last recorded verdict | ok: true in the primary checkout's .cache/gates.json (file last written 2026-09-22 10:01 local; the entry is written even though the gate is never reused from the cache). wt-MAP-PRECOMMIT has no .cache/gates.json. |
What it refuses
In the battery: a non-primary worktree whose .venv is a symlink into another
checkout's virtualenv — the one failure mode that poisons every other checkout too.
The standalone script checks more (below).
Why it exists
Module docstring, quoting the ruling verbatim and the incident:
"Worktree-environment gate — RUL-021 ('every damn feature branch gets a full env until we merge then it gets deleted', verbatim, 2026-07-27).
See
~/workspace/knowledge-wiki/rulings/RUL-021-worktree-owns-its-env.mdfor the ruling text and the incident (wt-QWEN's.venvwas a symlink to the main checkout's virtualenv, and itsvendor/wheels/was empty, so the relative[tool.uv.sources]grpcio pin resolved to nothing anduvfell back to a from-sdist build — one that re-enables the GIL process-wide on import, a SPEC-023 violation, in a virtualenv every other checkout used)."
Why it entered the battery at all (gates.py registry comment): "The detector
existed but was wired into NOTHING, which is how the incident escaped: a gate
nobody runs is not a gate."
How it runs
- Battery command:
python scripts/check_worktree_envs.py --checks venv-symlink - Full standalone checks (docstring) — for every NON-PRIMARY worktree in
git worktree list --porcelain:.venvis a symlink → violation ("must be a real, independently-owned virtualenv")..venvmissing while a branch is checked out → violation.- A relative
[tool.uv.sources]vendor path not resolving inside THAT worktree's ownpyproject.toml→ violation ("branches differ, so each worktree's own file is read, never the primary's"). - Branch already merged into master → violation ("stale, worktree and env are due for deletion"); detached HEAD is informational only.
- Why the battery restricts to check 1 (registry comment): "venv-missing /
env-missing are normal transient states for an agent worktree between
git worktree addand itsuv sync, and a battery run racing that window must not flake." - When it fires: the
scripts/gates.pybattery (registered asworktree-envs) andscripts/health_check.py. - Inputs / caching: never cached — the root is the
!alwayssentinel: "worktree state is invisible to the content fingerprint" (worktrees appear and vanish with zero tracked-file change), so a cached PASS "would be a statement about a world that no longer exists." - Flags:
--fix-wheels("Populate missing vendored wheels by HARDLINK from the primary worktree… The only auto-fix: creating virtualenvs and deleting worktrees stay manual, per the ruling"),--json,--repo-root("Never hardcode an absolute path here — the primary worktree is always derived fromgit worktree list").
Typical failures and how to fix them
- Exit codes: "Exit 0 clean, exit 1 on any violation. A directory that is not a git repository (or has no worktrees) is a SKIP (exit 0, reason printed) — same honesty rule as rulings_lint.py's missing-RULINGS_DIR skip."
- What a failure looks like (representative): the offending worktree path with
its violation class (
venv-symlinkin the battery) and the target the symlink resolves to. - Un-blocking: give the worktree its own real virtualenv (
uv syncinside it); for the standalone checks, populate wheels via--fix-wheelsor delete a merged worktree (scripts/worktrees.py gc). - A worktree that passes this gate but still imports another checkout's code.
This gate checks only the
.venvsymlink. A leakedPYTHONPATHor a venv whose editable install points at a different checkout is caught by source-provenance in the suite preflight, not here. - A transient failure right after
git worktree add. The battery restricts itself tovenv-symlinkso a worktree that has not runuv syncyet does not fail it; the standalone run with all checks will report.venvmissing until the sync finishes.
Wiring
- Invokers:
scripts/gates.py(battery),scripts/health_check.py; worktree lifecycle tooling lives inscripts/worktrees.py(creation with a full private env, tracking, gc) and theworktree-gc.shSessionStart hook cleans merged ones. - Tests:
tests/scripts/test_check_worktree_envs.py, plustests/scripts/test_worktrees.py/test_worktrees_provision.pyfor the lifecycle side.
Related
- Gates master index
- Development workflow
- RUL-021 — A worktree owns its env
- source-provenance — the import-path half of the same ownership problem
- prod-separation — the same PYTHONPATH/ownership trap, hit by systemd units instead of worktrees
- generated-artifacts —
the other
!alwaysgate, for the same "no content hash can see it" reason