7Per-worktree uv environments: prepare and teardown.
11Plan 241 (ENV-01). The shared ``G:\\PythonApps\\option_c\\.venv`` carries
12thirteen editable installs that all point at the MAIN tree, so a worker's
13probe or suite silently imports the main tree unless ``PYTHONPATH`` lists
14the worktree first — the F17 shadow. This module ends that class: it builds
15a venv INSIDE the worktree whose editable installs point at that worktree,
16and it VERIFIES the result by importing every package in a clean
17interpreter with no ``PYTHONPATH`` before it reports success.
21- :data:`EDITABLE_PACKAGES` — the one declared table of the monorepo's
22 editable packages (path, distribution name, import name, extras, and
23 the modules those extras must make importable).
24- :data:`SMOKE_CHECKS` — plan 243 P4: the two commands every worker runs
25 first, executed inside the prepared worktree on ``--smoke``.
26- :func:`ensure_branch_and_worktree` — the idempotent git anchors; the ONE
27 implementation, shared with :mod:`oc_orchestrator.dispatch`.
28- :func:`prepare_worktree` — branch, worktree, ``uv venv``, the editable
29 installs FROM THE WORKTREE, the ``sitecustomize.py`` copy, and the
30 verification. Idempotent: a second run reports "already prepared" and
31 still repairs a missing ``sitecustomize.py``.
32- :func:`teardown_worktree` — the symmetric removal, which names the known
33 Windows ``.git/worktrees/<name>`` permission failure rather than leaving
34 the orchestrator to recognise it.
35- :class:`WorktreeEnvError` — every refusal, named, raised BEFORE the step
36 it refuses so a rerun can always repair what a failure left behind.
37- :data:`OctHook` / :func:`use_oct_hook` — plan 270's seam: the optional
38 ``oct`` timing ledger, absent by default, registered by a consumer
45 L2 — lifecycle events (prepare/teardown attempted, refused, completed).
46 L3 — semantic details (discovery, install, verification).
51- Verification IS the contract (plan 243 P3): a prepare that prints
52 "verified" means STEP-0 and both suites can run in that worktree, and a
53 prepare that cannot say so refuses naming EVERY module that failed —
54 never the first one only. No worker repairs a prepared venv by hand.
55- ``uv`` is an external tool the operator installs; this module CHECKS for
56 it and refuses by name when it is absent. It never installs it, and it
57 never silently falls back to the shared venv, which would re-open the
58 shadow this module exists to close.
59- The uv cache must live on the same drive as the venv it fills: across
60 drives uv cannot hardlink and silently copies. A cross-drive cache is
61 refused with both paths named.
62- Nothing outside ``worktree`` (and the git metadata of ``repo_root``) is
63 written. The shared main venv is never touched.
64- ``oct`` is optional and absent by default (plan 270 P1/P4). Every edge
65 to it goes through :data:`OctHook`, whose default refuses, so the
66 timing ledger degrades to a null context and nothing in this package's
67 import graph names the toolchain.
68- :data:`EDITABLE_PACKAGES` and :data:`SMOKE_CHECKS` are DATA describing
69 the Option C monorepo, not a contract of this package: a different
70 consumer supplies a different tree and would not use them. They are
71 the honest residue of the split, named here rather than hidden.
75- oc_diagnostics (structured debug logging via _dbg; also the home of the venv repair file this module copies into a fresh venv)
76- uv (not imported; external tool run as a subprocess for the venv and its editable installs, checked for, never installed)
79from __future__
import annotations
81from dataclasses
import dataclass, field
84from pathlib
import Path
90from typing
import Any, Callable
94except ModuleNotFoundError:
98from oc_diagnostics
import _dbg
100_dbg(
"OC_CHAT.MAIN",
"module",
"worktree_env loaded", 4)
104 """A named refusal at the worktree-environment boundary."""
108OctHook = Callable[[str], Any]
113OCT_HOOK_TARGETS = (
"timed_stage",)
117 """Refuse every target: this package does not know ``oct`` exists.
119 Plan 257 P5 made ``oct`` reachable through exactly one seam so that a
120 packaging plan could make it a genuine optional extra by replacing
121 this default and changing nothing else. Plan 270 is that plan, and
122 this is that replacement: the default now raises ``ImportError`` for
123 a known target, which is precisely what :func:`_engine_timing` reads
124 as "oct is absent" and degrades to a null context for. A standalone
125 dispatch therefore records no timings and reaches no toolchain, and
126 ``oct`` appears nowhere in this package's import graph.
128 A consumer that HAS ``oct`` installs the live hook at its boundary
129 with :func:`use_oct_hook` — ``oc_chat`` does, in its
130 ``core/worktree_env.py`` shim — and the timing ledger fills exactly
131 as it did before the split.
133 An UNKNOWN target is still a :class:`WorktreeEnvError`, deliberately
134 not an ``ImportError`` subclass, so the timing edge's ``except
135 ImportError`` cannot swallow a programming mistake as "oct is absent".
137 func =
"_default_oct_hook"
138 if target
in OCT_HOOK_TARGETS:
139 _dbg(
"OC_CHAT.MAIN", func, f
"no oct hook installed for {target!r}", 4)
141 f
"no oct hook is installed, so {target!r} cannot be resolved; "
142 f
"a consumer that has oct registers one with use_oct_hook()"
144 _dbg(
"OC_CHAT.MAIN", func, f
"VALIDATION_ERROR: unknown target {target!r}", 3)
150_oct_hook: OctHook = _default_oct_hook
154 """Install *hook* as this module's default, returning the previous one.
156 The registration point for a consumer that ships ``oct`` (plan 270
157 P4). Passing ``None`` restores :func:`_default_oct_hook`, which is
158 what a test that borrowed the seam uses to give it back.
160 func =
"use_oct_hook"
163 _oct_hook = hook
if hook
is not None else _default_oct_hook
164 _dbg(
"OC_CHAT.MAIN", func, f
"oct hook is now {getattr(_oct_hook, '__name__', _oct_hook)!r}", 3)
168@dataclass(frozen=True)
170 """One editable install: where it lives, what it is called, what it needs."""
175 extras: tuple[str, ...] = ()
182 verify_imports: tuple[str, ...] = ()
185 """This package's project directory beneath *root*."""
189 """The ``uv pip install -e`` argument for this package under *root*."""
192 return f
"{target}[{','.join(self.extras)}]"
196 """Whether this package's own pyproject declares every named extra.
198 A stand-in package built by a fixture (and any checkout that has
199 renamed an extra) declares none of them: ``uv`` only warns for an
200 extra that does not exist, so nothing is installed and there is
201 nothing to verify. Saying so is honest; demanding the modules
202 anyway would refuse every fixture tree. The live checkout is
203 fenced separately by the plan-243 W5 witness, which asserts each
204 declared extra really exists.
210 path = self.
source_dir(root) /
"pyproject.toml"
212 with path.open(
"rb")
as handle:
213 loaded = tomllib.load(handle)
214 except (OSError, ValueError):
216 project = loaded.get(
"project")
217 declared = project.get(
"optional-dependencies")
if isinstance(project, dict)
else None
218 if not isinstance(declared, dict):
220 return all(extra
in declared
for extra
in self.
extras)
235EDITABLE_PACKAGES: tuple[EditablePackage, ...] = (
250 "mcp.server.mcpserver",
261 EditablePackage(
"oc_orchestrator",
"oc-orchestrator",
"oc_orchestrator"),
263 "contracts/option_c_agent_contracts",
264 "option-c-agent-contracts",
265 "option_c_agent_contracts",
268 "contracts/option_c_connector_contracts",
269 "option-c-connector-contracts",
270 "option_c_connector_contracts",
273 "contracts/option_c_registry_contracts",
274 "option-c-registry-contracts",
275 "option_c_registry_contracts",
278 "oc_chat/oc_ui",
"oc_ui",
"oc_ui", (
"terminal",), (
"rich",
"prompt_toolkit")
284 (
"terminal",
"test"),
285 (
"yaml",
"setuptools_scm",
"setuptools",
"rich",
"prompt_toolkit"),
288 "oc_chat/providers/deepseek",
"providers-deepseek",
"providers.deepseek"
291 "oc_chat/providers/example",
"providers-example",
"providers.example"
294 "oc_chat/providers/ollama",
"providers-ollama",
"providers.ollama"
297 "oc_chat/providers/openai_compat",
298 "providers-openai-compat",
299 "providers.openai_compat",
305STAMP_FILENAME =
".oc_prepare.json"
309UV_CACHE_DIRNAME =
".uv-cache"
321SMOKE_CHECKS: tuple[tuple[str, tuple[str, ...]], ...] = (
323 "STEP-0 tool (plan_status --help)",
324 (
"-m",
"oc_chat.scripts.plan_status",
"--help"),
327 "dev-engine imports",
328 (
"-c",
"import yaml, mcp, jinja2, setuptools_scm, setuptools"),
333 "install it (https://docs.astral.sh/uv/) and re-run, or dispatch with "
334 "--shared-venv to render the legacy shared-venv brief explicitly"
336_HARDLINK_FALLBACK_MARKER =
"failed to hardlink"
343 repo_root: Path, *args: str, check: bool =
True
344) -> subprocess.CompletedProcess[str]:
345 """Run one git command in *repo_root*, refusing by name when it fails."""
346 result = subprocess.run(
353 if check
and result.returncode != 0:
354 detail = (result.stderr
or result.stdout).strip()
361 repo_root,
"show-ref",
"--verify", f
"refs/heads/{branch}", check=
False
363 return probe.returncode == 0
367 return str(left.resolve()).casefold() == str(right.resolve()).casefold()
371 """Return ``(path, branch)`` for every worktree registered to *repo_root*."""
372 result =
_run_git(repo_root,
"worktree",
"list",
"--porcelain")
373 entries: list[tuple[Path, str |
None]] = []
374 path: Path |
None =
None
375 branch: str |
None =
None
376 for line
in [*result.stdout.splitlines(),
""]:
377 if line.startswith(
"worktree "):
378 path = Path(line.removeprefix(
"worktree ")).resolve()
379 elif line.startswith(
"branch refs/heads/"):
380 branch = line.removeprefix(
"branch refs/heads/")
381 elif not line
and path
is not None:
382 entries.append((path, branch))
393 base_sha: str |
None =
None,
395 """Create the branch and worktree when absent; reuse them when present.
397 *base_sha* is the anchor the branch is created at (``master`` when it is
398 ``None``). When it is supplied AND the branch already exists at a
399 different commit the call refuses: an operator who names an anchor is
400 asserting where the branch starts, and silently accepting a branch that
401 starts somewhere else is the lie plan 223 P4 removed from BASE_SHA.
402 Dispatch passes ``None`` — it has already recorded the merge-base.
404 func =
"ensure_branch_and_worktree"
406 for existing_path, existing_branch
in entries:
407 if existing_branch == branch:
409 _dbg(
"OC_CHAT.MAIN", func, f
"{branch} already at {worktree}", 3)
412 f
"branch {branch!r} is already checked out at {existing_path}"
416 f
"worktree path {worktree} is registered to branch {existing_branch!r}"
418 if worktree.exists():
420 f
"worktree path {worktree} exists but is not registered with git"
424 _run_git(repo_root,
"branch", branch, base_sha
or "master")
425 elif base_sha
is not None:
426 tip =
_run_git(repo_root,
"rev-parse", branch).stdout.strip()
427 anchor =
_run_git(repo_root,
"rev-parse", base_sha).stdout.strip()
430 f
"branch {branch!r} exists at {tip} but the requested anchor is "
431 f
"{anchor}; refusing to move or reuse a branch that starts elsewhere"
434 worktree.parent.mkdir(parents=
True, exist_ok=
True)
435 _run_git(repo_root,
"worktree",
"add", str(worktree), branch)
438 _run_git(repo_root,
"branch",
"-d", branch, check=
False)
440 _dbg(
"OC_CHAT.MAIN", func, f
"{branch} checked out at {worktree}", 2)
447 """The venv's executables directory (``Scripts`` on Windows, ``bin`` else)."""
449 if posix.is_dir()
and not (venv /
"Scripts").is_dir():
451 return venv /
"Scripts"
455 """The venv's interpreter path."""
457 return scripts / (
"python.exe" if scripts.name ==
"Scripts" else "python")
461 """The venv's ``site-packages`` directory, refusing when it is absent."""
462 windows = venv /
"Lib" /
"site-packages"
465 for candidate
in sorted((venv /
"lib").glob(
"python*/site-packages")):
471 """The drive (or UNC share) *path* lives on; patched by the W2 witness."""
472 return Path(path).drive.casefold()
476 """Return the declared packages that actually exist beneath *root*."""
477 func =
"discover_packages"
480 for package
in EDITABLE_PACKAGES
481 if (package.source_dir(root) /
"pyproject.toml").is_file()
483 _dbg(
"OC_CHAT.MAIN", func, f
"{len(found)} package(s) under {root}", 3)
488 """Locate ``venv_sitecustomize.py``, preferring the worktree's own copy.
490 A fresh venv without this file resolves ``oc_diagnostics`` as a bare
491 namespace package (``__file__`` is ``None``) — the OCON incident.
493 Plan 270 P1 moved the file into ``oc_diagnostics``, the package it
494 repairs, so the order is: the worktree's own copy first (a checkout
495 ships its own), then the copy inside the ``oc_diagnostics`` importable
496 in THIS process, then — for one arc — the pre-270 location under the
497 worktree's ``oct/oct/tools/``, which is what a checkout cut before the
498 move and a fixture tree with no toolchain of its own still carry. With
499 the file re-homed, ``oct`` is no longer reached from this edge at all,
500 which is what takes ``oct`` out of the dispatcher package's graph.
502 Unlike the timing edge, this one is a HARD refusal, and it stays that
503 way — an unusable venv is worse than a loud failure. The refusal names
504 every location that was tried, never just the first.
506 func =
"sitecustomize_source"
508 worktree /
"oc_diagnostics" /
"oc_diagnostics" /
"venv_sitecustomize.py",
509 Path(oc_diagnostics.__file__).resolve().parent /
"venv_sitecustomize.py"
510 if oc_diagnostics.__file__
512 worktree /
"oct" /
"oct" /
"tools" /
"venv_sitecustomize.py",
514 for candidate
in candidates:
515 if candidate
is not None and candidate.is_file():
516 _dbg(
"OC_CHAT.MAIN", func, f
"venv_sitecustomize.py at {candidate}", 3)
518 tried =
", ".join(str(candidate)
for candidate
in candidates
if candidate
is not None)
519 raise WorktreeEnvError(f
"cannot locate venv_sitecustomize.py: tried {tried}")
525@dataclass(frozen=True)
527 """All caller-controlled inputs to one prepare attempt."""
532 base_sha: str |
None =
None
533 uv_executable: str =
"uv"
534 uv_cache_dir: Path |
None =
None
535 interpreter: Path = field(default_factory=
lambda: Path(sys.executable))
541@dataclass(frozen=True)
543 """What one prepare attempt actually did."""
549 installed: tuple[str, ...]
550 verified: tuple[tuple[str, str], ...]
551 uv_cache_dir: Path |
None
553 already_prepared: bool
554 repaired: tuple[str, ...]
555 skipped_reason: str |
None
556 elapsed_seconds: float
559 verified_extras: tuple[tuple[str, str], ...] = ()
562 extras_not_declared: tuple[str, ...] = ()
564 smoke_checks: tuple[str, ...] = ()
566 stale_reason: str |
None =
None
569 """Operator-facing lines; surfaced verbatim by dispatch and the CLI."""
571 return [f
"prepare skipped: {self.skipped_reason}"]
572 state =
"already prepared" if self.already_prepared
else "prepared"
574 f
"Worktree env {state}: {self.worktree}",
575 f
" venv: {self.venv}",
576 f
" uv cache: {self.uv_cache_dir} (link mode: {self.link_mode})",
577 f
" installed: {len(self.installed)} editable package(s)",
578 f
" verified: {len(self.verified)} import(s) inside the worktree",
579 f
" extras: {self._extras_summary()}",
580 f
" wall clock: {self.elapsed_seconds:.1f}s",
583 lines.append(f
" stale: {self.stale_reason}")
585 lines.append(f
" smoke: {', '.join(self.smoke_checks)}")
587 lines.append(f
" repaired: {repair}")
591 """One line naming what the declared extras were asked to supply."""
592 summary = f
"{len(self.verified_extras)} declared import(s) verified"
595 f
" ({len(self.extras_not_declared)} package(s) declare no such "
596 f
"extra: {', '.join(self.extras_not_declared)})"
602 """Return the resolved ``uv`` path, or refuse by name with the hint."""
603 resolved = shutil.which(executable)
606 f
"{executable!r} is not on PATH; per-worktree environments need it — "
607 f
"{_UV_INSTALL_HINT}"
613 """Pin the uv cache to the worktree's drive, or refuse naming both paths."""
614 func =
"_resolve_cache_dir"
616 Path(request.uv_cache_dir).resolve()
617 if request.uv_cache_dir
is not None
618 else (request.repo_root.resolve() / UV_CACHE_DIRNAME)
622 f
"uv cache {cache} is on a different drive from the venv it fills "
623 f
"({worktree}); across drives uv cannot hardlink and silently copies. "
624 f
"Point --uv-cache-dir at a directory on the venv's drive."
626 cache.mkdir(parents=
True, exist_ok=
True)
627 _dbg(
"OC_CHAT.MAIN", func, f
"uv cache at {cache}", 3)
632 env = dict(os.environ)
633 env[
"UV_CACHE_DIR"] = str(cache)
638 uv: str, args: list[str], cache: Path, *, context: str
639) -> subprocess.CompletedProcess[str]:
640 """Run one uv command, refusing with uv's own diagnostic when it fails."""
642 result = subprocess.run(
649 if result.returncode != 0:
650 detail = (result.stderr
or result.stdout).strip()
652 _dbg(
"OC_CHAT.MAIN", func, f
"{context} ok", 3)
657 packages: tuple[EditablePackage, ...], worktree: Path, interpreter: Path
658) -> dict[str, object]:
660 "version": STAMP_VERSION,
661 "worktree": str(worktree),
662 "interpreter": str(interpreter),
663 "packages": [package.install_spec(worktree)
for package
in packages],
668 path = venv / STAMP_FILENAME
669 if not path.is_file():
672 loaded = json.loads(path.read_text(encoding=
"utf-8"))
673 except (OSError, ValueError):
675 return loaded
if isinstance(loaded, dict)
else None
678def _link_mode(install_output: str, *, installed: bool) -> str:
679 """Report the link strategy uv actually used, never a guess."""
681 return "n/a (nothing installed this run)"
682 if _HARDLINK_FALLBACK_MARKER
in install_output.casefold():
683 return "copy (uv fell back from hardlink)"
684 configured = os.environ.get(
"UV_LINK_MODE")
685 return configured
if configured
else "uv default (hardlink on Windows NTFS)"
688@dataclass(frozen=True)
690 """What one verification run proved, split by what it proves about."""
692 packages: tuple[tuple[str, str], ...]
693 extras: tuple[tuple[str, str], ...]
694 not_declared: tuple[str, ...]
698 """The probe environment: no ``PYTHONPATH``, no user site-packages."""
699 env = {key: value
for key, value
in os.environ.items()
if key !=
"PYTHONPATH"}
700 env[
"PYTHONNOUSERSITE"] =
"1"
706 packages: tuple[EditablePackage, ...],
710 """Import every package and every declared extra, refusing by name.
712 Runs with ``PYTHONPATH`` REMOVED and from a neutral temporary directory,
713 so a pass means the editable installs alone resolve inside the worktree —
714 which is the whole point of the exercise.
716 Plan 243 P3 adds the second class: the modules the declared ``extras``
717 exist to supply. Those live in the venv's ``site-packages`` rather than
718 in the checkout, so they are held to importability and venv residence.
719 EVERY failure across both classes is named in one refusal — a worker
720 who has to re-run prepare once per missing package learns nothing the
721 first refusal could not have told them.
723 func =
"_verify_imports"
726 "import importlib, sys\n"
727 "for name in sys.argv[1:]:\n"
729 " module = importlib.import_module(name)\n"
730 " except Exception as exc:\n"
731 " print(name + '\\tERROR\\t' + type(exc).__name__ + ': ' + str(exc))\n"
733 " print(name + '\\tOK\\t' + str(getattr(module, '__file__', None)))\n"
735 names = [package.import_name
for package
in packages]
736 owners = {package.import_name: package.dist_name
for package
in packages}
737 extra_names: list[str] = []
738 not_declared: list[str] = []
739 for package
in packages:
740 if not package.verify_imports:
742 if not package.declares_its_extras(worktree):
743 not_declared.append(package.install_spec(worktree))
745 for module
in package.verify_imports:
746 if module
not in extra_names
and module
not in names:
747 extra_names.append(module)
748 owners[module] = f
"{package.dist_name}[{','.join(package.extras)}]"
749 with tempfile.TemporaryDirectory(prefix=
"oc_verify_")
as neutral:
750 result = subprocess.run(
751 [str(python),
"-c", script, *names, *extra_names],
758 if result.returncode != 0:
760 f
"verification interpreter {python} failed: "
761 f
"{(result.stderr or result.stdout).strip()}"
764 resolved = worktree.resolve()
765 resolved_venv = venv.resolve()
766 verified: list[tuple[str, str]] = []
767 verified_extras: list[tuple[str, str]] = []
768 problems: list[str] = []
769 seen: set[str] = set()
770 for line
in result.stdout.splitlines():
771 if line.count(
"\t") < 2:
773 name, status, value = line.split(
"\t", 2)
775 owner = owners.get(name,
"?")
776 is_extra = name
in extra_names
778 problems.append(f
"{name}: import failed ({value}) — declared by {owner}")
780 if value
in (
"None",
""):
782 f
"{name}: resolved as a namespace package (__file__ is None) — "
783 f
"the sitecustomize.py copy is missing or ineffective"
787 if not Path(value).resolve().is_relative_to(resolved_venv):
789 f
"{name}: resolves OUTSIDE the prepared venv at {value} — "
790 f
"{owner} did not supply it, something else on the path did"
793 verified_extras.append((name, value))
795 if not Path(value).resolve().is_relative_to(resolved):
796 problems.append(f
"{name}: resolves OUTSIDE the worktree at {value}")
798 verified.append((name, value))
800 f
"{name}: not reported by the probe"
801 for name
in [*names, *extra_names]
806 f
"prepared venv {python.parent.parent} failed verification:\n "
807 +
"\n ".join(problems)
812 f
"verified {len(verified)} package + {len(verified_extras)} extra import(s)",
816 packages=tuple(verified),
817 extras=tuple(verified_extras),
818 not_declared=tuple(not_declared),
822def _run_smoke(python: Path, worktree: Path) -> tuple[str, ...]:
823 """Run the worker's own first two commands, refusing by name.
825 Plan 243 P4. Verification proves the modules import in a neutral
826 directory; this proves the STEP-0 tool and the suite prerequisites run
827 from the worktree the worker will actually sit in. Every check runs so
828 one refusal names everything that is wrong.
831 problems: list[str] = []
832 passed: list[str] = []
833 for label, argv
in SMOKE_CHECKS:
834 result = subprocess.run(
835 [str(python), *argv],
842 rendered =
" ".join(argv)
843 if result.returncode == 0:
846 detail = (result.stderr
or result.stdout).strip().splitlines()
847 tail = detail[-1]
if detail
else "(no output)"
849 f
"{label}: `python {rendered}` exited {result.returncode} — {tail}"
853 f
"prepared venv {python.parent.parent} failed the --smoke checks "
854 f
"in {worktree}:\n " +
"\n ".join(problems)
856 _dbg(
"OC_CHAT.MAIN", func, f
"{len(passed)} smoke check(s) passed", 2)
861 """Say what an older prepare's stamp disagrees with the table about.
863 Plan 243 P3. A table change (an added extra, a new package) must make
864 an already-prepared worktree report itself stale rather than silently
865 reinstall, so an operator reading the summary learns WHY the run was
866 not the no-op they expected.
870 parts: list[str] = []
871 added = [spec
for spec
in new
if spec
not in old]
872 removed = [spec
for spec
in old
if spec
not in new]
874 parts.append(
"now installs " +
", ".join(added))
876 parts.append(
"no longer installs " +
", ".join(removed))
877 if previous.get(
"interpreter") != current.get(
"interpreter"):
879 f
"interpreter changed from {previous.get('interpreter')} to "
880 f
"{current.get('interpreter')}"
883 parts.append(
"the recorded stamp differs from the declared table")
884 return "declared spec list changed since this venv was prepared: " +
"; ".join(parts)
888 packages = stamp.get(
"packages")
889 return list(packages)
if isinstance(packages, list)
else []
893 repo_root: Path, stage: str, plan: str, *, oct_hook: OctHook |
None =
None
895 """Load optional timing support without making oct an engine dependency."""
896 from contextlib
import nullcontext
898 func =
"_engine_timing"
900 timed_stage = (oct_hook
or _oct_hook)(
"timed_stage")
902 _dbg(
"OC_CHAT.MAIN", func,
"timing unavailable without oct", 3)
903 return nullcontext(
None)
904 match = re.search(
r"(?:^|[/_-])(\d+)(?:$|[/_-])", plan)
905 return timed_stage(repo_root, stage, scope=match.group(1)
if match
else None,
906 files=0
if stage ==
"engine.prepare" else None)
910 """Create the branch, worktree and its own uv venv, then verify it."""
911 func =
"prepare_worktree"
912 with _engine_timing(request.repo_root,
"engine.prepare", request.branch)
as timing:
913 started = time.monotonic()
914 repo_root = Path(request.repo_root).resolve()
915 worktree = Path(request.worktree).resolve()
916 _dbg(
"OC_CHAT.MAIN", func, f
"prepare {request.branch} at {worktree}", 2)
919 repo_root, request.branch, worktree, base_sha=request.base_sha
925 f
"no declared Option C package roots exist under {worktree}; "
926 f
"there is nothing to install and no editable shadow to close"
928 _dbg(
"OC_CHAT.MAIN", func, reason, 2)
931 branch=request.branch,
938 already_prepared=
False,
940 skipped_reason=reason,
941 elapsed_seconds=time.monotonic() - started,
946 venv = worktree /
".venv"
949 if not python.is_file():
952 [
"venv",
"--python", str(request.interpreter), str(venv)],
954 context=f
"uv venv at {venv}",
957 if not python.is_file():
962 already = previous == stamp
963 stale_reason =
None if already
or previous
is None else _describe_stale(previous, stamp)
964 if stale_reason
is not None:
965 _dbg(
"OC_CHAT.MAIN", func, f
"stale stamp: {stale_reason}", 2)
966 installed: tuple[str, ...] = ()
969 specs = [package.install_spec(worktree)
for package
in packages]
972 [
"pip",
"install",
"--python", str(python), *
_editable(specs)],
974 context=f
"uv pip install -e into {venv}",
976 install_output = f
"{result.stdout}\n{result.stderr}"
977 installed = tuple(package.dist_name
for package
in packages)
978 if timing
is not None:
979 timing.files = len(installed)
980 (venv / STAMP_FILENAME).write_text(
981 json.dumps(stamp, indent=2, sort_keys=
True), encoding=
"utf-8"
987 smoke_checks =
_run_smoke(python, worktree)
if request.smoke
else ()
988 except WorktreeEnvError:
996 (venv / STAMP_FILENAME).unlink(missing_ok=
True)
998 elapsed = time.monotonic() - started
1002 f
"prepared {worktree} in {elapsed:.1f}s "
1003 f
"({len(verification.packages)} verified)",
1008 branch=request.branch,
1011 installed=installed,
1012 verified=verification.packages,
1014 link_mode=
_link_mode(install_output, installed=bool(installed)),
1015 already_prepared=already,
1017 skipped_reason=
None,
1018 elapsed_seconds=elapsed,
1019 verified_extras=verification.extras,
1020 extras_not_declared=verification.not_declared,
1021 smoke_checks=smoke_checks,
1022 stale_reason=stale_reason,
1027 """Interleave ``-e`` before every install spec."""
1028 argv: list[str] = []
1030 argv.extend([
"-e", spec])
1035 """Copy ``venv_sitecustomize.py`` in, reporting a repair when it was gone."""
1036 func =
"_ensure_sitecustomize"
1039 existed = target.is_file()
1040 if existed
and target.read_bytes() == source.read_bytes():
1042 shutil.copyfile(source, target)
1043 verb =
"refreshed" if existed
else "installed"
1044 note = f
"sitecustomize.py {verb} at {target}"
1045 _dbg(
"OC_CHAT.MAIN", func, note, 3)
1052@dataclass(frozen=True)
1054 """What one teardown attempt actually did."""
1058 messages: tuple[str, ...]
1061 """Operator-facing lines; surfaced verbatim by the CLI."""
1063 f
"Removed worktree {self.worktree}"
1065 else f
"Worktree {self.worktree} was NOT removed"
1067 return [head, *(f
" {message}" for message
in self.
messages)]
1071 repo_root: Path, worktree: Path, *, branch: str |
None =
None
1073 """Remove a prepared worktree and its venv, naming any residue by path."""
1074 func =
"teardown_worktree"
1075 with _engine_timing(repo_root,
"engine.teardown", branch
or worktree.name)
as timing:
1076 repo_root = Path(repo_root).resolve()
1077 worktree = Path(worktree).resolve()
1078 metadata = repo_root /
".git" /
"worktrees" / worktree.name
1083 if worktree.exists():
1085 f
"{worktree} exists but is not a worktree of {repo_root}; "
1086 f
"refusing to delete a directory git does not own"
1088 _dbg(
"OC_CHAT.MAIN", func, f
"{worktree} already gone", 2)
1092 messages=(
"already removed (not registered, nothing on disk)",),
1096 repo_root,
"worktree",
"remove",
"--force", str(worktree), check=
False
1098 if removal.returncode != 0:
1099 detail = (removal.stderr
or removal.stdout).strip()
1100 _dbg(
"OC_CHAT.MAIN", func, f
"remove refused: {detail}", 2)
1101 if timing
is not None:
1102 timing.outcome =
"error"
1107 f
"git worktree remove failed: {detail}",
1108 f
"the known Windows failure leaves {metadata} behind; nothing "
1109 f
"else was deleted. Remove that directory by hand, then run "
1110 f
"`git worktree prune` in {repo_root}.",
1114 _run_git(repo_root,
"worktree",
"prune", check=
False)
1116 _dbg(
"OC_CHAT.MAIN", func, f
"branch {branch} left in place", 3)
1117 if worktree.exists():
1118 if timing
is not None:
1119 timing.outcome =
"error"
1124 f
"git reported success but {worktree} is still on disk; "
1125 f
"a handle is open on it (an editor, a shell, or a running "
1126 f
"interpreter from {worktree / '.venv'}).",
1129 _dbg(
"OC_CHAT.MAIN", func, f
"removed {worktree}", 2)
1133 messages=(f
"venv and checkout deleted; {metadata} pruned",),
1138 "EDITABLE_PACKAGES",
1149 "discover_packages",
1150 "ensure_branch_and_worktree",
1152 "sitecustomize_source",
1153 "teardown_worktree",
1157 "venv_site_packages",
str install_spec(self, Path root)
bool declares_its_extras(self, Path root)
Path source_dir(self, Path root)
str _extras_summary(self)
list[str] report_lines(self)
tuple extras_not_declared
list[str] report_lines(self)
bool _branch_exists(Path repo_root, str branch)
Path sitecustomize_source(Path worktree)
bool _same_path(Path left, Path right)
dict[str, str] _uv_env(Path cache)
_engine_timing(Path repo_root, str stage, str plan, *, OctHook|None oct_hook=None)
str _describe_stale(dict[str, object] previous, dict[str, object] current)
Path venv_python(Path venv)
str _link_mode(str install_output, *, bool installed)
dict[str, str] _clean_env()
OctHook use_oct_hook(OctHook|None hook)
None ensure_branch_and_worktree(Path repo_root, str branch, Path worktree, *, str|None base_sha=None)
_Verification _verify_imports(Path python, tuple[EditablePackage,...] packages, Path worktree, Path venv)
tuple[EditablePackage,...] discover_packages(Path root)
Path _resolve_cache_dir(PrepareRequest request, Path worktree)
subprocess.CompletedProcess[str] _run_uv(str uv, list[str] args, Path cache, *, str context)
dict[str, object] _stamp_payload(tuple[EditablePackage,...] packages, Path worktree, Path interpreter)
list[object] _stamp_specs(dict[str, object] stamp)
tuple[str,...] _run_smoke(Path python, Path worktree)
str _resolve_uv(str executable)
list[str] _editable(list[str] specs)
dict[str, object]|None _read_stamp(Path venv)
Path venv_scripts_dir(Path venv)
Any _default_oct_hook(str target)
Path venv_site_packages(Path venv)
PrepareResult prepare_worktree(PrepareRequest request)
subprocess.CompletedProcess[str] _run_git(Path repo_root, *str args, bool check=True)
TeardownResult teardown_worktree(Path repo_root, Path worktree, *, str|None branch=None)
tuple[str,...] _ensure_sitecustomize(Path venv, Path worktree)
list[tuple[Path, str|None]] worktree_entries(Path repo_root)