From 6228e4fc664cf445bfc0be2c96a91f782504621e Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 22 May 2026 16:16:26 +0200 Subject: [PATCH] setup-steward: support single directory-symlink layout (Pattern D) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Pattern D to the adopter skills-dir conventions: one of `.claude/skills` / `.github/skills` is itself a directory symlink to the other, so both paths always reflect the same set of skills without per-skill plumbing. Two orientations: - **D.1** — canonical content under `.github/skills/`, `.claude/skills` is the symlink. Natural for projects that use `.github/` as their canonical infra-glue root (e.g. apache/airflow). - **D.2** — canonical content under `.claude/skills/`, `.github/skills` is the symlink. Natural for flat-Pattern-A projects that want a `.github/skills/` view too. Pattern D coexists with project-native skills and framework symlinks in the same canonical directory — the directory symlink fans them out automatically. Pre-Pattern-D consolidation: when both directories exist as regular directories with independent content (the would-be clobber-on-symlink case), the adopt flow surfaces the conflict and proposes a one-time consolidation — move every skill into one side, replace the other with a directory symlink — before wiring any framework symlink. Never auto-renames adopter content; falls back to Pattern A if the user declines. Updates to per-flow handling: - `conventions.md` — Pattern D section + two orientations, new detection-algorithm branches, table row, ambiguous-case rules, consolidation flow. - `adopt.md` — Step 0.4 calls into the consolidation flow when detection is ambiguous; Step 7 splits the `.gitignore` template per pattern (Pattern D only needs the canonical-side lines); Step 8 wires symlinks at the canonical side only for Pattern D. - `upgrade.md` — Step 4b adds a Pattern D branch (overwrite `setup-steward` at the canonical side; the directory symlink resolves the other side automatically); Step 6 refreshes only the canonical-side per-skill symlinks under Pattern D. - `verify.md` — Check 4 splits the required `.gitignore` entries per pattern. - `unadopt.md` — Inventory + execute-removal split per pattern; the directory symlink itself is adopter-owned and preserved. - `worktree-init.md` — Per-pattern layer count for the worktree's framework-skill symlinks. - `docs/setup/install-recipes.md`, `docs/setup/unadopt.md`, `SKILL.md` — same per-pattern guidance reflected in the contributor-facing docs. --- .claude/skills/setup-steward/SKILL.md | 2 +- .claude/skills/setup-steward/adopt.md | 152 ++++++++++++++---- .claude/skills/setup-steward/conventions.md | 152 +++++++++++++++++- .claude/skills/setup-steward/unadopt.md | 28 +++- .claude/skills/setup-steward/upgrade.md | 56 +++++-- .claude/skills/setup-steward/verify.md | 21 ++- .claude/skills/setup-steward/worktree-init.md | 23 ++- docs/setup/install-recipes.md | 54 +++++-- docs/setup/unadopt.md | 16 +- 9 files changed, 430 insertions(+), 74 deletions(-) diff --git a/.claude/skills/setup-steward/SKILL.md b/.claude/skills/setup-steward/SKILL.md index 84ac1fd8..301803f2 100644 --- a/.claude/skills/setup-steward/SKILL.md +++ b/.claude/skills/setup-steward/SKILL.md @@ -149,7 +149,7 @@ proposed `/setup-steward upgrade`. | [`adopt.md`](adopt.md) | First-time adoption walk-through — recognise existing-snapshot vs needs-bootstrap, write the two lock files, ask the user which skill families to wire up, create the gitignored symlinks, scaffold `.apache-steward-overrides/`, install the post-checkout hook, update project docs. The default sub-action. | | [`upgrade.md`](upgrade.md) | Refresh the gitignored snapshot per the committed lock, reconcile any agentic overrides + symlinks against the new framework structure, surface conflicts. Drives the on-drift remediation flow. | | [`verify.md`](verify.md) | Read-only health check — snapshot present + intact, both lock files in sync, symlinks point at live targets, `.gitignore` correct, `.apache-steward-overrides/` exists, drift status (committed vs local), the `setup-steward` skill itself is current. | -| [`conventions.md`](conventions.md) | Adopter skills-dir convention auto-detection — flat `.claude/skills//`, the `.claude/skills/` → `.github/skills//` double-symlink pattern (e.g. apache/airflow), or neither yet. | +| [`conventions.md`](conventions.md) | Adopter skills-dir convention auto-detection — four patterns: A (flat `.claude/skills//`), B (per-skill `.claude/skills/` → `.github/skills//` double-symlink), C (none yet), D (single directory symlink where one of `.claude/skills` / `.github/skills` is itself a symlink to the other; two orientations). | | [`overrides.md`](overrides.md) | Agentic-override file management — open / scaffold an override for a framework skill, list existing overrides, help reconcile when the framework changes the underlying skill's structure on upgrade. | | [`unadopt.md`](unadopt.md) | Reverse the adoption — remove snapshot, locks, symlinks, post-checkout hook, `.gitignore` entries, the adoption sections in `README.md` / `AGENTS.md` / `CONTRIBUTING.md`, and the committed `setup-steward` skill itself. Preserves `.apache-steward-overrides/` by default; `--purge-overrides` removes it too. Surfaces the full removal plan before any write. | diff --git a/.claude/skills/setup-steward/adopt.md b/.claude/skills/setup-steward/adopt.md index b9d9e9be..99fe5a1a 100644 --- a/.claude/skills/setup-steward/adopt.md +++ b/.claude/skills/setup-steward/adopt.md @@ -71,6 +71,40 @@ between automatically: result as `` for the rest of this flow. + If detection returns *"ambiguous → propose Pattern D + consolidation"* (both `.claude/skills/` and + `.github/skills/` exist as regular directories with + independent, non-aliased content), run the + **Pre-Pattern-D consolidation** flow described under + [section D of `conventions.md`](conventions.md#d-single-directory-symlink--one-of-claudeskills--githubskills-is-a-symlink-to-the-other) + before continuing: + + - List the skills in each directory with their content + fingerprint (real dir vs symlink, target if symlink, + SKILL.md presence). + - Flag any name collisions where the two sides have + different content for the same name. + - Use a structured prompt (`AskUserQuestion` when the + harness offers one) with three options: **D.1** + (consolidate under `.github/skills/`), **D.2** + (consolidate under `.claude/skills/`), or **decline** + (fall back to Pattern A treating `.claude/skills/` as + canonical and leaving `.github/skills/` alone). + - On D.1 / D.2 confirmation: move every skill from the + side that will become the symlink into the side that + will become the real directory (resolving any flagged + name collisions first — never auto-rename adopter + content), then replace the now-empty side with a + relative symlink to the other side, then re-run + detection to confirm the pattern is now D. + - If the user declines or unresolved name collisions + block consolidation, fall back to Pattern A and pin + `` = `.claude/skills/` as usual. + + The consolidation is a one-time, deliberate layout + change; the adopt flow surfaces every step before + writing. + ## Step 1 — Detect adoption shape ```text @@ -284,26 +318,74 @@ fetched_at: The bootstrap recipe wrote these already; this step is idempotent — re-add them if they're missing. +**Base entries — always needed**: + ```text /.apache-steward/ /.apache-steward.local.lock /.claude/settings.local.json -/.claude/skills/security-* -/.claude/skills/pr-management-* -/.claude/skills/issue-* -/.claude/skills/setup-isolated-setup-* -/.claude/skills/setup-override-upstream -/.claude/skills/setup-shared-config-sync -/.claude/skills/list-steward-* -/.github/skills/security-* -/.github/skills/pr-management-* -/.github/skills/issue-* -/.github/skills/setup-isolated-setup-* -/.github/skills/setup-override-upstream -/.github/skills/setup-shared-config-sync -/.github/skills/list-steward-* ``` +**Symlink-pattern entries — vary by adopter +[skills-dir convention](conventions.md)**: + +- **Pattern A (flat)** — only the `.claude/skills/...` lines: + + ```text + /.claude/skills/security-* + /.claude/skills/pr-management-* + /.claude/skills/issue-* + /.claude/skills/setup-isolated-setup-* + /.claude/skills/setup-override-upstream + /.claude/skills/setup-shared-config-sync + /.claude/skills/list-steward-* + ``` + +- **Pattern B (double-symlinked)** — both `.claude/skills/...` + AND `.github/skills/...` lines, because each framework skill + has two physical symlinks (outer at `.claude/skills/`, + inner at `.github/skills/`): + + ```text + /.claude/skills/security-* + /.claude/skills/pr-management-* + /.claude/skills/issue-* + /.claude/skills/setup-isolated-setup-* + /.claude/skills/setup-override-upstream + /.claude/skills/setup-shared-config-sync + /.claude/skills/list-steward-* + /.github/skills/security-* + /.github/skills/pr-management-* + /.github/skills/issue-* + /.github/skills/setup-isolated-setup-* + /.github/skills/setup-override-upstream + /.github/skills/setup-shared-config-sync + /.github/skills/list-steward-* + ``` + +- **Pattern D (single directory symlink)** — only the + *canonical-side* `.../skills/...` lines. With D.1 + (canonical = `.github/skills/`): + + ```text + /.github/skills/security-* + /.github/skills/pr-management-* + /.github/skills/issue-* + /.github/skills/setup-isolated-setup-* + /.github/skills/setup-override-upstream + /.github/skills/setup-shared-config-sync + /.github/skills/list-steward-* + ``` + + With D.2 (canonical = `.claude/skills/`), mirror the same + list under `.claude/skills/` instead. Pattern D does not + need ignore lines on the *symlinked* side because that side + is itself a single tracked symlink — git does not descend + into it, so the symlinked-side paths match no tracked file. + +- **Pattern C (none yet)** — same as the pattern the user + picks during adopt (defaults to A). + The `setup-override-upstream`, `setup-shared-config-sync`, `setup-isolated-setup-*`, and `list-steward-*` entries are the always-on families per @@ -320,9 +402,6 @@ that each worktree carries independently). Most adopters already gitignore this file by Claude Code convention; the adopt flow checks for the line and adds it if missing. -Mirror under `.github/skills/` only if the adopter uses the -double-symlinked convention. - ## Step 8 — Wire up the framework-skill symlinks The skill walks `/.claude/skills/` and creates @@ -349,11 +428,24 @@ adoption path where the committed lock only records the opt-in pick. Compute the family glob fresh from the snapshot contents on disk — do not hard-code skill names. -If the adopter uses the double-symlinked convention -(see [`conventions.md`](conventions.md)), create both -layers — the inner one in `.github/skills/` points at the -snapshot, the outer `.claude/skills/` points at the -inner. Both gitignored. +Per-pattern symlink wiring (see +[`conventions.md`](conventions.md)): + +- **Pattern A (flat)** — one symlink per skill at + `.claude/skills/` → snapshot. Gitignored. +- **Pattern B (double-symlinked)** — two symlinks per skill: + the inner one in `.github/skills/` → snapshot, the outer + `.claude/skills/` → `../../.github/skills//`. Both + gitignored. +- **Pattern D (single directory symlink)** — one symlink per + skill at the *canonical-side* `/skills/` → + snapshot. **Skip the symlinked side entirely** — one of + `.claude/skills` / `.github/skills` is itself a directory + symlink into the other, so the symlinked-side path is + automatically resolved. With D.1 the canonical side is + `.github/skills/`; with D.2 it is `.claude/skills/`. + Gitignored. +- **Pattern C (none yet)** — same as A. **Never overwrite an existing committed skill** of the same name. Surface conflicts and stop. `setup-steward` itself is @@ -662,8 +754,8 @@ framework before they hit a "skill not found" error: Trim the skill-family list to what was actually picked in Step 5 (only mention `security-*` if the adopter installed that family, etc.). Adjust the skill paths to the adopter's - convention (flat vs double-symlinked — see - [`conventions.md`](conventions.md)). Skip this sub-step + convention (flat / double-symlinked / single-directory-symlink + — see [`conventions.md`](conventions.md)). Skip this sub-step entirely if `README.md` does not exist. 2. **`AGENTS.md` (agent-facing detail, ONLY if the file @@ -871,11 +963,13 @@ Committed (you'll see in `git status`): Gitignored (do NOT commit): .apache-steward/ .apache-steward.local.lock - .claude/skills/{security,pr-management}-* # opt-in families - .claude/skills/setup-isolated-setup-* # always-on - .claude/skills/{setup-override-upstream,setup-shared-config-sync} # always-on - .claude/skills/list-steward-* # always-on - (and same patterns under .github/skills/ for double-symlinked layouts) + /{security,pr-management}-* # opt-in families + /setup-isolated-setup-* # always-on + /{setup-override-upstream,setup-shared-config-sync} # always-on + /list-steward-* # always-on + # Pattern A: = .claude/skills/ + # Pattern B: = both .claude/skills/ AND .github/skills/ + # Pattern D: = .github/skills/ only ``` Then suggest the user `git add` the committed files and open diff --git a/.claude/skills/setup-steward/conventions.md b/.claude/skills/setup-steward/conventions.md index 29775f48..f8703168 100644 --- a/.claude/skills/setup-steward/conventions.md +++ b/.claude/skills/setup-steward/conventions.md @@ -88,15 +88,148 @@ The skill creates the directory layout the adopter prefers (default: pattern A, flat — simpler). If the user has a preference, they say so during the adopt flow. +### D. Single directory symlink — one of `.claude/skills` / `.github/skills` is a symlink to the other + +```text +# D.1 — content under .github/skills/, .claude/skills is the symlink: +/ +├── .claude/ +│ └── skills → ../.github/skills/ +└── .github/ + └── skills/ + ├── / + │ └── SKILL.md + ├── → ../../.apache-steward/.claude/skills// + └── ... +``` + +```text +# D.2 — content under .claude/skills/, .github/skills is the symlink: +/ +├── .claude/ +│ └── skills/ +│ ├── / +│ │ └── SKILL.md +│ ├── → ../../.apache-steward/.claude/skills// +│ └── ... +└── .github/ + └── skills → ../.claude/skills/ +``` + +A simplification of Pattern B: instead of one per-skill +symlink mirroring every entry from one directory to the +other, **one of the two directories is itself a symlink to +the other**. Both `.claude/skills/` and +`.github/skills/` always resolve to the same content for +every skill — the project's native skills and the framework's +gitignored symlinks alike — without any per-skill plumbing. +Adding a new skill (project-native or framework) just means +adding it once in the canonical directory; the mirror is +automatic. + +**Two orientations** — same shape, opposite direction: + +- **D.1** — content lives under `.github/skills/`, + `.claude/skills` is the symlink. The natural choice for + projects whose canonical skills directory is `.github/` + (e.g. apache/airflow, which uses `.github/` as its + infra-glue root and `.claude/` as a Claude-Code-facing + view). +- **D.2** — content lives under `.claude/skills/`, + `.github/skills` is the symlink. The natural choice for + projects whose canonical skills directory is `.claude/` + (e.g. a Pattern A project that wants `.github/skills/` + available too without duplicating content). + +**Detection signal**: exactly one of `.claude/skills` / +`.github/skills` is a symlink (test with `[ -L ]` / +`readlink `) and resolves to the other path in the same +repo. Either orientation counts as Pattern D. + +For framework symlinks: create them at **only one layer** — +the *real* directory side, never the symlinked side. With +D.1 that means `.github/skills/` → relative path into +`.apache-steward/.claude/skills//`; with D.2 it means +`.claude/skills/` → the same. The opposite path is +automatically the same content via the directory symlink. + +Gitignore consequences: only entries on the real-directory +side are needed (e.g. `/.github/skills/security-*` for D.1, +or `/.claude/skills/security-*` for D.2). Git treats the +symlinked side as a single tracked symlink and does not +descend into it, so ignore entries on that side would match +no actual tracked path and are unnecessary. + +The directory symlink itself is **adopter-owned** — created +deliberately by the adopter as part of the project's layout +choice, and not touched by `/setup-steward unadopt`. The +framework treats it the same way it treats the real-directory +side: as part of the surrounding repo layout. + +**Pre-Pattern-D consolidation** — if both `.claude/skills/` +and `.github/skills/` exist as **regular directories** (not +yet symlinked to each other) and contain skill content that +is not already aliased through symlinks, the adopt flow +**does not silently apply Pattern D**. Each directory's +contents are an independent set; turning one into a symlink +to the other would clobber the symlinked side's content. The +flow surfaces the conflict and offers a consolidation prompt: + +1. List the skills present in each directory (real + directories, regular files, and any non-Pattern-B + symlinks). +2. Flag name collisions where the same skill name exists in + both directories with different content. +3. Ask the user to pick D.1 or D.2 and confirm the + consolidation steps: + - Move every skill from the side that will become the + symlink into the side that will become the real + directory, resolving any flagged name collisions first. + - Replace the now-empty side with a relative symlink to + the other side. +4. Only after the consolidation is complete does the adopt + flow proceed to wire framework symlinks at the chosen + real-directory side. + +If the consolidation cannot proceed (unresolved name +collisions the user has not addressed), the adopt flow stops +and lets the user resolve in their own commit before +re-invoking — the framework never auto-renames adopter-owned +content. + ## Detection algorithm ```text -if .claude/skills/ exists: +# Pattern D first — either orientation: +if .claude/skills is a symlink: + if it resolves to .github/skills/ in the same repo: + pattern = D.1 (single directory symlink; canonical = .github/skills/) + else: + # operator pointed `.claude/skills` somewhere else + # deliberately; surface, do not guess. + pattern = ambiguous → prompt the user +elif .github/skills is a symlink: + if it resolves to .claude/skills/ in the same repo: + pattern = D.2 (single directory symlink; canonical = .claude/skills/) + else: + # same — surface the unexpected target, do not guess. + pattern = ambiguous → prompt the user + +# Otherwise fall through to A / B / C: +elif .claude/skills/ exists (regular directory): if any entry in .claude/skills/ is a symlink resolving into .github/skills/: pattern = B (double-symlinked) else: - pattern = A (flat) + if .github/skills/ also exists as a regular directory + with independent content: + pattern = ambiguous → propose Pattern D + consolidation (see *Pre-Pattern-D + consolidation* under section D + above), with A as the fallback + if the user declines + else: + pattern = A (flat) elif .github/skills/ exists: pattern = B (the user has a `.github/skills/` half but hasn't wired up `.claude/` yet — the adopt @@ -114,6 +247,8 @@ else: | A — flat | `.claude/skills/` | None | | B — double-symlinked | `.github/skills/` (the inner layer); `.claude/skills/` symlinks to it | If `.github/skills/` for a framework skill already exists as a real directory (an old in-repo copy), refuse and let the user resolve | | C — none yet | `.claude/skills/` | Create the directory | +| D.1 — single directory symlink, canonical `.github/skills/` | `.github/skills/` (the only layer; `.claude/skills` resolves into it via the directory symlink) | None — no outer-layer plumbing to create | +| D.2 — single directory symlink, canonical `.claude/skills/` | `.claude/skills/` (the only layer; `.github/skills` resolves into it via the directory symlink) | None — no outer-layer plumbing to create | ## Ambiguous cases @@ -126,3 +261,16 @@ else: consistency. If the user wants absolute, they say so; otherwise relative is the default — it survives a repo move. +- **`.claude/skills` (or `.github/skills`) is a symlink but + resolves outside the repo or to a path other than the + expected counterpart directory**. The operator pointed it + somewhere deliberately (e.g. a sibling worktree). The + adopt flow surfaces the resolved target and asks the user; + it does not match Pattern D automatically. +- **Both `.claude/skills/` and `.github/skills/` exist as + regular directories with independent (non-aliased) + content**. Surfaced as a Pattern D consolidation + opportunity per the **Pre-Pattern-D consolidation** flow + under section D above. The user picks D.1 or D.2 (or + declines, in which case the flow falls back to Pattern A + treating `.claude/skills/` as canonical). diff --git a/.claude/skills/setup-steward/unadopt.md b/.claude/skills/setup-steward/unadopt.md index e04005a5..27433215 100644 --- a/.claude/skills/setup-steward/unadopt.md +++ b/.claude/skills/setup-steward/unadopt.md @@ -88,7 +88,7 @@ every artefact). | Local lock | `` | exists | | Committed lock | `` | exists | | `.gitignore` entries | `/.gitignore` | which of the entries from [`adopt.md` Step 7](adopt.md) are present | -| Framework-skill symlinks | `/` (and `.github/skills/` if double-symlinked) | each symlink whose target resolves into `/.claude/skills/` | +| Framework-skill symlinks | `/` — both layers under Pattern B; canonical side only under Pattern D (D.1: `.github/skills/`; D.2: `.claude/skills/`); single layer under Pattern A | each symlink whose target resolves into `/.claude/skills/` | | Post-checkout hook | `/.git/hooks/post-checkout` | exists + invokes `~/.claude/scripts/sandbox-add-project-root.sh` | | Doc section: `README.md` | `/README.md` | contains the `## Agent-assisted contribution (apache-steward)` heading | | Doc section: `AGENTS.md` | `/AGENTS.md` | contains the `## apache-steward framework` heading | @@ -117,8 +117,12 @@ The following will be REMOVED: .apache-steward.local.lock / → .apache-steward/.claude/skills// / → ... - .github/skills/ (if double-symlinked layout) + .github/skills/ (Pattern B only — second physical layer) .git/hooks/post-checkout (if it contains the steward recipe) + # Pattern A: = .claude/skills/ + # Pattern B: spans .claude/skills/ AND .github/skills/ + # Pattern D: = canonical side only + # (D.1: .github/skills/; D.2: .claude/skills/) Committed (will show in `git status`): .apache-steward.lock (the project's pin) @@ -188,9 +192,20 @@ half-completed unadopt never leaves a dangling symlink pointing at a deleted snapshot. 1. **Framework-skill symlinks.** For each entry in the - inventory, `rm` the symlink. If the adopter uses the - double-symlinked convention, remove both layers. Never - touch a non-symlink at the same path. + inventory, `rm` the symlink. Per-pattern: + + - **Pattern A** — one layer; just remove + `.claude/skills/`. + - **Pattern B** — two layers; remove both + `.claude/skills/` and `.github/skills/`. + - **Pattern D** — one layer at the canonical side + (D.1: `.github/skills/`; D.2: `.claude/skills/`). + The directory symlink itself (`.claude/skills` or + `.github/skills`) is **adopter-owned** and **not + removed by unadopt** — it predates framework adoption + and serves the adopter's own native skills too. + + Never touch a non-symlink at the same path. 2. **Post-checkout hook.** Remove only if its content matches the steward recipe verbatim (i.e. the hook the adopt flow wrote — a single @@ -262,7 +277,7 @@ A summary of what was removed + what remains: ```text ✓ Snapshot removed: .apache-steward/ ✓ Locks removed: .apache-steward.lock, .apache-steward.local.lock -✓ Symlinks removed: (under /[, .github/skills/]) +✓ Symlinks removed: (per-pattern — A: under .claude/skills/; B: under both .claude/skills/ AND .github/skills/; D: under the canonical side only) ✓ Post-checkout hook: removed (or: preserved — contained extra adopter logic) ✓ Doc sections removed: README.md[, AGENTS.md][, CONTRIBUTING.md] ✓ .gitignore cleaned: entries removed @@ -271,6 +286,7 @@ A summary of what was removed + what remains: Preserved: .apache-steward-overrides/ (M files; pass `--purge-overrides` to remove) ~/.config/apache-steward/user.md (per-user; shared with other adopters on this machine — remove manually if this was your last adoption) + .claude/skills (or .github/skills) (Pattern D directory symlink — adopter-owned, predates framework adoption) Staged for commit (you'll see in `git status`): diff --git a/.claude/skills/setup-steward/upgrade.md b/.claude/skills/setup-steward/upgrade.md index 9112ea2f..2544645f 100644 --- a/.claude/skills/setup-steward/upgrade.md +++ b/.claude/skills/setup-steward/upgrade.md @@ -160,18 +160,29 @@ bootstrap logic. It implements over the committed copy: ```bash - # For the flat layout: + # For the flat layout (Pattern A): rm -rf .claude/skills/setup-steward cp -r .apache-steward/.claude/skills/setup-steward \ .claude/skills/setup-steward - # For the double-symlinked layout (e.g. apache/airflow): + # For the double-symlinked layout (Pattern B): rm -rf .github/skills/setup-steward cp -r .apache-steward/.claude/skills/setup-steward \ .github/skills/setup-steward - # The .claude/skills/setup-steward symlink does not need - # touching — it points at .github/skills/setup-steward + # The .claude/skills/setup-steward per-skill symlink does + # not need touching — it points at .github/skills/setup-steward # which is now the new content. + + # For the single directory-symlink layout (Pattern D), + # write to the *canonical* side only. With D.1 + # (canonical = .github/skills/): + rm -rf .github/skills/setup-steward + cp -r .apache-steward/.claude/skills/setup-steward \ + .github/skills/setup-steward + # With D.2 (canonical = .claude/skills/), write to + # .claude/skills/setup-steward instead. Either way: the + # symlinked side resolves to the refreshed content + # automatically — nothing to touch there. ``` 4. **Reload in-flight.** Immediately after the copy lands — @@ -259,12 +270,23 @@ family, reconcile the adopter's `.gitignore` so the new family's snapshot symlinks are gitignored. Append the `.gitignore` lines from [`adopt.md` Step 7](adopt.md#step-7--gitignore-entries-fresh-only) -for the new family's prefix (e.g. `/.claude/skills/issue-*` -and the `.github/skills/` mirror when the adopter uses the -double-symlinked convention). The append is idempotent — -skip lines that already exist. The same idempotence covers -adopters whose `.gitignore` already had the entries (e.g. -from a manually-edited block or a previous adopt run). +for the new family's prefix, matching the adopter's +[skills-dir convention](conventions.md): + +- Pattern A — `/.claude/skills/-*` only. +- Pattern B — both `/.claude/skills/-*` and + `/.github/skills/-*` (two physical symlinks per + skill). +- Pattern D — only the *canonical-side* `/-*` + ignore line. D.1 → `/.github/skills/-*`; D.2 → + `/.claude/skills/-*`. The symlinked side's + directory symlink does not need its own ignore line — git + does not descend into it. + +The append is idempotent — skip lines that already exist. +The same idempotence covers adopters whose `.gitignore` +already had the entries (e.g. from a manually-edited block +or a previous adopt run). The post-upgrade state must be: *every framework skill in the new snapshot that belongs to the effective family set @@ -301,7 +323,19 @@ Run two passes: release notes), offer to re-symlink to the new name. - If removed, offer to remove the stale symlink. -For the double-symlinked convention, refresh both layers. +Per-pattern symlink layers to refresh: + +- **Pattern A (flat)** — refresh the single layer at + `.claude/skills/`. +- **Pattern B (double-symlinked)** — refresh both layers + (inner at `.github/skills/`, outer at + `.claude/skills/` → inner). +- **Pattern D (single directory symlink)** — refresh only + the *canonical-side* layer at + `/skills/` (D.1 → `.github/skills/`; + D.2 → `.claude/skills/`). The symlinked-side path + resolves through the directory symlink and needs no + per-skill plumbing. ## Step 6b — Sync locally-installed hooks and configuration diff --git a/.claude/skills/setup-steward/verify.md b/.claude/skills/setup-steward/verify.md index 6f02157a..ddbbd8a9 100644 --- a/.claude/skills/setup-steward/verify.md +++ b/.claude/skills/setup-steward/verify.md @@ -112,12 +112,21 @@ Check that the entries from must never be committed since the content is machine-specific absolute paths) -Recommended: - -- The framework-skill symlink patterns (`security-*`, - `pr-management-*`, `issue-*`, `setup-isolated-setup-*`, - `setup-shared-config-sync`, `list-steward-*`) under both - `.claude/skills/` and `.github/skills/` per convention. +Recommended (the family patterns the adopter's +[skills-dir convention](conventions.md) requires): + +- **Pattern A** — framework-skill symlink patterns + (`security-*`, `pr-management-*`, `issue-*`, + `setup-isolated-setup-*`, `setup-shared-config-sync`, + `list-steward-*`) under `.claude/skills/` only. +- **Pattern B** — same patterns under **both** + `.claude/skills/` and `.github/skills/` (one ignore line + per physical symlink). +- **Pattern D** — same patterns under the **canonical side + only** (`.github/skills/` for D.1; `.claude/skills/` for + D.2). The symlinked side does not need its own ignore + lines because git does not descend into a directory + symlink. - ✗ if `/.apache-steward/` is not gitignored — the snapshot is at risk of being accidentally committed. diff --git a/.claude/skills/setup-steward/worktree-init.md b/.claude/skills/setup-steward/worktree-init.md index 2125abb5..888ae2f0 100644 --- a/.claude/skills/setup-steward/worktree-init.md +++ b/.claude/skills/setup-steward/worktree-init.md @@ -101,9 +101,26 @@ For each framework skill in the effective family set: repair it. Reuse the convention detection from -[`conventions.md`](conventions.md): flat vs double-symlinked -layout drives where the inner / outer links land. Both -layers gitignored. +[`conventions.md`](conventions.md). The pattern drives how +many layers the worktree's `` needs: + +- **Pattern A (flat)** — one layer at + `.claude/skills/`. +- **Pattern B (double-symlinked)** — two layers (inner at + `.github/skills/`, outer at `.claude/skills/` → + inner). Both gitignored. +- **Pattern D (single directory symlink)** — one layer at + the canonical side (D.1: `.github/skills/`; + D.2: `.claude/skills/`). The symlinked side resolves + automatically through the directory symlink, so there is + no per-skill plumbing to add or repair on that side. + +The worktree's `.claude/skills` / `.github/skills` directory +symlink itself (for Pattern D) is **not** a framework +artefact — it is checked into the repo as part of the +adopter's layout, so every worktree inherits it via the +ordinary `git worktree add` flow. `worktree-init` does not +touch it. Pick any framework skill symlink that should now exist (e.g. `/.claude/skills/security-issue-sync/SKILL.md`) and diff --git a/docs/setup/install-recipes.md b/docs/setup/install-recipes.md index 6a8ca43c..d43c2013 100644 --- a/docs/setup/install-recipes.md +++ b/docs/setup/install-recipes.md @@ -32,14 +32,24 @@ Pick the recipe that matches your distribution preference: | [**git-branch**](#method-3--git-branch-defaults-to-main) | WIP path — track the framework's `main` branch directly. The default during the framework's pre-release phase. | Tracks branch tip | > **Adopter convention** — pick the right `cp` step per your -> existing skills layout: +> existing skills layout (see +> [`.claude/skills/setup-steward/conventions.md`](../../.claude/skills/setup-steward/conventions.md) +> for the full taxonomy): > -> - **flat** (`.claude/skills//SKILL.md` directly): copy +> - **A — flat** (`.claude/skills//SKILL.md` directly): copy > `setup-steward` into `.claude/skills/setup-steward/`. -> - **double-symlinked** (e.g. `apache/airflow` — -> `.claude/skills/` → `.github/skills//`): copy the -> content into `.github/skills/setup-steward/` AND symlink +> - **B — double-symlinked** (per-skill +> `.claude/skills/` → `.github/skills//` symlinks): copy +> the content into `.github/skills/setup-steward/` AND symlink > `.claude/skills/setup-steward` → `../../.github/skills/setup-steward`. +> - **D — single directory symlink** (one of +> `.claude/skills` / `.github/skills` is itself a directory +> symlink to the other): copy the content into the +> *canonical* side only — `.github/skills/setup-steward/` +> for D.1 (canonical `.github/skills/`) or +> `.claude/skills/setup-steward/` for D.2 (canonical +> `.claude/skills/`). The opposite side is the same +> directory via the symlink, so there is nothing to wire up. > > The `setup-steward` skill itself is the **only** framework > artefact you commit. Every other framework skill is wired @@ -98,10 +108,20 @@ rm -f ${ZIP} ${ZIP}.sha512 ${ZIP}.asc # A — flat layout (default): cp -r .apache-steward/.claude/skills/setup-steward .claude/skills/setup-steward # -# B — double-symlinked layout (e.g. apache/airflow): +# B — double-symlinked layout (per-skill symlinks): # mkdir -p .github/skills .claude/skills # cp -r .apache-steward/.claude/skills/setup-steward .github/skills/setup-steward # ln -sf ../../.github/skills/setup-steward .claude/skills/setup-steward +# +# D.1 — single directory symlink, canonical .github/skills/: +# (.claude/skills is itself a symlink → ../.github/skills/) +# cp -r .apache-steward/.claude/skills/setup-steward .github/skills/setup-steward +# (No second copy needed — .claude/skills/setup-steward resolves +# to .github/skills/setup-steward via the directory symlink.) +# +# D.2 — single directory symlink, canonical .claude/skills/: +# (.github/skills is itself a symlink → ../.claude/skills/) +# cp -r .apache-steward/.claude/skills/setup-steward .claude/skills/setup-steward # 3. Add gitignore entries (idempotent — re-run is safe) cat >> .gitignore <<'GITIGNORE' @@ -116,14 +136,20 @@ cat >> .gitignore <<'GITIGNORE' /.apache-steward.local.lock # Symlinks created by /setup-steward into the gitignored snapshot. +# Pattern A (flat) → keep only the .claude/skills/ block below. +# Pattern B (per-skill double-symlinked) → keep BOTH blocks (one +# physical symlink per layer needs its own ignore line). +# Pattern D.1 (.claude/skills → .github/skills/) → keep only the +# .github/skills/ block — git does not descend into the directory +# symlink, so .claude/skills/ ignore lines are unnecessary. +# Pattern D.2 (.github/skills → .claude/skills/) → keep only the +# .claude/skills/ block (same reason, opposite orientation). /.claude/skills/security-* /.claude/skills/pr-management-* /.claude/skills/issue-* /.claude/skills/setup-isolated-setup-* /.claude/skills/setup-shared-config-sync /.claude/skills/list-steward-* -# Mirror the same patterns under .github/skills/ if your repo uses -# the double-symlinked convention. /.github/skills/security-* /.github/skills/pr-management-* /.github/skills/issue-* @@ -156,11 +182,13 @@ git clone --depth=1 \ https://github.com/apache/airflow-steward.git \ .apache-steward -# Copy the `setup-steward` skill — pick A or B (see Method 1 step 2) +# Copy the `setup-steward` skill — pick A / B / D (see Method 1 step 2) cp -r .apache-steward/.claude/skills/setup-steward .claude/skills/setup-steward -# OR for double-symlinked: +# OR for double-symlinked (B): # cp -r .apache-steward/.claude/skills/setup-steward .github/skills/setup-steward # ln -sf ../../.github/skills/setup-steward .claude/skills/setup-steward +# OR for single directory-symlink (D) — copy to canonical side only; +# the .claude/skills ↔ .github/skills directory symlink does the rest. # Add gitignore entries (same block as Method 1 step 3 — see there) @@ -183,11 +211,13 @@ git clone --depth=1 \ https://github.com/apache/airflow-steward.git \ .apache-steward -# Copy the `setup-steward` skill — pick A or B (see Method 1 step 2) +# Copy the `setup-steward` skill — pick A / B / D (see Method 1 step 2) cp -r .apache-steward/.claude/skills/setup-steward .claude/skills/setup-steward -# OR for double-symlinked: +# OR for double-symlinked (B): # cp -r .apache-steward/.claude/skills/setup-steward .github/skills/setup-steward # ln -sf ../../.github/skills/setup-steward .claude/skills/setup-steward +# OR for single directory-symlink (D) — copy to canonical side only; +# the .claude/skills ↔ .github/skills directory symlink does the rest. # Add gitignore entries (same block as Method 1 step 3 — see there) diff --git a/docs/setup/unadopt.md b/docs/setup/unadopt.md index 120bf544..249c0eff 100644 --- a/docs/setup/unadopt.md +++ b/docs/setup/unadopt.md @@ -77,7 +77,7 @@ The following will be REMOVED: .apache-steward.local.lock / → .apache-steward/.claude/skills// / → ... - .github/skills/ (if double-symlinked layout) + .github/skills/ (Pattern B only — second physical layer) .git/hooks/post-checkout (if it contains the steward recipe) Committed (will show in `git status`): @@ -93,9 +93,17 @@ The following will be PRESERVED: .apache-steward-overrides/ (pass `--purge-overrides` to remove) ``` -`` resolves to your skills directory — -typically `.claude/skills/`, or `.github/skills/` for repos -using the double-symlinked layout. +`` resolves to your skills directory per the +[skills-dir convention](../../.claude/skills/setup-steward/conventions.md) +your repo uses: + +- **Pattern A** — `.claude/skills/`. +- **Pattern B** — both `.claude/skills/` and `.github/skills/` + (one physical symlink per layer). +- **Pattern D** — the canonical side only (D.1: + `.github/skills/`; D.2: `.claude/skills/`). The directory + symlink itself is adopter-owned and is **not** removed by + unadopt. If `--purge-overrides` is passed, `.apache-steward-overrides/` moves into the *removed* section with its files listed