Files
rune/scripts/factory_worktree.py
T
Nick 3432f6d3ba feat(factory): worktree isolation enforcement — per-wave git worktrees, landing gate judges the merged result
factory_worktree.py: create (git worktree add rune-wave-<hash>),
enter, land (merge inside throwaway rune-merge worktree, run
unified_check.py + check_landing.py against the MERGED tree, green =
CLEARED TO LAND), cleanup. No subcommand writes files or moves refs
in the main tree (update-ref ref-surgery rejected — phantom-dirty +
silent no-op, probe-proven). 8 frozen tests red-first. Landing gate
catches breakage at merge: unbuildable Go module, RED manifest,
missing manifest. Main-session verify: own runs green.
2026-10-18 00:38:39 +01:00

302 lines
12 KiB
Python

#!/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 <sibling>/rune-wave-<hash> -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-<hash>), then gate
the MERGED result:
1. EVIDENCE — the merged tree's own
scripts/unified_check.py runs there and writes
verification/unified-check-<merged-sha>.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 <merge worktree>: 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 <wave>
cd "$(python scripts/factory_worktree.py enter <wave>)"
python scripts/factory_worktree.py land <wave>
python scripts/factory_worktree.py cleanup <wave>
"""
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())