diff --git a/laptop/adopt-project-config.sh b/laptop/adopt-project-config.sh index 65aec6c..1ab607d 100755 --- a/laptop/adopt-project-config.sh +++ b/laptop/adopt-project-config.sh @@ -72,6 +72,25 @@ adopt() { [[ -e "$src" ]] || die "$src does not exist โ€” nothing to adopt for $name" [[ -d "$src" ]] || die "$src is not a directory" + + # A project that tracks its own .claude has already solved this problem: the + # configuration is versioned with the code and reaches both machines by the + # same pull that brings everything else. Replacing the directory with a + # symlink does not centralise it, it breaks it โ€” git will not follow a symlink + # to reach a tracked path, so every file under it reports as deleted on both + # machines, and committing that would remove them from the project. + # + # The premise of this whole scheme is a .claude that is NOT reliably + # committed. Where the premise does not hold, neither does the scheme. + if git -C "$project" rev-parse --git-dir >/dev/null 2>&1; then + local tracked + tracked=$(git -C "$project" ls-files -- .claude | head -3) + [[ -n "$tracked" ]] && die "$name tracks its own .claude in its repository: +$tracked +That configuration already reaches both machines through the project's own git +history, and linking it away would report every tracked file as deleted. Leave +this project alone." + fi # Refuse rather than merge. Two directories of the same name are two different # configurations, and picking one silently loses the other (ยง1 criterion 8). [[ -e "$dest" ]] && die "$dest already exists; move or remove it before adopting $name" diff --git a/server/lib/22-claude-config.sh b/server/lib/22-claude-config.sh index c911c14..19b1280 100755 --- a/server/lib/22-claude-config.sh +++ b/server/lib/22-claude-config.sh @@ -235,6 +235,19 @@ for cfg in "$DST"/project-config/*/; do continue fi + # Same refusal as laptop/adopt-project-config.sh, asserted again here because + # this side links from the delivered payload and would otherwise relink a + # project whose repository tracks its own .claude. git will not follow a + # symlink to reach a tracked path, so the link makes every file under it + # report as deleted, and an agent working in that project sees its + # configuration vanish from git while still being read. + if [[ -d "$proj/.git" ]] && [[ -n "$(git -C "$proj" ls-files -- .claude 2>/dev/null | head -1)" ]]; then + defer "$link" \ + "$pname tracks its own .claude in its repository, so it reaches both machines by git already; linking it away would report every tracked file as deleted" \ + "rm -rf '$DST/project-config/$pname' on the laptop's ~/.claude repository, and commit" + continue + fi + if [[ -d "$link" ]]; then # What blocks the replacement is divergence, not mere presence. A project # pushed from the laptop already has copies of everything the payload diff --git a/sync-design.md b/sync-design.md index 483166a..0a3223f 100644 --- a/sync-design.md +++ b/sync-design.md @@ -93,7 +93,18 @@ than a guess. box, seven hold nothing in `.claude/` but `settings.local.json` and lock files, which are per-machine by design and stay out of the repository either way. Linking those would centralise nothing while adding a link that can break, so -`untense`, `hezo` and `peek` are adopted and the rest are left alone. +they are left alone. `hezo` and `peek` are adopted. + +**A project that tracks its own `.claude` is refused.** The premise here is a +project configuration that is *not* reliably committed; where a project versions +it with the code, it already reaches both machines by the same pull, and the +symlink does not centralise it but breaks it โ€” git does not follow a symlink to +reach a tracked path, so every file under it reports as deleted on both machines +and committing that removes them from the project. `untense` was adopted before +this was understood, and an agent working on the box stopped rather than commit +a tree that had lost two skills. Both `laptop/adopt-project-config.sh` and +`server/lib/22-claude-config.sh` now refuse such a project, and the refusal is +asserted on both sides because either can create the link. ## The two pieces of work