repos_sync: ask a target repo's own layout lint before installing .claude/ - #322
Merged
Merged
Conversation
…yautomind) repos_sync.py --write installs .claude/hooks/session-start.sh, .claude/settings.json and the CLAUDE.md pointer into every checked-out repo in repos.yaml, knowing nothing about the target beyond "checked out, has an AGENTS.md". It does not know whether the target enforces a structure lint over its own top-level paths, so writing .claude/ there breaks that repo's CI and looks like the repo's fault rather than the sync script's. PyAutoMemory was the only repo carrying such a lint and was the only casualty; that break is fixed. The class is not: the next repo to grow a structure lint gets silently broken on the next --write. Filed as insurance, not as an outstanding regression — hence low priority. The prompt sketches three fixes (declare it in repos.yaml, detect the lint, verify after write) and argues for the first, since repo identity already lives in the body map. It also notes the constraint that the check_* side must agree with the write side or an exempted repo reads as permanent drift. lifecycle.py check: OK. Dashboard regenerated (141 prompts). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uvjb6PqUDyYJa3vXjnVHht
…aude/
--write creates exactly two top-level entries in every checked-out repo —
.claude/ and the CLAUDE.md pointer — and knew nothing about the target beyond
"checked out, has an AGENTS.md". A repo that lints its own layout has no way to
know the write is coming, so the write breaks that repo's CI and the breakage
reads as the repo's fault rather than as this script's. One repo was the
casualty and was fixed downstream; the class stayed open for the next repo to
grow a lint.
The guard reads the target's own allowlist rather than keeping a copy: parse
its layout lint with ast (never execute it), pull ALLOWED_TOP_DIRS /
ALLOWED_TOP_FILES, and skip the repo when the entry is not in the set the
entry's kind is governed by. The lint stays the single authority, so a repo
that allowlists .claude is covered again on the next run with no change here
and no per-repo registry to rot.
Not a key in repos.yaml, which was the obvious move and is the wrong one: that
file is identity-only by contract ("per-organ POLICY stays with the organ that
owns it"), and a structure_lint: key is policy. Detection is by convention
instead — STRUCTURE_LINT_CANDIDATES — which leaves a real, bounded gap for a
repo that lints from some other path, documented at the constant.
Three things the guard is careful about:
- Unreadable is not permissive, and not forbidding either. A computed or
unparseable allowlist cannot be read without running the lint, so it never
reads as an all-clear, and it does not block the write — "cannot tell" is not
"forbids", and refusing on a guess would strand the common case. It is
reported for a human.
- The write side and the drift side agree. check_session_hooks and
check_claude_md_pointers exempt a repo the writers skip, or a deliberately
unwritten repo would read as permanent drift on every run. The new
"target-repo layout lints" leg names it instead, so the skip is loud.
- An entry already on disk is the worse case and the one that actually
happened: a --write from before this guard left it behind, so the lint is
failing now. Skipping the next write does not undo that, and the message says
so rather than implying a write was declined.
Verified against the real lint this was written for: replaying its pre-fix
allowlist, the guard's verdict matches the repo's own lint exactly (both
entries, same two findings) and --write declines; against the current allowlist
it is silent. 15 new tests, fictional fixtures only (tests/** is KEEP-copied
into the public template), each failure mode driven with input that trips it.
Full suite 212 passed; repos_sync --check clean on all 13 legs; --write
idempotent; lifecycle check OK.
Implements draft/maintenance/pyautomind/repos_sync_target_repo_lint_awareness.md
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uvjb6PqUDyYJa3vXjnVHht
…ude-install-842ugg # Conflicts: # dashboard.html # dashboard.md
The merge commit captured the wrong side of the two generated dashboards. Both were staged from origin/main to clear the conflict and only regenerated afterwards, so `git commit --no-edit` committed the index — main's 140-prompt render — and left the regenerated 141-prompt copy unstaged. Effect was a dashboard that omitted the row for the very prompt this branch files: draft/maintenance/pyautomind/repos_sync_target_repo_lint_awareness.md was on disk and in the backlog counts nowhere. Regenerated with `pyauto-brain intake --apply dashboard` — the generated files are never resolved by hand. 141 prompts, maintenance 26, the prompt's row and /start_dev chip present in both .md and .html. lifecycle check: OK. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uvjb6PqUDyYJa3vXjnVHht
Jammy2211
pushed a commit
that referenced
this pull request
Aug 25, 2026
Shipped 2026-08-25 via PyAutoMind#322 (merge 06324be): repos_sync.py now reads a target repo's own layout lint before installing .claude/ or the CLAUDE.md pointer, and skips a repo that has not allowlisted the entry. Advances the task to complete/2026/08/repos-sync-target-repo-lint-awareness.md — prompt folded under ## Original prompt, retired from draft/maintenance/ pyautomind/, index refreshed (1135 records), dashboard regenerated (back to 140 backlog prompts). The task never held an issue or an active.md entry: it was filed and implemented in one session straight from draft/, so both commits landed in #322 together and there was nothing to close or release at close-out. The record keeps three things worth remembering: - The filed prompt argued for a structure_lint: key in repos.yaml. That is wrong and the implementation rejected it — repos.yaml is identity-only by contract ("per-organ POLICY stays with the organ that owns it"), and a lint declaration is policy. Detection is by convention instead, with the resulting gap documented at the constant rather than papered over. - Staged-then-regenerated: the two generated dashboards were staged from origin/main to clear a merge conflict and only regenerated afterwards, so `git commit --no-edit` committed the index and left the regenerated copy unstaged. Dashboard Refresh went red on that head. Regenerate before staging. - A shallow web-session clone fakes a diverged main ("ahead 55, behind 200" against a remote that contained the work). merge-base --is-ancestor on the branch tip is the reliable merge proof; the local main ref is not evidence. lifecycle check: OK. 212 tests pass. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uvjb6PqUDyYJa3vXjnVHht
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The hole
repos_sync.py --writecreates exactly two top-level entries in every checked-out repo —.claude/(hook + settings) and theCLAUDE.mdpointer — and knew nothing about the target beyond "checked out, has anAGENTS.md".A repo that lints its own layout has no way to know the write is coming. So the write breaks that repo's CI, and the breakage reads as the repo's fault rather than as this script's. That already happened once: PyAutoMemory's
498e1a8landed a tracked.claude/that was not inALLOWED_TOP_DIRS, and bothmake validateandmake testfailed on arrival. It was fixed there, but the class stayed open for the next repo to grow a lint.The guard
Before writing either entry, read the target's own allowlist — parse its layout lint with
ast, never execute it — pullALLOWED_TOP_DIRS/ALLOWED_TOP_FILES, and skip the repo when the entry is missing from the set its kind is governed by.The lint stays the single authority. Its allowlist is read, never copied here, so a repo that allowlists
.claudeis covered again on the next run with no change on the Mind side and no per-repo registry to rot.Not a key in repos.yaml
That was the obvious move and it is the wrong one.
repos.yamlis identity-only by contract — its own header says "per-organ POLICY stays with the organ that owns it" — and astructure_lint:key is policy.Detection is by convention instead (
STRUCTURE_LINT_CANDIDATES), which leaves a real, bounded gap for a repo that lints from some other path. That gap is documented at the constant rather than papered over; extending the tuple is the fix if it happens.Three things the guard is careful about
check_session_hooksandcheck_claude_md_pointersexempt a repo the writers skip, or a deliberately-unwritten repo would read as permanent drift on every run. The newtarget-repo layout lintsleg names it instead, so the skip is loud rather than silent.--writefrom before this guard left it behind, so the lint is failing now. Skipping the next write does not undo that, and the message says so rather than implying a write was declined.Verification
Replayed against the real artifact, not just fixtures. Cloned PyAutoMemory read-only and restored its pre-fix allowlist: the guard flags exactly the two entries its own lint flags, with the same verdict, and
--writedeclines instead of re-breaking it. Against the current allowlist it is silent.tests/**is KEEP-copied into the public template), each failure mode driven with input that trips itrepos_sync.py --checkclean on all 13 legs--writeidempotent, no stray changeslifecycle.py checkOKImplements
draft/maintenance/pyautomind/repos_sync_target_repo_lint_awareness.md, filed in the first commit of this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01Uvjb6PqUDyYJa3vXjnVHht
Generated by Claude Code