diff --git a/apps/vision-board-studio/photos-restore/2_build_gallery.py b/apps/vision-board-studio/photos-restore/2_build_gallery.py index 512848a48..2c96b835f 100644 --- a/apps/vision-board-studio/photos-restore/2_build_gallery.py +++ b/apps/vision-board-studio/photos-restore/2_build_gallery.py @@ -13,7 +13,7 @@ if u not in uset: continue p=os.path.join(root,f); sz=os.path.getsize(p) if u not in best or sz>best[u][1]: best[u]=(p,sz) -board=json.load(open('/Users/4jp/Workspace/limen/.claude/worktrees/feat-vision-board-studio/apps/vision-board-studio/boards/tony-2017.json')) +board=json.load(open('/Users/4jp/Workspace/library/engine/organvm/limen/.claude/worktrees/feat-vision-board-studio/apps/vision-board-studio/boards/tony-2017.json')) tiles=board['tiles'] # image embeddings of salvage crops def embed_imgs(paths): diff --git a/cli/src/limen/capacity.py b/cli/src/limen/capacity.py index a481ae32a..a06327b7e 100644 --- a/cli/src/limen/capacity.py +++ b/cli/src/limen/capacity.py @@ -1,7 +1,8 @@ from __future__ import annotations -import os import json +import math +import os import re import shlex import shutil @@ -392,8 +393,11 @@ def _usage_int(value: object) -> int | None: if isinstance(value, bool): return None try: - return int(float(str(value))) - except (TypeError, ValueError): + parsed = float(str(value)) + if not math.isfinite(parsed) or parsed < 0: + return None + return int(parsed) + except (OverflowError, TypeError, ValueError): return None @@ -420,11 +424,16 @@ def _live_usage_capacity( return None cap = _usage_int(info.get("possible")) remaining = _usage_int(info.get("remaining")) - if cap is None or cap <= 0 or remaining is None: + if cap is None or cap <= 0 or remaining is None or remaining < 0 or remaining > cap: + return None + raw_consumed = info.get("consumed") + consumed = _usage_int(raw_consumed) + if raw_consumed not in (None, "") and consumed is None: + return None + if consumed is not None and (consumed > cap or consumed + remaining > cap): return None - consumed = _usage_int(info.get("consumed")) spent = consumed if consumed is not None else max(0, cap - remaining) - return cap, max(0, spent), max(0, remaining) + return cap, spent, remaining def capacity_census(board: object = None, budget_limit: int | None = None) -> list[CapacityRow]: @@ -522,7 +531,11 @@ def format_capacity_census(rows: list[CapacityRow]) -> str: def _root() -> Path: - return Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) + configured = os.environ.get("LIMEN_ROOT") + if configured: + return Path(configured).expanduser() + workspace = Path(os.environ.get("WORKSPACE_ROOT", str(Path.home() / "Workspace"))).expanduser() + return workspace / "library" / "engine" / "organvm" / "limen" def _load_usage(root: Path | None = None) -> dict[str, object]: diff --git a/cli/src/limen/cli.py b/cli/src/limen/cli.py index 949cd4226..fd586da45 100644 --- a/cli/src/limen/cli.py +++ b/cli/src/limen/cli.py @@ -67,6 +67,45 @@ def main(): main.add_command(fanout_group) +@main.group("worktree") +def worktree_group() -> None: + """Inspect and reclaim disposable worktree lifecycle units.""" + + +@worktree_group.command( + "reap", + context_settings={ + "ignore_unknown_options": True, + "allow_extra_args": True, + "help_option_names": [], + }, +) +@click.argument("reaper_args", nargs=-1, type=click.UNPROCESSED) +def worktree_reap(reaper_args: tuple[str, ...]) -> None: + """Delegate unchanged arguments to the bounded worktree reaper.""" + + root = resolve_limen_repo_root() + script = root / "scripts" / "reclaim-worktrees.py" + if not script.is_file(): + raise click.ClickException(f"worktree reaper is missing: {script}") + try: + result = subprocess.run( + [sys.executable, str(script), *reaper_args], + cwd=root, + capture_output=True, + text=True, + check=False, + ) + except OSError as exc: + raise click.ClickException(f"could not launch worktree reaper: {exc}") from exc + if result.stdout: + click.echo(result.stdout, nl=False) + if result.stderr: + click.echo(result.stderr, nl=False, err=True) + if result.returncode: + raise click.exceptions.Exit(result.returncode) + + def _host_owner() -> tuple[str, int]: pid = os.getppid() label = os.environ.get("LIMEN_HOST_ADMISSION_OWNER") or os.environ.get("LIMEN_SESSION_ID") diff --git a/cli/src/limen/conduct/campaign_relay.py b/cli/src/limen/conduct/campaign_relay.py index aa4614c86..5158f0407 100644 --- a/cli/src/limen/conduct/campaign_relay.py +++ b/cli/src/limen/conduct/campaign_relay.py @@ -24,6 +24,7 @@ ContractError, validate_contract, ) +from limen.worktree_layout import runtime_worktree_path _RECEIPT_CEILING = 65_536 _PREDECESSOR_RECEIPT_CEILING = 262_144 @@ -379,12 +380,28 @@ def _relay_worktree( ) -> Path: """Resolve the generated successor worktree and bind it to the same repository.""" - primary = _primary_checkout( - root, + primary = _primary_checkout(root, deadline_monotonic=deadline_monotonic) + listing = _git( + primary, + "worktree", + "list", + "--porcelain", deadline_monotonic=deadline_monotonic, ) - candidate = primary / ".worktrees" / successor_slug + expected_branch = f"refs/heads/work/{successor_slug}" + candidates: list[Path] = [] + for record in listing.split("\n\n"): + fields = dict(line.split(" ", 1) for line in record.splitlines() if " " in line) + if fields.get("branch") == expected_branch and fields.get("worktree"): + candidates.append(Path(fields["worktree"])) + if len(candidates) != 1: + raise CampaignRelayError( + "relay_worktree_invalid", + "campaign relay successor worktree is unavailable", + ) + candidate = candidates[0] try: + expected_candidate = runtime_worktree_path(primary, successor_slug) resolved = candidate.resolve(strict=True) top_level = Path( _git( @@ -403,12 +420,18 @@ def _relay_worktree( primary, deadline_monotonic=deadline_monotonic, ) - except OSError as exc: + except (OSError, ValueError) as exc: raise CampaignRelayError( "relay_worktree_invalid", "campaign relay successor worktree is unavailable", ) from exc - if candidate.is_symlink() or not resolved.is_dir() or top_level != resolved or worktree_common != primary_common: + if ( + candidate.is_symlink() + or not resolved.is_dir() + or candidate != expected_candidate + or top_level != resolved + or worktree_common != primary_common + ): raise CampaignRelayError( "relay_worktree_invalid", "campaign relay successor worktree does not match the primary checkout", diff --git a/cli/src/limen/dispatch.py b/cli/src/limen/dispatch.py index 9b1d5a7b9..04c0e53c2 100644 --- a/cli/src/limen/dispatch.py +++ b/cli/src/limen/dispatch.py @@ -130,6 +130,14 @@ ) +def _limen_root() -> Path: + configured = os.environ.get("LIMEN_ROOT") + if configured: + return Path(configured).expanduser() + workspace = Path(os.environ.get("WORKSPACE_ROOT", str(Path.home() / "Workspace"))).expanduser() + return workspace / "library" / "engine" / "organvm" / "limen" + + def _int_or_default(raw: object, default: int) -> int: if isinstance(raw, bool): return default @@ -213,7 +221,7 @@ def _usage_dead_lanes() -> set[str]: signal, never pinned: a lane auto-rejoins the instant its rolling window refills (no manual edit). This is what makes dispatch HONEST — we never assign a task to a lane that physically cannot produce, and we never burn one to 0.""" - f = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) / "logs" / "usage.json" + f = _limen_root() / "logs" / "usage.json" try: vendors = (json.loads(f.read_text()) or {}).get("vendors", {}) except (OSError, ValueError): @@ -334,7 +342,7 @@ def _down_lanes() -> set[str]: when every observed provider is in auth/rate cooldown. Rebalance + dispatch + route skip these so tasks aren't wasted on a lane that can't produce. Sources 2–4 self-heal; remove a line from source 1 when that lane is healthy again.""" - f = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) / "logs" / "lanes-down.txt" + f = _limen_root() / "logs" / "lanes-down.txt" manual: set[str] = set() try: manual = {ln.split("#")[0].strip() for ln in f.read_text().splitlines() if ln.split("#")[0].strip()} @@ -393,7 +401,7 @@ def run_always_working_before_dispatch(tasks_path: Path, *, dry_run: bool = Fals """ if dry_run or os.environ.get("LIMEN_ALWAYS_WORKING_BEFORE_DISPATCH", "1") != "1": return True - root = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))).resolve() + root = _limen_root().resolve() try: if tasks_path.resolve() != (root / "tasks.yaml").resolve(): return True @@ -1159,7 +1167,7 @@ def _value_tier_repos() -> set[str]: repos: set[str] = {r.strip() for r in os.environ.get("LIMEN_VALUE_REPOS", "").split(",") if r.strip()} fpath = os.environ.get( "LIMEN_VALUE_REPOS_FILE", - str(Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) / "value-repos.json"), + str(_limen_root() / "value-repos.json"), ) try: data = json.loads(Path(fpath).read_text()) @@ -1718,13 +1726,13 @@ def _journaled_agent_dispatch( def _remote_receipt_store() -> ReceiptStore: - root = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) + root = _limen_root() configured = os.environ.get("LIMEN_REMOTE_RECEIPT_ROOT") return ReceiptStore(Path(configured).expanduser() if configured else root / "logs" / "remote-execution") def _receipt_path_for_board(path: Path) -> str: - root = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) + root = _limen_root() try: return str(path.resolve().relative_to(root.resolve())) except ValueError: @@ -1732,7 +1740,7 @@ def _receipt_path_for_board(path: Path) -> str: def _authoritative_remote_verification(task: Task) -> tuple[Task, dict[str, object]]: - root = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) + root = _limen_root() tasks_path = Path(os.environ.get("LIMEN_TASKS", str(root / "tasks.yaml"))).expanduser() if not tasks_path.is_file(): raise RemoteExecutionError("authoritative task board is unavailable for verification-only dispatch") @@ -1903,7 +1911,7 @@ def _flame_preamble() -> str: a missing/unreadable kernel must NEVER block a dispatch (derive-never-pin, fail-open).""" if os.environ.get("LIMEN_FLAME_KERNEL", "1") != "1": return "" - root = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) + root = _limen_root() f = root / os.environ.get("LIMEN_FLAME_FILE", "FLAME.md") try: key = f"{f}:{f.stat().st_mtime_ns}" @@ -3049,7 +3057,7 @@ def _local_repo_matches(repo: str | None, identity: str) -> bool: for raw in ( os.environ.get("LIMEN_ROOT"), os.environ.get("LIMEN_LIVE_ROOT"), - str(Path.home() / "Workspace" / "limen"), + str(Path.home() / "Workspace" / "library" / "engine" / "organvm" / "limen"), ) if raw } @@ -4286,7 +4294,7 @@ def _bridge_agy_scratch(task: Task, wt: Path) -> None: def _lane_run_env(agent: str, wt: Path | None = None, task: Task | None = None) -> dict[str, str]: run_env = os.environ.copy() if wt is not None: - live_root = os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen")) + live_root = str(_limen_root()) run_env["LIMEN_LIVE_ROOT"] = live_root run_env["LIMEN_ROOT"] = str(wt) run_env["LIMEN_TASKS"] = str(wt / "tasks.yaml") @@ -4300,9 +4308,7 @@ def _lane_run_env(agent: str, wt: Path | None = None, task: Task | None = None) # agy/antigravity defense-in-depth: if auth falls through to browser opening mid-run, # make the opener a no-op inside the lane subprocess only. if agent in ("agy", "antigravity"): - shim = str( - Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) / "scripts" / "agy-noop-shim" - ) + shim = str(_limen_root() / "scripts" / "agy-noop-shim") run_env["PATH"] = shim + os.pathsep + run_env.get("PATH", "") run_env["BROWSER"] = "true" # claude fleet auth must not share or mutate the interactive session's macOS Keychain token. @@ -4585,7 +4591,7 @@ def _record_worktree_lifecycle( generated_cleanup: str, pushed: bool, ) -> None: - root = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) + root = _limen_root() try: log = root / "logs" / "worktree-lifecycle.jsonl" log.parent.mkdir(parents=True, exist_ok=True) @@ -4966,7 +4972,7 @@ def _window_hours(agent: str) -> float: (5h rolling windows) refill ~5x/day instead of being throttled by a once-a-day cap, while jules/gemini/opencode/agy refill daily.""" try: - root = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) + root = _limen_root() with open(root / "logs" / "usage-limits.json") as fh: limits = json.load(fh) window = str((limits.get(agent) or {}).get("window", "")) @@ -5505,7 +5511,7 @@ def _apply_result( def _ledger_lanes() -> dict[str, dict[str, list[str]]]: """logs/ledger.json lanes map (waste_classes/win_classes per lane) — fail-open to {}.""" try: - root = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) + root = _limen_root() return json.loads((root / "logs" / "ledger.json").read_text()).get("lanes", {}) or {} except Exception: return {} @@ -5514,7 +5520,7 @@ def _ledger_lanes() -> dict[str, dict[str, list[str]]]: def _lane_fitness() -> dict[str, dict[str, dict]]: """logs/lane-fitness.json per-(agent, task_class) fitness map — fail-open to {}.""" try: - root = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) + root = _limen_root() return json.loads((root / "logs" / "lane-fitness.json").read_text()).get("pairs", {}) or {} except Exception: return {} @@ -5673,7 +5679,7 @@ def _claude_tier_overrides() -> dict[str, list[str]]: Fail-open to {} (→ the ledger-DISCOVERED default). Demoted to an override: the default pre-assign set is discovered from the ledger, not pinned. Same read pattern as _ledger_lanes().""" try: - root = Path(os.environ.get("LIMEN_ROOT", str(Path.home() / "Workspace" / "limen"))) + root = _limen_root() return json.loads((root / "logs" / "model-tiers.json").read_text()).get("claude") or {} except Exception: return {} diff --git a/cli/src/limen/provider_health.py b/cli/src/limen/provider_health.py index 3f82ccd22..7d45dfccd 100644 --- a/cli/src/limen/provider_health.py +++ b/cli/src/limen/provider_health.py @@ -91,7 +91,9 @@ def provider_health_policy() -> ProviderHealthPolicy: def provider_outcome_ledger_path() -> Path: - root = Path(os.environ.get("LIMEN_ROOT", Path.home() / "Workspace" / "limen")).expanduser() + root = Path( + os.environ.get("LIMEN_ROOT", Path.home() / "Workspace" / "library" / "engine" / "organvm" / "limen") + ).expanduser() raw = str(parameter("LIMEN_PROVIDER_OUTCOME_LEDGER", str(root / "logs" / "provider-outcomes.jsonl"))) return Path(raw.replace("$LIMEN_ROOT", str(root))).expanduser() diff --git a/cli/src/limen/repository_ignored.py b/cli/src/limen/repository_ignored.py new file mode 100644 index 000000000..1aa080917 --- /dev/null +++ b/cli/src/limen/repository_ignored.py @@ -0,0 +1,41 @@ +"""Shared loss-free classification for ignored repository entries. + +The literal-substrate court and the clone reaper must agree about which ignored +working-tree payloads are reproducible. Anything outside this positive +allowlist requires custody and therefore prevents checkout removal. +""" + +from __future__ import annotations + + +REGENERABLE_DIRS = frozenset( + "node_modules .venv venv .venv-demucs __pycache__ .pytest_cache .mypy_cache .ruff_cache " + ".tox dist build .next .nuxt .svelte-kit .astro .turbo .parcel-cache .vercel .wrangler " + ".gradle coverage .nyc_output .eggs .ipynb_checkpoints".split() +) +REGENERABLE_SUFFIXES = (".pyc", ".pyo") +REGENERABLE_FILES = frozenset({".DS_Store"}) + + +def ignored_entries_from_porcelain(output: str) -> tuple[str, ...]: + """Return raw relative paths from ``git status --porcelain=v1 -z --ignored``.""" + + return tuple( + record[3:].rstrip("/") for record in output.split("\0") if record.startswith("!! ") and record[3:].rstrip("/") + ) + + +def ignored_entry_is_regenerable(path: str) -> bool: + """Whether one enumerated ignored entry is safe to recreate after cloning.""" + + normalized = path.rstrip("/") + if not normalized or normalized.startswith("/") or "\0" in normalized: + return False + parts = normalized.split("/") + if any(part in {"", ".", ".."} for part in parts): + return False + top = parts[0] + base = parts[-1] + if top in REGENERABLE_DIRS: + return True + return len(parts) == 1 and (base in REGENERABLE_FILES or base.endswith(REGENERABLE_SUFFIXES)) diff --git a/cli/src/limen/substrate_convergence.py b/cli/src/limen/substrate_convergence.py new file mode 100644 index 000000000..4f9533adb --- /dev/null +++ b/cli/src/limen/substrate_convergence.py @@ -0,0 +1,1689 @@ +"""Literal Workspace manifest validation and machine-wide convergence court. + +PORTVS owns the manifest. This module deliberately owns only the court: it +validates the contract, compares it with the physical tree, discovers repository +roots recursively, and verifies the custody assertions that make a private move +safe. Repository and private interiors remain recursively owned by Git and the +sealed inventory named by the manifest. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import UTC, datetime +import hashlib +import json +import os +from pathlib import Path, PurePosixPath +import re +import stat +import subprocess +import time +from typing import Any, Iterable, Mapping, Sequence +from urllib.parse import urlparse + +import yaml + +from limen.repository_ignored import ignored_entry_is_regenerable, ignored_entries_from_porcelain + + +SCHEMA = "portvs.workspace_manifest.v1" +REPORT_SCHEMA = "limen.substrate_convergence_report.v1" +KINDS = frozenset({"structural", "repository", "private", "ephemeral", "index"}) +RESIDENCIES = frozenset({"structural", "laptop", "private", "ephemeral", "remote-index"}) +OPAQUE_KINDS = frozenset({"private", "ephemeral"}) +DIRECTORY_KINDS = frozenset({"structural", "repository", "private", "ephemeral"}) +LIMIT_KEYS = ( + "max_scan_entries", + "max_violations", + "max_unmeasured", + "max_compatibility_links", +) +GIT_TIMEOUT_SECONDS = 60 +REPOSITORY_FETCH_BUDGET_SECONDS = 120.0 +LSOF_TIMEOUT_SECONDS = 15 +CUSTODY_LEDGER_MAX_BYTES = 16 * 1024 * 1024 +CUSTODY_LEDGER_MAX_ROWS = 50_000 +CUSTODY_LEDGER_DEADLINE_SECONDS = 10.0 +_monotonic = time.monotonic +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +_CUSTODY_IDENTITY_FIELDS = ("inventory_sha256", "plan_sha256", "content_sha256") + + +class ManifestError(ValueError): + """The workspace manifest is missing, malformed, or unsafe.""" + + +class ActiveCwdDiscoveryError(RuntimeError): + """Live process CWDs could not be measured safely.""" + + +@dataclass(frozen=True) +class Row: + path: str + kind: str + owner_ref: str + residency: str + raw: Mapping[str, Any] + + +@dataclass(frozen=True) +class Violation: + code: str + path: str + message: str + + def as_dict(self) -> dict[str, str]: + return {"code": self.code, "path": self.path, "message": self.message} + + +@dataclass +class ScanBudget: + """One shared budget for every structural, repository, and runtime scan.""" + + limit: int + consumed: int = 0 + + def consume(self, count: int = 1) -> bool: + if count < 0: + raise ValueError("scan budget debit cannot be negative") + if self.consumed + count > self.limit: + return False + self.consumed += count + return True + + @property + def remaining(self) -> int: + return self.limit - self.consumed + + +@dataclass +class RepositoryFetchBudget: + """One aggregate wall-clock deadline shared by every declared origin fetch.""" + + limit_seconds: float + started_at: float = field(default_factory=lambda: _monotonic()) + exhausted: bool = False + + def timeout(self) -> float | None: + remaining = self.limit_seconds - (_monotonic() - self.started_at) + if remaining <= 0: + self.exhausted = True + return None + return min(float(GIT_TIMEOUT_SECONDS), remaining) + + +@dataclass(frozen=True) +class CustodyLedgerRows: + """One bounded, coherent selection from a private custody ledger.""" + + available: bool + rows: tuple[Mapping[str, Any], ...] = () + inspection_error: str | None = None + + +@dataclass(frozen=True) +class _CustodyLedgerSource: + available: bool + kind: str = "" + value: Any = None + inspection_error: str | None = None + + +@dataclass +class CustodyLedgerReader: + """Read every unique custody source once under one aggregate byte/row/deadline budget.""" + + max_bytes: int = CUSTODY_LEDGER_MAX_BYTES + max_rows: int = CUSTODY_LEDGER_MAX_ROWS + deadline_seconds: float = CUSTODY_LEDGER_DEADLINE_SECONDS + started_at: float = field(default_factory=lambda: _monotonic()) + bytes_consumed: int = 0 + rows_consumed: int = 0 + inspection_errors: list[tuple[Path, str]] = field(default_factory=list) + _sources: dict[Path, _CustodyLedgerSource] = field(default_factory=dict) + _selections: dict[tuple[Path, tuple[str, ...]], CustodyLedgerRows] = field(default_factory=dict) + + def _deadline_error(self) -> str | None: + if _monotonic() - self.started_at >= self.deadline_seconds: + return "aggregate custody ledger inspection deadline exhausted" + return None + + def _record_error(self, path: Path, error: str) -> None: + item = (path, error) + if item not in self.inspection_errors: + self.inspection_errors.append(item) + + def _source_error(self, path: Path, message: str, *, available: bool = True) -> _CustodyLedgerSource: + self._record_error(path, message) + return _CustodyLedgerSource(available=available, inspection_error=message) + + def _read_jsonl(self, path: Path, handle: Any) -> _CustodyLedgerSource: + rows: list[Mapping[str, Any]] = [] + while True: + if error := self._deadline_error(): + return self._source_error(path, error) + remaining = self.max_bytes - self.bytes_consumed + raw = handle.readline(max(1, remaining + 1)) + if not raw: + break + if len(raw) > remaining: + self.bytes_consumed += max(0, remaining) + return self._source_error(path, "aggregate custody ledger byte ceiling exceeded") + self.bytes_consumed += len(raw) + if error := self._deadline_error(): + return self._source_error(path, error) + if not raw.strip(): + continue + if self.rows_consumed + 1 > self.max_rows: + return self._source_error(path, "aggregate custody ledger row ceiling exceeded") + self.rows_consumed += 1 + try: + value = json.loads(raw.decode("utf-8")) + except UnicodeDecodeError: + return self._source_error(path, "custody ledger is not valid UTF-8") + except json.JSONDecodeError: + return self._source_error(path, "custody ledger contains malformed JSONL") + if isinstance(value, dict): + rows.append(value) + if error := self._deadline_error(): + return self._source_error(path, error) + return _CustodyLedgerSource(available=True, kind="jsonl", value=tuple(rows)) + + def _read_json(self, path: Path, handle: Any) -> _CustodyLedgerSource: + if error := self._deadline_error(): + return self._source_error(path, error) + remaining = self.max_bytes - self.bytes_consumed + raw = handle.read(max(1, remaining + 1)) + if len(raw) > remaining: + self.bytes_consumed += max(0, remaining) + return self._source_error(path, "aggregate custody ledger byte ceiling exceeded") + self.bytes_consumed += len(raw) + if error := self._deadline_error(): + return self._source_error(path, error) + try: + value = json.loads(raw.decode("utf-8")) + except UnicodeDecodeError: + return self._source_error(path, "custody ledger is not valid UTF-8") + except json.JSONDecodeError: + return self._source_error(path, "custody ledger contains malformed JSON") + if error := self._deadline_error(): + return self._source_error(path, error) + return _CustodyLedgerSource(available=True, kind="json", value=value) + + def _source(self, path: Path) -> tuple[Path, _CustodyLedgerSource]: + normalized = Path(os.path.abspath(path.expanduser())) + cached = self._sources.get(normalized) + if cached is not None: + return normalized, cached + if error := self._deadline_error(): + source = self._source_error(normalized, error, available=False) + self._sources[normalized] = source + return normalized, source + + try: + before = normalized.lstat() + except FileNotFoundError: + source = _CustodyLedgerSource(available=False) + self._sources[normalized] = source + return normalized, source + except OSError as exc: + source = self._source_error( + normalized, + f"custody ledger lstat failed: {type(exc).__name__}", + ) + self._sources[normalized] = source + return normalized, source + if stat.S_ISLNK(before.st_mode): + source = self._source_error(normalized, "custody ledger source is a symlink") + self._sources[normalized] = source + return normalized, source + if not stat.S_ISREG(before.st_mode): + source = self._source_error(normalized, "custody ledger source is not a regular file") + self._sources[normalized] = source + return normalized, source + + descriptor: int | None = None + try: + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NONBLOCK", 0) + flags |= getattr(os, "O_NOFOLLOW", 0) + descriptor = os.open(normalized, flags) + opened = os.fstat(descriptor) + after_open = normalized.lstat() + before_identity = (before.st_dev, before.st_ino) + if ( + not stat.S_ISREG(opened.st_mode) + or stat.S_ISLNK(after_open.st_mode) + or not stat.S_ISREG(after_open.st_mode) + or (opened.st_dev, opened.st_ino) != before_identity + or (after_open.st_dev, after_open.st_ino) != before_identity + ): + raise OSError("custody ledger source identity changed during open") + with os.fdopen(descriptor, "rb") as handle: + descriptor = None + source = ( + self._read_jsonl(normalized, handle) + if normalized.suffix.lower() == ".jsonl" + else self._read_json(normalized, handle) + ) + after_read = os.fstat(handle.fileno()) + if ( + after_read.st_dev, + after_read.st_ino, + after_read.st_size, + after_read.st_mtime_ns, + ) != ( + opened.st_dev, + opened.st_ino, + opened.st_size, + opened.st_mtime_ns, + ): + source = self._source_error( + normalized, + "custody ledger source changed during inspection", + ) + except OSError as exc: + source = self._source_error( + normalized, + f"custody ledger read failed: {type(exc).__name__}", + ) + finally: + if descriptor is not None: + os.close(descriptor) + self._sources[normalized] = source + return normalized, source + + def read_rows( + self, + path: Path, + *, + collection_keys: Sequence[str] = ("receipts",), + ) -> CustodyLedgerRows: + resolved, source = self._source(path) + if source.inspection_error: + return CustodyLedgerRows(source.available, inspection_error=source.inspection_error) + if not source.available: + return CustodyLedgerRows(False) + if source.kind == "jsonl": + selection_key = (resolved, ("__jsonl__",)) + rows = tuple(source.value) + result = CustodyLedgerRows(True, rows=rows) + self._selections.setdefault(selection_key, result) + return self._selections[selection_key] + + value = source.value + selection_id: tuple[str, ...] + raw_rows: list[object] + if isinstance(value, list): + selection_id = ("__top_level__",) + raw_rows = value + elif isinstance(value, dict): + selected_key = next( + (key for key in collection_keys if isinstance(value.get(key), list)), + None, + ) + if selected_key is None: + selection_id = ("__mapping__",) + raw_rows = [value] + else: + selection_id = ("__collection__", selected_key) + raw_rows = value[selected_key] + else: + selection_id = ("__top_level__",) + raw_rows = [value] + cache_key = (resolved, selection_id) + cached = self._selections.get(cache_key) + if cached is not None: + return cached + if error := self._deadline_error(): + self._record_error(resolved, error) + result = CustodyLedgerRows(True, inspection_error=error) + elif self.rows_consumed + len(raw_rows) > self.max_rows: + error = "aggregate custody ledger row ceiling exceeded" + self._record_error(resolved, error) + result = CustodyLedgerRows(True, inspection_error=error) + else: + self.rows_consumed += len(raw_rows) + rows = tuple(row for row in raw_rows if isinstance(row, dict)) + if error := self._deadline_error(): + self._record_error(resolved, error) + result = CustodyLedgerRows(True, inspection_error=error) + else: + result = CustodyLedgerRows(True, rows=rows) + self._selections[cache_key] = result + return result + + +def _nonempty_string(value: object) -> bool: + return isinstance(value, str) and bool(value.strip()) + + +def _safe_relative_path(value: object, *, field: str = "path") -> str: + if not _nonempty_string(value): + raise ManifestError(f"{field} must be a non-empty relative POSIX path") + text = str(value) + if "\\" in text: + raise ManifestError(f"{field} {text!r} must use POSIX separators") + pure = PurePosixPath(text) + if pure.is_absolute() or text in {".", ".."} or any(part in {"", ".", ".."} for part in pure.parts): + raise ManifestError(f"{field} {text!r} is not a normalized relative path") + normalized = pure.as_posix() + if normalized != text.rstrip("/"): + raise ManifestError(f"{field} {text!r} is not normalized (want {normalized!r})") + return normalized + + +def _required(row: Mapping[str, Any], keys: Iterable[str], *, path: str) -> None: + missing = [key for key in keys if not _nonempty_string(row.get(key))] + if missing: + raise ManifestError(f"{path}: missing required field(s): {', '.join(missing)}") + + +def load_manifest(path: Path) -> tuple[dict[str, Any], list[Row], bytes]: + if not path.is_file(): + raise ManifestError(f"workspace manifest missing: {path}") + raw_bytes = path.read_bytes() + try: + data = yaml.safe_load(raw_bytes) or {} + except yaml.YAMLError as exc: + raise ManifestError(f"workspace manifest is not valid YAML: {exc}") from exc + if not isinstance(data, dict): + raise ManifestError("workspace manifest root must be a mapping") + if data.get("schema") != SCHEMA: + raise ManifestError(f"workspace manifest schema must be {SCHEMA!r}") + limits = data.get("limits") + if not isinstance(limits, dict): + raise ManifestError("limits must be a mapping") + missing_limits = [key for key in LIMIT_KEYS if key not in limits] + if missing_limits: + raise ManifestError(f"limits missing required field(s): {', '.join(missing_limits)}") + raw_rows = data.get("rows") + if not isinstance(raw_rows, list) or not raw_rows: + raise ManifestError("workspace manifest rows must be a non-empty list") + + rows: list[Row] = [] + seen_paths: set[str] = set() + repository_remotes: dict[str, str] = {} + for index, value in enumerate(raw_rows): + if not isinstance(value, dict): + raise ManifestError(f"rows[{index}] must be a mapping") + _required(value, ("path", "kind", "owner_ref", "residency"), path=f"rows[{index}]") + row_path = _safe_relative_path(value["path"]) + kind = str(value["kind"]) + residency = str(value["residency"]) + if kind not in KINDS: + raise ManifestError(f"{row_path}: kind {kind!r} is not one of {sorted(KINDS)}") + if residency not in RESIDENCIES: + raise ManifestError(f"{row_path}: residency {residency!r} is not one of {sorted(RESIDENCIES)}") + if row_path in seen_paths: + raise ManifestError(f"duplicate manifest path: {row_path}") + seen_paths.add(row_path) + + if kind == "repository": + _required(value, ("remote", "custody_ref"), path=row_path) + remote = canonical_remote(str(value["remote"])) + if remote in repository_remotes: + raise ManifestError( + f"{row_path}: remote duplicates {repository_remotes[remote]} ({remote}); " + "one repository has one canonical physical home" + ) + repository_remotes[remote] = row_path + elif kind == "ephemeral": + _required(value, ("reaper",), path=row_path) + if "expires_after" not in value: + raise ManifestError(f"{row_path}: missing required field: expires_after") + expires_after = value["expires_after"] + if not isinstance(expires_after, int) or isinstance(expires_after, bool) or expires_after <= 0: + raise ManifestError(f"{row_path}: expires_after must be a positive integer number of seconds") + elif kind == "private": + _required( + value, + ("sealed_inventory_ref", "restoration_receipt_ref", "custody_label"), + path=row_path, + ) + elif kind == "index": + _required(value, ("source_ref", "generator"), path=row_path) + + rows.append( + Row( + path=row_path, + kind=kind, + owner_ref=str(value["owner_ref"]), + residency=residency, + raw=value, + ) + ) + + _validate_parent_ownership(rows) + _validate_compatibility_rows(data) + return data, rows, raw_bytes + + +def _validate_parent_ownership(rows: Sequence[Row]) -> None: + by_path = {row.path: row for row in rows} + for row in rows: + parts = PurePosixPath(row.path).parts + for depth in range(1, len(parts)): + parent_path = PurePosixPath(*parts[:depth]).as_posix() + parent = by_path.get(parent_path) + if parent is None: + raise ManifestError( + f"{row.path}: parent {parent_path!r} has no manifest row; structural containment must be literal" + ) + if parent.kind != "structural": + raise ManifestError( + f"{row.path}: nested below {parent.path} ({parent.kind}); " + "that owner's interior is opaque to the Workspace manifest" + ) + + +def _validate_compatibility_rows(data: Mapping[str, Any]) -> None: + migration = data.get("migration") or {} + if not isinstance(migration, dict): + raise ManifestError("migration must be a mapping") + links = migration.get("compatibility_links") or [] + if not isinstance(links, list): + raise ManifestError("migration.compatibility_links must be a list") + seen: set[str] = set() + targets: dict[str, str] = {} + for index, row in enumerate(links): + if not isinstance(row, dict): + raise ManifestError(f"migration.compatibility_links[{index}] must be a mapping") + _required(row, ("path", "target", "owner_ref", "expires_at"), path=f"compatibility_links[{index}]") + path = _safe_relative_path(row["path"], field="compatibility link path") + target = _safe_relative_path(row["target"], field="compatibility link target") + if path in seen: + raise ManifestError(f"duplicate compatibility link path: {path}") + seen.add(path) + targets[path] = target + try: + datetime.fromisoformat(str(row["expires_at"]).replace("Z", "+00:00")) + except ValueError as exc: + raise ManifestError(f"{path}: expires_at must be an ISO-8601 timestamp") from exc + if path == target or target.startswith(path + "/"): + raise ManifestError(f"{path}: compatibility link target would form a cycle") + + dependencies = { + path: {candidate for candidate in targets if target == candidate or target.startswith(candidate + "/")} + for path, target in targets.items() + } + visiting: set[str] = set() + visited: set[str] = set() + + def visit(path: str, chain: tuple[str, ...]) -> None: + if path in visiting: + cycle = " -> ".join((*chain, path)) + raise ManifestError(f"compatibility link graph contains a cycle: {cycle}") + if path in visited: + return + visiting.add(path) + for dependency in sorted(dependencies[path]): + visit(dependency, (*chain, path)) + visiting.remove(path) + visited.add(path) + + for path in sorted(targets): + visit(path, ()) + + +def resolve_workspace_root(data: Mapping[str, Any], override: Path | None = None) -> Path: + if override is not None: + root = override.expanduser() + else: + configured = data.get("workspace_root") + if not _nonempty_string(configured): + raise ManifestError("workspace_root must be a non-empty absolute path") + root = Path(os.path.expandvars(str(configured))).expanduser() + if not root.is_absolute(): + raise ManifestError(f"workspace root must be absolute: {root}") + # Preserve the lexical root until the physical-directory check has run. + # ``resolve`` here would erase the evidence that Workspace itself is a + # symlink and make the workspace_symlink verdict unreachable. + return Path(os.path.abspath(root)) + + +def canonical_remote(value: str) -> str: + text = value.strip() + if not text: + raise ManifestError("repository remote cannot be empty") + if text.startswith("git@github.com:"): + text = "https://github.com/" + text.removeprefix("git@github.com:") + elif text.startswith("ssh://git@github.com/"): + text = "https://github.com/" + text.removeprefix("ssh://git@github.com/") + parsed = urlparse(text) + if parsed.scheme in {"http", "https"} and parsed.netloc.lower() == "github.com": + path = parsed.path.strip("/") + if path.endswith(".git"): + path = path[:-4] + if path.count("/") != 1: + raise ManifestError(f"GitHub remote has unexpected shape: {value!r}") + return f"https://github.com/{path.lower()}" + if text.startswith("file://"): + return str(Path(parsed.path).resolve(strict=False)) + candidate_path = Path(text).expanduser() + if candidate_path.is_absolute(): + return str(candidate_path.resolve(strict=False)) + raise ManifestError(f"unsupported repository remote: {value!r}") + + +def _run_git( + repo: Path, + *args: str, + timeout: float = GIT_TIMEOUT_SECONDS, +) -> subprocess.CompletedProcess[str]: + command = ["git", "-C", str(repo), *args] + try: + return subprocess.run( + command, + capture_output=True, + text=True, + check=False, + timeout=timeout, + env={ + **os.environ, + "GIT_OPTIONAL_LOCKS": "0", + "GIT_TERMINAL_PROMPT": "0", + }, + ) + except (OSError, subprocess.SubprocessError) as exc: + return subprocess.CompletedProcess(command, 1, "", str(exc)) + + +def _is_within(path: Path, root: Path) -> bool: + try: + path.relative_to(root) + return True + except ValueError: + return False + + +def _resolve_bounded(path: Path, *, max_symlinks: int = 64) -> Path: + """Resolve a path without allowing a filesystem symlink loop to hang or escape measurement.""" + + current = Path(os.path.abspath(path)) + seen: set[Path] = set() + for _ in range(max_symlinks): + parts = current.parts + cursor = Path(parts[0]) + for index, part in enumerate(parts[1:], start=1): + cursor /= part + if not cursor.is_symlink(): + continue + lexical = Path(os.path.abspath(cursor)) + if lexical in seen: + raise RuntimeError(f"symlink loop detected at {lexical}") + seen.add(lexical) + raw_target = Path(os.readlink(cursor)) + target = raw_target if raw_target.is_absolute() else cursor.parent / raw_target + current = Path(os.path.abspath(target.joinpath(*parts[index + 1 :]))) + break + else: + return current + raise RuntimeError(f"symlink resolution exceeded {max_symlinks} links") + + +def _check_symlink_chain(root: Path, relative: str) -> Violation | None: + cursor = root + for part in PurePosixPath(relative).parts: + cursor = cursor / part + if not cursor.exists() and not cursor.is_symlink(): + return None + if cursor.is_symlink(): + try: + target = _resolve_bounded(cursor) + except (OSError, RuntimeError) as exc: + return Violation( + "unmeasured_state", + relative, + f"cannot resolve symlink component {cursor}: {exc}", + ) + if not _is_within(target, root): + return Violation( + "symlink_escape", + relative, + f"symlink component {cursor} resolves outside the Workspace root", + ) + return Violation( + "structural_symlink", + relative, + f"canonical Workspace entries must be physical, not symlinks ({cursor})", + ) + return None + + +def _expected_direct_children(rows: Sequence[Row]) -> dict[str, set[str]]: + expected: dict[str, set[str]] = {"": set()} + for row in rows: + pure = PurePosixPath(row.path) + parent = "" if str(pure.parent) == "." else pure.parent.as_posix() + expected.setdefault(parent, set()).add(pure.name) + return expected + + +def _discover_tree( + root: Path, + rows: Sequence[Row], + *, + max_scan_entries: int, + now: datetime, +) -> tuple[list[Violation], list[str], list[dict[str, Any]], bool]: + violations: list[Violation] = [] + repositories: list[str] = [] + ephemeral_roots: list[dict[str, Any]] = [] + truncated = False + budget = ScanBudget(max_scan_entries) + fetch_budget = RepositoryFetchBudget(REPOSITORY_FETCH_BUDGET_SECONDS) + expected_children = _expected_direct_children(rows) + + if not root.is_dir(): + return ( + [Violation("workspace_missing", ".", f"Workspace root is absent: {root}")], + repositories, + ephemeral_roots, + False, + ) + if root.is_symlink(): + violations.append(Violation("workspace_symlink", ".", "Workspace root must be a physical directory")) + + structural_paths = [""] + [row.path for row in rows if row.kind == "structural"] + for parent_rel in structural_paths: + parent = root if not parent_rel else root / parent_rel + if not parent.is_dir() or parent.is_symlink(): + continue + expected = expected_children.get(parent_rel, set()) + try: + actual_entries = parent.iterdir() + except OSError as exc: + violations.append(Violation("unmeasured_state", parent_rel or ".", f"cannot list directory: {exc}")) + continue + try: + for child in actual_entries: + if not budget.consume(): + violations.append( + Violation( + "unmeasured_state", + parent_rel or ".", + f"shared scan budget exhausted after {budget.consumed} entries", + ) + ) + truncated = True + break + if child.name not in expected: + rel = child.relative_to(root).as_posix() + violations.append( + Violation( + "undeclared_entry", + rel, + "entry has no Workspace manifest row", + ) + ) + except OSError as exc: + violations.append(Violation("unmeasured_state", parent_rel or ".", f"cannot list directory: {exc}")) + if truncated: + break + + for row in rows: + candidate = root / row.path + escape = _check_symlink_chain(root, row.path) + if escape is not None: + violations.append(escape) + continue + if not candidate.exists(): + violations.append(Violation("declared_entry_missing", row.path, f"declared {row.kind} is absent")) + continue + if row.kind in DIRECTORY_KINDS: + if not candidate.is_dir(): + violations.append(Violation("wrong_entry_type", row.path, f"{row.kind} must be a directory")) + continue + elif row.kind == "index" and not candidate.is_file(): + violations.append(Violation("wrong_entry_type", row.path, "index must be a regular file")) + + if row.kind == "repository": + repositories.append(row.path) + violations.extend(_audit_repository(candidate, row, fetch_budget=fetch_budget)) + nested, nested_truncated = _discover_nested_repositories( + candidate, + row.path, + budget=budget, + ) + violations.extend(nested) + if nested_truncated: + truncated = True + elif row.kind == "ephemeral": + ephemeral_violations, receipt, ephemeral_truncated = _audit_ephemeral_root( + candidate, + row, + now=now, + budget=budget, + ) + violations.extend(ephemeral_violations) + ephemeral_roots.append(receipt) + if ephemeral_truncated: + truncated = True + + return violations, sorted(repositories), ephemeral_roots, truncated + + +def _registered_submodules(repo: Path) -> set[Path]: + gitmodules = repo / ".gitmodules" + if not gitmodules.is_file(): + return set() + proc = _run_git(repo, "config", "-f", str(gitmodules), "--get-regexp", r"^submodule\..*\.path$") + if proc.returncode not in {0, 1}: + return set() + result: set[Path] = set() + for line in proc.stdout.splitlines(): + _, _, value = line.partition(" ") + if value.strip(): + result.add((repo / value.strip()).resolve(strict=False)) + return result + + +def _discover_nested_repositories( + repo: Path, + manifest_path: str, + *, + budget: ScanBudget, +) -> tuple[list[Violation], bool]: + """Find competing checkouts inside a declared repository. + + Registered Git submodules are Git-owned interiors and therefore valid. + Ad-hoc nested clones and in-repository worktrees are competing physical + homes and must move to ``runtime/worktrees``. + """ + + if budget.remaining <= 0: + return [ + Violation( + "unmeasured_state", + manifest_path, + "shared scan budget left no entries for recursive repository discovery", + ) + ], True + registered_submodules = _registered_submodules(repo) + violations: list[Violation] = [] + for dirpath, dirnames, filenames in os.walk(repo, followlinks=False): + current = Path(dirpath) + if current == repo and ".git" in dirnames: + dirnames.remove(".git") + debit = len(dirnames) + len(filenames) + if not budget.consume(debit): + return ( + violations + + [ + Violation( + "unmeasured_state", + manifest_path, + f"recursive repository scan exhausted the shared budget after {budget.consumed} entries", + ) + ], + True, + ) + if current == repo: + continue + has_git = ".git" in dirnames or ".git" in filenames + if has_git: + resolved = current.resolve(strict=False) + if resolved not in registered_submodules: + rel = current.relative_to(repo).as_posix() + violations.append( + Violation( + "undeclared_nested_repository", + f"{manifest_path}/{rel}", + "nested checkout is not a registered Git submodule; use the canonical " + "Workspace row or runtime/worktrees", + ) + ) + dirnames[:] = [] + continue + dirnames[:] = [name for name in dirnames if name not in {".git", "__pycache__", "node_modules"}] + return violations, False + + +def _audit_ephemeral_root( + root: Path, + row: Row, + *, + now: datetime, + budget: ScanBudget, +) -> tuple[list[Violation], dict[str, Any], bool]: + """Measure lifecycle units and emit a bounded reaper receipt. + + ``runtime/worktrees`` has one collision-resistant repository namespace + level. Its worktree slugs, not the namespace directories, are the + lifecycle units whose expiry the reaper owns. + """ + + expires_after = int(row.raw["expires_after"]) + reaper = str(row.raw["reaper"]) + entry_count = 0 + expired_names: list[str] = [] + oldest_age_seconds = 0 + violations: list[Violation] = [] + truncated = False + namespace_count = 0 + units: list[tuple[str, Path]] = [] + try: + for entry in root.iterdir(): + if not budget.consume(): + violations.append( + Violation( + "unmeasured_state", + row.path, + f"ephemeral scan exhausted the shared budget after {budget.consumed} entries", + ) + ) + truncated = True + break + if row.path == "runtime/worktrees" and entry.is_symlink(): + violations.append( + Violation( + "ephemeral_nonphysical_entry", + f"{row.path}/{entry.name}", + "repository namespace must be a physical directory, not a symlink", + ) + ) + units.append((entry.name, entry)) + elif row.path == "runtime/worktrees" and not entry.is_dir(): + violations.append( + Violation( + "ephemeral_nonphysical_entry", + f"{row.path}/{entry.name}", + "repository namespace must be a physical directory", + ) + ) + units.append((entry.name, entry)) + elif row.path == "runtime/worktrees" and entry.is_dir(): + namespace_count += 1 + namespace_unit_count = 0 + namespace_measured = True + try: + for unit in entry.iterdir(): + if not budget.consume(): + violations.append( + Violation( + "unmeasured_state", + f"{row.path}/{entry.name}", + f"ephemeral scan exhausted the shared budget after {budget.consumed} entries", + ) + ) + truncated = True + namespace_measured = False + break + if unit.is_symlink(): + violations.append( + Violation( + "ephemeral_nonphysical_entry", + f"{row.path}/{entry.name}/{unit.name}", + "worktree lifecycle unit must be a physical directory, not a symlink", + ) + ) + elif not unit.is_dir(): + violations.append( + Violation( + "ephemeral_nonphysical_entry", + f"{row.path}/{entry.name}/{unit.name}", + "worktree lifecycle unit must be a physical directory", + ) + ) + units.append((f"{entry.name}/{unit.name}", unit)) + namespace_unit_count += 1 + except OSError as exc: + namespace_measured = False + violations.append( + Violation( + "unmeasured_state", + f"{row.path}/{entry.name}", + f"cannot list worktree namespace: {exc}", + ) + ) + if namespace_measured and namespace_unit_count == 0: + violations.append( + Violation( + "ephemeral_empty_namespace", + f"{row.path}/{entry.name}", + "empty repository namespace is accidental state, not a lifecycle unit", + ) + ) + if truncated: + break + else: + units.append((entry.name, entry)) + for unit_name, entry in units: + entry_count += 1 + try: + age_seconds = max(0, int(now.timestamp() - entry.lstat().st_mtime)) + except OSError as exc: + violations.append( + Violation( + "unmeasured_state", + f"{row.path}/{unit_name}", + f"cannot stat ephemeral lifecycle unit: {exc}", + ) + ) + continue + oldest_age_seconds = max(oldest_age_seconds, age_seconds) + if age_seconds > expires_after: + expired_names.append(unit_name) + except OSError as exc: + violations.append(Violation("unmeasured_state", row.path, f"cannot list ephemeral root: {exc}")) + + if expired_names: + sample = ", ".join(sorted(expired_names)[:5]) + violations.append( + Violation( + "ephemeral_entries_expired", + row.path, + f"{len(expired_names)} lifecycle unit(s) exceed expires_after={expires_after}; " + f"reaper={reaper!r}; sample={sample}", + ) + ) + receipt = { + "path": row.path, + "reaper": reaper, + "expires_after": expires_after, + "namespace_count": namespace_count, + "entry_count": entry_count, + "expired_entry_count": len(expired_names), + "oldest_age_seconds": oldest_age_seconds, + } + return violations, receipt, truncated + + +def _audit_repository( + repo: Path, + row: Row, + *, + fetch_budget: RepositoryFetchBudget, +) -> list[Violation]: + violations: list[Violation] = [] + if not (repo / ".git").exists(): + return [Violation("repository_missing_git", row.path, "declared repository is not a Git worktree")] + top = _run_git(repo, "rev-parse", "--show-toplevel") + if top.returncode != 0 or Path(top.stdout.strip()).resolve(strict=False) != repo.resolve(strict=False): + violations.append(Violation("repository_wrong_root", row.path, "path is not the Git worktree root")) + return violations + + live_remote_verified = False + origin = _run_git(repo, "remote", "get-url", "origin") + if origin.returncode != 0: + violations.append(Violation("repository_remote_missing", row.path, "origin remote is unavailable")) + else: + try: + actual_remote = canonical_remote(origin.stdout.strip()) + expected_remote = canonical_remote(str(row.raw["remote"])) + if actual_remote != expected_remote: + violations.append( + Violation( + "repository_remote_mismatch", + row.path, + f"origin is {actual_remote}, manifest requires {expected_remote}", + ) + ) + else: + fetch_timeout = fetch_budget.timeout() + if fetch_timeout is None: + violations.append( + Violation( + "repository_unmeasured", + row.path, + "aggregate repository fetch deadline exhausted; " + "custody and preservation cannot be accepted", + ) + ) + else: + fetch = _run_git( + repo, + "fetch", + "--prune", + "--no-tags", + "origin", + "+refs/heads/*:refs/remotes/origin/*", + timeout=fetch_timeout, + ) + if fetch.returncode != 0: + violations.append( + Violation( + "repository_unmeasured", + row.path, + "live origin fetch failed within the aggregate deadline; " + "custody and preservation cannot be accepted", + ) + ) + else: + live_remote_verified = True + except ManifestError as exc: + violations.append(Violation("repository_remote_invalid", row.path, str(exc))) + + custody_ref = str(row.raw["custody_ref"]) + if not custody_ref.startswith("refs/remotes/origin/"): + violations.append( + Violation( + "repository_custody_invalid", + row.path, + "custody_ref must name the freshly fetched refs/remotes/origin namespace", + ) + ) + elif live_remote_verified: + custody = _run_git(repo, "rev-parse", "--verify", "--quiet", f"{custody_ref}^{{commit}}") + if custody.returncode != 0: + violations.append( + Violation("repository_custody_missing", row.path, f"live custody ref is absent: {custody_ref}") + ) + + status = _run_git(repo, "status", "--porcelain=v1", "--untracked-files=all") + if status.returncode != 0: + violations.append(Violation("repository_unmeasured", row.path, "git status failed")) + elif status.stdout.strip(): + violations.append(Violation("repository_dirty", row.path, "repository has tracked or untracked changes")) + + ignored_status = _run_git( + repo, + "status", + "--porcelain=v1", + "-z", + "--ignored=matching", + "--untracked-files=all", + ) + if ignored_status.returncode != 0: + violations.append(Violation("repository_unmeasured", row.path, "cannot enumerate ignored entries")) + else: + ignored_entries = ignored_entries_from_porcelain(ignored_status.stdout) + uncustodied = sorted(path for path in ignored_entries if not ignored_entry_is_regenerable(path)) + if uncustodied: + sample = ", ".join(uncustodied[:10]) + violations.append( + Violation( + "repository_uncustodied_ignored", + row.path, + f"{len(uncustodied)} ignored entry or subtree root(s) lack explicit " + f"regenerable evidence or custody: {sample}", + ) + ) + + if live_remote_verified: + remote_refs_result = _run_git( + repo, + "for-each-ref", + "--format=%(refname)", + "refs/remotes/origin", + ) + if remote_refs_result.returncode != 0: + violations.append( + Violation("repository_unmeasured", row.path, "cannot enumerate freshly fetched origin refs") + ) + else: + remote_refs = [ref for ref in remote_refs_result.stdout.splitlines() if ref and not ref.endswith("/HEAD")] + head_contains = _run_git( + repo, + "for-each-ref", + "--format=%(refname)", + "--contains", + "HEAD", + "refs/remotes/origin", + ) + if head_contains.returncode != 0 or not any( + ref and not ref.endswith("/HEAD") for ref in head_contains.stdout.splitlines() + ): + violations.append( + Violation( + "repository_unpreserved", + row.path, + "exact HEAD is not reachable from any freshly fetched live origin ref", + ) + ) + violations.extend(_audit_local_branch_custody(repo, row.path, remote_refs)) + violations.extend(_audit_stash_custody(repo, row.path, remote_refs)) + return violations + + +def _unreachable_commit_count(repo: Path, commitish: str, remote_refs: Sequence[str]) -> int | None: + args = ["rev-list", "--count", commitish] + if remote_refs: + args.extend(["--not", *remote_refs]) + result = _run_git(repo, *args) + if result.returncode != 0: + return None + try: + return int(result.stdout.strip()) + except ValueError: + return None + + +def _audit_local_branch_custody( + repo: Path, + manifest_path: str, + remote_refs: Sequence[str], +) -> list[Violation]: + branches = _run_git(repo, "for-each-ref", "--format=%(refname)", "refs/heads") + if branches.returncode != 0: + return [Violation("repository_unmeasured", manifest_path, "cannot enumerate local branches")] + unpreserved: list[tuple[str, int]] = [] + for ref in branches.stdout.splitlines(): + if not ref: + continue + count = _unreachable_commit_count(repo, ref, remote_refs) + if count is None: + return [ + Violation( + "repository_unmeasured", + manifest_path, + f"cannot measure local branch custody for {ref.removeprefix('refs/heads/')}", + ) + ] + if count: + unpreserved.append((ref.removeprefix("refs/heads/"), count)) + if not unpreserved: + return [] + summary = ", ".join(f"{name}={count}" for name, count in sorted(unpreserved)[:10]) + return [ + Violation( + "repository_unpreserved_branches", + manifest_path, + f"{len(unpreserved)} local branch(es) contain commits absent from live origin refs: {summary}", + ) + ] + + +def _audit_stash_custody( + repo: Path, + manifest_path: str, + remote_refs: Sequence[str], +) -> list[Violation]: + stash_ref = _run_git(repo, "rev-parse", "--verify", "--quiet", "refs/stash") + if stash_ref.returncode == 1 and not stash_ref.stderr.strip(): + return [] + if stash_ref.returncode != 0: + return [Violation("repository_unmeasured", manifest_path, "cannot inspect refs/stash")] + reflog = _run_git(repo, "reflog", "show", "--format=%H", "refs/stash") + if reflog.returncode != 0: + return [Violation("repository_unmeasured", manifest_path, "cannot enumerate stash custody")] + stash_commits = sorted({commit for commit in reflog.stdout.splitlines() if commit}) + uncustodied = 0 + for commit in stash_commits: + count = _unreachable_commit_count(repo, commit, remote_refs) + if count is None: + return [Violation("repository_unmeasured", manifest_path, "cannot measure stash custody")] + if count: + uncustodied += 1 + if not uncustodied: + return [] + return [ + Violation( + "repository_uncustodied_stashes", + manifest_path, + f"{uncustodied} stash snapshot(s) are not reachable from freshly fetched live origin refs", + ) + ] + + +def _resolve_reference( + ref: str, + *, + manifest_path: Path, + workspace_root: Path, + rows: Sequence[Row], +) -> tuple[Path, str | None]: + path_text, separator, fragment = ref.partition("#") + if path_text.startswith("workspace://"): + relative = _safe_relative_path(path_text.removeprefix("workspace://"), field="workspace reference") + path = workspace_root / relative + if not path.exists(): + # During migration, references may resolve through the declared + # repository's unique legacy home. The canonical row still remains + # absent and therefore red until the physical move completes. + candidates: list[Path] = [] + for owner in sorted(rows, key=lambda item: len(PurePosixPath(item.path).parts), reverse=True): + if owner.kind != "repository": + continue + prefix = owner.path + "/" + if relative != owner.path and not relative.startswith(prefix): + continue + suffix = relative.removeprefix(owner.path).lstrip("/") + for legacy in owner.raw.get("legacy_paths") or []: + legacy_rel = _safe_relative_path(legacy, field=f"{owner.path} legacy path") + candidate = workspace_root / legacy_rel + if suffix: + candidate /= suffix + if candidate.exists(): + candidates.append(candidate) + break + unique = {candidate.resolve(strict=False) for candidate in candidates} + if len(unique) == 1: + path = unique.pop() + elif path_text.startswith("manifest://"): + relative = _safe_relative_path(path_text.removeprefix("manifest://"), field="manifest reference") + path = manifest_path.parent / relative + elif path_text.startswith("file://"): + path = Path(urlparse(path_text).path) + else: + path = Path(path_text) + if not path.is_absolute(): + path = manifest_path.parent / path + return Path(os.path.abspath(path.expanduser())), fragment if separator else None + + +def _copy_count(value: object) -> int: + if isinstance(value, bool): + return 0 + if isinstance(value, int): + return value + if isinstance(value, str): + try: + return int(value) + except ValueError: + return 0 + return 0 + + +def _valid_custody_identity(row: Mapping[str, Any]) -> bool: + return all( + isinstance(row.get(field), str) and _SHA256_RE.fullmatch(str(row[field])) is not None + for field in _CUSTODY_IDENTITY_FIELDS + ) + + +def _audit_private_custody( + manifest_path: Path, + workspace_root: Path, + rows: Sequence[Row], +) -> tuple[list[Violation], list[dict[str, Any]]]: + violations: list[Violation] = [] + receipts: list[dict[str, Any]] = [] + reader = CustodyLedgerReader( + max_bytes=CUSTODY_LEDGER_MAX_BYTES, + max_rows=CUSTODY_LEDGER_MAX_ROWS, + deadline_seconds=CUSTODY_LEDGER_DEADLINE_SECONDS, + ) + for row in rows: + if row.kind != "private": + continue + inventory, inventory_fragment = _resolve_reference( + str(row.raw["sealed_inventory_ref"]), + manifest_path=manifest_path, + workspace_root=workspace_root, + rows=rows, + ) + receipt_path, fragment = _resolve_reference( + str(row.raw["restoration_receipt_ref"]), + manifest_path=manifest_path, + workspace_root=workspace_root, + rows=rows, + ) + label = str(row.raw["custody_label"]) + inventory_result = reader.read_rows( + inventory, + collection_keys=("sealed_inventories", "inventories"), + ) + receipt_result = reader.read_rows(receipt_path) + row_inspection_errors: dict[Path, str] = {} + for source_path, result in ( + (inventory, inventory_result), + (receipt_path, receipt_result), + ): + if result.inspection_error: + row_inspection_errors[source_path.resolve(strict=False)] = result.inspection_error + for source_path, error in row_inspection_errors.items(): + violations.append( + Violation( + "unmeasured_state", + row.path, + f"private custody ledger inspection failed closed: {source_path.name}: {error}", + ) + ) + + inventory_rows = inventory_result.rows + inventory_label_matches = inventory_fragment in {None, label} + matching_inventories = [ + candidate + for candidate in inventory_rows + if str(candidate.get("label", candidate.get("custody_label", ""))) == label + and candidate.get("sealed") is True + ] + # Inventories are append-only custody history. The last matching seal is + # the current identity; an older receipt must not authorize a later seal. + sealed_inventory = matching_inventories[-1] if matching_inventories else None + inventory_verified = ( + inventory_label_matches and sealed_inventory is not None and _valid_custody_identity(sealed_inventory) + ) + sealed_identity = ( + {field: str(sealed_inventory[field]) for field in _CUSTODY_IDENTITY_FIELDS} + if inventory_verified and sealed_inventory is not None + else None + ) + if not inventory_result.available: + violations.append( + Violation("private_inventory_missing", row.path, "sealed inventory reference is unavailable") + ) + elif inventory_result.inspection_error is None and not inventory_verified: + violations.append( + Violation( + "private_inventory_unverified", + row.path, + "current sealed inventory lacks a matching label or lowercase SHA-256 " + "inventory/plan/content identity", + ) + ) + receipt_label_matches = fragment in {None, label} + candidates = [candidate for candidate in receipt_result.rows if str(candidate.get("label", "")) == label] + valid = next( + ( + candidate + for candidate in candidates + if candidate.get("restoration_passed") is True + and _copy_count(candidate.get("copy_count", 0)) >= 2 + and candidate.get("independent_physical_devices") is True + and _valid_custody_identity(candidate) + and sealed_identity is not None + and all(candidate.get(field) == sealed_identity[field] for field in _CUSTODY_IDENTITY_FIELDS) + ), + None, + ) + restoration_verified = receipt_label_matches and valid is not None + receipts.append( + { + "path": row.path, + "custody_label": label, + "inventory_available": inventory_result.available, + "sealed_inventory_verified": inventory_verified, + "restoration_receipt_available": receipt_result.available, + "restoration_identity_verified": valid is not None, + "restoration_verified": restoration_verified, + "custody_verified": inventory_verified and restoration_verified, + "inspection_complete": not row_inspection_errors, + } + ) + if not restoration_verified and not row_inspection_errors: + violations.append( + Violation( + "private_restoration_unverified", + row.path, + "no matching two-copy restoration receipt is bound to the current " + "sealed inventory and inventory/plan/content identity", + ) + ) + if reader.inspection_errors: + # Aggregate exhaustion means the court did not measure the complete private-custody + # evidence set. Earlier valid rows remain useful diagnostics, but cannot authorize custody. + for receipt in receipts: + receipt["inspection_complete"] = False + receipt["custody_verified"] = False + return violations, receipts + + +def _compatibility_violations( + data: Mapping[str, Any], + root: Path, + *, + now: datetime, + active_cwds: Sequence[Path], +) -> tuple[list[Violation], list[dict[str, Any]]]: + violations: list[Violation] = [] + report: list[dict[str, Any]] = [] + migration = data.get("migration") or {} + for raw in migration.get("compatibility_links") or []: + rel = str(raw["path"]) + target_rel = str(raw["target"]) + path = root / rel + target = root / target_rel + expires = datetime.fromisoformat(str(raw["expires_at"]).replace("Z", "+00:00")) + if expires.tzinfo is None: + expires = expires.replace(tzinfo=UTC) + # Compare lexical paths. Resolving the compatibility symlink makes its + # prefix indistinguishable from the canonical target and falsely + # classifies canonical consumers as legacy users. + active = sorted(str(cwd) for cwd in active_cwds if _is_within(cwd, path)) + # Once the doorway itself is a symlink, the kernel retains only the + # physical cwd inode. Tools such as macOS lsof therefore report the + # canonical target for both direct consumers and processes that entered + # through the legacy doorway. Do not call those consumers legacy, but + # also do not silently accept removal while their lexical entry path is + # unknowable. + ambiguous = sorted( + str(cwd) + for cwd in active_cwds + if path.is_symlink() and _is_within(cwd, target) and not _is_within(cwd, path) + ) + present = path.exists() or path.is_symlink() + resolution_error: str | None = None + correct = False + if path.is_symlink(): + try: + correct = _resolve_bounded(path) == _resolve_bounded(target) + except (OSError, RuntimeError) as exc: + resolution_error = str(exc) + report.append( + { + "path": rel, + "target": target_rel, + "present": present, + "correct": correct, + "expired": now.astimezone(UTC) >= expires.astimezone(UTC), + "active_cwd_count": len(active), + "ambiguous_cwd_count": len(ambiguous), + } + ) + if path.exists() or path.is_symlink(): + violations.append( + Violation( + "compatibility_link_unresolved", + rel, + "temporary compatibility doorway remains; final architecture requires zero", + ) + ) + if not correct: + violations.append(Violation("compatibility_link_mismatch", rel, f"link must resolve to {target_rel}")) + if resolution_error is not None: + violations.append( + Violation( + "unmeasured_state", + rel, + f"compatibility symlink resolution failed closed: {resolution_error}", + ) + ) + if active: + violations.append( + Violation( + "active_legacy_path", + rel, + f"{len(active)} active process cwd(s) still depend on this compatibility path", + ) + ) + if ambiguous: + violations.append( + Violation( + "unmeasured_state", + rel, + f"{len(ambiguous)} canonical-target cwd(s) have no observable lexical entry path; " + "compatibility removal must wait until they close", + ) + ) + if present and now.astimezone(UTC) >= expires.astimezone(UTC): + violations.append(Violation("compatibility_link_expired", rel, "compatibility link has expired")) + return violations, report + + +def collect_active_cwds() -> list[Path]: + command = ["lsof", "-a", "-d", "cwd", "-F", "n"] + try: + proc = subprocess.run( + command, + capture_output=True, + text=True, + check=False, + timeout=LSOF_TIMEOUT_SECONDS, + ) + except (OSError, subprocess.SubprocessError) as exc: + raise ActiveCwdDiscoveryError(f"lsof unavailable: {exc}") from exc + if proc.returncode != 0: + raise ActiveCwdDiscoveryError(f"lsof exited {proc.returncode}") + raw_paths = [Path(line[1:]) for line in proc.stdout.splitlines() if line.startswith("n/")] + if not raw_paths: + raise ActiveCwdDiscoveryError("lsof returned no absolute CWD records") + return _normalize_active_cwds(raw_paths) + + +def _normalize_active_cwds(values: Sequence[Path]) -> list[Path]: + result: set[Path] = set() + for item in values: + expanded = item.expanduser() + if not expanded.is_absolute(): + raise ManifestError("active CWD entries must be absolute") + # abspath/normpath removes lexical "." and ".." without dereferencing a + # compatibility link. That lexical prefix is the dependency evidence. + result.add(Path(os.path.abspath(expanded))) + return sorted(result, key=str) + + +def load_active_cwds(path: Path) -> list[Path]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise ManifestError(f"active CWD fixture is invalid: {path}: {exc}") from exc + if not isinstance(value, list) or not all(_nonempty_string(item) for item in value): + raise ManifestError("active CWD fixture must be a JSON array of absolute paths") + raw_paths = [Path(str(item)).expanduser() for item in value] + if any(not item.is_absolute() for item in raw_paths): + raise ManifestError("active CWD fixture entries must be absolute") + return _normalize_active_cwds(raw_paths) + + +def audit( + manifest_path: Path, + *, + workspace_root: Path | None = None, + active_cwds: Sequence[Path] | None = None, + now: datetime | None = None, +) -> dict[str, Any]: + data, rows, manifest_bytes = load_manifest(manifest_path) + root = resolve_workspace_root(data, workspace_root) + limits = data["limits"] + if not isinstance(limits, dict): + raise ManifestError("limits must be a mapping") + max_scan_entries = limits["max_scan_entries"] + if not isinstance(max_scan_entries, int) or isinstance(max_scan_entries, bool) or max_scan_entries <= 0: + raise ManifestError("limits.max_scan_entries must be a positive integer") + max_violations = limits["max_violations"] + max_unmeasured = limits["max_unmeasured"] + max_compatibility_links = limits["max_compatibility_links"] + for key, value in ( + ("max_violations", max_violations), + ("max_unmeasured", max_unmeasured), + ("max_compatibility_links", max_compatibility_links), + ): + if not isinstance(value, int) or isinstance(value, bool) or value < 0: + raise ManifestError(f"limits.{key} must be a non-negative integer") + + audit_now = now or datetime.now(UTC) + if audit_now.tzinfo is None: + audit_now = audit_now.replace(tzinfo=UTC) + violations, repositories, ephemeral_roots, truncated = _discover_tree( + root, + rows, + max_scan_entries=max_scan_entries, + now=audit_now, + ) + custody_violations, custody = _audit_private_custody(manifest_path, root, rows) + violations.extend(custody_violations) + if active_cwds is None: + try: + measured_cwds = collect_active_cwds() + except ActiveCwdDiscoveryError as exc: + measured_cwds = [] + violations.append( + Violation( + "unmeasured_state", + ".", + f"active CWD discovery failed closed: {exc}", + ) + ) + else: + measured_cwds = _normalize_active_cwds(list(active_cwds)) + compat_violations, compatibility = _compatibility_violations( + data, + root, + now=audit_now, + active_cwds=measured_cwds, + ) + violations.extend(compat_violations) + + unmeasured_count = sum(v.code in {"unmeasured_state", "repository_unmeasured"} for v in violations) + compatibility_count = sum(1 for row in compatibility if row["present"]) + if len(violations) > max_violations: + violations.append( + Violation( + "residue_cap_breached", + ".", + f"violations={len(violations)} exceeds max_violations={max_violations}", + ) + ) + if unmeasured_count > max_unmeasured: + violations.append( + Violation( + "unmeasured_cap_breached", + ".", + f"unmeasured={unmeasured_count} exceeds max_unmeasured={max_unmeasured}", + ) + ) + if compatibility_count > max_compatibility_links: + violations.append( + Violation( + "compatibility_cap_breached", + ".", + f"compatibility_links={compatibility_count} exceeds max_compatibility_links={max_compatibility_links}", + ) + ) + + ordered = sorted(violations, key=lambda item: (item.path, item.code, item.message)) + root_text = str(root) + try: + root_stat = root.stat() + root_device: int | None = root_stat.st_dev + root_inode: int | None = root_stat.st_ino + except OSError: + root_device = None + root_inode = None + return { + "schema": REPORT_SCHEMA, + "ok": not ordered, + "manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(), + "workspace_root": str(root), + "workspace_root_identity": { + "lexical_sha256": hashlib.sha256(root_text.encode("utf-8")).hexdigest(), + "device": root_device, + "inode": root_inode, + }, + "counts": { + "manifest_rows": len(rows), + "repositories": len(repositories), + "private_roots": sum(row.kind == "private" for row in rows), + "compatibility_links": compatibility_count, + "violations": len(ordered), + "unmeasured": unmeasured_count, + }, + "scan_truncated": truncated, + "repositories": repositories, + "ephemeral_roots": ephemeral_roots, + "private_custody": custody, + "compatibility_links": compatibility, + "violations": [item.as_dict() for item in ordered], + } + + +def render_text(report: Mapping[str, Any]) -> str: + verdict = "OK" if report["ok"] else "FAIL" + counts = report["counts"] + lines = [ + f"substrate-convergence: {verdict}", + f" root: {report['workspace_root']}", + f" manifest: sha256:{report['manifest_sha256']}", + ( + " counts: " + f"rows={counts['manifest_rows']} repositories={counts['repositories']} " + f"private={counts['private_roots']} compatibility={counts['compatibility_links']} " + f"violations={counts['violations']} unmeasured={counts['unmeasured']}" + ), + ] + for violation in report["violations"]: + lines.append(f" [{violation['code']}] {violation['path']}: {violation['message']}") + return "\n".join(lines) diff --git a/cli/src/limen/substrate_paths.py b/cli/src/limen/substrate_paths.py new file mode 100644 index 000000000..e190f66a1 --- /dev/null +++ b/cli/src/limen/substrate_paths.py @@ -0,0 +1,102 @@ +"""Executable path-indirection contract for the literal Workspace substrate.""" + +from __future__ import annotations + +from pathlib import Path +import re + + +_SCAN_ROOTS = ( + "install.sh", + "scripts", + "cli/src", + "mcp/src", + "container", + "ianva", + "organs", + "apps", + "institutio/governance/parameters.yaml", + "pillars.yaml", + "his-hand-levers.json", +) +_SKIP_PARTS = { + ".git", + ".worktrees", + ".limen-private", + "__pycache__", + "docs", + "logs", + "tests", + "public-portal", +} +_TEXT_SUFFIXES = { + "", + ".fish", + ".html", + ".json", + ".plist", + ".py", + ".sh", + ".toml", + ".tsv", + ".yaml", + ".yml", + ".zsh", +} + +# Keep the disallowed spellings out of this source itself so the court can scan +# its own implementation without an exception. +_LEGACY_PATTERNS = ( + re.compile(r"(?:~|\$HOME|/Users/[^/]+)" + r"/Workspace/" + r"limen(?:/|\b)"), + re.compile(r"(?:~|\$HOME|/Users/[^/]+)" + r"/Workspace/" + r"domus-genoma(?:/|\b)"), + re.compile(r"(?:~|\$HOME|/Users/[^/]+)" + r"/Workspace/" + r"4444J99/portvs(?:/|\b)"), + re.compile(r"Path\.home\(\)\s*/\s*['\"]Workspace['\"]\s*/\s*['\"]" + r"limen" + r"['\"]"), + re.compile(r"Path\((?:HOME|home)\)\s*/\s*['\"]Workspace['\"]\s*/\s*['\"]" + r"limen" + r"['\"]"), + re.compile(r"\{(?:HOME|home)\}" + r"/Workspace/" + r"limen(?:/|\b)"), + re.compile( + r"os\.path\.join\(\s*(?:HOME|home)\s*,\s*['\"]Workspace" + r"(?:/limen['\"]|['\"]\s*,\s*['\"]limen['\"])" + ), + re.compile(r"(?:HOME|home)\s*\+\s*['\"]" + r"/Workspace/limen" + r"(?:/|['\"])"), +) + + +def _candidate_files(root: Path) -> list[Path]: + candidates: set[Path] = set() + for raw in _SCAN_ROOTS: + entry = root / raw + if entry.is_file(): + candidates.add(entry) + continue + if not entry.is_dir(): + continue + for path in entry.rglob("*"): + relative = path.relative_to(root) + if not path.is_file() or any(part in _SKIP_PARTS for part in relative.parts): + continue + if path.suffix.lower() in _TEXT_SUFFIXES: + candidates.add(path) + return sorted(candidates) + + +def find_legacy_references(root: Path) -> list[dict[str, object]]: + """Return executable/config references that bypass the canonical root contract.""" + + findings: list[dict[str, object]] = [] + for path in _candidate_files(root): + try: + lines = path.read_text(encoding="utf-8").splitlines() + except UnicodeDecodeError: + continue + for line_number, line in enumerate(lines, start=1): + for pattern in _LEGACY_PATTERNS: + match = pattern.search(line) + if match: + findings.append( + { + "path": path.relative_to(root).as_posix(), + "line": line_number, + "reference": match.group(0), + } + ) + break + return findings diff --git a/cli/src/limen/work_loan_journal.py b/cli/src/limen/work_loan_journal.py index 784d59772..de4cfe144 100644 --- a/cli/src/limen/work_loan_journal.py +++ b/cli/src/limen/work_loan_journal.py @@ -703,7 +703,9 @@ def operation(events: list[WorkLoanJournalEventV1]) -> None: def default_store(root: Path | None = None) -> WorkLoanJournalStore: - owner = root or Path(os.environ.get("LIMEN_ROOT", Path.home() / "Workspace" / "limen")) + owner = root or Path( + os.environ.get("LIMEN_ROOT", Path.home() / "Workspace" / "library" / "engine" / "organvm" / "limen") + ) return WorkLoanJournalStore(owner / "logs" / "work-loan-journal.jsonl") diff --git a/cli/src/limen/worktree_abandonment.py b/cli/src/limen/worktree_abandonment.py index a246dd63f..a9c0c0dc2 100644 --- a/cli/src/limen/worktree_abandonment.py +++ b/cli/src/limen/worktree_abandonment.py @@ -219,6 +219,70 @@ def _default_cwd_owner_probe(target: Path) -> int | None: return None +def _normalized_absolute_path(path: Path, *, label: str) -> Path: + expanded = path.expanduser() + if not expanded.is_absolute(): + raise ValueError(f"{label}-must-be-absolute") + return Path(os.path.abspath(expanded)) + + +def _require_physical_directory_chain(path: Path, *, allow_missing: bool) -> None: + """Reject symlink and non-directory components without following either.""" + + current = Path(path.anchor) + for part in path.parts[1:]: + current /= part + try: + info = current.lstat() + except FileNotFoundError: + if allow_missing: + continue + raise ValueError(f"quarantine-directory-missing:{current}") from None + except OSError as exc: + raise ValueError(f"quarantine-directory-unmeasured:{current}:{type(exc).__name__}") from exc + if stat.S_ISLNK(info.st_mode): + raise ValueError(f"quarantine-directory-symlink:{current}") + if not stat.S_ISDIR(info.st_mode): + raise ValueError(f"quarantine-directory-not-directory:{current}") + + +def xdg_data_home() -> Path: + """Return the normalized physical XDG data root used for recoverable payloads.""" + + configured = os.environ.get("XDG_DATA_HOME") + candidate = Path(configured).expanduser() if configured else Path("~/.local/share").expanduser() + candidate = _normalized_absolute_path(candidate, label="xdg-data-home") + _require_physical_directory_chain(candidate, allow_missing=True) + return candidate + + +def validated_quarantine_root(quarantine_root: Path, *, workspace_root: Path | None = None) -> Path: + """Confine quarantine payloads to the persistent XDG-owned Limen inventory.""" + + candidate = _normalized_absolute_path(quarantine_root, label="quarantine-root") + data_root = xdg_data_home() + limen_data_root = data_root / "limen" + + workspace = workspace_root + if workspace is None: + workspace = Path(os.environ.get("WORKSPACE_ROOT", str(Path("~/Workspace").expanduser()))) + workspace = _normalized_absolute_path(workspace, label="workspace-root") + _require_physical_directory_chain(candidate, allow_missing=True) + candidate_physical = candidate.resolve(strict=False) + workspace_physical = workspace.resolve(strict=False) + if ( + candidate == workspace + or candidate.is_relative_to(workspace) + or candidate_physical == workspace_physical + or candidate_physical.is_relative_to(workspace_physical) + ): + raise ValueError("quarantine-root-must-be-outside-workspace") + if candidate == limen_data_root or not candidate.is_relative_to(limen_data_root): + raise ValueError("quarantine-root-must-be-inside-xdg-limen") + + return candidate + + def detach_registered_worktree( superproject: Path, target: Path, @@ -325,7 +389,7 @@ def quarantine_path( if owner is not None: code = "owner-probe-unavailable" if owner == -1 else f"active-process-cwd:{owner}" raise RuntimeError(code) - proposed_quarantine_root = quarantine_root.expanduser().resolve(strict=False) + proposed_quarantine_root = validated_quarantine_root(quarantine_root) if ( proposed_quarantine_root == source or source in proposed_quarantine_root.parents @@ -333,7 +397,10 @@ def quarantine_path( ): raise RuntimeError("quarantine-source-destination-nesting") proposed_quarantine_root.mkdir(parents=True, exist_ok=True) + _require_physical_directory_chain(proposed_quarantine_root, allow_missing=False) quarantine_root = proposed_quarantine_root.resolve(strict=True) + if quarantine_root != proposed_quarantine_root: + raise RuntimeError("quarantine-root-physical-identity-mismatch") if quarantine_root == source or source in quarantine_root.parents or quarantine_root in source.parents: raise RuntimeError("quarantine-source-destination-nesting") if not _same_filesystem(source, quarantine_root): @@ -353,6 +420,11 @@ def quarantine_path( "source_device": source.stat().st_dev, "destination": str(destination), "recoverable": True, + "restoration_pointer": { + "from": str(destination), + "to": str(source), + "method": "same-filesystem-atomic-rename", + }, }, ) phase = "move" @@ -816,4 +888,6 @@ def remove_stable_zero_byte_lock( "purge_remote_proven_path", "quarantine_path", "remove_stable_zero_byte_lock", + "validated_quarantine_root", + "xdg_data_home", ] diff --git a/cli/src/limen/worktree_debt.py b/cli/src/limen/worktree_debt.py index 125cb957e..e3312bec6 100644 --- a/cli/src/limen/worktree_debt.py +++ b/cli/src/limen/worktree_debt.py @@ -6,9 +6,11 @@ import shutil import subprocess import time +from collections.abc import Mapping from pathlib import Path from typing import Any, TypedDict +from limen.worktree_receipts import matching_worktree_receipt from limen.worktree_roots import effective_worktree_root, iter_worktree_targets DEBT_REASONS = { @@ -185,28 +187,25 @@ def _int_env(name: str, default: int) -> int: return default -def _load_preservation_receipts(limen_root: Path) -> dict[str, dict[str, Any]]: +def _load_preservation_receipts(limen_root: Path) -> list[dict[str, Any]]: path = limen_root / "docs" / "worktree-preservation-receipts.json" try: data = json.loads(path.read_text(encoding="utf-8", errors="replace")) except (OSError, ValueError): - return {} + return [] if not isinstance(data, dict): - return {} - receipts: dict[str, dict[str, object]] = {} + return [] + receipts: list[dict[str, Any]] = [] items = data.get("receipts") if isinstance(items, list): for receipt in items: if not isinstance(receipt, dict): continue - root = receipt.get("root") - if isinstance(root, str) and root: - receipts[root] = receipt + receipts.append(receipt) return receipts -def _is_documented_residue(path: Path, preservation_receipts: dict[str, dict[str, object]]) -> bool: - receipt = preservation_receipts.get(path.name) +def _is_documented_residue(receipt: Mapping[str, object] | None) -> bool: if not receipt: return False lane = str(receipt.get("lane") or "") @@ -219,8 +218,7 @@ def _is_documented_residue(path: Path, preservation_receipts: dict[str, dict[str ) -def _is_remote_superseded(path: Path, preservation_receipts: dict[str, dict[str, object]]) -> bool: - receipt = preservation_receipts.get(path.name) +def _is_remote_superseded(receipt: Mapping[str, object] | None) -> bool: if not receipt: return False lane = str(receipt.get("lane") or "") @@ -228,9 +226,8 @@ def _is_remote_superseded(path: Path, preservation_receipts: dict[str, dict[str, return lane in REMOTE_SUPERSEDED_LANES or status in REMOTE_SUPERSEDED_STATUSES -def _is_remote_merged(path: Path, preservation_receipts: dict[str, dict[str, object]]) -> bool: +def _is_remote_merged(receipt: Mapping[str, object] | None) -> bool: """Match the accepted reaper's loss-free merged-PR receipt predicate exactly.""" - receipt = preservation_receipts.get(path.name) if not receipt: return False if receipt.get("private_receipt") or receipt.get("private_patch_sha256"): @@ -247,8 +244,7 @@ def _is_remote_merged(path: Path, preservation_receipts: dict[str, dict[str, obj ) -def _is_remote_pr_open(path: Path, preservation_receipts: dict[str, dict[str, object]]) -> bool: - receipt = preservation_receipts.get(path.name) +def _is_remote_pr_open(receipt: Mapping[str, object] | None) -> bool: if not receipt: return False lane = str(receipt.get("lane") or "") @@ -256,8 +252,7 @@ def _is_remote_pr_open(path: Path, preservation_receipts: dict[str, dict[str, ob return lane in REMOTE_PR_OPEN_LANES or status in REMOTE_PR_OPEN_STATUSES -def _is_owner_blocker(path: Path, preservation_receipts: dict[str, dict[str, object]]) -> bool: - receipt = preservation_receipts.get(path.name) +def _is_owner_blocker(receipt: Mapping[str, object] | None) -> bool: if not receipt: return False lane = str(receipt.get("lane") or "") @@ -295,7 +290,7 @@ def _classify( now: float, min_age_h: float, self_guard: set[Path], - preservation_receipts: dict[str, dict[str, object]], + preservation_receipts: object, ) -> str: try: resolved = path.resolve() @@ -305,13 +300,14 @@ def _classify( return "self/live-checkout" if _inside_agy_scratch_root(path): return "antigravity-scratch-managed" - if _is_documented_residue(path, preservation_receipts): + receipt = matching_worktree_receipt(path, preservation_receipts) + if _is_documented_residue(receipt): return "documented-residue" - if _is_remote_superseded(path, preservation_receipts): + if _is_remote_superseded(receipt): return "remote-superseded" - if _is_remote_pr_open(path, preservation_receipts): + if _is_remote_pr_open(receipt): return "remote-pr-open" - if _is_owner_blocker(path, preservation_receipts): + if _is_owner_blocker(receipt): return "owner-blocker" top = _git_toplevel(path) if top is None: @@ -327,7 +323,7 @@ def _classify( return "dirty" if not (path / ".git").is_file() and not _all_local_refs_remote(path): return "unpreserved-local-refs" - if _is_remote_merged(path, preservation_receipts): + if _is_remote_merged(receipt): return "receipt-remote-merged+clean+idle" head = _git(["rev-parse", "HEAD"], path).stdout.strip() patch_equivalent = _patch_equivalent_to_default(path) @@ -348,7 +344,18 @@ def worktree_debt_report(limen_root: Path | None = None, *, strict: bool = False that cannot be read or registered repositories whose Git inventory fails then raise instead of disappearing from an apparently empty report. """ - root = limen_root or Path(os.environ.get("LIMEN_ROOT", f"{os.environ.get('HOME', '/Users/4jp')}/Workspace/limen")) + workspace = Path( + os.environ.get( + "WORKSPACE_ROOT", + str(Path(os.environ.get("HOME", str(Path.home()))) / "Workspace"), + ) + ) + root = limen_root or Path( + os.environ.get( + "LIMEN_ROOT", + str(workspace / "library" / "engine" / "organvm" / "limen"), + ) + ) self_guard: set[Path] = set() for candidate in (root, Path.cwd()): try: diff --git a/cli/src/limen/worktree_layout.py b/cli/src/limen/worktree_layout.py new file mode 100644 index 000000000..0b33bb9f0 --- /dev/null +++ b/cli/src/limen/worktree_layout.py @@ -0,0 +1,151 @@ +"""Canonical placement for disposable Git worktrees. + +The literal Workspace tree owns worktrees as ephemeral lifecycle units. A +repository's remote identity (or, for a repository without a remote, its shared +Git common-directory identity) supplies a collision-resistant namespace so +repositories with the same basename never share a slug directory. +""" + +from __future__ import annotations + +import hashlib +import os +from pathlib import Path +import re +import subprocess +from urllib.parse import urlsplit + + +_SAFE_KEY_RE = re.compile(r"[^a-z0-9._-]+") +_SCP_REMOTE_RE = re.compile(r"^(?P[^@/:]+@)?(?P[^/:]+):(?P.+)$") +_SLUG_RE = re.compile(r"^[a-z0-9_][a-z0-9._-]*$") +_URL_SCHEMES = {"file", "git", "http", "https", "ssh"} + + +def canonical_workspace_root(configured: str | None = None) -> Path: + """Expand variables and ``~`` before binding a lexical absolute root.""" + + value = configured if configured is not None else os.environ.get("WORKSPACE_ROOT", "~/Workspace") + expanded = os.path.expanduser(os.path.expandvars(value)) + return Path(os.path.abspath(expanded)) + + +def _git(repo: Path, *args: str) -> str: + result = subprocess.run( + ["git", "-C", str(repo), *args], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return "" + return result.stdout.strip() + + +def _canonical_remote(remote: str, *, relative_to: Path) -> tuple[str, str]: + """Return a credential-free identity and a human-readable repository label.""" + + text = remote.strip().rstrip("/") + parsed = urlsplit(text) + scp_match = _SCP_REMOTE_RE.fullmatch(text) if parsed.scheme.lower() not in _URL_SCHEMES else None + if scp_match: + text = f"ssh://{scp_match.group('host')}/{scp_match.group('path')}" + + parsed = urlsplit(text) + if parsed.scheme: + if parsed.scheme == "file": + path = Path(parsed.path).expanduser() + if not path.is_absolute(): + path = relative_to / path + path = path.resolve(strict=False) + identity = f"file://{path}" + parts = path.parts + else: + hostname = (parsed.hostname or "").lower() + port = f":{parsed.port}" if parsed.port else "" + path_text = parsed.path.rstrip("/") + if path_text.endswith(".git"): + path_text = path_text[:-4] + # A repository's network identity is host + path, not the transport chosen by one + # checkout. HTTPS, SSH, Git, and SCP-style URLs for that same repository must share + # one runtime namespace. + identity = f"network://{hostname}{port}/{path_text.lstrip('/')}" + parts = tuple(part for part in path_text.split("/") if part) + else: + path = Path(text).expanduser() + if not path.is_absolute(): + path = relative_to / path + path = path.resolve(strict=False) + identity = f"file://{path}" + parts = path.parts + + label_parts = [part.removesuffix(".git") for part in parts[-2:]] + label = "--".join(label_parts) or "repository" + return identity, label + + +def repository_storage_key(repo: Path) -> str: + """Derive a stable, collision-resistant directory key for one repository.""" + + repository = repo.resolve(strict=True) + common = _git(repository, "rev-parse", "--path-format=absolute", "--git-common-dir") + common_path = Path(common).resolve(strict=False) if common else repository + common_owner = common_path.parent if common_path.name == ".git" else common_path + remote = _git(repository, "remote", "get-url", "origin") + if remote: + identity, label = _canonical_remote(remote, relative_to=common_owner) + identity = f"origin:{identity}" + else: + identity = f"common:{common_path}" + label = common_owner.name + + safe_label = _SAFE_KEY_RE.sub("-", label.lower()).strip("-._") or "repository" + digest = hashlib.sha256(identity.encode("utf-8")).hexdigest() + return f"{safe_label}--{digest}" + + +def _validate_slug(slug: str) -> None: + if slug in {".", ".."} or _SLUG_RE.fullmatch(slug) is None: + raise ValueError("worktree slug must be one safe lowercase path component") + + +def _validate_runtime_container(root: Path, namespace: Path, target: Path) -> None: + """Reject non-physical nodes or escapes in the canonical worktree path.""" + + runtime_root = root / "runtime" / "worktrees" + for candidate in (root, root / "runtime", runtime_root, namespace): + if candidate.is_symlink(): + raise ValueError(f"canonical worktree container must be physical: {candidate}") + if candidate.exists() and not candidate.is_dir(): + raise ValueError(f"canonical worktree container must be a directory: {candidate}") + if target.is_symlink(): + raise ValueError(f"canonical worktree target must be physical: {target}") + if target.exists() and not target.is_dir(): + raise ValueError(f"canonical worktree target must be a directory: {target}") + + physical_root = root.resolve(strict=False) + physical_runtime_root = runtime_root.resolve(strict=False) + physical_namespace = namespace.resolve(strict=False) + physical_target = target.resolve(strict=False) + if not physical_runtime_root.is_relative_to(physical_root): + raise ValueError("canonical runtime worktree root escapes WORKSPACE_ROOT") + if not physical_namespace.is_relative_to(physical_runtime_root): + raise ValueError("canonical repository namespace escapes WORKSPACE_ROOT") + if physical_target.parent != physical_namespace: + raise ValueError("canonical worktree target escapes its repository namespace") + + +def runtime_worktree_path( + repo: Path, + slug: str, + *, + workspace_root: Path | None = None, +) -> Path: + """Return the canonical physical home for ``repo``'s worktree ``slug``.""" + + _validate_slug(slug) + root = workspace_root if workspace_root is not None else canonical_workspace_root() + namespace = root / "runtime" / "worktrees" / repository_storage_key(repo) + target = namespace / slug + _validate_runtime_container(root, namespace, target) + return target diff --git a/cli/src/limen/worktree_receipts.py b/cli/src/limen/worktree_receipts.py new file mode 100644 index 000000000..694640e42 --- /dev/null +++ b/cli/src/limen/worktree_receipts.py @@ -0,0 +1,107 @@ +"""Exact identity binding for worktree preservation receipts. + +Receipt-assisted lifecycle decisions are safe only when the durable evidence +names the same physical checkout, repository, and commit that is live now. +Legacy basename-only rows remain readable history but never authorize a +classification or removal. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from pathlib import Path +import subprocess +from typing import Any + +from limen.worktree_layout import repository_storage_key + + +def _git(path: Path, *args: str) -> str: + try: + result = subprocess.run( + ["git", "-C", str(path), *args], + capture_output=True, + text=True, + timeout=30, + check=False, + ) + except (OSError, subprocess.SubprocessError): + return "" + if result.returncode != 0: + return "" + return result.stdout.strip() + + +def live_worktree_receipt_fields(path: Path) -> dict[str, str] | None: + """Return the exact live identity required on a preservation receipt.""" + + try: + resolved = path.expanduser().resolve(strict=True) + except OSError: + return None + top = _git(resolved, "rev-parse", "--path-format=absolute", "--show-toplevel") + head = _git(resolved, "rev-parse", "--verify", "HEAD") + if not top or not head: + return None + try: + if Path(top).resolve(strict=True) != resolved: + return None + repository_key = repository_storage_key(resolved) + except (OSError, ValueError): + return None + return { + "worktree": str(resolved), + "head": head, + "repository_key": repository_key, + } + + +def receipt_worktree_key(receipt: Mapping[str, Any]) -> str | None: + """Return one receipt's normalized absolute worktree key, if it has one.""" + + raw = receipt.get("worktree") + if not isinstance(raw, str) or not raw: + return None + candidate = Path(raw).expanduser() + if not candidate.is_absolute(): + return None + try: + return str(candidate.resolve(strict=False)) + except OSError: + return None + + +def _receipt_rows(receipts: object) -> Iterable[Mapping[str, Any]]: + if isinstance(receipts, Mapping): + if "root" in receipts: + values: Iterable[object] = (receipts,) + else: + values = receipts.values() + elif isinstance(receipts, Iterable) and not isinstance(receipts, (str, bytes)): + values = receipts + else: + values = () + return (value for value in values if isinstance(value, Mapping)) + + +def matching_worktree_receipt(path: Path, receipts: object) -> Mapping[str, Any] | None: + """Select exactly one fully bound receipt; missing or ambiguous evidence fails closed.""" + + identity = live_worktree_receipt_fields(path) + if identity is None: + return None + matches = [ + receipt + for receipt in _receipt_rows(receipts) + if receipt_worktree_key(receipt) == identity["worktree"] + and receipt.get("head") == identity["head"] + and receipt.get("repository_key") == identity["repository_key"] + ] + return matches[0] if len(matches) == 1 else None + + +__all__ = [ + "live_worktree_receipt_fields", + "matching_worktree_receipt", + "receipt_worktree_key", +] diff --git a/cli/src/limen/worktree_roots.py b/cli/src/limen/worktree_roots.py index daeceb71f..7dec6fcc5 100644 --- a/cli/src/limen/worktree_roots.py +++ b/cli/src/limen/worktree_roots.py @@ -7,6 +7,8 @@ from pathlib import Path from typing import Iterable +from limen.worktree_layout import canonical_workspace_root + @dataclass(frozen=True) class WorktreeTarget: @@ -36,6 +38,7 @@ class WorktreeInventoryError(RuntimeError): "site-packages", "vendor", } +CANONICAL_RUNTIME_MIN_AGE_H = 168.0 def _flag(name: str, default: bool) -> bool: @@ -89,6 +92,23 @@ def effective_worktree_root() -> Path: return default_worktrees_root() +def canonical_runtime_worktree_root() -> Path: + """Physical two-level lifecycle root declared by the Workspace manifest.""" + + return canonical_workspace_root() / "runtime" / "worktrees" + + +def is_canonical_runtime_unit(path: Path) -> bool: + """Return whether ``path`` is one physical ``/`` unit.""" + + try: + resolved = path.expanduser().resolve(strict=False) + root = canonical_runtime_worktree_root().resolve(strict=False) + except OSError: + return False + return resolved.parent.parent == root + + def _existing_ancestor(path: Path) -> Path | None: probe = path.expanduser() for _ in range(64): @@ -176,6 +196,106 @@ def _children(root: Path, min_age_h: float, source: str, *, strict: bool = False ] +def _physical_inventory_dir(path: Path, *, strict: bool, source: str) -> bool: + """Reject symlink traversal while inventorying the canonical runtime tree.""" + + try: + info = path.lstat() + except FileNotFoundError: + return False + except OSError as exc: + if strict: + raise WorktreeInventoryError(f"cannot stat {source} {path}: {exc}") from exc + return False + if stat.S_ISLNK(info.st_mode): + if strict: + raise WorktreeInventoryError(f"{source} must be a physical directory: {path}") + return False + if not stat.S_ISDIR(info.st_mode): + if strict: + raise WorktreeInventoryError(f"{source} must be a directory: {path}") + return False + return True + + +def _canonical_runtime_units( + root: Path, + min_age_h: float, + *, + strict: bool = False, +) -> list[WorktreeTarget]: + """Enumerate ``/`` units without treating namespaces as targets.""" + + workspace = root.parent.parent + chain = ( + (workspace, "canonical Workspace root"), + (workspace / "runtime", "canonical runtime root"), + (root, "canonical runtime worktree root"), + ) + for candidate, source in chain: + if not _physical_inventory_dir(candidate, strict=strict, source=source): + return [] + try: + namespaces = sorted(root.iterdir()) + except OSError as exc: + if strict: + raise WorktreeInventoryError(f"cannot enumerate canonical runtime worktree root {root}: {exc}") from exc + return [] + + targets: list[WorktreeTarget] = [] + for namespace in namespaces: + if not _physical_inventory_dir( + namespace, + strict=strict, + source="canonical runtime repository namespace", + ): + continue + try: + units = sorted(namespace.iterdir()) + except OSError as exc: + if strict: + raise WorktreeInventoryError( + f"cannot enumerate canonical runtime repository namespace {namespace}: {exc}" + ) from exc + continue + targets.extend( + WorktreeTarget( + path=unit, + min_age_h=min_age_h, + source=f"canonical-runtime-worktree:{namespace.name}", + ) + for unit in units + if _physical_inventory_dir( + unit, + strict=strict, + source="canonical runtime worktree unit", + ) + ) + return targets + + +def _same_inventory_root(left: Path, right: Path) -> bool: + try: + return left.resolve(strict=False) == right.resolve(strict=False) + except OSError: + return Path(os.path.abspath(left.expanduser())) == Path(os.path.abspath(right.expanduser())) + + +def _flat_children( + root: Path, + min_age_h: float, + source: str, + *, + canonical_root: Path, + strict: bool, +) -> list[WorktreeTarget]: + """Keep the canonical two-level root out of every flat legacy inventory rail.""" + + if _same_inventory_root(root, canonical_root): + return [] + return _children(root, min_age_h, source, strict=strict) + + def _discover_repo_local_roots(limen_root: Path, *, strict: bool = False) -> list[Path]: explicit = _path_list("LIMEN_RECLAIM_REPO_LOCAL_ROOTS", []) roots = [path for path in explicit if _inventory_is_dir(path, strict=strict, source="explicit repo-local root")] @@ -345,25 +465,38 @@ def iter_worktree_targets(limen_root: Path | None = None, *, strict: bool = Fals unreadable configured scope or failed registered-repo query means inventory is incomplete and must block new local creation or a false zero-debt verdict. """ - root = limen_root or Path(os.environ.get("LIMEN_ROOT", Path.home() / "Workspace" / "limen")) + root = limen_root or Path( + os.environ.get("LIMEN_ROOT", Path.home() / "Workspace" / "library" / "engine" / "organvm" / "limen") + ) targets: list[WorktreeTarget] = [] dispatch_root = effective_worktree_root() + min_age_h = _float_env("LIMEN_RECLAIM_MIN_AGE_H", 6) + canonical_root = canonical_runtime_worktree_root() targets.extend( - _children( + _canonical_runtime_units( + canonical_root, + _float_env("LIMEN_RECLAIM_CANONICAL_AGE_H", CANONICAL_RUNTIME_MIN_AGE_H), + strict=strict, + ) + ) + targets.extend( + _flat_children( dispatch_root, - _float_env("LIMEN_RECLAIM_MIN_AGE_H", 6), + min_age_h, "dispatch-root", + canonical_root=canonical_root, strict=strict, ) ) clone_cache = dispatch_clone_cache_root() if clone_cache is not None: targets.extend( - _children( + _flat_children( clone_cache, - _float_env("LIMEN_RECLAIM_MIN_AGE_H", 6), + min_age_h, "dispatch-clone-cache", + canonical_root=canonical_root, strict=strict, ) ) @@ -373,20 +506,22 @@ def iter_worktree_targets(limen_root: Path | None = None, *, strict: bool = Fals legacy_age = _float_env("LIMEN_RECLAIM_LEGACY_DISPATCH_AGE_H", _float_env("LIMEN_RECLAIM_MIN_AGE_H", 6)) for legacy_root in _legacy_dispatch_roots(dispatch_root, strict=strict): targets.extend( - _children( + _flat_children( legacy_root, legacy_age, f"legacy-dispatch-root:{legacy_root}", + canonical_root=canonical_root, strict=strict, ) ) if _flag("LIMEN_RECLAIM_CLAUDE_WT", True): targets.extend( - _children( + _flat_children( root / ".claude" / "worktrees", _float_env("LIMEN_RECLAIM_CLAUDE_AGE_H", 24), "claude-worktrees", + canonical_root=canonical_root, strict=strict, ) ) @@ -396,10 +531,11 @@ def iter_worktree_targets(limen_root: Path | None = None, *, strict: bool = Fals os.environ.get("LIMEN_AGY_SCRATCH_ROOT", Path.home() / ".gemini" / "antigravity-cli" / "scratch") ) targets.extend( - _children( + _flat_children( agy_scratch, _float_env("LIMEN_AGY_SCRATCH_MIN_IDLE_H", 24), "agy-scratch", + canonical_root=canonical_root, strict=strict, ) ) @@ -407,7 +543,15 @@ def iter_worktree_targets(limen_root: Path | None = None, *, strict: bool = Fals if _flag("LIMEN_RECLAIM_REPO_LOCAL_WT", broad_default): repo_age = _float_env("LIMEN_RECLAIM_REPO_LOCAL_AGE_H", 24) for repo_root in _discover_repo_local_roots(root, strict=strict): - targets.extend(_children(repo_root, repo_age, f"repo-local:{repo_root}", strict=strict)) + targets.extend( + _flat_children( + repo_root, + repo_age, + f"repo-local:{repo_root}", + canonical_root=canonical_root, + strict=strict, + ) + ) if _flag("LIMEN_RECLAIM_REGISTERED_WT", broad_default): registered_age = _float_env("LIMEN_RECLAIM_REGISTERED_AGE_H", 24) diff --git a/cli/tests/test_agent_state_tree_pipeline.py b/cli/tests/test_agent_state_tree_pipeline.py index a6673f07d..055a08e5c 100644 --- a/cli/tests/test_agent_state_tree_pipeline.py +++ b/cli/tests/test_agent_state_tree_pipeline.py @@ -230,6 +230,10 @@ def test_custody_guard_uses_filesystem_identity_for_case_aliases( source = tmp_path / "Data" alias = tmp_path / "data" source.mkdir() + if alias.exists(): + # The default macOS volume is case-insensitive; retain two distinct + # fixture paths and let the samefile shim model the alias identity. + alias = tmp_path / "case-alias" alias.mkdir() real_samefile = tree_pipeline.os.path.samefile diff --git a/cli/tests/test_always_working.py b/cli/tests/test_always_working.py index 7d9fc0007..e9ec48e7a 100644 --- a/cli/tests/test_always_working.py +++ b/cli/tests/test_always_working.py @@ -643,7 +643,9 @@ def test_contribution_balance_receipt_assigns_review_first(monkeypatch): assert receipt["id"] == "PUBLIC-FACE-CONTRIBUTION-BALANCE" assert receipt["evidence"]["shares"]["reviews"] == 0.0061 assert "substantive PR review" in receipt["assignment_packet"]["task"] - assert "~/Workspace/limen/docs/github-contribution-balance.md" in receipt["existing_receipts"] + assert ( + "~/Workspace/library/engine/organvm/limen/docs/github-contribution-balance.md" in receipt["existing_receipts"] + ) assert "https://github.com/organvm/limen/issues/687" in receipt["existing_receipts"] diff --git a/cli/tests/test_campaign_relay.py b/cli/tests/test_campaign_relay.py index 61f95ad85..893836a76 100644 --- a/cli/tests/test_campaign_relay.py +++ b/cli/tests/test_campaign_relay.py @@ -16,6 +16,7 @@ campaign_relay_lock, reserve_relay, ) +from limen.worktree_layout import runtime_worktree_path from limen.conduct.models import CampaignRelayReceiptV1 from limen.workstream_contract import RECEIPT_MODULES, new_contract, new_contract_v2 @@ -138,18 +139,38 @@ def test_unconsumed_reservation_tracks_current_main_when_main_moves(relay_repo) def test_primary_checkout_and_successor_worktree_derive_from_shared_common_dir( relay_repo, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, ) -> None: root, _predecessor = relay_repo - successor = root / ".worktrees" / "successor" + monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path / "Workspace")) + successor = runtime_worktree_path(root, "successor") observer = root.parent / "observer" - successor.parent.mkdir() - _git(root, "worktree", "add", "--detach", str(successor), "HEAD") + successor.parent.mkdir(parents=True) + _git(root, "worktree", "add", "-b", "work/successor", str(successor), "HEAD") _git(root, "worktree", "add", "--detach", str(observer), "HEAD") assert _primary_checkout(observer) == root.resolve() assert _relay_worktree(observer, "successor") == successor.resolve() +def test_relay_rejects_same_branch_worktree_in_wrong_runtime_namespace( + relay_repo, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + root, _predecessor = relay_repo + monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path / "Workspace")) + successor = tmp_path / "Workspace" / "runtime" / "worktrees" / "wrong-repository-key" / "successor" + successor.parent.mkdir(parents=True) + _git(root, "worktree", "add", "-b", "work/successor", str(successor), "HEAD") + + with pytest.raises(CampaignRelayError) as raised: + _relay_worktree(root, "successor") + + assert raised.value.code == "relay_worktree_invalid" + + @pytest.mark.parametrize("length", [40, 64]) def test_relay_receipt_accepts_exact_git_object_lengths(relay_repo, length) -> None: root, predecessor = relay_repo diff --git a/cli/tests/test_campaign_relay_effector.py b/cli/tests/test_campaign_relay_effector.py index 345af838a..eec14d1f2 100644 --- a/cli/tests/test_campaign_relay_effector.py +++ b/cli/tests/test_campaign_relay_effector.py @@ -30,6 +30,7 @@ from limen.conduct.campaign_relay_state import _read_relay, _replace_relay from limen.conduct.models import CampaignRelayReceiptV1 from limen.workstream_contract import RECEIPT_MODULES, new_contract +from limen.worktree_layout import runtime_worktree_path ROOT = Path(__file__).resolve().parents[2] @@ -59,7 +60,11 @@ def _git(root: Path, *args: str) -> str: @pytest.fixture -def effector_repo(tmp_path: Path) -> tuple[Path, Path, Path, int]: +def effector_repo( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> tuple[Path, Path, Path, int]: + monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path / "Workspace")) root = tmp_path / "repo" remote = tmp_path / "origin.git" binary_dir = tmp_path / "bin" @@ -230,7 +235,7 @@ def spawn(command, **kwargs): os.kill(launch.receipt.launch_pid, 0) status = json.loads( ( - root / ".worktrees" / launch.receipt.successor_slug / ".limen-workstream" / "conduct-keepalive.json" + runtime_worktree_path(root, launch.receipt.successor_slug) / ".limen-workstream" / "conduct-keepalive.json" ).read_text(encoding="utf-8") ) os.kill(int(status["keepalive_pid"]), 0) @@ -281,7 +286,7 @@ def spawn(command, **kwargs): else: pytest.fail("fixture provider did not exit naturally") - worktree = root / ".worktrees" / launch.receipt.successor_slug + worktree = runtime_worktree_path(root, launch.receipt.successor_slug) (worktree / "provider-result.txt").write_text("descendant\n", encoding="utf-8") _git(worktree, "add", "provider-result.txt") _git(worktree, "commit", "-qm", "provider descendant") @@ -756,6 +761,7 @@ def test_startup_output_ceiling_crossed_before_selected_ack_fails_closed( observed_digests: list[relay_process._BoundedStreamDigest] = [] registrations: list[bool] = [] ack_observation = tmp_path / "ack-observation.txt" + output_release = tmp_path / "output-release" original_digest = relay_process._BoundedStreamDigest class ObservableDigest(original_digest): @@ -765,6 +771,7 @@ def __init__(self) -> None: def register(**kwargs): registrations.append(kwargs["accepting_work"]) + output_release.write_text("release\n", encoding="utf-8") assert observed_digests[0].wait_for_output_ceiling(timeout=2) raw = json.dumps( { @@ -777,12 +784,21 @@ def register(**kwargs): return hashlib.sha256(raw).hexdigest(), len(raw) def spawn(_command, **kwargs): - successor = root / ".worktrees" / reservation.receipt.successor_slug - successor.parent.mkdir(exist_ok=True) - _git(root, "worktree", "add", "--detach", str(successor), "HEAD") + successor = runtime_worktree_path(root, reservation.receipt.successor_slug) + successor.parent.mkdir(parents=True, exist_ok=True) + _git( + root, + "worktree", + "add", + "-b", + reservation.receipt.successor_branch, + str(successor), + "HEAD", + ) child_source = f""" import json import os +import time from pathlib import Path control_fd = {kwargs["control_descriptor"]} @@ -799,6 +815,8 @@ def spawn(_command, **kwargs): payload = (json.dumps(event, sort_keys=True, separators=(",", ":")) + "\\n").encode() while payload: payload = payload[os.write(control_fd, payload):] +while not Path(os.environ["OUTPUT_RELEASE"]).exists(): + time.sleep(0.001) payload = b"x" * {relay_process._STARTUP_OUTPUT_CEILING + 8192} while payload: payload = payload[os.write(1, payload):] @@ -807,7 +825,11 @@ def spawn(_command, **kwargs): os.close(control_fd) os.close(exec_fd) """ - env = {**kwargs["env"], "ACK_OBSERVATION": str(ack_observation)} + env = { + **kwargs["env"], + "ACK_OBSERVATION": str(ack_observation), + "OUTPUT_RELEASE": str(output_release), + } return subprocess.Popen( [sys.executable, "-c", child_source], cwd=root, @@ -906,7 +928,7 @@ def fail_ready_publication(*_args, **_kwargs): assert launch.receipt.terminal_code == "relay_ready_publication_failed" assert launch.receipt.activation_response_sha256 is None assert registrations == [False, True, False] - marker = root / ".worktrees" / launch.receipt.successor_slug / ".limen-workstream" / "relay-activated" + marker = runtime_worktree_path(root, launch.receipt.successor_slug) / ".limen-workstream" / "relay-activated" assert not marker.exists() assert not _git( root, @@ -1291,7 +1313,7 @@ def unavailable_reconciliation(root, ref, **kwargs): assert uncertain.receipt.terminal_code == "relay_ready_publication_uncertain" assert uncertain.receipt.activation_response_sha256 is not None assert registrations == [False, True] - marker = root / ".worktrees" / uncertain.receipt.successor_slug / ".limen-workstream" / "relay-activated" + marker = runtime_worktree_path(root, uncertain.receipt.successor_slug) / ".limen-workstream" / "relay-activated" assert marker.read_text(encoding="utf-8") == f"{uncertain.receipt.relay_id}\n" assert _git( root, @@ -1523,10 +1545,18 @@ def test_linked_worktree_reconciliation_rolls_back_the_primary_successor_session predecessor, exact_remote_main=_git(root, "rev-parse", "HEAD"), ) - successor = root / ".worktrees" / reservation.receipt.successor_slug + successor = runtime_worktree_path(root, reservation.receipt.successor_slug) observer = tmp_path / "observer" - successor.parent.mkdir(exist_ok=True) - _git(root, "worktree", "add", "--detach", str(successor), "HEAD") + successor.parent.mkdir(parents=True, exist_ok=True) + _git( + root, + "worktree", + "add", + "-b", + reservation.receipt.successor_branch, + str(successor), + "HEAD", + ) _git(root, "worktree", "add", "--detach", str(observer), "HEAD") capsule_dir = successor / ".limen-workstream" capsule_dir.mkdir() diff --git a/cli/tests/test_capacity.py b/cli/tests/test_capacity.py index 50ff97d77..62336c2ce 100644 --- a/cli/tests/test_capacity.py +++ b/cli/tests/test_capacity.py @@ -568,6 +568,154 @@ def test_capacity_census_uses_current_live_meter_over_stale_board_spend( assert "opencode" in auto +@pytest.mark.parametrize( + "meter_update", + [ + {"possible": "1e999"}, + {"possible": "NaN"}, + {"possible": "Infinity"}, + {"possible": "-Infinity"}, + {"possible": -1}, + {"remaining": -1}, + {"remaining": -0.5}, + {"remaining": 101}, + {"consumed": -1}, + {"consumed": -0.5}, + {"consumed": "NaN"}, + {"consumed": 101}, + {"consumed": 80, "remaining": 30}, + ], + ids=[ + "overflow", + "nan", + "positive-infinity", + "negative-infinity", + "negative-cap", + "negative-remaining", + "negative-fractional-remaining", + "remaining-over-cap", + "negative-consumed", + "negative-fractional-consumed", + "non-finite-consumed", + "consumed-over-cap", + "contradictory-accounting", + ], +) +def test_capacity_census_malformed_live_meter_falls_back_to_exhausted_board( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + meter_update: dict[str, object], +) -> None: + meter: dict[str, object] = { + "signal": "vendor-meter", + "possible": 100, + "consumed": 50, + "remaining": 50, + "health": "ok", + } + meter.update(meter_update) + (tmp_path / "logs").mkdir() + (tmp_path / "logs" / "usage.json").write_text( + json.dumps( + { + "generated": "2026-07-10T07:24:23+00:00", + "vendors": {"jules": meter}, + } + ), + encoding="utf-8", + ) + monkeypatch.setenv("LIMEN_ROOT", str(tmp_path)) + monkeypatch.setattr( + "limen.capacity.agent_status", + lambda agent: { + "agent": agent, + "kind": "cloud-cli", + "reachable": True, + "detail": "test binary", + "command": ["test"], + }, + ) + board = { + "portal": { + "budget": { + "daily": 100, + "per_agent": {"jules": 5}, + "track": { + "date": "2026-07-10", + "spent": 0, + "per_agent": {"jules": 5}, + }, + } + } + } + + jules = next(row for row in capacity_census(board) if row["agent"] == "jules") + + assert jules["limit"] == 5 + assert jules["spent"] == 5 + assert jules["remaining"] == 0 + assert jules["reachable"] is False + assert "live usage meter" not in jules["detail"] + + +def test_capacity_census_derives_limen_root_from_workspace_root( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + workspace = tmp_path / "Custom Workspace" + limen_root = workspace / "library" / "engine" / "organvm" / "limen" + (limen_root / "logs").mkdir(parents=True) + (limen_root / "logs" / "usage.json").write_text( + json.dumps( + { + "generated": "2026-07-10T07:24:23+00:00", + "vendors": { + "jules": { + "signal": "dispatch-count", + "unit": "runs", + "possible": 100, + "consumed": 89, + "remaining": 11, + "health": "throttle", + } + }, + } + ), + encoding="utf-8", + ) + monkeypatch.delenv("LIMEN_ROOT", raising=False) + monkeypatch.setenv("WORKSPACE_ROOT", str(workspace)) + monkeypatch.setattr( + "limen.capacity.agent_status", + lambda agent: { + "agent": agent, + "kind": "cloud-cli", + "reachable": True, + "detail": "test binary", + "command": ["test"], + }, + ) + board = { + "portal": { + "budget": { + "daily": 600, + "per_agent": {"jules": 100}, + "track": { + "date": "2026-07-10", + "spent": 244, + "per_agent": {"jules": 122}, + }, + } + } + } + + jules = next(row for row in capacity_census(board) if row["agent"] == "jules") + + assert jules["spent"] == 89 + assert jules["remaining"] == 11 + assert "live usage meter" in jules["detail"] + + def test_capacity_census_dict_board_shape() -> None: board = { "portal": { diff --git a/cli/tests/test_cartridge_connected.py b/cli/tests/test_cartridge_connected.py index ba4841b64..5cb01f4d3 100644 --- a/cli/tests/test_cartridge_connected.py +++ b/cli/tests/test_cartridge_connected.py @@ -83,6 +83,23 @@ def test_main_disconnected_dummy(monkeypatch, tmp_path): assert m.main() == 1 +def test_disconnected_recovery_command_derives_canonical_domus_root( + monkeypatch, + tmp_path, + capsys, +): + m = _cc() + workspace = tmp_path / "portable-workspace" + monkeypatch.setenv("WORKSPACE_ROOT", str(workspace)) + monkeypatch.delenv("DOMUS_ROOT", raising=False) + _wire(m, monkeypatch, source=str(tmp_path), remote_origin="/tmp/dummy-remote.git") + + assert m.main() == 1 + assert ( + f"chezmoi init --source {workspace}/library/engine/organvm/domus-genoma --data --promptDefaults" + ) in capsys.readouterr().out + + def test_main_no_remote_is_disconnection(monkeypatch, tmp_path): m = _cc() _wire(m, monkeypatch, source=str(tmp_path), remote_origin=None, remotes=None) diff --git a/cli/tests/test_container_migrate_contract.py b/cli/tests/test_container_migrate_contract.py new file mode 100644 index 000000000..a71a5b17a --- /dev/null +++ b/cli/tests/test_container_migrate_contract.py @@ -0,0 +1,14 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +MIGRATE = ROOT / "container" / "migrate.sh" + + +def test_committed_plist_is_source_and_deployed_plist_is_destination() -> None: + text = MIGRATE.read_text(encoding="utf-8") + + assert 'CANONICAL_PLIST="$CONT/launchd/com.limen.heartbeat.plist"' in text + assert 'cp "$CANONICAL_PLIST" "$PLIST"' in text + assert 'cp -p "$PLIST" "$CONT/launchd/$LABEL.plist"' not in text + assert '[ -f "$CANONICAL_PLIST" ] || die "committed canonical plist missing:' in text diff --git a/cli/tests/test_continuation_beat.py b/cli/tests/test_continuation_beat.py index 465de2d21..73888886d 100644 --- a/cli/tests/test_continuation_beat.py +++ b/cli/tests/test_continuation_beat.py @@ -133,6 +133,16 @@ def test_receipt_refresh_does_not_churn_timestamp_without_new_evidence(tmp_path, receipt_path = root / "docs" / "worktree-preservation-receipts.json" receipt_path.write_text(json.dumps({"receipts": [{"root": "lane"}]}), encoding="utf-8") module = _load(monkeypatch, root, photos, portvs) + worktree = tmp_path / "missing-worktree" + monkeypatch.setattr( + module, + "live_worktree_receipt_fields", + lambda path: { + "worktree": str(path.resolve(strict=False)), + "head": "abc123", + "repository_key": "example--" + "a" * 64, + }, + ) monkeypatch.setattr( module, "pr_view", @@ -150,7 +160,7 @@ def test_receipt_refresh_does_not_churn_timestamp_without_new_evidence(tmp_path, rows = [ { "name": "lane", - "path": str(tmp_path / "missing-worktree"), + "path": str(worktree), "repo": "organvm/example", "branch": "work/lane", "url": "https://example.test/pull/7", @@ -166,6 +176,60 @@ def test_receipt_refresh_does_not_churn_timestamp_without_new_evidence(tmp_path, assert receipt_path.read_bytes() == first_bytes +def test_pr_receipts_retain_distinct_same_slug_worktrees(tmp_path, monkeypatch): + root = tmp_path / "limen" + photos = tmp_path / "photos" + portvs = tmp_path / "portvs" + (root / "docs").mkdir(parents=True) + photos.mkdir() + portvs.mkdir() + receipt_path = root / "docs" / "worktree-preservation-receipts.json" + receipt_path.write_text('{"receipts": []}\n', encoding="utf-8") + module = _load(monkeypatch, root, photos, portvs) + paths = [tmp_path / owner / "same-slug" for owner in ("first", "second")] + for path in paths: + path.mkdir(parents=True) + monkeypatch.setattr( + module, + "live_worktree_receipt_fields", + lambda path: { + "worktree": str(path.resolve(strict=False)), + "head": "abc123", + "repository_key": f"{path.parent.name}--" + "a" * 64, + }, + ) + monkeypatch.setattr( + module, + "pr_view", + lambda repo, number_or_url, path: { + "number": int(number_or_url), + "state": "OPEN", + "isDraft": True, + "headRefName": "work/same-slug", + "headRefOid": "abc123", + "url": f"https://example.test/{repo}/pull/{number_or_url}", + }, + ) + rows = [ + { + "name": "same-slug", + "path": str(path), + "repo": f"{path.parent.name}/same-slug", + "branch": "work/same-slug", + "url": f"https://example.test/pull/{index}", + } + for index, path in enumerate(paths, start=41) + ] + + result = module.update_pr_receipts(rows, apply=True) + receipts = json.loads(receipt_path.read_text(encoding="utf-8"))["receipts"] + + assert result["updated"] == 2 + assert [row["root"] for row in receipts] == ["same-slug", "same-slug"] + assert len({row["worktree"] for row in receipts}) == 2 + assert len({row["repository_key"] for row in receipts}) == 2 + + def test_merged_pr_custody_clears_exact_retry_ref_then_clean_beat_is_fixed_point(tmp_path, monkeypatch): root = tmp_path / "limen" photos = tmp_path / "photos" diff --git a/cli/tests/test_install_script.py b/cli/tests/test_install_script.py index 6b7d16c5a..86e1175a7 100644 --- a/cli/tests/test_install_script.py +++ b/cli/tests/test_install_script.py @@ -50,9 +50,15 @@ def _fake_path(tmp_path: Path) -> Path: return bin_dir -def _run_install(tmp_path: Path, *args: str) -> subprocess.CompletedProcess[str]: +def _run_install( + tmp_path: Path, + *args: str, + zshenv: str | None = None, +) -> subprocess.CompletedProcess[str]: home = tmp_path / "home" home.mkdir() + if zshenv is not None: + (home / ".zshenv").write_text(zshenv, encoding="utf-8") fake_bin = _fake_path(tmp_path) env = { **os.environ, @@ -91,3 +97,29 @@ def test_install_host_mutation_is_explicit_opt_in(tmp_path): assert (home / ".zshenv").exists() assert (home / ".local" / "bin" / "limen").exists() assert "LIMEN_ROOT" in (home / ".zshenv").read_text(encoding="utf-8") + + +def test_install_migrates_its_exact_legacy_limen_root(tmp_path): + result = _run_install( + tmp_path, + "--host-mutation", + zshenv='export LIMEN_ROOT="$HOME/limen"\n', + ) + text = (tmp_path / "home" / ".zshenv").read_text(encoding="utf-8") + + assert result.returncode == 0, result.stderr + assert 'export LIMEN_ROOT="$HOME/limen"' not in text + assert text.count('export WORKSPACE_ROOT="$HOME/Workspace"') == 1 + assert text.count('export LIMEN_ROOT="$WORKSPACE_ROOT/library/engine/organvm/limen"') == 1 + assert "migrated installer-owned legacy LIMEN_ROOT" in result.stdout + + +def test_install_preserves_user_owned_limen_root_override(tmp_path): + custom = 'export LIMEN_ROOT="/srv/operator-owned/limen"\n' + result = _run_install(tmp_path, "--host-mutation", zshenv=custom) + text = (tmp_path / "home" / ".zshenv").read_text(encoding="utf-8") + + assert result.returncode == 0, result.stderr + assert custom in text + assert 'export LIMEN_ROOT="$WORKSPACE_ROOT/library/engine/organvm/limen"' not in text + assert "LIMEN_ROOT already set" in result.stdout diff --git a/cli/tests/test_lead_spawn.py b/cli/tests/test_lead_spawn.py index 7983f8546..786c42d00 100644 --- a/cli/tests/test_lead_spawn.py +++ b/cli/tests/test_lead_spawn.py @@ -4,6 +4,7 @@ import importlib.util import json from pathlib import Path +import subprocess import pytest @@ -30,9 +31,19 @@ def _mint_lead( duration: int = 7 * 86400, deadline_epoch: float | None = None, ) -> Path: + if not (repo / ".git").exists(): + subprocess.run(["git", "init", "-q", "-b", "main", str(repo)], check=True) + subprocess.run(["git", "-C", str(repo), "config", "user.email", "test@example.invalid"], check=True) + subprocess.run(["git", "-C", str(repo), "config", "user.name", "Test"], check=True) + (repo / "scripts").mkdir(exist_ok=True) + (repo / "scripts" / "start-worktree-session.sh").write_text("#!/bin/bash\n") + subprocess.run(["git", "-C", str(repo), "add", "."], check=True) + subprocess.run(["git", "-C", str(repo), "commit", "-qm", "fixture"], check=True) lead = repo / ".worktrees" / slug + lead.parent.mkdir(exist_ok=True) + subprocess.run(["git", "-C", str(repo), "worktree", "add", "--detach", str(lead), "HEAD"], check=True) capsule = lead / ".limen-workstream" - capsule.mkdir(parents=True) + capsule.mkdir() runway: dict = {"duration_seconds": duration, "deadline_epoch": deadline_epoch} (capsule / "workstream.json").write_text(json.dumps({"schema": "limen.workstream.contract.v1", "runway": runway})) receipt_dir = lead / "docs" / "continuations" / slug @@ -40,8 +51,6 @@ def _mint_lead( (receipt_dir / "workstream.json").write_text( json.dumps({"schema": "limen.workstream.receipt.v1", "slug": slug, "workstream": handle}) ) - (repo / "scripts").mkdir(exist_ok=True) - (repo / "scripts" / "start-worktree-session.sh").write_text("#!/bin/bash\n") return lead @@ -94,7 +103,7 @@ def test_battle_cap_refused_with_live_list(tmp_path: Path, monkeypatch: pytest.M monkeypatch.setenv("LIMEN_LEAD_MAX_BATTLES", "2") lead = _mint_lead(tmp_path) for name in ("battle-one", "battle-two"): - receipt_dir = tmp_path / ".worktrees" / name / "docs" / "continuations" / name + receipt_dir = lead.parent / name / "docs" / "continuations" / name receipt_dir.mkdir(parents=True) (receipt_dir / "workstream.json").write_text(json.dumps({"workstream": "substrate"})) intent = tmp_path / "battle.md" diff --git a/cli/tests/test_overnight_launchd_runtime.py b/cli/tests/test_overnight_launchd_runtime.py index ff8bb5c14..5c9aff181 100644 --- a/cli/tests/test_overnight_launchd_runtime.py +++ b/cli/tests/test_overnight_launchd_runtime.py @@ -19,7 +19,7 @@ def test_overnight_watch_launchd_uses_signed_immutable_runtime() -> None: f"{IMMUTABLE_ROOT}/venv/bin/python", f"{IMMUTABLE_ROOT}/source/scripts/overnight-watch.py", ] - assert payload["EnvironmentVariables"]["LIMEN_ROOT"] == "/Users/4jp/Workspace/limen" + assert payload["EnvironmentVariables"]["LIMEN_ROOT"] == "/Users/4jp/Workspace/library/engine/organvm/limen" assert payload["EnvironmentVariables"]["PYTHONPATH"] == (f"{IMMUTABLE_ROOT}/source/cli/src") assert payload["StartInterval"] == 300 diff --git a/cli/tests/test_reap_clones.py b/cli/tests/test_reap_clones.py index 0fcfc1be7..f685d9712 100644 --- a/cli/tests/test_reap_clones.py +++ b/cli/tests/test_reap_clones.py @@ -16,12 +16,32 @@ from pathlib import Path SCRIPTS = Path(__file__).resolve().parents[2] / "scripts" +REPO = SCRIPTS.parent _spec = importlib.util.spec_from_file_location("reap_clones", SCRIPTS / "reap-clones.py") reap = importlib.util.module_from_spec(_spec) sys.modules["reap_clones"] = reap # dataclass needs the module discoverable during exec _spec.loader.exec_module(reap) +def test_help_imports_checkout_modules_before_canonical_cutover(): + """The bridge checkout remains directly executable while canonical LIMEN_ROOT is empty.""" + + env = os.environ.copy() + for name in ("PYTHONPATH", "LIMEN_ROOT", "WORKSPACE_ROOT"): + env.pop(name, None) + result = subprocess.run( + [sys.executable, str(SCRIPTS / "reap-clones.py"), "--help"], + cwd=REPO, + env=env, + capture_output=True, + text=True, + timeout=15, + ) + + assert result.returncode == 0, result.stderr + assert "Reap pure pushed-mirror clones" in result.stdout + + # ---------------------------------------------------------------- git helpers def _git(cwd: Path, *args: str) -> None: subprocess.run(["git", "-C", str(cwd), *args], check=True, capture_output=True, text=True) @@ -360,7 +380,9 @@ def test_local_only_tag_is_never_reaped(tmp_path): (clone / "release.bin").write_text("release artifact v1.0\n") _git(clone, "add", "-A") _git(clone, "commit", "-qm", "release v1.0") - _git(clone, "tag", "v1.0-local") # local only, never pushed + # A developer-global tag.gpgSign=true must not turn this lightweight + # fixture tag into an interactive annotated-tag editor session. + _git(clone, "-c", "tag.gpgSign=false", "tag", "v1.0-local") # local only, never pushed _git(clone, "reset", "--hard", "HEAD~1") # orphan the tagged commit off refs/heads v = _verdict(clone, age_days=99, pressure=True) assert v.reap is False diff --git a/cli/tests/test_reclaim_worktrees.py b/cli/tests/test_reclaim_worktrees.py index 5a2feb6d1..6b2e1918a 100644 --- a/cli/tests/test_reclaim_worktrees.py +++ b/cli/tests/test_reclaim_worktrees.py @@ -10,6 +10,8 @@ import pytest +from limen.worktree_receipts import live_worktree_receipt_fields + ROOT = Path(__file__).resolve().parents[2] SCRIPT = ROOT / "scripts" / "reclaim-worktrees.py" @@ -23,6 +25,16 @@ def load_reclaim_worktrees(): return module +def xdg_quarantine( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + name: str, +) -> Path: + data_home = tmp_path / "xdg-data" + monkeypatch.setenv("XDG_DATA_HOME", str(data_home)) + return data_home / "limen" / name + + def acceptance_event(root: Path, action: str = "remove-worktree", reason: str = "clean+merged+idle") -> dict: return { "accepted_at": "2026-07-06T06:00:00Z", @@ -30,6 +42,7 @@ def acceptance_event(root: Path, action: str = "remove-worktree", reason: str = "accepted": True, "action": action, "reason": reason, + "path": str(root.resolve()), "archive_status": "not_required_clean_merged_remote", "archive_proof": "remote/default preservation verified", "redaction_review": "not_required_remote_only", @@ -37,6 +50,73 @@ def acceptance_event(root: Path, action: str = "remove-worktree", reason: str = } +def bound_receipt(path: Path, **values: object) -> dict[str, object]: + identity = live_worktree_receipt_fields(path) + assert identity is not None + return {"root": path.name, **identity, **values} + + +def canonical_same_slug_worktree(tmp_path: Path, workspace: Path, owner: str) -> Path: + owner_root = tmp_path / owner + primary = owner_root / "primary" + remote = owner_root / "origin.git" + primary.mkdir(parents=True) + remote.mkdir() + subprocess.run(["git", "init", "-q", "-b", "main"], cwd=primary, check=True) + subprocess.run(["git", "init", "-q", "--bare"], cwd=remote, check=True) + subprocess.run(["git", "config", "user.email", "test@example.invalid"], cwd=primary, check=True) + subprocess.run(["git", "config", "user.name", "Test User"], cwd=primary, check=True) + subprocess.run(["git", "commit", "-qm", "base", "--allow-empty"], cwd=primary, check=True) + subprocess.run(["git", "remote", "add", "origin", str(remote)], cwd=primary, check=True) + subprocess.run(["git", "push", "-qu", "origin", "main"], cwd=primary, check=True) + key = live_worktree_receipt_fields(primary) + assert key is not None + worktree = workspace / "runtime" / "worktrees" / key["repository_key"] / "same-slug" + worktree.parent.mkdir(parents=True, exist_ok=True) + subprocess.run( + ["git", "worktree", "add", "-q", "-b", "work/same-slug", str(worktree), "HEAD"], + cwd=primary, + check=True, + ) + return worktree + + +def merged_receipt(path: Path) -> dict[str, object]: + return bound_receipt( + path, + lane="remote-merged", + status="merged_pr_preserved", + pr_state="MERGED", + pr_url="https://github.com/example/repo/pull/7", + ) + + +def test_reaper_remote_merged_receipt_is_bound_to_exact_same_slug_worktree_and_head( + tmp_path: Path, +) -> None: + reclaim = load_reclaim_worktrees() + workspace = tmp_path / "Workspace" + first = canonical_same_slug_worktree(tmp_path, workspace, "first") + second = canonical_same_slug_worktree(tmp_path, workspace, "second") + foreign = merged_receipt(first) + stale = merged_receipt(second) + + subprocess.run( + ["git", "commit", "-qm", "local-only", "--allow-empty"], + cwd=second, + check=True, + ) + current = merged_receipt(second) + args = (second, time.time(), 0) + + assert reclaim.classify(*args, [foreign]) == ("skip", "unpushed-commits") + assert reclaim.classify(*args, [stale]) == ("skip", "unpushed-commits") + assert reclaim.classify(*args, [current]) == ( + "remove-worktree", + "receipt-remote-merged+clean+idle", + ) + + def test_reclaim_standing_grant_accepts_loss_free_class_without_ledger(tmp_path: Path) -> None: # covenant standing grant 2026-07-09: clean+merged+idle needs no per-root ledger event reclaim = load_reclaim_worktrees() @@ -76,15 +156,14 @@ def test_debt_classifier_matches_accepted_reaper_for_remote_merged_receipt(tmp_p subprocess.run(["git", "config", "user.email", "test@example.invalid"], cwd=root, check=True) subprocess.run(["git", "config", "user.name", "Test User"], cwd=root, check=True) subprocess.run(["git", "commit", "-qm", "init", "--allow-empty"], cwd=root, check=True) - receipts = { - root.name: { - "root": root.name, - "lane": "remote-merged", - "status": "merged_pr_preserved", - "pr_state": "MERGED", - "pr_url": "https://github.com/example/repo/pull/7", - } - } + receipt = bound_receipt( + root, + lane="remote-merged", + status="merged_pr_preserved", + pr_state="MERGED", + pr_url="https://github.com/example/repo/pull/7", + ) + receipts = {root.name: receipt} now = time.time() action, reaper_reason = reclaim.classify(root, now, 0, receipts) @@ -338,6 +417,88 @@ def test_apply_requires_matching_plan_digest_before_abandonment(tmp_path: Path, assert candidate.exists() +def test_apply_records_unsafe_quarantine_root_per_candidate_and_continues( + tmp_path: Path, + monkeypatch, + capsys, +) -> None: + reclaim = load_reclaim_worktrees() + unsafe = tmp_path / "unsafe" + safe = tmp_path / "safe" + unsafe.mkdir() + safe.mkdir() + targets = [type("Target", (), {"path": path, "min_age_h": 0, "source": "test"})() for path in (unsafe, safe)] + candidates = [ + { + "root": path.name, + "path": str(path), + "source": "test", + "action": "remove-residue", + "reason": "generated-log-shell", + } + for path in (unsafe, safe) + ] + plan_sha = "a" * 64 + monkeypatch.setattr(reclaim, "APPLY", True) + monkeypatch.setattr(reclaim, "CHECK", False) + monkeypatch.setattr(reclaim, "JSON_OUT", True) + monkeypatch.setattr(reclaim, "FORCE", True) + monkeypatch.setattr(reclaim, "GENERATED_ONLY", False) + monkeypatch.setattr(reclaim, "EXPECTED_PLAN_SHA", plan_sha) + monkeypatch.setattr(reclaim, "iter_worktree_targets", lambda _root: targets) + monkeypatch.setattr(reclaim, "active_process_cwds", dict) + monkeypatch.setattr(reclaim, "load_preservation_receipts", dict) + monkeypatch.setattr(reclaim, "load_reclaim_acceptance", list) + monkeypatch.setattr(reclaim, "load_estate_custody_context", lambda: None) + monkeypatch.setattr( + reclaim, + "build_candidate_manifest", + lambda *_args, **_kwargs: ( + {"schema": "test", "candidates": candidates}, + plan_sha, + [], + [], + ), + ) + monkeypatch.setattr( + reclaim, + "classify", + lambda *_args, **_kwargs: ("remove-residue", "generated-log-shell"), + ) + monkeypatch.setattr(reclaim, "reclaim_accepted", lambda *_args, **_kwargs: (True, "accepted")) + monkeypatch.setattr( + reclaim, + "abandonment_quarantine_root", + lambda path: ( + (_ for _ in ()).throw(ValueError("quarantine-must-be-outside-canonical-worktree-inventory")) + if path == unsafe + else tmp_path / "quarantine" + ), + ) + monkeypatch.setattr( + reclaim, + "quarantine_path", + lambda *_args, **_kwargs: {"receipt_path": tmp_path / "receipt.json"}, + ) + monkeypatch.setattr(reclaim, "persist_apply_receipt", lambda **_kwargs: None) + + assert reclaim.main() == 1 + payload = json.loads(capsys.readouterr().out) + assert payload["failed"] == [ + { + "root": "unsafe", + "reason": "quarantine-must-be-outside-canonical-worktree-inventory", + } + ] + assert payload["reclaimed"] == [ + { + "root": "safe", + "detail": "remove-residue:generated-log-shell:abandonment=receipt.json", + } + ] + assert unsafe.exists() + + def test_persist_apply_receipt_records_finite_completion_in_log_and_marker(tmp_path: Path, monkeypatch) -> None: reclaim = load_reclaim_worktrees() marker = tmp_path / "logs" / ".reclaim-last" @@ -402,6 +563,37 @@ def test_reclaim_acceptance_requires_archive_and_redaction_proofs(tmp_path: Path assert reason == "missing-reclaim-acceptance" +def test_reclaim_acceptance_requires_exact_path_even_for_same_root_name(tmp_path: Path) -> None: + reclaim = load_reclaim_worktrees() + reclaim.STANDING_ACCEPTANCE = False + first = tmp_path / "first" / "same-slug" + second = tmp_path / "second" / "same-slug" + first.mkdir(parents=True) + second.mkdir(parents=True) + foreign = acceptance_event(first) + pathless = acceptance_event(second) + pathless.pop("path") + + assert reclaim.reclaim_accepted( + second, + "remove-worktree", + "clean+merged+idle", + [foreign], + ) == (False, "missing-reclaim-acceptance") + assert reclaim.reclaim_accepted( + second, + "remove-worktree", + "clean+merged+idle", + [pathless], + ) == (False, "missing-reclaim-acceptance") + assert reclaim.reclaim_accepted( + second, + "remove-worktree", + "clean+merged+idle", + [acceptance_event(second)], + ) == (True, "reclaim-accepted") + + def test_reclaim_generated_payloads_cleans_inactive_ignored_dirs(tmp_path: Path, monkeypatch) -> None: reclaim = load_reclaim_worktrees() monkeypatch.setenv("LIMEN_RECLAIM_GENERATED", "1") @@ -418,7 +610,7 @@ def test_reclaim_generated_payloads_cleans_inactive_ignored_dirs(tmp_path: Path, ) (repo / "node_modules").mkdir() (repo / "node_modules" / "dep.txt").write_text("generated\n", encoding="utf-8") - quarantine = tmp_path / "quarantine" + quarantine = xdg_quarantine(tmp_path, monkeypatch, "generated") monkeypatch.setattr(reclaim, "active_async_task_prefixes", lambda: set()) monkeypatch.setattr(reclaim, "ABANDONMENT_QUARANTINE", str(quarantine)) @@ -478,13 +670,14 @@ def test_reclaim_generated_payloads_preserves_tracked_generated_name( ) monkeypatch.setattr(reclaim, "active_async_task_prefixes", lambda: set()) - monkeypatch.setattr(reclaim, "ABANDONMENT_QUARANTINE", str(tmp_path / "quarantine")) + quarantine = xdg_quarantine(tmp_path, monkeypatch, "generated") + monkeypatch.setattr(reclaim, "ABANDONMENT_QUARANTINE", str(quarantine)) target = type("Target", (), {"path": repo, "min_age_h": 0})() result = reclaim.reclaim_generated_payloads([target]) assert result["cleaned"] == [{"root": "repo", "detail": "quarantined:0"}] assert (repo / "dist" / "bundle.js").read_text(encoding="utf-8") == "tracked\n" - assert not (tmp_path / "quarantine").exists() + assert not quarantine.exists() def test_reclaim_root_apply_does_not_run_generated_cleanup( @@ -1033,11 +1226,16 @@ def test_orphan_detector_finds_dead_gitdir_and_ignores_live(tmp_path: Path) -> N assert reclaim.orphan_gitdir_name(live) is None -def test_reclaim_quarantines_dead_gitdir_orphan_under_throwaway_when_armed(tmp_path: Path, monkeypatch) -> None: +@pytest.mark.parametrize("source", ["dispatch-root", "canonical-runtime-worktree:owner--repo"]) +def test_reclaim_quarantines_dead_gitdir_orphan_under_throwaway_when_armed( + tmp_path: Path, + monkeypatch, + source: str, +) -> None: reclaim = load_reclaim_worktrees() monkeypatch.setattr(reclaim, "ORPHAN_SWEEP", True) orphan = _dead_gitdir_orphan(tmp_path) - action, reason = reclaim.classify(orphan, time.time(), 0, source="dispatch-root") + action, reason = reclaim.classify(orphan, time.time(), 0, source=source) assert (action, reason) == ("quarantine-orphan", reclaim.ORPHAN_REASON) @@ -1046,7 +1244,7 @@ def test_quarantine_orphan_moves_and_never_deletes(tmp_path: Path, monkeypatch) # work that never walked its lifecycle". After the sweep the source is gone but every byte, walked # or not, survives in quarantine, and a receipt records where it went. reclaim = load_reclaim_worktrees() - qroot = tmp_path / "quarantine" + qroot = xdg_quarantine(tmp_path, monkeypatch, "orphans") monkeypatch.setattr(reclaim, "ORPHAN_QUARANTINE", str(qroot)) monkeypatch.setattr(reclaim, "ORPHAN_QUARANTINE_LOG", tmp_path / "orphan-quarantine.jsonl") orphan = _dead_gitdir_orphan(tmp_path, "wt-unwalked") @@ -1064,11 +1262,64 @@ def test_quarantine_orphan_moves_and_never_deletes(tmp_path: Path, monkeypatch) assert receipt["recoverable"] == "moved-not-deleted" and receipt["to"] == dest +def test_canonical_quarantine_roots_are_persistent_xdg_data_outside_workspace( + tmp_path: Path, + monkeypatch, +) -> None: + workspace = tmp_path / "Workspace" + canonical_root = workspace / "runtime" / "worktrees" + unit = canonical_root / "owner--repo--digest" / "same-slug" + unit.mkdir(parents=True) + monkeypatch.setenv("WORKSPACE_ROOT", str(workspace)) + data_home = tmp_path / "xdg-data" + monkeypatch.setenv("XDG_DATA_HOME", str(data_home)) + reclaim = load_reclaim_worktrees() + monkeypatch.setattr(reclaim, "ABANDONMENT_QUARANTINE", "") + monkeypatch.setattr(reclaim, "ORPHAN_QUARANTINE", "") + + abandonment = reclaim.abandonment_quarantine_root(unit) + orphan = reclaim.orphan_quarantine_root(unit) + abandonment.mkdir(parents=True) + orphan.mkdir(parents=True) + + assert abandonment == data_home / "limen" / "worktree-abandonment" + assert orphan == data_home / "limen" / "orphan-quarantine" + assert not abandonment.is_relative_to(workspace) + assert not orphan.is_relative_to(workspace) + assert abandonment.stat().st_dev == unit.stat().st_dev == orphan.stat().st_dev + assert list(canonical_root.glob("*/*")) == [unit] + + +@pytest.mark.parametrize("variable", ["ABANDONMENT_QUARANTINE", "ORPHAN_QUARANTINE"]) +def test_quarantine_override_inside_canonical_inventory_is_denied( + tmp_path: Path, + monkeypatch, + variable: str, +) -> None: + workspace = tmp_path / "Workspace" + canonical_root = workspace / "runtime" / "worktrees" + unit = canonical_root / "owner--repo--digest" / "same-slug" + unit.mkdir(parents=True) + monkeypatch.setenv("WORKSPACE_ROOT", str(workspace)) + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "xdg-data")) + reclaim = load_reclaim_worktrees() + monkeypatch.setattr(reclaim, variable, str(canonical_root / "_unsafe")) + + with pytest.raises(ValueError, match="quarantine-root"): + if variable == "ABANDONMENT_QUARANTINE": + reclaim.abandonment_quarantine_root(unit) + else: + reclaim.orphan_quarantine_root(unit) + + def test_quarantine_orphan_refuses_when_destination_unwritable(tmp_path: Path, monkeypatch) -> None: # Fail-closed: if quarantine can't be prepared, the orphan is LEFT in place, never half-removed. reclaim = load_reclaim_worktrees() - monkeypatch.setattr(reclaim, "ORPHAN_QUARANTINE", str(tmp_path / "afile" / "under")) - (tmp_path / "afile").write_text("not a dir\n", encoding="utf-8") # mkdir under a file → OSError + data_home = tmp_path / "xdg-data" + monkeypatch.setenv("XDG_DATA_HOME", str(data_home)) + (data_home / "limen").mkdir(parents=True) + (data_home / "limen" / "afile").write_text("not a dir\n", encoding="utf-8") + monkeypatch.setattr(reclaim, "ORPHAN_QUARANTINE", str(data_home / "limen" / "afile" / "under")) orphan = _dead_gitdir_orphan(tmp_path, "wt-keep") ok, reason = reclaim.quarantine_orphan(orphan, "20260715T000000Z") assert ok is False diff --git a/cli/tests/test_substrate_convergence.py b/cli/tests/test_substrate_convergence.py new file mode 100644 index 000000000..88d6e19f3 --- /dev/null +++ b/cli/tests/test_substrate_convergence.py @@ -0,0 +1,1301 @@ +"""Contract tests for the literal Workspace convergence court.""" + +from __future__ import annotations + +from datetime import UTC, datetime +import importlib.util +import json +import os +from pathlib import Path +import socket +import subprocess +import sys +import tempfile + +import pytest +import yaml + +import limen.substrate_convergence as convergence +from limen.substrate_convergence import ManifestError, audit, load_active_cwds + + +_INVENTORY_SHA256 = "1" * 64 +_PLAN_SHA256 = "2" * 64 +_CONTENT_SHA256 = "3" * 64 + + +def _sealed_inventory( + *, + content_sha256: str = _CONTENT_SHA256, +) -> dict[str, object]: + return { + "label": "life", + "sealed": True, + "inventory_sha256": _INVENTORY_SHA256, + "plan_sha256": _PLAN_SHA256, + "content_sha256": content_sha256, + } + + +def _restoration_receipt( + inventory: dict[str, object], + *, + restoration_passed: bool = True, +) -> dict[str, object]: + return { + "label": str(inventory.get("label", "life")), + "restoration_passed": restoration_passed, + "copy_count": 2 if restoration_passed else 1, + "independent_physical_devices": restoration_passed, + "inventory_sha256": inventory["inventory_sha256"], + "plan_sha256": inventory["plan_sha256"], + "content_sha256": inventory["content_sha256"], + } + + +def _git(repo: Path, *args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["git", "-C", str(repo), "-c", "user.name=Test", "-c", "user.email=test@example.invalid", *args], + capture_output=True, + text=True, + check=True, + ) + + +def _seed_repo(path: Path, remote: Path) -> None: + remote.mkdir(parents=True) + _git(remote, "init", "--bare", "-q") + path.mkdir(parents=True) + _git(path, "init", "-q") + (path / "README.md").write_text("fixture\n", encoding="utf-8") + _git(path, "add", "README.md") + _git(path, "commit", "-qm", "seed") + _git(path, "branch", "-M", "main") + _git(path, "remote", "add", "origin", str(remote)) + _git(path, "push", "-u", "origin", "main") + + +def _base_rows(remote: Path) -> list[dict[str, object]]: + return [ + {"path": "library", "kind": "structural", "owner_ref": "portvs", "residency": "structural"}, + { + "path": "library/engine", + "kind": "structural", + "owner_ref": "portvs", + "residency": "structural", + }, + { + "path": "library/engine/organvm", + "kind": "structural", + "owner_ref": "portvs", + "residency": "structural", + }, + { + "path": "library/engine/organvm/limen", + "kind": "repository", + "owner_ref": "organvm/limen", + "residency": "laptop", + "remote": str(remote), + "custody_ref": "refs/remotes/origin/main", + }, + {"path": "domains", "kind": "structural", "owner_ref": "portvs", "residency": "structural"}, + { + "path": "domains/governance", + "kind": "structural", + "owner_ref": "limen", + "residency": "structural", + }, + {"path": "private", "kind": "structural", "owner_ref": "portvs", "residency": "structural"}, + { + "path": "private/life", + "kind": "private", + "owner_ref": "private-inventory/life", + "residency": "private", + "sealed_inventory_ref": "manifest://inventory.json", + "restoration_receipt_ref": "manifest://receipts.json#life", + "custody_label": "life", + }, + {"path": "runtime", "kind": "structural", "owner_ref": "limen", "residency": "structural"}, + { + "path": "runtime/worktrees", + "kind": "ephemeral", + "owner_ref": "limen/reaper", + "residency": "ephemeral", + "expires_after": 604800, + "reaper": "limen worktree reap", + }, + ] + + +def _fixture( + tmp_path: Path, + *, + rows: list[dict[str, object]] | None = None, + compatibility_links: list[dict[str, str]] | None = None, + valid_custody: bool = True, +) -> tuple[Path, Path, Path]: + workspace = tmp_path / "Workspace" + remote = tmp_path / "remote.git" + manifest_dir = tmp_path / "portvs" / "governance" + manifest_dir.mkdir(parents=True) + selected_rows = rows or _base_rows(remote) + seeded_remotes: set[str] = set() + for row in selected_rows: + path = workspace / str(row["path"]) + if row["kind"] == "repository": + remote_key = str(row["remote"]) + if remote_key not in seeded_remotes: + _seed_repo(path, Path(remote_key)) + seeded_remotes.add(remote_key) + else: + path.mkdir(parents=True, exist_ok=True) + elif row["kind"] == "index": + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("{}\n", encoding="utf-8") + else: + path.mkdir(parents=True, exist_ok=True) + sealed_inventory = _sealed_inventory() + (manifest_dir / "inventory.json").write_text( + json.dumps({"sealed_inventories": [sealed_inventory]}) + "\n", + encoding="utf-8", + ) + (manifest_dir / "receipts.json").write_text( + json.dumps( + { + "receipts": [ + _restoration_receipt( + sealed_inventory, + restoration_passed=valid_custody, + ) + ] + } + ) + + "\n", + encoding="utf-8", + ) + data = { + "schema": "portvs.workspace_manifest.v1", + "workspace_root": str(workspace), + "limits": { + "max_scan_entries": 10_000, + "max_violations": 0, + "max_unmeasured": 0, + "max_compatibility_links": 0, + }, + "rows": selected_rows, + "migration": {"compatibility_links": compatibility_links or []}, + } + manifest = manifest_dir / "workspace-manifest.yaml" + manifest.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + return manifest, workspace, remote + + +def _codes(report: dict[str, object]) -> set[str]: + return {str(item["code"]) for item in report["violations"]} # type: ignore[index] + + +def test_exact_literal_tree_passes(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + report = audit(manifest, workspace_root=workspace, active_cwds=[], now=datetime(2026, 7, 30, tzinfo=UTC)) + assert report["ok"] is True, report + assert report["counts"]["violations"] == 0 + + +def test_empty_declared_container_persists_and_missing_container_fails(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + governance = workspace / "domains" / "governance" + assert list(governance.iterdir()) == [] + governance.rmdir() + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + assert "declared_entry_missing" in _codes(report) + + +def test_undeclared_root_is_rejected_without_wildcard_bypass(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + (workspace / ".DS_Store").write_text("noise", encoding="utf-8") + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + assert "undeclared_entry" in _codes(report) + assert any(item["path"] == ".DS_Store" for item in report["violations"]) + + +def test_manifest_rejects_traversal(tmp_path: Path) -> None: + manifest, workspace, remote = _fixture(tmp_path) + data = yaml.safe_load(manifest.read_text(encoding="utf-8")) + data["rows"].append( + { + "path": "domains/../escape", + "kind": "structural", + "owner_ref": "portvs", + "residency": "structural", + } + ) + manifest.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + with pytest.raises(ManifestError, match="not a normalized relative path"): + audit(manifest, workspace_root=workspace, active_cwds=[]) + assert remote.exists() + + +def test_symlink_escape_is_rejected(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + outside = tmp_path / "outside" + outside.mkdir() + governance = workspace / "domains" / "governance" + governance.rmdir() + governance.symlink_to(outside, target_is_directory=True) + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + assert "symlink_escape" in _codes(report) + + +def test_repository_in_wrong_container_fails(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + expected = workspace / "library" / "engine" / "organvm" / "limen" + wrong = workspace / "domains" / "limen" + expected.rename(wrong) + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + assert "declared_entry_missing" in _codes(report) + assert "undeclared_entry" in _codes(report) + + +def test_duplicate_checkout_remote_is_rejected_by_manifest(tmp_path: Path) -> None: + remote = tmp_path / "remote.git" + rows = _base_rows(remote) + rows.extend( + [ + { + "path": "library/storefront", + "kind": "structural", + "owner_ref": "portvs", + "residency": "structural", + }, + { + "path": "library/storefront/duplicate", + "kind": "repository", + "owner_ref": "organvm/limen", + "residency": "laptop", + "remote": str(remote), + "custody_ref": "refs/remotes/origin/main", + }, + ] + ) + manifest, workspace, _ = _fixture(tmp_path, rows=rows) + with pytest.raises(ManifestError, match="one canonical physical home"): + audit(manifest, workspace_root=workspace, active_cwds=[]) + + +def test_nested_unregistered_repository_fails(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + nested = workspace / "library" / "engine" / "organvm" / "limen" / ".worktrees" / "competing" + nested.mkdir(parents=True) + _git(nested, "init", "-q") + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + assert "undeclared_nested_repository" in _codes(report) + + +def test_canonical_runtime_worktree_is_measured_without_nested_repository_violation( + tmp_path: Path, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + worktree = workspace / "runtime" / "worktrees" / "organvm--limen--fixture" / "bounded-fix" + worktree.mkdir(parents=True) + _git(worktree, "init", "-q") + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert "undeclared_nested_repository" not in _codes(report) + runtime_receipt = next(row for row in report["ephemeral_roots"] if row["path"] == "runtime/worktrees") + assert runtime_receipt["namespace_count"] == 1 + assert runtime_receipt["entry_count"] == 1 + assert report["ok"] is True, report + + +def test_xdg_quarantine_defaults_leave_workspace_court_byte_identical( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + unit = workspace / "runtime" / "worktrees" / "organvm--limen--fixture" / "bounded-fix" + unit.mkdir(parents=True) + _git(unit, "init", "-q") + data_home = tmp_path / "xdg-data" + monkeypatch.setenv("WORKSPACE_ROOT", str(workspace)) + monkeypatch.setenv("XDG_DATA_HOME", str(data_home)) + + script = Path(__file__).resolve().parents[2] / "scripts" / "reclaim-worktrees.py" + module_name = "reclaim_worktrees_substrate_court" + spec = importlib.util.spec_from_file_location(module_name, script) + reclaim = importlib.util.module_from_spec(spec) + sys.modules[module_name] = reclaim + assert spec and spec.loader + spec.loader.exec_module(reclaim) + + now = datetime(2026, 7, 30, tzinfo=UTC) + before = audit(manifest, workspace_root=workspace, active_cwds=[], now=now) + abandonment = reclaim.abandonment_quarantine_root(unit) + orphan = reclaim.orphan_quarantine_root(unit) + abandonment.mkdir(parents=True) + orphan.mkdir(parents=True) + after = audit(manifest, workspace_root=workspace, active_cwds=[], now=now) + + assert abandonment == data_home / "limen" / "worktree-abandonment" + assert orphan == data_home / "limen" / "orphan-quarantine" + assert not abandonment.is_relative_to(workspace) + assert not orphan.is_relative_to(workspace) + assert json.dumps(after, sort_keys=True) == json.dumps(before, sort_keys=True) + + +@pytest.mark.parametrize("symlink_level", ["namespace", "unit"]) +def test_runtime_worktree_symlink_is_a_bounded_physical_containment_violation( + tmp_path: Path, + symlink_level: str, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + runtime_root = workspace / "runtime" / "worktrees" + outside = tmp_path / "outside-runtime" + outside.mkdir() + (outside / "sentinel").write_text("do not follow\n", encoding="utf-8") + namespace = runtime_root / "foreign-key" + if symlink_level == "namespace": + namespace.symlink_to(outside, target_is_directory=True) + expected_path = "runtime/worktrees/foreign-key" + expected_namespace_count = 0 + else: + namespace.mkdir() + (namespace / "foreign-unit").symlink_to(outside, target_is_directory=True) + expected_path = "runtime/worktrees/foreign-key/foreign-unit" + expected_namespace_count = 1 + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + matching = [ + violation + for violation in report["violations"] + if violation["code"] == "ephemeral_nonphysical_entry" and violation["path"] == expected_path + ] + runtime_receipt = next(row for row in report["ephemeral_roots"] if row["path"] == "runtime/worktrees") + assert report["ok"] is False + assert len(matching) == 1 + assert runtime_receipt["namespace_count"] == expected_namespace_count + assert runtime_receipt["entry_count"] == 1 + assert (outside / "sentinel").read_text(encoding="utf-8") == "do not follow\n" + + +@pytest.mark.parametrize("invalid_shape", ["empty-namespace", "namespace-file", "unit-file"]) +def test_runtime_worktree_invalid_physical_shape_fails_closed( + tmp_path: Path, + invalid_shape: str, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + runtime_root = workspace / "runtime" / "worktrees" + namespace = runtime_root / "arbitrary-key" + if invalid_shape == "empty-namespace": + namespace.mkdir() + expected_code = "ephemeral_empty_namespace" + expected_path = "runtime/worktrees/arbitrary-key" + expected_namespace_count = 1 + expected_entry_count = 0 + elif invalid_shape == "namespace-file": + namespace.write_text("not a namespace\n", encoding="utf-8") + expected_code = "ephemeral_nonphysical_entry" + expected_path = "runtime/worktrees/arbitrary-key" + expected_namespace_count = 0 + expected_entry_count = 1 + else: + namespace.mkdir() + (namespace / "not-a-worktree").write_text("not a worktree\n", encoding="utf-8") + expected_code = "ephemeral_nonphysical_entry" + expected_path = "runtime/worktrees/arbitrary-key/not-a-worktree" + expected_namespace_count = 1 + expected_entry_count = 1 + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + matching = [ + violation + for violation in report["violations"] + if violation["code"] == expected_code and violation["path"] == expected_path + ] + runtime_receipt = next(row for row in report["ephemeral_roots"] if row["path"] == "runtime/worktrees") + assert report["ok"] is False + assert len(matching) == 1 + assert runtime_receipt["namespace_count"] == expected_namespace_count + assert runtime_receipt["entry_count"] == expected_entry_count + + +def test_dirty_and_unpushed_repository_cannot_converge(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + repo = workspace / "library" / "engine" / "organvm" / "limen" + (repo / "dirty.txt").write_text("local only\n", encoding="utf-8") + _git(repo, "add", "dirty.txt") + _git(repo, "commit", "-qm", "not pushed") + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + assert {"repository_unpreserved", "repository_unpreserved_branches"} <= _codes(report) + + +def test_deleted_live_remote_ref_revokes_local_custody(tmp_path: Path) -> None: + manifest, workspace, remote = _fixture(tmp_path) + _git(remote, "update-ref", "-d", "refs/heads/main") + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert { + "repository_custody_missing", + "repository_unpreserved", + "repository_unpreserved_branches", + } <= _codes(report) + + +def test_unreachable_live_remote_is_unmeasured_not_cached_green(tmp_path: Path) -> None: + manifest, workspace, remote = _fixture(tmp_path) + remote.rename(tmp_path / "remote-offline.git") + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert "repository_unmeasured" in _codes(report) + assert report["counts"]["unmeasured"] == 1 + + +def test_unpushed_non_current_branch_blocks_convergence(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + repo = workspace / "library" / "engine" / "organvm" / "limen" + _git(repo, "switch", "-qc", "local-only") + (repo / "branch-only.txt").write_text("unique branch state\n", encoding="utf-8") + _git(repo, "add", "branch-only.txt") + _git(repo, "commit", "-qm", "local branch state") + _git(repo, "switch", "-q", "main") + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert "repository_unpreserved_branches" in _codes(report) + assert "repository_unpreserved" not in _codes(report) + + +def test_uncustodied_stash_blocks_convergence(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + repo = workspace / "library" / "engine" / "organvm" / "limen" + (repo / "README.md").write_text("stashed fixture\n", encoding="utf-8") + _git(repo, "stash", "push", "-qm", "private stash") + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert "repository_uncustodied_stashes" in _codes(report) + + +def test_stash_inspection_error_is_unmeasured( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + real_run_git = convergence._run_git + + def failing_stash_probe( + repo: Path, + *args: str, + timeout: float = convergence.GIT_TIMEOUT_SECONDS, + ) -> subprocess.CompletedProcess[str]: + if args == ("rev-parse", "--verify", "--quiet", "refs/stash"): + return subprocess.CompletedProcess(["git"], 128, "", "fatal: cannot inspect refs/stash") + return real_run_git(repo, *args, timeout=timeout) + + monkeypatch.setattr(convergence, "_run_git", failing_stash_probe) + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert "repository_unmeasured" in _codes(report) + assert any("cannot inspect refs/stash" in row["message"] for row in report["violations"]) + + +@pytest.mark.parametrize( + ("ignored_path", "expected_code"), + [ + (".env", "repository_uncustodied_ignored"), + ("node_modules/cache/index.bin", None), + ], +) +def test_ignored_repository_entries_require_loss_free_evidence( + tmp_path: Path, + ignored_path: str, + expected_code: str | None, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + repo = workspace / "library" / "engine" / "organvm" / "limen" + top = ignored_path.split("/", 1)[0] + (repo / ".gitignore").write_text(f"/{top}/\n" if "/" in ignored_path else f"/{top}\n", encoding="utf-8") + _git(repo, "add", ".gitignore") + _git(repo, "commit", "-qm", "declare ignored fixture") + _git(repo, "push", "origin", "main") + ignored = repo / ignored_path + ignored.parent.mkdir(parents=True, exist_ok=True) + ignored.write_text("local ignored payload\n", encoding="utf-8") + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + if expected_code is None: + assert "repository_uncustodied_ignored" not in _codes(report) + assert report["ok"] is True, report + else: + assert expected_code in _codes(report) + + +def test_private_root_requires_dual_copy_restoration_receipt(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path, valid_custody=False) + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + assert "private_restoration_unverified" in _codes(report) + + +@pytest.mark.parametrize( + "inventory", + [ + {"sealed_inventories": [{"label": "other", "sealed": True}]}, + {"sealed_inventories": [{"label": "life", "sealed": False}]}, + {"sealed": True}, + ], +) +def test_private_inventory_requires_matching_sealed_evidence( + tmp_path: Path, + inventory: dict[str, object], +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + inventory_path = manifest.parent / "inventory.json" + inventory_path.write_text(json.dumps(inventory), encoding="utf-8") + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + custody = report["private_custody"][0] + + assert "private_inventory_unverified" in _codes(report) + assert custody["inventory_available"] is True + assert custody["sealed_inventory_verified"] is False + assert custody["custody_verified"] is False + + +def test_malformed_private_inventory_fails_closed(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + (manifest.parent / "inventory.json").write_text("{not-json", encoding="utf-8") + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert "unmeasured_state" in _codes(report) + assert report["counts"]["unmeasured"] >= 1 + assert report["private_custody"][0]["custody_verified"] is False + + +@pytest.mark.parametrize("suffix", ["json", "jsonl"]) +def test_shared_private_custody_source_is_opened_once_across_rows( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + suffix: str, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + data = yaml.safe_load(manifest.read_text(encoding="utf-8")) + life = _sealed_inventory() + finance = {**_sealed_inventory(), "label": "finance"} + shared = manifest.parent / f"shared.{suffix}" + if suffix == "json": + shared.write_text( + json.dumps( + { + "sealed_inventories": [life, finance], + "receipts": [_restoration_receipt(life), _restoration_receipt(finance)], + } + ), + encoding="utf-8", + ) + else: + shared.write_text( + "\n".join( + json.dumps(row) for row in (life, finance, _restoration_receipt(life), _restoration_receipt(finance)) + ) + + "\n", + encoding="utf-8", + ) + life_row = next(row for row in data["rows"] if row["kind"] == "private") + life_row["sealed_inventory_ref"] = f"manifest://shared.{suffix}#life" + life_row["restoration_receipt_ref"] = f"manifest://shared.{suffix}#life" + data["rows"].append( + { + "path": "private/finance", + "kind": "private", + "owner_ref": "private-inventory/finance", + "residency": "private", + "sealed_inventory_ref": f"manifest://shared.{suffix}#finance", + "restoration_receipt_ref": f"manifest://shared.{suffix}#finance", + "custody_label": "finance", + } + ) + (workspace / "private" / "finance").mkdir() + manifest.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + monkeypatch.setattr(convergence, "CUSTODY_LEDGER_MAX_ROWS", 4) + real_open = os.open + real_read_text = Path.read_text + open_count = 0 + + def counting_open(path: os.PathLike[str] | str, *args: object, **kwargs: object): + nonlocal open_count + if Path(path).resolve(strict=False) == shared.resolve(strict=False): + open_count += 1 + return real_open(path, *args, **kwargs) + + def guarded_read_text(path: Path, *args: object, **kwargs: object): + if path.resolve(strict=False) == shared.resolve(strict=False): + raise AssertionError("custody source must use the bounded binary reader") + return real_read_text(path, *args, **kwargs) + + monkeypatch.setattr(os, "open", counting_open) + monkeypatch.setattr(Path, "read_text", guarded_read_text) + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert report["ok"] is True, report + assert open_count == 1 + assert len(report["private_custody"]) == 2 + assert all(row["custody_verified"] is True for row in report["private_custody"]) + + +def test_private_custody_aggregate_byte_exhaustion_is_unmeasured( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + data = yaml.safe_load(manifest.read_text(encoding="utf-8")) + finance = {**_sealed_inventory(), "label": "finance"} + data["rows"].append( + { + "path": "private/finance", + "kind": "private", + "owner_ref": "private-inventory/finance", + "residency": "private", + "sealed_inventory_ref": "manifest://finance-inventory.json", + "restoration_receipt_ref": "manifest://finance-receipts.json#finance", + "custody_label": "finance", + } + ) + (workspace / "private" / "finance").mkdir() + finance_inventory = manifest.parent / "finance-inventory.json" + finance_receipts = manifest.parent / "finance-receipts.json" + finance_inventory.write_text( + json.dumps({"sealed_inventories": [finance]}), + encoding="utf-8", + ) + finance_receipts.write_text( + json.dumps({"receipts": [_restoration_receipt(finance)]}) + (" " * 1024), + encoding="utf-8", + ) + manifest.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + complete_prefix_bytes = sum( + path.stat().st_size + for path in ( + manifest.parent / "inventory.json", + manifest.parent / "receipts.json", + finance_inventory, + ) + ) + monkeypatch.setattr(convergence, "CUSTODY_LEDGER_MAX_BYTES", complete_prefix_bytes + 1) + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert "unmeasured_state" in _codes(report) + assert report["counts"]["unmeasured"] >= 1 + assert report["private_custody"][0]["sealed_inventory_verified"] is True + assert report["private_custody"][0]["restoration_verified"] is True + assert [row["custody_verified"] for row in report["private_custody"]] == [False, False] + assert not any(row["code"] == "private_restoration_unverified" for row in report["violations"]) + + +def test_private_custody_aggregate_row_exhaustion_counts_irrelevant_records( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + receipt_path = manifest.parent / "receipts.json" + valid = _restoration_receipt(_sealed_inventory()) + receipt_path.write_text( + json.dumps({"receipts": [valid, "irrelevant-but-counted"]}), + encoding="utf-8", + ) + monkeypatch.setattr(convergence, "CUSTODY_LEDGER_MAX_ROWS", 2) + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert "unmeasured_state" in _codes(report) + assert report["counts"]["unmeasured"] >= 1 + assert report["private_custody"][0]["custody_verified"] is False + assert any("row ceiling exceeded" in row["message"] for row in report["violations"]) + + +def test_private_custody_shared_deadline_stops_later_source( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + receipt_path = (manifest.parent / "receipts.json").resolve() + clock = [0.0] + receipt_opens = 0 + real_read_rows = convergence.CustodyLedgerReader.read_rows + real_open = Path.open + calls = 0 + + def advance_after_first_selection( + reader: convergence.CustodyLedgerReader, + path: Path, + *, + collection_keys: tuple[str, ...] = ("receipts",), + ) -> convergence.CustodyLedgerRows: + nonlocal calls + result = real_read_rows(reader, path, collection_keys=collection_keys) + calls += 1 + if calls == 1: + clock[0] = convergence.CUSTODY_LEDGER_DEADLINE_SECONDS + 1 + return result + + def counting_open(path: Path, *args: object, **kwargs: object): + nonlocal receipt_opens + if path.resolve(strict=False) == receipt_path: + receipt_opens += 1 + return real_open(path, *args, **kwargs) + + monkeypatch.setattr(convergence, "_monotonic", lambda: clock[0]) + monkeypatch.setattr(convergence.CustodyLedgerReader, "read_rows", advance_after_first_selection) + monkeypatch.setattr(Path, "open", counting_open) + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert "unmeasured_state" in _codes(report) + assert report["counts"]["unmeasured"] >= 1 + assert report["private_custody"][0]["custody_verified"] is False + assert receipt_opens == 0 + assert any("deadline exhausted" in row["message"] for row in report["violations"]) + + +@pytest.mark.parametrize("failure", ["invalid-utf8", "permission"]) +def test_private_custody_inspection_failure_is_unmeasured( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + failure: str, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + inventory = (manifest.parent / "inventory.json").resolve() + if failure == "invalid-utf8": + inventory.write_bytes(b"\xff\xfe") + else: + real_open = os.open + + def denied_open(path: os.PathLike[str] | str, *args: object, **kwargs: object): + if Path(path).resolve(strict=False) == inventory: + raise PermissionError("fixture denial") + return real_open(path, *args, **kwargs) + + monkeypatch.setattr(os, "open", denied_open) + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert "unmeasured_state" in _codes(report) + assert report["counts"]["unmeasured"] >= 1 + assert report["private_custody"][0]["custody_verified"] is False + + +def test_custody_reader_rejects_special_files_without_opening_them(tmp_path: Path) -> None: + regular = tmp_path / "regular.json" + regular.write_text("{}\n", encoding="utf-8") + symlink = tmp_path / "symlink.json" + symlink.symlink_to(regular) + fifo = tmp_path / "fifo.jsonl" + os.mkfifo(fifo) + directory = tmp_path / "directory.json" + directory.mkdir() + with tempfile.TemporaryDirectory(prefix="limen-custody-socket-") as socket_dir: + socket_path = Path(socket_dir) / "ledger.socket" + unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + unix_socket.bind(str(socket_path)) + try: + for special in (symlink, fifo, directory, socket_path, Path("/dev/null")): + result = convergence.CustodyLedgerReader(deadline_seconds=0.1).read_rows(special) + assert result.available is True + assert result.rows == () + assert result.inspection_error is not None + assert ( + "source is a symlink" in result.inspection_error + or "source is not a regular file" in result.inspection_error + ) + finally: + unix_socket.close() + + +def test_custody_reader_fifo_probe_is_subprocess_bounded_and_reaped(tmp_path: Path) -> None: + fifo = tmp_path / "blocked.jsonl" + os.mkfifo(fifo) + cli_src = Path(__file__).resolve().parents[1] / "src" + script = "\n".join( + ( + "import json, sys", + "from pathlib import Path", + "from limen.substrate_convergence import CustodyLedgerReader", + "result = CustodyLedgerReader(deadline_seconds=0.1).read_rows(Path(sys.argv[1]))", + "print(json.dumps({'available': result.available, 'error': result.inspection_error}))", + ) + ) + env = dict(os.environ) + env["PYTHONPATH"] = os.pathsep.join(filter(None, (str(cli_src), env.get("PYTHONPATH", "")))) + process = subprocess.Popen( + [sys.executable, "-c", script, str(fifo)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + ) + try: + stdout, stderr = process.communicate(timeout=2) + except subprocess.TimeoutExpired: + process.kill() + process.communicate() + pytest.fail("custody FIFO inspection exceeded its bounded subprocess deadline") + + assert process.returncode == 0, stderr + payload = json.loads(stdout) + assert payload["available"] is True + assert "not a regular file" in payload["error"] + with pytest.raises(ProcessLookupError): + os.kill(process.pid, 0) + + +def test_private_reference_resolves_through_declared_legacy_repo_during_migration( + tmp_path: Path, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + data = yaml.safe_load(manifest.read_text(encoding="utf-8")) + repo_row = next(row for row in data["rows"] if row["kind"] == "repository") + repo_row["legacy_paths"] = ["limen"] + sealed_inventory = _sealed_inventory() + custody = workspace / repo_row["path"] / "custody.json" + custody.write_text( + json.dumps( + { + "sealed_inventories": [sealed_inventory], + "receipts": [_restoration_receipt(sealed_inventory)], + } + ), + encoding="utf-8", + ) + private_row = next(row for row in data["rows"] if row["kind"] == "private") + private_row["sealed_inventory_ref"] = f"workspace://{repo_row['path']}/custody.json" + private_row["restoration_receipt_ref"] = f"workspace://{repo_row['path']}/custody.json#life" + manifest.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + (workspace / repo_row["path"]).rename(workspace / "limen") + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + custody_row = next(row for row in report["private_custody"] if row["path"] == "private/life") + assert custody_row["inventory_available"] is True + assert custody_row["restoration_verified"] is True + + +def test_private_receipt_selects_current_identity_and_reseal_invalidates_stale_receipts( + tmp_path: Path, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + receipt_path = manifest.parent / "receipts.json" + receipt_data = json.loads(receipt_path.read_text(encoding="utf-8")) + stale_inventory = _sealed_inventory(content_sha256="4" * 64) + receipt_data["receipts"].insert(0, _restoration_receipt(stale_inventory)) + receipt_path.write_text(json.dumps(receipt_data), encoding="utf-8") + + current_report = audit(manifest, workspace_root=workspace, active_cwds=[]) + assert current_report["ok"] is True, current_report + current_custody = current_report["private_custody"][0] + assert current_custody["restoration_identity_verified"] is True + + resealed_inventory = _sealed_inventory(content_sha256="5" * 64) + inventory_path = manifest.parent / "inventory.json" + inventory_path.write_text( + json.dumps({"sealed_inventories": [_sealed_inventory(), resealed_inventory]}), + encoding="utf-8", + ) + stale_report = audit(manifest, workspace_root=workspace, active_cwds=[]) + stale_custody = stale_report["private_custody"][0] + + assert "private_restoration_unverified" in _codes(stale_report) + assert stale_custody["sealed_inventory_verified"] is True + assert stale_custody["restoration_identity_verified"] is False + assert stale_custody["custody_verified"] is False + + +def test_active_compatibility_path_blocks_removal(tmp_path: Path) -> None: + link = { + "path": "limen", + "target": "library/engine/organvm/limen", + "owner_ref": "organvm/limen", + "expires_at": "2026-08-15T00:00:00Z", + } + manifest, workspace, _ = _fixture(tmp_path, compatibility_links=[link]) + (workspace / "limen").symlink_to(workspace / "library" / "engine" / "organvm" / "limen") + report = audit( + manifest, + workspace_root=workspace, + active_cwds=[workspace / "limen" / "scripts"], + now=datetime(2026, 7, 30, tzinfo=UTC), + ) + assert {"compatibility_link_unresolved", "active_legacy_path"} <= _codes(report) + + +def test_absent_compatibility_link_does_not_expire(tmp_path: Path) -> None: + link = { + "path": "limen", + "target": "library/engine/organvm/limen", + "owner_ref": "organvm/limen", + "expires_at": "2026-07-01T00:00:00Z", + } + manifest, workspace, _ = _fixture(tmp_path, compatibility_links=[link]) + + report = audit( + manifest, + workspace_root=workspace, + active_cwds=[], + now=datetime(2026, 7, 30, tzinfo=UTC), + ) + + assert "compatibility_link_expired" not in _codes(report) + assert report["ok"] is True, report + + +def test_manifest_rejects_multi_link_compatibility_cycle(tmp_path: Path) -> None: + links = [ + {"path": "a", "target": "b", "owner_ref": "a", "expires_at": "2026-08-15T00:00:00Z"}, + {"path": "b", "target": "a", "owner_ref": "b", "expires_at": "2026-08-15T00:00:00Z"}, + ] + manifest, workspace, _ = _fixture(tmp_path, compatibility_links=links) + + with pytest.raises(ManifestError, match="compatibility link graph contains a cycle"): + audit(manifest, workspace_root=workspace, active_cwds=[]) + + +def test_filesystem_compatibility_symlink_loop_fails_bounded(tmp_path: Path) -> None: + links = [ + { + "path": "a", + "target": "library/engine/organvm/limen", + "owner_ref": "a", + "expires_at": "2026-08-15T00:00:00Z", + }, + { + "path": "b", + "target": "library/engine/organvm/limen", + "owner_ref": "b", + "expires_at": "2026-08-15T00:00:00Z", + }, + ] + manifest, workspace, _ = _fixture(tmp_path, compatibility_links=links) + (workspace / "a").symlink_to("b") + (workspace / "b").symlink_to("a") + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert "unmeasured_state" in _codes(report) + assert all(row["present"] is True for row in report["compatibility_links"]) + + +def test_canonical_cwd_is_ambiguous_not_legacy_for_symlink_doorway(tmp_path: Path) -> None: + link = { + "path": "limen", + "target": "library/engine/organvm/limen", + "owner_ref": "organvm/limen", + "expires_at": "2026-08-15T00:00:00Z", + } + manifest, workspace, _ = _fixture(tmp_path, compatibility_links=[link]) + canonical = workspace / "library" / "engine" / "organvm" / "limen" + (workspace / "limen").symlink_to(canonical) + + report = audit( + manifest, + workspace_root=workspace, + active_cwds=[canonical / "scripts"], + now=datetime(2026, 7, 30, tzinfo=UTC), + ) + + assert "compatibility_link_unresolved" in _codes(report) + assert "active_legacy_path" not in _codes(report) + assert "unmeasured_state" in _codes(report) + link_report = report["compatibility_links"][0] + assert link_report["active_cwd_count"] == 0 + assert link_report["ambiguous_cwd_count"] == 1 + + +def test_physical_legacy_directory_cwd_remains_measurable(tmp_path: Path) -> None: + link = { + "path": "limen", + "target": "library/engine/organvm/limen", + "owner_ref": "organvm/limen", + "expires_at": "2026-08-15T00:00:00Z", + } + manifest, workspace, _ = _fixture(tmp_path, compatibility_links=[link]) + legacy = workspace / "limen" + legacy.mkdir() + + report = audit( + manifest, + workspace_root=workspace, + active_cwds=[legacy / "scripts"], + now=datetime(2026, 7, 30, tzinfo=UTC), + ) + + assert "active_legacy_path" in _codes(report) + assert "unmeasured_state" not in _codes(report) + link_report = report["compatibility_links"][0] + assert link_report["active_cwd_count"] == 1 + assert link_report["ambiguous_cwd_count"] == 0 + + +def test_workspace_root_symlink_is_rejected_before_resolution(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + alias = tmp_path / "Workspace-alias" + alias.symlink_to(workspace, target_is_directory=True) + + report = audit(manifest, workspace_root=alias, active_cwds=[]) + + assert "workspace_symlink" in _codes(report) + + +@pytest.mark.parametrize( + "missing", + [ + "max_scan_entries", + "max_violations", + "max_unmeasured", + "max_compatibility_links", + ], +) +def test_every_limit_field_is_required(tmp_path: Path, missing: str) -> None: + manifest, workspace, _ = _fixture(tmp_path) + data = yaml.safe_load(manifest.read_text(encoding="utf-8")) + del data["limits"][missing] + manifest.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + + with pytest.raises(ManifestError, match="limits missing required field"): + audit(manifest, workspace_root=workspace, active_cwds=[]) + + +def test_recursive_repositories_share_one_scan_budget(tmp_path: Path) -> None: + primary_remote = tmp_path / "remote.git" + secondary_remote = tmp_path / "secondary.git" + rows = _base_rows(primary_remote) + rows.extend( + [ + { + "path": "library/storefront", + "kind": "structural", + "owner_ref": "portvs", + "residency": "structural", + }, + { + "path": "library/storefront/second", + "kind": "repository", + "owner_ref": "organvm/second", + "residency": "laptop", + "remote": str(secondary_remote), + "custody_ref": "refs/remotes/origin/main", + }, + ] + ) + manifest, workspace, _ = _fixture(tmp_path, rows=rows) + for relative in ( + Path("library/engine/organvm/limen"), + Path("library/storefront/second"), + ): + repo = workspace / relative + for index in range(8): + (repo / f"fixture-{index}.txt").write_text(f"{index}\n", encoding="utf-8") + _git(repo, "add", *[f"fixture-{index}.txt" for index in range(8)]) + _git(repo, "commit", "-qm", "expand scan fixture") + _git(repo, "push", "origin", "main") + data = yaml.safe_load(manifest.read_text(encoding="utf-8")) + data["limits"]["max_scan_entries"] = 28 + manifest.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert report["scan_truncated"] is True + assert "unmeasured_state" in _codes(report) + + +def test_repository_fetches_share_one_wall_clock_deadline( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + primary_remote = tmp_path / "remote.git" + secondary_remote = tmp_path / "secondary.git" + rows = _base_rows(primary_remote) + rows.extend( + [ + { + "path": "library/storefront", + "kind": "structural", + "owner_ref": "portvs", + "residency": "structural", + }, + { + "path": "library/storefront/second", + "kind": "repository", + "owner_ref": "organvm/second", + "residency": "laptop", + "remote": str(secondary_remote), + "custody_ref": "refs/remotes/origin/main", + }, + ] + ) + manifest, workspace, _ = _fixture(tmp_path, rows=rows) + clock = [0.0] + fetches: list[float] = [] + real_run_git = convergence._run_git + + def bounded_run_git( + repo: Path, + *args: str, + timeout: float = convergence.GIT_TIMEOUT_SECONDS, + ) -> subprocess.CompletedProcess[str]: + if args and args[0] == "fetch": + fetches.append(timeout) + result = real_run_git(repo, *args, timeout=timeout) + clock[0] = convergence.REPOSITORY_FETCH_BUDGET_SECONDS + 1 + return result + return real_run_git(repo, *args, timeout=timeout) + + monkeypatch.setattr(convergence, "_monotonic", lambda: clock[0]) + monkeypatch.setattr(convergence, "_run_git", bounded_run_git) + + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + assert len(fetches) == 1 + assert fetches[0] <= convergence.GIT_TIMEOUT_SECONDS + assert any("aggregate repository fetch deadline exhausted" in row["message"] for row in report["violations"]) + + +def test_expired_ephemeral_units_require_reaper_action(tmp_path: Path) -> None: + manifest, workspace, _ = _fixture(tmp_path) + unit = workspace / "runtime" / "worktrees" / "fixture-repository" / "expired" + unit.mkdir(parents=True) + old = datetime(2026, 7, 1, tzinfo=UTC).timestamp() + os.utime(unit, (old, old)) + + report = audit( + manifest, + workspace_root=workspace, + active_cwds=[], + now=datetime(2026, 7, 30, tzinfo=UTC), + ) + + assert "ephemeral_entries_expired" in _codes(report) + assert report["ephemeral_roots"][0]["expired_entry_count"] == 1 + assert report["ephemeral_roots"][0]["reaper"] == "limen worktree reap" + + +def test_relative_active_cwd_fixture_is_rejected(tmp_path: Path) -> None: + fixture = tmp_path / "active-cwds.json" + fixture.write_text('["relative/worktree"]\n', encoding="utf-8") + + with pytest.raises(ManifestError, match="must be absolute"): + load_active_cwds(fixture) + + +@pytest.mark.parametrize("failure", ["missing", "nonzero"]) +def test_failed_lsof_becomes_bounded_unmeasured_failure( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + failure: str, +) -> None: + manifest, workspace, _ = _fixture(tmp_path) + real_run = subprocess.run + + def fake_run(command: list[str], *args: object, **kwargs: object) -> subprocess.CompletedProcess[str]: + if command and command[0] == "lsof": + if failure == "missing": + raise FileNotFoundError("fixture has no lsof") + return subprocess.CompletedProcess(command, 2, "", "fixture lsof failure") + return real_run(command, *args, **kwargs) # type: ignore[arg-type] + + monkeypatch.setattr(convergence.subprocess, "run", fake_run) + report = audit(manifest, workspace_root=workspace, active_cwds=None) + + assert "unmeasured_state" in _codes(report) + assert report["counts"]["unmeasured"] == 1 + assert any("active CWD discovery failed closed" in row["message"] for row in report["violations"]) + + +def test_default_manifest_uses_workspace_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + script = Path(__file__).resolve().parents[2] / "scripts" / "substrate-convergence.py" + spec = importlib.util.spec_from_file_location("substrate_convergence_script", script) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + monkeypatch.delenv("PORTVS_WORKSPACE_MANIFEST", raising=False) + monkeypatch.delenv("PORTVS_ROOT", raising=False) + monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path / "custom-workspace")) + + assert module.default_manifest() == ( + tmp_path + / "custom-workspace" + / "library" + / "engine" + / "organvm" + / "portvs" + / "governance" + / "workspace-manifest.yaml" + ) + + +def test_receipt_retains_fixture_root_and_binds_canonical_redaction( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + script = Path(__file__).resolve().parents[2] / "scripts" / "substrate-convergence.py" + spec = importlib.util.spec_from_file_location("substrate_convergence_receipt_script", script) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + manifest, workspace, _ = _fixture(tmp_path) + report = audit(manifest, workspace_root=workspace, active_cwds=[]) + + monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path / "different-live-root")) + fixture_receipt = module.prepare_receipt_report(report) + assert fixture_receipt["workspace_root"] == str(workspace) + assert fixture_receipt["workspace_root_is_canonical_live"] is False + + monkeypatch.setenv("WORKSPACE_ROOT", str(workspace)) + live_receipt = module.prepare_receipt_report(report) + assert live_receipt["workspace_root"] == "$WORKSPACE_ROOT" + assert live_receipt["workspace_root_is_canonical_live"] is True + + tampered = dict(report) + tampered["workspace_root"] = str(tmp_path / "forged-root") + with pytest.raises(ManifestError, match="identity does not match"): + module.prepare_receipt_report(tampered) + + +def test_tilde_workspace_root_expands_before_abspath_and_redacts_from_arbitrary_cwd( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + script = Path(__file__).resolve().parents[2] / "scripts" / "substrate-convergence.py" + spec = importlib.util.spec_from_file_location("substrate_convergence_tilde_script", script) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + manifest, workspace, _ = _fixture(tmp_path) + elsewhere = tmp_path / "arbitrary" / "cwd" + elsewhere.mkdir(parents=True) + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("WORKSPACE_ROOT", "~/Workspace") + monkeypatch.chdir(elsewhere) + + assert module.canonical_live_workspace_root() == workspace + receipt = module.prepare_receipt_report(audit(manifest, workspace_root=workspace, active_cwds=[])) + assert receipt["workspace_root"] == "$WORKSPACE_ROOT" + assert receipt["workspace_root_is_canonical_live"] is True + + +def test_same_manifest_and_root_are_independent_of_cwd(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + manifest, workspace, _ = _fixture(tmp_path) + first = audit(manifest, workspace_root=workspace, active_cwds=[]) + elsewhere = tmp_path / "some" / "worktree" + elsewhere.mkdir(parents=True) + monkeypatch.chdir(elsewhere) + second = audit(manifest, workspace_root=workspace, active_cwds=[]) + assert second == first diff --git a/cli/tests/test_substrate_paths.py b/cli/tests/test_substrate_paths.py new file mode 100644 index 000000000..e5f7f7e17 --- /dev/null +++ b/cli/tests/test_substrate_paths.py @@ -0,0 +1,77 @@ +from pathlib import Path + +from limen.substrate_paths import find_legacy_references + + +def test_canonical_and_indirected_paths_pass(tmp_path: Path) -> None: + scripts = tmp_path / "scripts" + scripts.mkdir() + (scripts / "ok.sh").write_text( + 'root="${LIMEN_ROOT:-${WORKSPACE_ROOT:-$HOME/Workspace}/library/engine/organvm/limen}"\n', + encoding="utf-8", + ) + assert find_legacy_references(tmp_path) == [] + + +def test_old_executable_paths_fail(tmp_path: Path) -> None: + scripts = tmp_path / "scripts" + scripts.mkdir() + (scripts / "bad.py").write_text( + 'root = Path.home() / "Workspace" / "limen"\nother = "/Users/example/Workspace/4444J99/portvs"\n', + encoding="utf-8", + ) + findings = find_legacy_references(tmp_path) + assert [(item["path"], item["line"]) for item in findings] == [ + ("scripts/bad.py", 1), + ("scripts/bad.py", 2), + ] + + +def test_constructed_legacy_defaults_fail(tmp_path: Path) -> None: + scripts = tmp_path / "scripts" + scripts.mkdir() + (scripts / "constructed.py").write_text( + "\n".join( + [ + 'first = f"{HOME}/Workspace/limen"', + 'second = os.path.join(HOME, "Workspace/limen")', + 'third = os.path.join(HOME, "Workspace", "limen")', + 'fourth = Path(HOME) / "Workspace" / "limen"', + 'fifth = HOME + "/Workspace/limen"', + ] + ) + + "\n", + encoding="utf-8", + ) + + findings = find_legacy_references(tmp_path) + + assert [(item["path"], item["line"]) for item in findings] == [ + ("scripts/constructed.py", 1), + ("scripts/constructed.py", 2), + ("scripts/constructed.py", 3), + ("scripts/constructed.py", 4), + ("scripts/constructed.py", 5), + ] + + +def test_historical_docs_and_tests_are_not_executable_consumers(tmp_path: Path) -> None: + for directory in ("docs", "scripts/tests"): + path = tmp_path / directory + path.mkdir(parents=True) + (path / "history.md").write_text("~/Workspace/limen\n", encoding="utf-8") + assert find_legacy_references(tmp_path) == [] + + +def test_continuation_launcher_derives_canonical_limen_root() -> None: + root = Path(__file__).resolve().parents[2] + readme = (root / "docs" / "continuations" / "omega-substrate-literal" / "README.md").read_text(encoding="utf-8") + + assert "$HOME/Workspace/limen/.worktrees" not in readme + assert 'workspace_root="${WORKSPACE_ROOT:-$HOME/Workspace}"' in readme + assert 'canonical_limen_root="${LIMEN_ROOT:-$workspace_root/library/engine/organvm/limen}"' in readme + assert 'canonical_capsule="$canonical_limen_root/.worktrees/omega-substrate-literal"' in readme + assert 'legacy_capsule="$workspace_root/limen/.worktrees/omega-substrate-literal"' in readme + assert readme.index('if [[ -x "$canonical_capsule/.limen-workstream/kickstart.sh" ]]') < readme.index( + 'elif [[ -x "$legacy_capsule/.limen-workstream/kickstart.sh" ]]' + ) diff --git a/cli/tests/test_sync_reclaim.py b/cli/tests/test_sync_reclaim.py index ab9c43b83..92f65da75 100644 --- a/cli/tests/test_sync_reclaim.py +++ b/cli/tests/test_sync_reclaim.py @@ -20,6 +20,8 @@ import pytest +from limen.worktree_receipts import live_worktree_receipt_fields + ROOT = Path(__file__).resolve().parents[2] SYNC = ROOT / "scripts" / "sync-release.sh" RECLAIM = ROOT / "scripts" / "reclaim-worktrees.py" @@ -313,7 +315,7 @@ def _age(path: Path, hours: float): def _write_reclaim_acceptance( limen_root: Path, - root: str, + target: Path, action: str = "remove-worktree", reason: str | None = None, archive_status: str = "not_required_clean_merged_remote", @@ -321,9 +323,11 @@ def _write_reclaim_acceptance( ) -> None: path = limen_root / "docs" / "worktree-reclaim-acceptance.jsonl" path.parent.mkdir(exist_ok=True) + root = target.name event = { "accepted_at": "2026-07-06T05:30:00Z", "root": root, + "path": str(target.resolve()), "action": action, "accepted": True, "archive_status": archive_status, @@ -364,7 +368,7 @@ def test_reclaim_removes_clean_pushed_idle_with_acceptance(tmp_path): dead = _add_wt(main, wtroot, "dead-task") # clean, on origin/main, will be aged _age(dead, 5) (main / "logs").mkdir(exist_ok=True) - _write_reclaim_acceptance(main, "dead-task", reason="clean+merged+idle") + _write_reclaim_acceptance(main, dead, reason="clean+merged+idle") r = _run_reclaim(wtroot, main, apply=True) assert r.returncode == 0, r.stderr assert not dead.exists(), r.stdout @@ -445,6 +449,8 @@ def test_reclaim_removes_clean_idle_remote_merged_receipt_under_standing_grant(t _git("checkout", "-q", "-b", "merged-pr", cwd=merged) _commit(merged, "squashed.txt", "merged elsewhere\n", "local pre-squash commit") _age(merged, 5) + identity = live_worktree_receipt_fields(merged) + assert identity is not None receipts.write_text( json.dumps( @@ -452,6 +458,7 @@ def test_reclaim_removes_clean_idle_remote_merged_receipt_under_standing_grant(t "receipts": [ { "root": "receipt-merged", + **identity, "lane": "remote-merged", "status": "merged_pr_preserved", "pr_state": "MERGED", @@ -520,7 +527,7 @@ def test_reclaim_removes_patch_equivalent_local_replay(tmp_path): _commit(replay, "same.txt", "same change\n", "local replay of same patch") _git("fetch", "-q", "origin", cwd=replay) _age(replay, 5) - _write_reclaim_acceptance(main, "patch-equivalent", reason="clean+merged+idle") + _write_reclaim_acceptance(main, replay, reason="clean+merged+idle") r = _run_reclaim(wtroot, main, apply=True) @@ -567,7 +574,7 @@ def test_reclaim_removes_generated_log_shell(tmp_path): (shell / "logs" / "session-lifecycle-pressure.json").write_text("{}", encoding="utf-8") _write_reclaim_acceptance( limen_root, - "generated-log-shell", + shell, action="remove-residue", reason="generated-log-shell", archive_status="not_required_generated_residue", diff --git a/cli/tests/test_workstream_branch_prefix.py b/cli/tests/test_workstream_branch_prefix.py index 760b2cd86..fc54ec19b 100644 --- a/cli/tests/test_workstream_branch_prefix.py +++ b/cli/tests/test_workstream_branch_prefix.py @@ -41,6 +41,11 @@ def repo(tmp_path: Path) -> Path: return r +@pytest.fixture(autouse=True) +def canonical_runtime_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path / "Workspace")) + + def _run(*args: str, path: str | None = None): env = {**os.environ} if path is not None: diff --git a/cli/tests/test_workstream_command.py b/cli/tests/test_workstream_command.py index 1b0d9c6ce..20a78227f 100644 --- a/cli/tests/test_workstream_command.py +++ b/cli/tests/test_workstream_command.py @@ -11,11 +11,22 @@ from pathlib import Path from click.testing import CliRunner +import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "cli" / "src")) from limen.cli import main # noqa: E402 +from limen.worktree_layout import repository_storage_key, runtime_worktree_path # noqa: E402 + + +@pytest.fixture(autouse=True) +def _canonical_runtime_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path / "Workspace")) + + +def _worktree(repo: Path, slug: str) -> Path: + return runtime_worktree_path(repo, slug) def _git(*args: str, cwd: Path) -> subprocess.CompletedProcess[str]: @@ -55,7 +66,7 @@ def test_workstream_command_writes_private_kickstart_packet(tmp_path: Path, monk ) assert result.exit_code == 0, result.output - wt = repo / ".worktrees" / "demo-packet" + wt = _worktree(repo, "demo-packet") readme = wt / ".limen-workstream" / "README.md" intent = wt / ".limen-workstream" / "intent.md" kickstart = wt / ".limen-workstream" / "kickstart.sh" @@ -99,6 +110,309 @@ def test_workstream_command_writes_private_kickstart_packet(tmp_path: Path, monk assert "workstream contract is missing" in partial.output +def test_launcher_isolates_same_basename_repositories_under_runtime_root( + tmp_path: Path, +) -> None: + repos = [tmp_path / owner / "shared-name" for owner in ("first", "second")] + worktrees: list[Path] = [] + for index, repo in enumerate(repos): + repo.mkdir(parents=True) + _git("init", "-q", "-b", "main", cwd=repo) + _git("config", "user.email", "test@example.invalid", cwd=repo) + _git("config", "user.name", "Test User", cwd=repo) + _git("remote", "add", "origin", f"https://example.invalid/owner-{index}/shared-name.git", cwd=repo) + (repo / "README.md").write_text(f"fixture {index}\n", encoding="utf-8") + _git("add", "README.md", cwd=repo) + _git("commit", "-qm", "init", cwd=repo) + + result = subprocess.run( + [ + "bash", + str(ROOT / "scripts" / "start-worktree-session.sh"), + "--no-readme", + str(repo), + "same-slug", + ], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + worktrees.append(_worktree(repo, "same-slug")) + + runtime_root = tmp_path / "Workspace" / "runtime" / "worktrees" + assert all(worktree.is_relative_to(runtime_root) and worktree.is_dir() for worktree in worktrees) + assert worktrees[0].parent != worktrees[1].parent + assert all(worktree.name == "same-slug" for worktree in worktrees) + for repo in repos: + exclude = Path( + _git("rev-parse", "--path-format=absolute", "--git-path", "info/exclude", cwd=repo).stdout.strip() + ) + assert ".worktrees/" not in exclude.read_text(encoding="utf-8").splitlines() + + +def test_relative_local_origin_key_is_stable_across_cwd_and_linked_worktree( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + origin = tmp_path / "origin.git" + origin.mkdir() + _git("init", "--bare", "-q", cwd=origin) + repo = tmp_path / "primary" + repo.mkdir() + _git("init", "-q", "-b", "main", cwd=repo) + _git("config", "user.email", "test@example.invalid", cwd=repo) + _git("config", "user.name", "Test User", cwd=repo) + _git("remote", "add", "origin", "../origin.git", cwd=repo) + (repo / "README.md").write_text("fixture\n", encoding="utf-8") + _git("add", "README.md", cwd=repo) + _git("commit", "-qm", "init", cwd=repo) + linked = tmp_path / "linked" + _git("worktree", "add", "-q", "-b", "work/linked", str(linked), "HEAD", cwd=repo) + + monkeypatch.chdir(tmp_path) + primary_key = repository_storage_key(repo) + primary_path = runtime_worktree_path(repo, "cwd-stable") + arbitrary_cwd = tmp_path / "arbitrary" / "deep" + arbitrary_cwd.mkdir(parents=True) + monkeypatch.chdir(arbitrary_cwd) + + assert repository_storage_key(repo) == primary_key + assert repository_storage_key(linked) == primary_key + assert runtime_worktree_path(repo, "cwd-stable") == primary_path + assert runtime_worktree_path(linked, "cwd-stable") == primary_path + + +def test_no_origin_key_is_stable_across_linked_worktree_and_ref_changes( + tmp_path: Path, +) -> None: + repo = tmp_path / "primary" + repo.mkdir() + _git("init", "-q", "-b", "main", cwd=repo) + _git("config", "user.email", "test@example.invalid", cwd=repo) + _git("config", "user.name", "Test User", cwd=repo) + (repo / "README.md").write_text("fixture\n", encoding="utf-8") + _git("add", "README.md", cwd=repo) + _git("commit", "-qm", "init", cwd=repo) + linked = tmp_path / "differently-named-linked" + _git("worktree", "add", "-q", "-b", "work/linked", str(linked), "HEAD", cwd=repo) + + initial = repository_storage_key(repo) + tree = _git("rev-parse", "HEAD^{tree}", cwd=repo).stdout.strip() + orphan = _git("commit-tree", tree, "-m", "orphan root", cwd=repo).stdout.strip() + _git("update-ref", "refs/heads/orphan-history", orphan, cwd=repo) + + assert repository_storage_key(repo) == initial + assert repository_storage_key(linked) == initial + + +def test_relative_file_uri_origins_are_cwd_stable_and_do_not_collide( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + repos: list[Path] = [] + for owner in ("first", "second"): + owner_root = tmp_path / owner + origin = owner_root / "origin.git" + repo = owner_root / "primary" + origin.mkdir(parents=True) + repo.mkdir() + _git("init", "--bare", "-q", cwd=origin) + _git("init", "-q", "-b", "main", cwd=repo) + _git("remote", "add", "origin", "file:../origin.git", cwd=repo) + repos.append(repo) + + monkeypatch.chdir(tmp_path) + keys = [repository_storage_key(repo) for repo in repos] + arbitrary_cwd = tmp_path / "arbitrary" / "deep" + arbitrary_cwd.mkdir(parents=True) + monkeypatch.chdir(arbitrary_cwd) + + assert repository_storage_key(repos[0]) == keys[0] + assert repository_storage_key(repos[1]) == keys[1] + assert keys[0] != keys[1] + + +def test_network_transport_changes_preserve_repository_storage_key(tmp_path: Path) -> None: + repo = tmp_path / "primary" + repo.mkdir() + _git("init", "-q", "-b", "main", cwd=repo) + _git("remote", "add", "origin", "https://github.com/organvm/limen.git", cwd=repo) + + https_key = repository_storage_key(repo) + _git("remote", "set-url", "origin", "git@github.com:organvm/limen.git", cwd=repo) + + assert repository_storage_key(repo) == https_key + + +@pytest.mark.parametrize("unsafe_slug", [".", "..", "../escape", "nested/escape"]) +def test_launcher_rejects_unsafe_slug_without_creating_outside_runtime( + tmp_path: Path, + unsafe_slug: str, +) -> None: + repo = tmp_path / "slug-repo" + repo.mkdir() + _git("init", "-q", "-b", "main", cwd=repo) + _git("config", "user.email", "test@example.invalid", cwd=repo) + _git("config", "user.name", "Test User", cwd=repo) + (repo / "README.md").write_text("fixture\n", encoding="utf-8") + _git("add", "README.md", cwd=repo) + _git("commit", "-qm", "init", cwd=repo) + + with pytest.raises(ValueError, match="safe lowercase path component"): + runtime_worktree_path(repo, unsafe_slug) + result = subprocess.run( + [ + "bash", + str(ROOT / "scripts" / "start-worktree-session.sh"), + "--no-readme", + str(repo), + unsafe_slug, + ], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert not (tmp_path / "escape").exists() + assert not (tmp_path / "Workspace" / "runtime" / "escape").exists() + + +@pytest.mark.parametrize("symlink_level", ["runtime", "worktrees", "namespace"]) +def test_launcher_rejects_runtime_container_symlink_escape( + tmp_path: Path, + symlink_level: str, +) -> None: + repo = tmp_path / "symlink-repo" + repo.mkdir() + _git("init", "-q", "-b", "main", cwd=repo) + _git("config", "user.email", "test@example.invalid", cwd=repo) + _git("config", "user.name", "Test User", cwd=repo) + (repo / "README.md").write_text("fixture\n", encoding="utf-8") + _git("add", "README.md", cwd=repo) + _git("commit", "-qm", "init", cwd=repo) + outside = tmp_path / "outside" + outside.mkdir() + workspace = tmp_path / "Workspace" + if symlink_level == "runtime": + workspace.mkdir() + (workspace / "runtime").symlink_to(outside, target_is_directory=True) + elif symlink_level == "worktrees": + (workspace / "runtime").mkdir(parents=True) + (workspace / "runtime" / "worktrees").symlink_to(outside, target_is_directory=True) + else: + namespace = _worktree(repo, "preflight").parent + namespace.parent.mkdir(parents=True, exist_ok=True) + namespace.symlink_to(outside, target_is_directory=True) + + result = subprocess.run( + [ + "bash", + str(ROOT / "scripts" / "start-worktree-session.sh"), + "--no-readme", + str(repo), + "symlink-escape", + ], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert list(outside.iterdir()) == [] + + +def test_launcher_rejects_final_worktree_symlink_to_matching_external_checkout( + tmp_path: Path, +) -> None: + repo = tmp_path / "symlink-final-repo" + repo.mkdir() + _git("init", "-q", "-b", "main", cwd=repo) + _git("config", "user.email", "test@example.invalid", cwd=repo) + _git("config", "user.name", "Test User", cwd=repo) + (repo / "README.md").write_text("fixture\n", encoding="utf-8") + _git("add", "README.md", cwd=repo) + _git("commit", "-qm", "init", cwd=repo) + + slug = "symlink-final" + canonical = _worktree(repo, slug) + outside = tmp_path / "outside-worktree" + _git("worktree", "add", "-q", "-b", f"work/{slug}", str(outside), "HEAD", cwd=repo) + outside_head = _git("rev-parse", "HEAD", cwd=outside).stdout + outside_status = _git("status", "--porcelain=v1", "--untracked-files=all", cwd=outside).stdout + canonical.parent.mkdir(parents=True) + canonical.symlink_to(outside, target_is_directory=True) + + with pytest.raises(ValueError, match="target must be physical"): + runtime_worktree_path(repo, slug) + result = subprocess.run( + [ + "bash", + str(ROOT / "scripts" / "start-worktree-session.sh"), + "--no-readme", + str(repo), + slug, + ], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert _git("rev-parse", "HEAD", cwd=outside).stdout == outside_head + assert _git("status", "--porcelain=v1", "--untracked-files=all", cwd=outside).stdout == outside_status + assert not (outside / ".limen-workstream").exists() + + +def test_launcher_rejects_physical_canonical_leaf_nested_under_matching_checkout( + tmp_path: Path, +) -> None: + repo = tmp_path / "physical-nonroot-repo" + repo.mkdir() + _git("init", "-q", "-b", "main", cwd=repo) + _git("config", "user.email", "test@example.invalid", cwd=repo) + _git("config", "user.name", "Test User", cwd=repo) + (repo / "README.md").write_text("fixture\n", encoding="utf-8") + _git("add", "README.md", cwd=repo) + _git("commit", "-qm", "init", cwd=repo) + + slug = "physical-nonroot" + canonical = _worktree(repo, slug) + canonical.parent.parent.mkdir(parents=True) + _git( + "worktree", + "add", + "-q", + "-b", + f"work/{slug}", + str(canonical.parent), + "HEAD", + cwd=repo, + ) + canonical.mkdir() + sentinel = canonical / "sentinel.txt" + sentinel.write_text("do not reuse a nested directory\n", encoding="utf-8") + + result = subprocess.run( + [ + "bash", + str(ROOT / "scripts" / "start-worktree-session.sh"), + "--no-readme", + str(repo), + slug, + ], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert "not the resolved Git top-level" in result.stderr + assert sentinel.read_text(encoding="utf-8") == "do not reuse a nested directory\n" + assert not (canonical / ".limen-workstream").exists() + + def test_autonomous_jules_workstream_uses_remote_cloud_transport(tmp_path: Path, monkeypatch, capfd) -> None: repo = tmp_path / "demo-repo" repo.mkdir() @@ -118,7 +432,7 @@ def test_autonomous_jules_workstream_uses_remote_cloud_transport(tmp_path: Path, "#!/usr/bin/env bash\n" 'printf "jules\\n" >> "$EVENTS_CAPTURE"\n' 'printf "%s\\n" "$@" > "$SESSION_ARGS_CAPTURE"\n' - 'if [[ "${JULES_SLEEP:-0}" == "1" ]]; then sleep 5; fi\n' + 'if [[ "${JULES_SLEEP:-0}" == "1" ]]; then sleep "${JULES_SLEEP_SECONDS:-5}"; fi\n' 'printf "Session is created.\\nID: 12345678901234567890\\nTask: test\\n\\n' 'URL: https://jules.google.com/session/12345678901234567890\\n"\n' 'if [[ "${JULES_FAIL_AFTER_OUTPUT:-0}" == "1" ]]; then exit 42; fi\n' @@ -207,7 +521,7 @@ def test_autonomous_jules_workstream_uses_remote_cloud_transport(tmp_path: Path, assert args[5].startswith("Do NOT ask for feedback or approval.") assert "Ship the exact bounded packet." in args[5] assert "# Continuation capsule:" not in args[5] - wt = repo / ".worktrees" / "jules-cloud" + wt = _worktree(repo, "jules-cloud") receipt_path = wt / "docs" / "continuations" / "jules-cloud" / "workstream.json" receipt = json.loads(receipt_path.read_text(encoding="utf-8")) assert receipt["provider_run"] == { @@ -323,6 +637,7 @@ def test_autonomous_jules_workstream_uses_remote_cloud_transport(tmp_path: Path, timeout_events_before = events_capture.read_text(encoding="utf-8") monkeypatch.setenv("JULES_SLEEP", "1") + monkeypatch.setenv("JULES_SLEEP_SECONDS", "10") monkeypatch.setenv("LIMEN_WORKSTREAM_PREFLIGHT_TIMEOUT_SECONDS", "1") started = time.monotonic() timed_out = CliRunner().invoke( @@ -339,11 +654,12 @@ def test_autonomous_jules_workstream_uses_remote_cloud_transport(tmp_path: Path, ], ) assert timed_out.exit_code != 0 - assert time.monotonic() - started < 4 + assert time.monotonic() - started < 8 monkeypatch.delenv("JULES_SLEEP") + monkeypatch.delenv("JULES_SLEEP_SECONDS") monkeypatch.delenv("LIMEN_WORKSTREAM_PREFLIGHT_TIMEOUT_SECONDS") - timeout_wt = repo / ".worktrees" / "jules-timeout" + timeout_wt = _worktree(repo, "jules-timeout") args_capture.unlink(missing_ok=True) retried = subprocess.run( ["bash", str(timeout_wt / ".limen-workstream" / "kickstart.sh")], @@ -383,7 +699,7 @@ def test_autonomous_jules_workstream_uses_remote_cloud_transport(tmp_path: Path, assert failed_after_output.exit_code != 0 assert "durable session receipt" in capfd.readouterr().err monkeypatch.delenv("JULES_FAIL_AFTER_OUTPUT") - nonzero_wt = repo / ".worktrees" / "jules-nonzero-receipt" + nonzero_wt = _worktree(repo, "jules-nonzero-receipt") nonzero_receipt = json.loads( (nonzero_wt / "docs/continuations/jules-nonzero-receipt/workstream.json").read_text(encoding="utf-8") ) @@ -412,7 +728,7 @@ def test_autonomous_jules_workstream_uses_remote_cloud_transport(tmp_path: Path, assert "could not publish its receipt" in capfd.readouterr().err monkeypatch.delenv("FAIL_RECEIPT_COMMIT") - commit_wt = repo / ".worktrees" / "jules-commit-failure" + commit_wt = _worktree(repo, "jules-commit-failure") commit_receipt = commit_wt / "docs" / "continuations" / "jules-commit-failure" / "workstream.json" recovered_receipt = json.loads(commit_receipt.read_text(encoding="utf-8")) assert recovered_receipt["provider_run"]["id"] == "12345678901234567890" @@ -564,7 +880,7 @@ def test_codex_workstream_publishes_admitted_receipt_before_provider(tmp_path: P launched = subprocess.run(command, env=env, text=True, capture_output=True, timeout=15, check=False) assert launched.returncode == 0, launched.stdout + launched.stderr - wt = repo / ".worktrees" / "codex-admission-publication" + wt = _worktree(repo, "codex-admission-publication") branch = "work/codex-admission-publication" receipt_rel = "docs/continuations/codex-admission-publication/workstream.json" first_head = _git("rev-parse", "HEAD", cwd=wt).stdout.strip() @@ -659,7 +975,7 @@ def test_codex_workstream_denies_provider_when_admitted_receipt_push_fails(tmp_p assert not provider_marker.exists() pre_receive.unlink() - wt = repo / ".worktrees" / "codex-publication-rejected" + wt = _worktree(repo, "codex-publication-rejected") retried = subprocess.run( ["bash", str(wt / ".limen-workstream" / "kickstart.sh")], cwd=wt, @@ -762,9 +1078,7 @@ def test_explicit_codex_profile_validates_live_catalog_and_launches_exact_argv(t ] assert "# Continuation capsule: explicit-agent-launch" in prompt_capture.read_text(encoding="utf-8") contract = json.loads( - (repo / ".worktrees" / "explicit-agent-launch" / ".limen-workstream" / "workstream.json").read_text( - encoding="utf-8" - ) + (_worktree(repo, "explicit-agent-launch") / ".limen-workstream" / "workstream.json").read_text(encoding="utf-8") ) assert contract["schema"] == "limen.workstream.contract.v2" assert contract["primary_launch"] == { @@ -805,7 +1119,7 @@ def test_explicit_codex_profile_validates_live_catalog_and_launches_exact_argv(t ) assert rejected.returncode == 2 assert message in rejected.stderr - assert not (repo / ".worktrees" / slug.lower().replace(" ", "-")).exists() + assert not _worktree(repo, slug.lower().replace(" ", "-")).exists() def test_autonomous_workstream_requires_prompt_and_launches_with_dynamic_readme(tmp_path: Path, monkeypatch) -> None: @@ -832,7 +1146,7 @@ def test_autonomous_workstream_requires_prompt_and_launches_with_dynamic_readme( missing = CliRunner().invoke(main, ["workstream", "--autonomous", str(repo), "No Prompt"]) assert missing.exit_code == 2 assert "requires --prompt or --prompt-file" in missing.output - assert not (repo / ".worktrees" / "no-prompt").exists() + assert not _worktree(repo, "no-prompt").exists() unbounded = CliRunner().invoke( main, @@ -848,7 +1162,7 @@ def test_autonomous_workstream_requires_prompt_and_launches_with_dynamic_readme( ) assert unbounded.exit_code == 2 assert "invalid workstream contract" in unbounded.output - assert not (repo / ".worktrees" / "unbounded").exists() + assert not _worktree(repo, "unbounded").exists() no_readme = CliRunner().invoke( main, @@ -864,7 +1178,7 @@ def test_autonomous_workstream_requires_prompt_and_launches_with_dynamic_readme( ) assert no_readme.exit_code == 2 assert "cannot be combined with --no-readme" in no_readme.output - assert not (repo / ".worktrees" / "no-readme").exists() + assert not _worktree(repo, "no-readme").exists() result = CliRunner().invoke( main, @@ -883,7 +1197,7 @@ def test_autonomous_workstream_requires_prompt_and_launches_with_dynamic_readme( ) assert result.exit_code == 0, result.output - wt = repo / ".worktrees" / "next-epoch" + wt = _worktree(repo, "next-epoch") capsule = wt / ".limen-workstream" readme = capsule / "README.md" manifest = capsule / "manifest.md" @@ -1411,7 +1725,7 @@ def test_conduct_registration_precedes_runway_admission(tmp_path: Path, monkeypa ) assert rendered.exit_code == 0, rendered.output - wt = repo / ".worktrees" / "conduct-ordering" + wt = _worktree(repo, "conduct-ordering") capsule = wt / ".limen-workstream" kickstart = capsule / "kickstart.sh" contract = capsule / "workstream.json" @@ -1534,7 +1848,7 @@ def test_conduct_keepalive_refreshes_without_exposing_credential_to_provider( ) assert rendered.exit_code == 0, rendered.output - wt = repo / ".worktrees" / "conduct-keepalive" + wt = _worktree(repo, "conduct-keepalive") capsule = wt / ".limen-workstream" launch_env = { **os.environ, @@ -1638,7 +1952,7 @@ def test_workstream_rejects_symlinked_private_root_before_writing_prompt(tmp_pat created = CliRunner().invoke(main, ["workstream", "--no-readme", str(repo), "Symlink Root"]) assert created.exit_code == 0, created.output - wt = repo / ".worktrees" / "symlink-root" + wt = _worktree(repo, "symlink-root") tracked_target = wt / "tracked-capsule-leak" tracked_target.mkdir() (wt / ".limen-workstream").symlink_to(tracked_target, target_is_directory=True) @@ -1758,7 +2072,7 @@ def test_concurrent_capsule_render_keeps_partial_kickstart_unlaunchable(tmp_path time.sleep(0.01) assert sync_entered.exists(), rendering.stderr.read() if rendering.stderr else "" - wt = repo / ".worktrees" / "race-capsule" + wt = _worktree(repo, "race-capsule") capsule = wt / ".limen-workstream" kickstart = capsule / "kickstart.sh" identity = capsule / "capsule.identity" @@ -1805,7 +2119,7 @@ def test_concurrent_capsule_render_keeps_partial_kickstart_unlaunchable(tmp_path render_stdout, render_stderr = rendering.communicate() assert rendering.returncode == 0, render_stdout + render_stderr - wt = repo / ".worktrees" / "race-capsule" + wt = _worktree(repo, "race-capsule") capsule = wt / ".limen-workstream" kickstart = capsule / "kickstart.sh" assert (capsule / "capsule.identity").exists() diff --git a/cli/tests/test_worktree_abandonment.py b/cli/tests/test_worktree_abandonment.py index 90fa19098..c6bea9690 100644 --- a/cli/tests/test_worktree_abandonment.py +++ b/cli/tests/test_worktree_abandonment.py @@ -115,11 +115,24 @@ def test_registered_worktree_scan_fails_closed_on_unresolvable_path( abandonment._registered_worktree_paths(tmp_path) -def test_quarantine_atomically_preserves_bytes(tmp_path: Path) -> None: +def _xdg_quarantine( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + name: str = "quarantine", +) -> Path: + data_home = tmp_path / "xdg-data" + monkeypatch.setenv("XDG_DATA_HOME", str(data_home)) + return data_home / "limen" / name + + +def test_quarantine_atomically_preserves_bytes( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: source = tmp_path / "creation-root" / "candidate" source.mkdir(parents=True) (source / "private.txt").write_text("preserve\n", encoding="utf-8") - quarantine = tmp_path / "quarantine" + quarantine = _xdg_quarantine(tmp_path, monkeypatch) result = abandonment.quarantine_path( source, @@ -134,6 +147,12 @@ def test_quarantine_atomically_preserves_bytes(tmp_path: Path) -> None: assert not source.exists() assert (destination / "private.txt").read_text(encoding="utf-8") == "preserve\n" assert result["state"] == "completed" + assert result["result"]["restoration_pointer"] == { + "from": str(destination), + "to": str(source), + "method": "same-filesystem-atomic-rename", + } + assert Path(result["receipt_path"]).is_file() def test_quarantine_cross_filesystem_denial_preserves_source( @@ -142,7 +161,7 @@ def test_quarantine_cross_filesystem_denial_preserves_source( ) -> None: source = tmp_path / "source" source.mkdir() - quarantine = tmp_path / "quarantine" + quarantine = _xdg_quarantine(tmp_path, monkeypatch) monkeypatch.setattr(abandonment, "_same_filesystem", lambda _source, _root: False) with pytest.raises(abandonment.WorktreeAbandonmentError) as caught: @@ -165,12 +184,13 @@ def test_quarantine_rename_failure_is_typed_and_preserves_source( ) -> None: source = tmp_path / "source" source.mkdir() + quarantine = _xdg_quarantine(tmp_path, monkeypatch) monkeypatch.setattr(os, "rename", lambda _source, _destination: (_ for _ in ()).throw(OSError("boom"))) with pytest.raises(abandonment.WorktreeAbandonmentError) as caught: abandonment.quarantine_path( source, - tmp_path / "quarantine", + quarantine, reason="test", receipt_root=tmp_path / "receipts", owner_probe=lambda _path: None, @@ -187,23 +207,29 @@ def test_quarantine_defaults_to_fail_closed_owner_probe( ) -> None: source = tmp_path / "source" source.mkdir() + quarantine = _xdg_quarantine(tmp_path, monkeypatch) monkeypatch.setattr(abandonment, "_default_cwd_owner_probe", lambda _path: 4242) with pytest.raises(abandonment.WorktreeAbandonmentError, match="active-process-cwd:4242"): abandonment.quarantine_path( source, - tmp_path / "quarantine", + quarantine, reason="test", receipt_root=tmp_path / "receipts", ) assert source.exists() - assert not (tmp_path / "quarantine").exists() + assert not quarantine.exists() -def test_quarantine_nesting_denial_has_no_preflight_directory_side_effect(tmp_path: Path) -> None: - source = tmp_path / "source" - source.mkdir() +def test_quarantine_nesting_denial_has_no_preflight_directory_side_effect( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + data_home = tmp_path / "xdg-data" + monkeypatch.setenv("XDG_DATA_HOME", str(data_home)) + source = data_home / "limen" / "source" + source.mkdir(parents=True) quarantine = source / "nested" / "quarantine" with pytest.raises(abandonment.WorktreeAbandonmentError, match="nesting"): @@ -219,6 +245,98 @@ def test_quarantine_nesting_denial_has_no_preflight_directory_side_effect(tmp_pa assert not quarantine.exists() +def test_quarantine_rejects_symlink_directory_chain_and_retains_receipt( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + source = tmp_path / "source" + source.mkdir() + physical = tmp_path / "physical-data" + physical.mkdir() + data_home = tmp_path / "xdg-data" + data_home.symlink_to(physical, target_is_directory=True) + monkeypatch.setenv("XDG_DATA_HOME", str(data_home)) + + with pytest.raises(abandonment.WorktreeAbandonmentError, match="directory-symlink") as caught: + abandonment.quarantine_path( + source, + data_home / "limen" / "quarantine", + reason="test", + receipt_root=tmp_path / "receipts", + owner_probe=lambda _path: None, + ) + + assert source.exists() + assert caught.value.receipt["state"] == "crashed" + assert caught.value.receipt_path.is_file() + + +def test_quarantine_rejects_relative_xdg_data_home( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + source = tmp_path / "source" + source.mkdir() + monkeypatch.setenv("XDG_DATA_HOME", "relative/data") + + with pytest.raises(abandonment.WorktreeAbandonmentError, match="xdg-data-home-must-be-absolute"): + abandonment.quarantine_path( + source, + tmp_path / "absolute-quarantine", + reason="test", + receipt_root=tmp_path / "receipts", + owner_probe=lambda _path: None, + ) + + assert source.exists() + + +def test_quarantine_rejects_override_outside_xdg_inventory( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + source = tmp_path / "source" + source.mkdir() + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "xdg-data")) + + with pytest.raises(abandonment.WorktreeAbandonmentError, match="inside-xdg-limen"): + abandonment.quarantine_path( + source, + tmp_path / "undeclared-quarantine", + reason="test", + receipt_root=tmp_path / "receipts", + owner_probe=lambda _path: None, + ) + + assert source.exists() + + +def test_quarantine_rejects_physical_workspace_alias_containment( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + physical_workspace = tmp_path / "physical-workspace" + physical_workspace.mkdir() + workspace_alias = tmp_path / "Workspace" + workspace_alias.symlink_to(physical_workspace, target_is_directory=True) + data_home = physical_workspace / "xdg-data" + monkeypatch.setenv("WORKSPACE_ROOT", str(workspace_alias)) + monkeypatch.setenv("XDG_DATA_HOME", str(data_home)) + source = tmp_path / "source" + source.mkdir() + + with pytest.raises(abandonment.WorktreeAbandonmentError, match="outside-workspace"): + abandonment.quarantine_path( + source, + data_home / "limen" / "quarantine", + reason="test", + receipt_root=tmp_path / "receipts", + owner_probe=lambda _path: None, + ) + + assert source.exists() + + def test_custody_purge_requires_exact_identity_and_removes_only_isolated_tree(tmp_path: Path) -> None: source = tmp_path / "creation-root" / "candidate" source.mkdir(parents=True) diff --git a/cli/tests/test_worktree_debt.py b/cli/tests/test_worktree_debt.py index 0e77510b7..2d9891ec7 100644 --- a/cli/tests/test_worktree_debt.py +++ b/cli/tests/test_worktree_debt.py @@ -4,6 +4,7 @@ import os import subprocess import sys +import time from pathlib import Path import pytest @@ -14,6 +15,69 @@ from limen import worktree_debt as wd # noqa: E402 from limen import worktree_roots as wr # noqa: E402 from limen.worktree_debt import worktree_debt_report # noqa: E402 +from limen.worktree_layout import runtime_worktree_path # noqa: E402 +from limen.worktree_receipts import live_worktree_receipt_fields # noqa: E402 + + +def bound_receipt(path: Path, **values: object) -> dict[str, object]: + identity = live_worktree_receipt_fields(path) + assert identity is not None + return {"root": path.name, **identity, **values} + + +def canonical_same_slug_worktree(tmp_path: Path, workspace: Path, owner: str) -> Path: + owner_root = tmp_path / owner + primary = owner_root / "primary" + remote = owner_root / "origin.git" + primary.mkdir(parents=True) + remote.mkdir() + subprocess.run(["git", "init", "-q", "-b", "main"], cwd=primary, check=True) + subprocess.run(["git", "init", "-q", "--bare"], cwd=remote, check=True) + subprocess.run(["git", "config", "user.email", "test@example.invalid"], cwd=primary, check=True) + subprocess.run(["git", "config", "user.name", "Test User"], cwd=primary, check=True) + subprocess.run(["git", "commit", "-qm", "base", "--allow-empty"], cwd=primary, check=True) + subprocess.run(["git", "remote", "add", "origin", str(remote)], cwd=primary, check=True) + subprocess.run(["git", "push", "-qu", "origin", "main"], cwd=primary, check=True) + worktree = runtime_worktree_path(primary, "same-slug", workspace_root=workspace) + worktree.parent.mkdir(parents=True, exist_ok=True) + subprocess.run( + ["git", "worktree", "add", "-q", "-b", "work/same-slug", str(worktree), "HEAD"], + cwd=primary, + check=True, + ) + return worktree + + +def merged_receipt(path: Path) -> dict[str, object]: + return bound_receipt( + path, + lane="remote-merged", + status="merged_pr_preserved", + pr_state="MERGED", + pr_url="https://github.com/example/repo/pull/7", + ) + + +def test_remote_merged_receipt_is_bound_to_exact_same_slug_worktree_and_head( + tmp_path: Path, +) -> None: + workspace = tmp_path / "Workspace" + first = canonical_same_slug_worktree(tmp_path, workspace, "first") + second = canonical_same_slug_worktree(tmp_path, workspace, "second") + foreign = merged_receipt(first) + stale = merged_receipt(second) + + subprocess.run( + ["git", "commit", "-qm", "local-only", "--allow-empty"], + cwd=second, + check=True, + ) + current = merged_receipt(second) + args = (second, time.time(), 0, set()) + + assert wd._classify(*args, [foreign]) == "unpushed-commits" + assert wd._classify(*args, [stale]) == "unpushed-commits" + assert wd._classify(*args, [current]) == "receipt-remote-merged+clean+idle" def test_reachable_from_remote_uses_single_contains_query(tmp_path: Path, monkeypatch): @@ -50,7 +114,7 @@ def fake_git(args: list[str], cwd: Path, timeout: int = 30) -> subprocess.Comple ] -def test_documented_non_source_residue_is_visible_but_not_debt(tmp_path: Path, monkeypatch): +def test_legacy_unbound_documented_residue_receipt_fails_closed_as_debt(tmp_path: Path, monkeypatch): worktrees = tmp_path / ".limen-worktrees" documented = worktrees / "cache-only-root" undocumented = worktrees / "unknown-root" @@ -81,11 +145,42 @@ def test_documented_non_source_residue_is_visible_but_not_debt(tmp_path: Path, m report = worktree_debt_report(tmp_path) by_name = {item["name"]: item for item in report["items"]} - assert by_name["cache-only-root"]["reason"] == "documented-residue" - assert by_name["cache-only-root"]["debt"] is False + assert by_name["cache-only-root"]["reason"] == "not-a-git-dir" + assert by_name["cache-only-root"]["debt"] is True assert by_name["unknown-root"]["reason"] == "not-a-git-dir" assert by_name["unknown-root"]["debt"] is True - assert report["debt"] == 1 + assert report["debt"] == 2 + + +def test_canonical_runtime_unit_and_legacy_dispatch_unit_are_classified_once( + tmp_path: Path, + monkeypatch, +) -> None: + workspace = tmp_path / "Workspace" + namespace = workspace / "runtime" / "worktrees" / "owner--repo--digest" + canonical_unit = namespace / "canonical-task" + canonical_unit.mkdir(parents=True) + legacy = tmp_path / "Scratch" / "limen-worktrees" + legacy_unit = legacy / "legacy-task" + legacy_unit.mkdir(parents=True) + limen_root = tmp_path / "limen" + limen_root.mkdir() + monkeypatch.setenv("WORKSPACE_ROOT", str(workspace)) + monkeypatch.setenv("LIMEN_WORKTREE_ROOT", str(legacy)) + monkeypatch.setenv("LIMEN_RECLAIM_CLAUDE_WT", "0") + monkeypatch.setenv("LIMEN_RECLAIM_AGY_SCRATCH", "0") + monkeypatch.setenv("LIMEN_RECLAIM_REPO_LOCAL_WT", "0") + monkeypatch.setenv("LIMEN_RECLAIM_REGISTERED_WT", "0") + + report = worktree_debt_report(limen_root) + paths = [Path(item["path"]) for item in report["items"]] + by_path = {Path(item["path"]): item for item in report["items"]} + + assert paths.count(canonical_unit) == 1 + assert paths.count(legacy_unit) == 1 + assert namespace not in paths + assert by_path[canonical_unit]["reason"] == "not-a-git-dir" + assert by_path[legacy_unit]["reason"] == "not-a-git-dir" def test_generated_log_shell_is_visible_but_not_debt(tmp_path: Path, monkeypatch): @@ -143,11 +238,11 @@ def test_remote_superseded_receipt_is_visible_but_not_debt(tmp_path: Path, monke json.dumps( { "receipts": [ - { - "root": "superseded-root", - "lane": "remote-superseded", - "status": "superseded_on_origin_main", - } + bound_receipt( + root, + lane="remote-superseded", + status="superseded_on_origin_main", + ) ] } ), @@ -180,13 +275,13 @@ def test_remote_merged_receipt_is_visible_but_not_debt(tmp_path: Path, monkeypat json.dumps( { "receipts": [ - { - "root": "merged-pr-root", - "lane": "remote-merged", - "status": "merged_pr_preserved", - "pr_state": "MERGED", - "pr_url": "https://github.com/example/repo/pull/7", - } + bound_receipt( + root, + lane="remote-merged", + status="merged_pr_preserved", + pr_state="MERGED", + pr_url="https://github.com/example/repo/pull/7", + ) ] } ), @@ -222,12 +317,12 @@ def test_remote_pr_open_receipt_is_visible_but_not_debt(tmp_path: Path, monkeypa json.dumps( { "receipts": [ - { - "root": "open-pr-root", - "lane": "remote-pr-open", - "status": "open_pr_preserved", - "pr_state": "OPEN", - } + bound_receipt( + root, + lane="remote-pr-open", + status="open_pr_preserved", + pr_state="OPEN", + ) ] } ), @@ -262,13 +357,13 @@ def test_owner_blocker_private_receipt_is_visible_but_not_debt(tmp_path: Path, m json.dumps( { "receipts": [ - { - "root": "owner-blocked-root", - "lane": "owner-blocker", - "status": "private_patch_preserved", - "private_receipt": ".limen-private/session-corpus/lifecycle/worktree-preserve/demo/receipt.json", - "private_patch_sha256": "abc123", - } + bound_receipt( + root, + lane="owner-blocker", + status="private_patch_preserved", + private_receipt=".limen-private/session-corpus/lifecycle/worktree-preserve/demo/receipt.json", + private_patch_sha256="abc123", + ) ] } ), diff --git a/cli/tests/test_worktree_reap_command.py b/cli/tests/test_worktree_reap_command.py new file mode 100644 index 000000000..3321a0d78 --- /dev/null +++ b/cli/tests/test_worktree_reap_command.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +from click.testing import CliRunner + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "cli" / "src")) + +from limen import cli # noqa: E402 + + +def _reaper_root(tmp_path: Path) -> Path: + root = tmp_path / "limen" + script = root / "scripts" / "reclaim-worktrees.py" + script.parent.mkdir(parents=True) + script.write_text("#!/usr/bin/env python3\n", encoding="utf-8") + return root + + +def test_worktree_reap_forwards_check_json_output_and_exit_status( + tmp_path: Path, + monkeypatch, +) -> None: + root = _reaper_root(tmp_path) + observed: list[tuple[list[str], Path]] = [] + + def fake_run(args, *, cwd, capture_output, text, check): + assert capture_output is text is True + assert check is False + observed.append((args, cwd)) + return subprocess.CompletedProcess(args, 7, '{"mode":"CHECK"}\n', "bounded warning\n") + + monkeypatch.setattr(cli, "resolve_limen_repo_root", lambda: root) + monkeypatch.setattr(cli.subprocess, "run", fake_run) + + result = CliRunner().invoke(cli.main, ["worktree", "reap", "--check", "--json"]) + + assert result.exit_code == 7 + assert observed == [ + ( + [ + sys.executable, + str(root / "scripts" / "reclaim-worktrees.py"), + "--check", + "--json", + ], + root, + ) + ] + assert '{"mode":"CHECK"}' in result.output + assert "bounded warning" in result.output + + +def test_worktree_reap_default_is_dry_run_and_help_is_forwarded( + tmp_path: Path, + monkeypatch, +) -> None: + root = _reaper_root(tmp_path) + observed: list[list[str]] = [] + + def fake_run(args, **_kwargs): + observed.append(args) + return subprocess.CompletedProcess(args, 0, "usage: reclaim-worktrees.py\n", "") + + monkeypatch.setattr(cli, "resolve_limen_repo_root", lambda: root) + monkeypatch.setattr(cli.subprocess, "run", fake_run) + + default = CliRunner().invoke(cli.main, ["worktree", "reap"]) + help_result = CliRunner().invoke(cli.main, ["worktree", "reap", "--help"]) + + assert default.exit_code == 0 + assert help_result.exit_code == 0 + assert observed == [ + [sys.executable, str(root / "scripts" / "reclaim-worktrees.py")], + [sys.executable, str(root / "scripts" / "reclaim-worktrees.py"), "--help"], + ] + assert all("--apply" not in args for args in observed) + assert "usage: reclaim-worktrees.py" in help_result.output diff --git a/cli/tests/test_worktree_roots.py b/cli/tests/test_worktree_roots.py index 64b634404..643a03424 100644 --- a/cli/tests/test_worktree_roots.py +++ b/cli/tests/test_worktree_roots.py @@ -23,6 +23,7 @@ _legacy_dispatch_roots, _path_list, _registered_repo_roots, + canonical_runtime_worktree_root, dispatch_clone_cache_root, iter_worktree_targets, ) @@ -365,6 +366,77 @@ def test_iter_worktree_targets_dispatch_root_children(tmp_path, monkeypatch): assert "active-task" in names +def test_iter_worktree_targets_enumerates_canonical_units_and_separate_legacy_root( + tmp_path, + monkeypatch, +): + workspace = tmp_path / "Workspace" + canonical = workspace / "runtime" / "worktrees" + namespace = canonical / "owner--repo--digest" + unit = namespace / "bounded-task" + unit.mkdir(parents=True) + legacy = tmp_path / "Scratch" / "limen-worktrees" + legacy_unit = legacy / "legacy-task" + legacy_unit.mkdir(parents=True) + monkeypatch.setenv("WORKSPACE_ROOT", str(workspace)) + monkeypatch.setenv("LIMEN_WORKTREE_ROOT", str(legacy)) + monkeypatch.setenv("LIMEN_RECLAIM_CLAUDE_WT", "0") + monkeypatch.setenv("LIMEN_RECLAIM_AGY_SCRATCH", "0") + monkeypatch.setenv("LIMEN_RECLAIM_REPO_LOCAL_WT", "0") + monkeypatch.setenv("LIMEN_RECLAIM_REGISTERED_WT", "0") + + targets = iter_worktree_targets(tmp_path) + + assert canonical_runtime_worktree_root() == canonical + assert sum(target.path == unit for target in targets) == 1 + canonical_target = next(target for target in targets if target.path == unit) + assert canonical_target.source.startswith("canonical-runtime-worktree:") + assert canonical_target.min_age_h == 168.0 + assert not any(target.path == namespace for target in targets) + assert any(target.path == legacy_unit and target.source == "dispatch-root" for target in targets) + + monkeypatch.setenv("LIMEN_WORKTREE_ROOT", str(canonical)) + monkeypatch.setenv("LIMEN_RECLAIM_LEGACY_DISPATCH_WT", "1") + monkeypatch.setenv("LIMEN_RECLAIM_LEGACY_WORKTREE_ROOTS", str(canonical)) + canonical_only = iter_worktree_targets(tmp_path) + assert sum(target.path == unit for target in canonical_only) == 1 + assert not any(target.path == namespace for target in canonical_only) + + +@pytest.mark.parametrize("symlink_level", ["workspace", "runtime", "worktrees"]) +def test_canonical_runtime_inventory_rejects_every_ancestor_symlink( + tmp_path, + monkeypatch, + symlink_level, +): + workspace = tmp_path / "Workspace" + outside = tmp_path / "outside" + (outside / "runtime" / "worktrees" / "owner--repo--digest" / "escaped").mkdir(parents=True) + if symlink_level == "workspace": + workspace.symlink_to(outside, target_is_directory=True) + elif symlink_level == "runtime": + workspace.mkdir() + (workspace / "runtime").symlink_to(outside / "runtime", target_is_directory=True) + else: + (workspace / "runtime").mkdir(parents=True) + (workspace / "runtime" / "worktrees").symlink_to( + outside / "runtime" / "worktrees", + target_is_directory=True, + ) + dispatch = tmp_path / "dispatch" + dispatch.mkdir() + monkeypatch.setenv("WORKSPACE_ROOT", str(workspace)) + monkeypatch.setenv("LIMEN_WORKTREE_ROOT", str(dispatch)) + monkeypatch.setenv("LIMEN_RECLAIM_CLAUDE_WT", "0") + monkeypatch.setenv("LIMEN_RECLAIM_AGY_SCRATCH", "0") + monkeypatch.setenv("LIMEN_RECLAIM_REPO_LOCAL_WT", "0") + monkeypatch.setenv("LIMEN_RECLAIM_REGISTERED_WT", "0") + + assert iter_worktree_targets(tmp_path) == [] + with pytest.raises(WorktreeInventoryError, match="must be a physical directory"): + iter_worktree_targets(tmp_path, strict=True) + + def test_iter_worktree_targets_workspace_checkouts_are_explicitly_armed(tmp_path, monkeypatch): dispatch = tmp_path / ".limen-worktrees" dispatch.mkdir() diff --git a/container/launchd/com.limen.claude-stub-heal.plist b/container/launchd/com.limen.claude-stub-heal.plist index 696d654e9..858ddf203 100644 --- a/container/launchd/com.limen.claude-stub-heal.plist +++ b/container/launchd/com.limen.claude-stub-heal.plist @@ -25,7 +25,7 @@ ProgramArguments /bin/bash - /Users/4jp/Workspace/limen/scripts/heal-claude-lsregister.sh + /Users/4jp/Workspace/library/engine/organvm/limen/scripts/heal-claude-lsregister.sh --apply WatchPaths @@ -40,7 +40,7 @@ RunAtLoad ThrottleInterval10 ProcessTypeBackground - StandardOutPath/Users/4jp/Workspace/limen/logs/claude-stub-heal.log - StandardErrorPath/Users/4jp/Workspace/limen/logs/claude-stub-heal.log + StandardOutPath/Users/4jp/Workspace/library/engine/organvm/limen/logs/claude-stub-heal.log + StandardErrorPath/Users/4jp/Workspace/library/engine/organvm/limen/logs/claude-stub-heal.log diff --git a/container/launchd/com.limen.creds-hydrate.plist b/container/launchd/com.limen.creds-hydrate.plist index 54610afc6..a59a7a58d 100644 --- a/container/launchd/com.limen.creds-hydrate.plist +++ b/container/launchd/com.limen.creds-hydrate.plist @@ -29,15 +29,15 @@ EnvironmentVariables HOME/Users/4jp - LIMEN_ROOT/Users/4jp/Workspace/limen - PYTHONPATH/Users/4jp/Workspace/limen/cli/src + LIMEN_ROOT/Users/4jp/Workspace/library/engine/organvm/limen + PYTHONPATH/Users/4jp/Workspace/library/engine/organvm/limen/cli/src StartInterval1800 RunAtLoad ProcessTypeBackground - StandardOutPath/Users/4jp/Workspace/limen/logs/creds-hydrate.out.log - StandardErrorPath/Users/4jp/Workspace/limen/logs/creds-hydrate.err.log + StandardOutPath/Users/4jp/Workspace/library/engine/organvm/limen/logs/creds-hydrate.out.log + StandardErrorPath/Users/4jp/Workspace/library/engine/organvm/limen/logs/creds-hydrate.err.log diff --git a/container/launchd/com.limen.heartbeat.plist b/container/launchd/com.limen.heartbeat.plist index ec046bc1a..6371cfaec 100644 --- a/container/launchd/com.limen.heartbeat.plist +++ b/container/launchd/com.limen.heartbeat.plist @@ -12,12 +12,12 @@ ProgramArguments /bin/bash - /Users/4jp/Workspace/limen/scripts/heartbeat-loop.sh + /Users/4jp/Workspace/library/engine/organvm/limen/scripts/heartbeat-loop.sh EnvironmentVariables HOME/Users/4jp - LIMEN_ROOT/Users/4jp/Workspace/limen + LIMEN_ROOT/Users/4jp/Workspace/library/engine/organvm/limen LIMEN_WORKDIR/Users/4jp/Workspace LIMEN_WORKTREES/Volumes/Scratch/limen-worktrees LIMEN_WORKTREE_ROOT/Volumes/Scratch/limen-worktrees @@ -30,7 +30,7 @@ RunAtLoad ThrottleInterval60 ProcessTypeBackground - StandardOutPath/Users/4jp/Workspace/limen/logs/heartbeat.out.log - StandardErrorPath/Users/4jp/Workspace/limen/logs/heartbeat.err.log + StandardOutPath/Users/4jp/Workspace/library/engine/organvm/limen/logs/heartbeat.out.log + StandardErrorPath/Users/4jp/Workspace/library/engine/organvm/limen/logs/heartbeat.err.log diff --git a/container/launchd/com.limen.overnight-watch.plist b/container/launchd/com.limen.overnight-watch.plist index c08f73376..c18b246da 100644 --- a/container/launchd/com.limen.overnight-watch.plist +++ b/container/launchd/com.limen.overnight-watch.plist @@ -18,7 +18,7 @@ EnvironmentVariables HOME/Users/4jp - LIMEN_ROOT/Users/4jp/Workspace/limen + LIMEN_ROOT/Users/4jp/Workspace/library/engine/organvm/limen PYTHONPATH/Users/4jp/.local/share/limen/current/source/cli/src LIMEN_OVERNIGHT_WATCH_EXPECT_DISPATCH_ASYNC1 LIMEN_OVERNIGHT_WATCH_EXPECT_DISPATCH_LANESauto @@ -26,7 +26,7 @@ StartInterval300 RunAtLoad ProcessTypeBackground - StandardOutPath/Users/4jp/Workspace/limen/logs/overnight-watch.out.log - StandardErrorPath/Users/4jp/Workspace/limen/logs/overnight-watch.err.log + StandardOutPath/Users/4jp/Workspace/library/engine/organvm/limen/logs/overnight-watch.out.log + StandardErrorPath/Users/4jp/Workspace/library/engine/organvm/limen/logs/overnight-watch.err.log diff --git a/container/launchd/com.limen.watchdog.plist b/container/launchd/com.limen.watchdog.plist index 00e40fc44..d3feaf1da 100644 --- a/container/launchd/com.limen.watchdog.plist +++ b/container/launchd/com.limen.watchdog.plist @@ -25,8 +25,8 @@ EnvironmentVariables HOME/Users/4jp - LIMEN_ROOT/Users/4jp/Workspace/limen - PYTHONPATH/Users/4jp/Workspace/limen/cli/src + LIMEN_ROOT/Users/4jp/Workspace/library/engine/organvm/limen + PYTHONPATH/Users/4jp/Workspace/library/engine/organvm/limen/cli/src LIMEN_WATCHDOG_HEAL1