Skip to content

repos_sync: ask a target repo's own layout lint before installing .claude/ - #322

Merged
Jammy2211 merged 4 commits into
mainfrom
claude/repos-sync-claude-install-842ugg
Aug 25, 2026
Merged

repos_sync: ask a target repo's own layout lint before installing .claude/#322
Jammy2211 merged 4 commits into
mainfrom
claude/repos-sync-claude-install-842ugg

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

The hole

repos_sync.py --write creates exactly two top-level entries in every checked-out repo — .claude/ (hook + settings) 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. That already happened once: PyAutoMemory's 498e1a8 landed a tracked .claude/ that was not in ALLOWED_TOP_DIRS, and both make validate and make test failed 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 — pull ALLOWED_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 .claude is 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.yaml is identity-only by contract — its own header says "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. 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

  • 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. It also 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 rather than silent.
  • An entry already on disk gets a different message. That is the case 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.

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 --write declines instead of re-breaking it. Against the current allowlist it is silent.

  • 212 tests pass — 15 new, fictional fixtures only (tests/** is KEEP-copied into the public template), each failure mode driven with input that trips it
  • repos_sync.py --check clean on all 13 legs
  • --write idempotent, no stray changes
  • lifecycle.py check OK
  • The Tenant Firewall Gate checks out Brain/Heart/Hands beside Mind and runs the full drift check; none of those four repos has a layout lint, so the new leg is a no-op there

Implements 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

claude added 4 commits August 25, 2026 13:35
…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
Jammy2211 merged commit 06324be into main Aug 25, 2026
4 checks passed
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
@github-actions
github-actions Bot deleted the claude/repos-sync-claude-install-842ugg branch August 26, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants