// Package mirror enforces one filesystem invariant, in every directory of a // tree: // // AGENTS.md is the real file; CLAUDE.md is a symlink pointing at it. // // Two agent harnesses read two different filenames for the same document, and a // repository that keeps both as real files keeps two documents — which drift, // silently, until somebody reads the stale one and believes it. One real file // with a link beside it is the only arrangement where that cannot happen. // // This package depends on nothing but the standard library. It performs no // merge and DELETES NO CONTENT: every branch is either a lossless repair or a // report, and the one case it refuses to resolve — two real files whose contents // differ — is the one where a wrong guess would destroy somebody's writing. package mirror import ( "bytes" "fmt" "io/fs" "os" "path/filepath" "strings" ) // The two names, and the link's target. The target is written relative on // purpose: a tree that is moved, copied or mounted somewhere else keeps working, // and an absolute link would point at wherever the repair happened to run. const ( Agents = "AGENTS.md" Claude = "CLAUDE.md" ) // skipDirs are never descended into. Each holds somebody else's tree — a // vendored dependency's AGENTS.md is that dependency's business, and rewriting // it would show up as a diff nobody asked for. Dot-directories are skipped by // the same argument and by a second one: `.git` is not a place to be creating // symlinks. var skipDirs = map[string]bool{ "node_modules": true, "__pycache__": true, "venv": true, "vendor": true, } // Result is what one walk found. Both halves are ordered by directory, because // the walk is, so two runs over the same tree report in the same order. type Result struct { // Fixes are the repairs made — or, from Check, the repairs that would be. Fixes []string // Conflicts are the directories this package refuses to resolve. A conflict // is reported identically by both entry points: nothing about it is a write. Conflicts []string } // Clean reports whether the tree was already canonical. func (r Result) Clean() bool { return len(r.Fixes) == 0 && len(r.Conflicts) == 0 } // Sync walks root and repairs every directory under it. func Sync(root string) Result { return walk(root, true) } // Check walks root and reports what Sync would do, writing nothing. // // The two share one code path with the writes turned off, so a check that says // nothing is a promise about the run that follows it rather than a second // implementation that might disagree. func Check(root string) Result { return walk(root, false) } func walk(root string, apply bool) Result { var res Result abs, err := filepath.Abs(root) if err != nil { return res } _ = filepath.WalkDir(abs, func(path string, d fs.DirEntry, err error) error { if err != nil { // An unreadable directory is skipped, never fatal: this runs over // somebody's whole working tree and one bad mode must not stop it. if d != nil && d.IsDir() { return fs.SkipDir } return nil } if !d.IsDir() { return nil } if path != abs { if name := d.Name(); skipDirs[name] || strings.HasPrefix(name, ".") { return fs.SkipDir } } fixDir(path, abs, apply, &res) return nil }) return res } // fixDir applies the invariant to one directory. // // The seven cases, and every one of them is either lossless or a refusal: // // AGENTS.md real, no CLAUDE.md ........ create the symlink // CLAUDE.md real, no AGENTS.md ........ rename to AGENTS.md, link back // CLAUDE.md symlink -> AGENTS.md ...... canonical, nothing to do // CLAUDE.md symlink elsewhere ......... re-point it // AGENTS.md symlink -> real CLAUDE.md . reversed layout, swap it round // both real, identical content ........ replace CLAUDE.md with the symlink // both real, different content ........ REFUSE, and say which directory // // A repair that fails halfway — an unwritable directory, a race with an editor — // reports nothing rather than a fix it did not make. Claiming a repair that did // not happen is worse than silence, because the next run would find the same // state and the operator would have been told twice that it was handled. func fixDir(dir, root string, apply bool, res *Result) { agents := filepath.Join(dir, Agents) claude := filepath.Join(dir, Claude) aInfo, aErr := os.Lstat(agents) cInfo, cErr := os.Lstat(claude) a, c := aErr == nil, cErr == nil if !a && !c { return } aLink := a && aInfo.Mode()&os.ModeSymlink != 0 cLink := c && cInfo.Mode()&os.ModeSymlink != 0 rel := func(p string) string { r, err := filepath.Rel(root, p) if err != nil { return p } if r == "." { return "" } return r } conflict := func(format string, v ...any) { res.Conflicts = append(res.Conflicts, fmt.Sprintf(format, v...)) } // fix runs the repair unless this is a check, and records it only if every // step of it succeeded. fix := func(msg string, steps ...func() error) { if apply { for _, step := range steps { if err := step(); err != nil { return } } } res.Fixes = append(res.Fixes, msg) } link := func() error { return os.Symlink(Agents, claude) } switch { case a && !c: if aLink && !exists(agents) { conflict("%s: broken symlink and no %s", rel(agents), Claude) return } fix(fmt.Sprintf("%s: created symlink -> %s", rel(claude), Agents), link) case c && !a: if cLink { target, _ := os.Readlink(claude) conflict("%s: symlink to missing target (%s)", rel(claude), target) return } fix(fmt.Sprintf("%s: renamed to %s, symlink left in place", rel(claude), Agents), func() error { return os.Rename(claude, agents) }, link) case cLink: if sameFile(claude, agents) { return // canonical } old, _ := os.Readlink(claude) fix(fmt.Sprintf("%s: re-pointed symlink (%s -> %s)", rel(claude), old, Agents), func() error { return os.Remove(claude) }, link) case aLink: // Reversed layout: AGENTS.md is the link and CLAUDE.md the real file. if !sameFile(agents, claude) { conflict("%s: symlink elsewhere while %s is a real file", rel(agents), Claude) return } fix(fmt.Sprintf("%s: swapped — %s is now the real file", rel(agents), Agents), func() error { return os.Remove(agents) }, func() error { return os.Rename(claude, agents) }, link) default: // Both are real files, and only their contents decide what happens. if !identical(agents, claude) { conflict("%s: %s and %s are different real files — merge manually", rel(dir), Agents, Claude) return } fix(fmt.Sprintf("%s: identical to %s, replaced with symlink", rel(claude), Agents), func() error { return os.Remove(claude) }, link) } } func exists(p string) bool { _, err := os.Stat(p) return err == nil } // sameFile reports whether two paths resolve to one file. func sameFile(a, b string) bool { ra, err := filepath.EvalSymlinks(a) if err != nil { return false } rb, err := filepath.EvalSymlinks(b) if err != nil { return false } return ra == rb } // identical compares two files by content, not by size or mtime. The whole // point of the comparison is to decide whether one of them may be deleted. func identical(a, b string) bool { ba, err := os.ReadFile(a) if err != nil { return false } bb, err := os.ReadFile(b) if err != nil { return false } return bytes.Equal(ba, bb) }