#!/usr/bin/env python3 """factory_worktree.py — the factory worktree-isolation law, mechanical. The 2026-10-17 failure: 30 factory waves shared ONE working tree, so a sibling's mid-flight edit (the rune-entry syntax error that broke the user's boot), `git add -A` file sweeps and compile races all landed on the SAME checkout the user boots. Owner dispatch (approval_ref 'user:wave-worktree-isolation dispatch'): waves work in ISOLATED git worktrees; the landing gate runs against the MERGED result, never the mid-flight tree; the main tree stays bootable ALWAYS. Commands (all take --repo-root, default this repo): create NAME git worktree add /rune-wave- -b wave/NAME at the main tree's HEAD. Sibling of the repo, never inside it. Duplicate wave names are refused (the factory duplicate-spawn law). enter NAME print the wave's worktree path (cd there to work). land NAME merge wave/NAME into the target branch inside a THROWAWAY merge worktree (rune-merge-), then gate the MERGED result: 1. EVIDENCE — the merged tree's own scripts/unified_check.py runs there and writes verification/unified-check-.json; 2. GATE — the owner's scripts/check_landing.py (the protected class the factory cannot modify, per the self-modification bootstrap ruling) runs with --repo-root : manifests, hygiene and the landed-sha build sweep all judge the MERGED tree. Green: the wave is CLEARED TO LAND — the tool prints the verified merged tree sha and the exact serial handoff (git merge --no-ff wave/NAME) for the MAIN SESSION to run. It moves no refs: update-ref surgery was tried and leaves the main checkout phantom-dirty with a no-op ff-only forever after. A target that moved mid-land refuses with re-land (the same-parents merge, and so the verified tree, would silently change underneath the gate). Red: refused; the main tree and the wave worktree are untouched. cleanup NAME git worktree remove + branch delete for the wave (and any crashed merge leftovers for that name). The main tree stays bootable ALWAYS because no subcommand writes a single file inside the main checkout — work happens in sibling worktrees and only refs move, only after the gate is green. The main session applies the fast-forward to its working tree serially (its call; `git merge --ff-only` shows the verified content). Usage: python scripts/factory_worktree.py --repo-root /path/to/rune create cd "$(python scripts/factory_worktree.py enter )" python scripts/factory_worktree.py land python scripts/factory_worktree.py cleanup """ from __future__ import annotations import argparse import hashlib import re import subprocess import sys from pathlib import Path SCRIPTS = Path(__file__).resolve().parent OWNER_GATE = SCRIPTS / "check_landing.py" WAVE_DIR_PREFIX = "rune-wave-" MERGE_DIR_PREFIX = "rune-merge-" WAVE_BRANCH = "wave/{name}" MERGE_BRANCH = "merge/{name}" NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") class Refused(Exception): """A landing/create the law refuses — message says the fix.""" def git(root: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess: r = subprocess.run(["git", "-C", str(root), *args], capture_output=True, text=True) if check and r.returncode != 0: raise RuntimeError(f"git {' '.join(args[:3])} failed: {r.stderr.strip()}") return r def main_branch(repo: Path) -> str: """The branch checked out in the MAIN worktree (the landing target).""" out = git(repo, "symbolic-ref", "--short", "HEAD").stdout.strip() if not out: raise Refused("the main tree is on a detached HEAD — land needs a " "branch target") return out def worktree_parent(repo: Path) -> Path: return repo.resolve().parent def wave_dir(repo: Path, name: str) -> Path: """Hash-named sibling dir (deterministic per wave name + base sha).""" base = git(repo, "rev-parse", "HEAD").stdout.strip() digest = hashlib.sha1(f"{name}@{base}".encode()).hexdigest()[:8] return worktree_parent(repo) / f"{WAVE_DIR_PREFIX}{digest}" def find_worktree(repo: Path, branch: str) -> Path | None: """Locate the linked worktree checked out on `branch`.""" blocks, cur = [], {} for line in git(repo, "worktree", "list", "--porcelain").stdout.splitlines(): if not line.strip(): if cur: blocks.append(cur) cur = {} continue key, _, val = line.partition(" ") cur[key] = val if cur: blocks.append(cur) for b in blocks: if b.get("branch") == f"refs/heads/{branch}": return Path(b["worktree"]) return None def require_name(name: str) -> None: if not NAME_RE.match(name): raise Refused(f"wave name {name!r} must match {NAME_RE.pattern} " "(it becomes a branch name)") def require_main_worktree(repo: Path) -> None: if not (repo / ".git").is_dir(): raise Refused("--repo-root must be the MAIN worktree (a linked " "worktree has a .git FILE); waves live beside it") def cmd_create(repo: Path, name: str) -> int: require_name(name) require_main_worktree(repo) branch = WAVE_BRANCH.format(name=name) if git(repo, "rev-parse", "--verify", "-q", f"refs/heads/{branch}", check=False).returncode == 0: raise Refused(f"wave branch {branch} already exists — one name, one " "wave (the duplicate-spawn law); pick a new name") target = wave_dir(repo, name) if target.exists(): raise Refused(f"worktree dir {target} already exists — refusing to " "overwrite it") base = main_branch(repo) git(repo, "worktree", "add", "-b", branch, str(target), base) print(f"GREEN: wave '{name}' isolated at {target}") print(f" branch {branch} off {base}; the main tree was not touched") print(target) return 0 def cmd_enter(repo: Path, name: str) -> int: require_name(name) wt = find_worktree(repo, WAVE_BRANCH.format(name=name)) if wt is None: raise Refused(f"no worktree for wave '{name}' — create it first") print(wt) return 0 def _cleanup_merge(repo: Path, name: str) -> None: """Best-effort removal of the throwaway merge worktree/branch.""" branch = MERGE_BRANCH.format(name=name) wt = find_worktree(repo, branch) if wt is not None: git(repo, "worktree", "remove", "--force", str(wt), check=False) git(repo, "branch", "-D", branch, check=False) git(repo, "worktree", "prune", check=False) def cmd_land(repo: Path, name: str, base: str | None) -> int: require_name(name) require_main_worktree(repo) wave_branch = WAVE_BRANCH.format(name=name) if find_worktree(repo, wave_branch) is None and git( repo, "rev-parse", "--verify", "-q", f"refs/heads/{wave_branch}", check=False).returncode != 0: raise Refused(f"no worktree or branch for wave '{name}'") target = base or main_branch(repo) if target == wave_branch: raise Refused("a wave cannot land onto its own branch") base_sha = git(repo, "rev-parse", f"refs/heads/{target}").stdout.strip() suffix = wave_dir(repo, name).name[len(WAVE_DIR_PREFIX):] merge_wt = worktree_parent(repo) / f"{MERGE_DIR_PREFIX}{suffix}" merge_branch = MERGE_BRANCH.format(name=name) _cleanup_merge(repo, name) # crashed-land leftovers are throwaways try: git(repo, "worktree", "add", "-b", merge_branch, str(merge_wt), base_sha) merge = git(merge_wt, "merge", "--no-ff", wave_branch, "-m", f"land wave {name} (gate on merged result)", check=False) if merge.returncode != 0: raise Refused(f"merge of {wave_branch} into {target} has " f"conflicts — resolve them in a fresh wave " f"commit; nothing landed:\n{merge.stdout}") # 1. EVIDENCE: the merged tree's own runner records the manifest runner = merge_wt / "scripts" / "unified_check.py" if not runner.is_file(): raise Refused(f"merged tree ships no scripts/unified_check.py " f"— no evidence can be recorded for {name}") run = subprocess.run([sys.executable, str(runner)], cwd=merge_wt, capture_output=True, text=True) if run.returncode != 0: raise Refused(f"unified runner RED on the merged tree — fix the " f"wave, never skip the gate:\n{run.stdout}") # 2. GATE: the OWNER's landing gate judges the MERGED result # (protected class — never the merged tree's own copy) gate = subprocess.run( [sys.executable, str(OWNER_GATE), "--repo-root", str(merge_wt), "HEAD"], capture_output=True, text=True) if gate.returncode != 0: raise Refused(f"landing gate REFUSED the merged result:\n" f"{gate.stdout}{gate.stderr}") # serial landing: if the target moved while we gated, the main # session's same-parents merge would NOT reproduce this verified # tree — the clearance is stale, refuse it now = git(repo, "rev-parse", f"refs/heads/{target}").stdout.strip() if now != base_sha: raise Refused(f"target {target} moved during the landing " f"({base_sha[:12]} -> {now[:12]}) — landings are " "serial; re-run land to gate the new merge") merged_tree = git(merge_wt, "rev-parse", "HEAD^{tree}").stdout.strip() print(f"GREEN: wave '{name}' CLEARED TO LAND onto {target}") print(f" gate passed on the MERGED tree {merged_tree}") print(f" main session lands serially:") print(f" git -C {repo} merge --no-ff {wave_branch} " f"-m 'land wave {name} (gate GREEN tree {merged_tree[:12]})'") print(f" (the merge must reproduce tree {merged_tree}; if {target} " "moved, re-run land)") return 0 finally: _cleanup_merge(repo, name) def cmd_cleanup(repo: Path, name: str, force: bool) -> int: require_name(name) branch = WAVE_BRANCH.format(name=name) wt = find_worktree(repo, branch) if wt is not None: args = ["worktree", "remove"] if force: args.append("--force") r = git(repo, *args, str(wt), check=False) if r.returncode != 0 and not force: raise Refused(f"worktree {wt} has uncommitted work — pass " "--force to discard it") if git(repo, "rev-parse", "--verify", "-q", f"refs/heads/{branch}", check=False).returncode == 0: git(repo, "branch", "-D", branch, check=False) _cleanup_merge(repo, name) git(repo, "worktree", "prune", check=False) print(f"GREEN: wave '{name}' cleaned up (worktree + branches)") return 0 def main(argv=None) -> int: ap = argparse.ArgumentParser(description="factory worktree isolation") ap.add_argument("--repo-root", default=str(SCRIPTS.parent)) sub = ap.add_subparsers(dest="cmd", required=True) for name in ("create", "enter", "land", "cleanup"): p = sub.add_parser(name) p.add_argument("wave") if name == "land": p.add_argument("--base", default=None, help="target branch (default: main tree's)") if name == "cleanup": p.add_argument("--force", action="store_true", help="discard uncommitted wave work") args = ap.parse_args(argv) repo = Path(args.repo_root).resolve() handlers = { "create": lambda: cmd_create(repo, args.wave), "enter": lambda: cmd_enter(repo, args.wave), "land": lambda: cmd_land(repo, args.wave, args.base), "cleanup": lambda: cmd_cleanup(repo, args.wave, args.force), } try: return handlers[args.cmd]() except Refused as exc: print(f"RED: {exc}") return 1 except RuntimeError as exc: print(f"RED: {exc}") return 1 if __name__ == "__main__": sys.exit(main())