workspace: the root CLAUDE.md becomes a symlink; check 5 learns two shapes (R-230(b))
gates / gates (push) Successful in 7s

Two files kept identical by hand and one check is a divergence class policed forever; one file
reachable by two paths cannot diverge at all. install_workspace.py now links by default, MIGRATES
an existing regular file (backing it up first and SAYING SO if it differed -- that difference is
the last chance to notice an unsynced edit), and keeps --copy for a clone that wants the old shape.

Check 5 asserts a different thing per shape: for a link, that it points at the versioned copy and
resolves to a real file; for two files, byte-identity as before. A dangling link is worse than a
diverged copy -- the instructions load NOTHING and there is no content left to notice is wrong --
so that case is red-proofed.

NOT yet proven to LOAD: that needs a fresh session and a hook line, which is Phase 7. If it does
not load, this reverts to the copy.
This commit is contained in:
2026-08-06 11:41:36 +02:00
parent 15fa5273ba
commit f49b1f390b
3 changed files with 185 additions and 7 deletions
+82 -5
View File
@@ -57,19 +57,91 @@ def read(path):
return fh.read()
def install_claude_md(workspace, dry_run):
def install_claude_md(workspace, dry_run, mode):
"""Install the workspace-root CLAUDE.md, as a symlink by default.
THE LINK IS THE POINT (R-230(b)). Two files kept identical by hand and one check is a
divergence class that has to be policed forever; one file reachable by two paths cannot diverge
at all. Claude Code reads through symlinks — the four skills have been symlinks into this tree
for months — but the link is still PROVEN from a fresh session rather than assumed, because a
workspace file that silently stops loading is worse than two files kept in step by hand.
`--copy` keeps the old two-file shape for a clone where linking is not wanted; check 5 accepts
either.
"""
target = os.path.join(workspace, "CLAUDE.md")
if not os.path.exists(CLAUDE_SRC):
problems.append("versioned source missing: %s" % CLAUDE_SRC)
return
src_text = read(CLAUDE_SRC)
rel = os.path.relpath(CLAUDE_SRC, workspace)
if mode == "link":
if os.path.islink(target):
if os.path.realpath(target) == os.path.realpath(CLAUDE_SRC):
if not os.path.isfile(os.path.realpath(target)):
problems.append(
"%s is a symlink to %s but the target does not resolve — the workspace "
"instructions load NOTHING." % (target, os.readlink(target))
)
else:
unchanged.append("CLAUDE.md (already a symlink -> %s)" % rel)
return
if dry_run:
changed.append("CLAUDE.md WOULD re-point the symlink to %s" % rel)
return
os.unlink(target)
os.symlink(rel, target)
changed.append("CLAUDE.md symlink re-pointed to %s" % rel)
return
if os.path.exists(target):
# MIGRATION: a real file is here. Back it up before it stops being a file, and say so
# if it differed — that difference is the last chance to notice an unsynced edit.
differed = read(target) != src_text
if dry_run:
changed.append(
"CLAUDE.md WOULD back up the regular file and replace it with a symlink%s"
% (" (IT DIFFERS from the versioned copy)" if differed else "")
)
return
b = backup(target)
os.unlink(target)
os.symlink(rel, target)
changed.append(
"CLAUDE.md MIGRATED file -> symlink (%s backed up to %s)%s"
% (
"content differed" if differed else "content was identical",
os.path.basename(b),
"\n THE LIVE FILE DIFFERED — check the backup before discarding it."
if differed
else "",
)
)
return
if dry_run:
changed.append("CLAUDE.md WOULD create symlink -> %s" % rel)
return
os.symlink(rel, target)
changed.append("CLAUDE.md symlink created -> %s" % rel)
return
# --- copy mode: the original two-file shape ---
if os.path.islink(target):
if dry_run:
changed.append("CLAUDE.md WOULD replace the symlink with a real copy")
return
os.unlink(target)
with open(target, "w", encoding="utf-8") as fh:
fh.write(src_text)
changed.append("CLAUDE.md symlink replaced with a real copy")
return
if os.path.exists(target):
if read(target) == src_text:
unchanged.append("CLAUDE.md (already byte-identical to the versioned copy)")
return
# Diverged. Back up FIRST, install, and report the direction so a human can judge which
# side was right — this script must not silently pick one.
if dry_run:
changed.append("CLAUDE.md WOULD back up and overwrite (live file differs from source)")
return
@@ -79,7 +151,8 @@ def install_claude_md(workspace, dry_run):
changed.append(
"CLAUDE.md DIVERGED -> backed up to %s, installed from %s.\n"
" If the LIVE file was the newer one, restore it and re-sync the versioned "
"copy instead; instructions_gate check 5 enforces they match." % (os.path.basename(b), CLAUDE_SRC)
"copy instead; instructions_gate check 5 enforces they match."
% (os.path.basename(b), CLAUDE_SRC)
)
return
@@ -157,18 +230,22 @@ def main(argv):
help="workspace root (default: the parent of this repo)")
ap.add_argument("--settings",
default=os.path.join(os.path.expanduser("~"), ".claude", "settings.json"))
ap.add_argument("--copy", action="store_true",
help="install the workspace CLAUDE.md as a real copy instead of a symlink "
"(the pre-2026-08-06 shape; check 5 accepts either)")
args = ap.parse_args(argv[1:])
print("install_workspace%s" % (" [--dry-run]" if args.dry_run else ""))
print(" workspace : %s" % args.workspace)
print(" settings : %s" % args.settings)
print(" mode : %s" % ("copy" if args.copy else "symlink"))
print("")
if not os.path.isdir(args.workspace):
print("FAIL: workspace root does not exist: %s" % args.workspace)
return 2
install_claude_md(args.workspace, args.dry_run)
install_claude_md(args.workspace, args.dry_run, "copy" if args.copy else "link")
install_hook(args.settings, args.dry_run)
for c in changed: