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.
302 lines
12 KiB
Python
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())
|