This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git
The following commit(s) were added to refs/heads/main by this push:
new 41bba7a docs(setup-steward): always-on families + reload + worktree
chain (#184)
41bba7a is described below
commit 41bba7a7a0303377963504ce7df4474d15421e8e
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat May 16 14:54:06 2026 +0200
docs(setup-steward): always-on families + reload + worktree chain (#184)
Adds five behaviours to /setup-steward: always-on setup-* / list-steward-*
families (no opt-out), in-flight skill reload after self-update,
hook+config sync from the snapshot, family-wide symlink refresh on every
upgrade, and automatic worktree-init on every linked worktree as the
final pass of adopt and upgrade. Documentation-only — no executable
code paths in this repo.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
.claude/skills/setup-steward/SKILL.md | 61 +++++-
.claude/skills/setup-steward/adopt.md | 229 ++++++++++++++++++---
.claude/skills/setup-steward/upgrade.md | 276 +++++++++++++++++++-------
.claude/skills/setup-steward/verify.md | 74 +++++--
.claude/skills/setup-steward/worktree-init.md | 54 ++++-
5 files changed, 577 insertions(+), 117 deletions(-)
diff --git a/.claude/skills/setup-steward/SKILL.md
b/.claude/skills/setup-steward/SKILL.md
index 5b0b927..b5b1ac9 100644
--- a/.claude/skills/setup-steward/SKILL.md
+++ b/.claude/skills/setup-steward/SKILL.md
@@ -236,6 +236,52 @@ patch tool. See
[`docs/setup/agentic-overrides.md`](../../../docs/setup/agentic-overrides.md)
for the contract.
+**Golden rule 8 — two families are *always* installed; the
+rest are opt-in.** Two skill families are wired up
+unconditionally on every adopt / upgrade / worktree-init run
+and the user is **never asked** about them:
+
+- **`setup-*`** — every framework skill whose name starts
+ with `setup-` *except* `setup-steward` itself (which is
+ copied per Rule 6, not symlinked). Concretely:
+ `setup-isolated-setup-install`,
+ `setup-isolated-setup-update`,
+ `setup-isolated-setup-verify`, `setup-override-upstream`,
+ `setup-shared-config-sync`, plus any new `setup-*` skill
+ the framework grows in the future.
+- **`list-steward-*`** — every framework skill whose name
+ starts with `list-steward-`. Today this is
+ `list-steward-skills` only; the prefix lets the framework
+ grow a discovery family without re-prompting every
+ adopter.
+
+These two families are not exposed in the `skill-families:`
+prompt and not stored as user-selectable in the lock files;
+every sub-action that wires symlinks always covers them in
+addition to the user's opt-in family picks (`security`,
+`pr-management`). Dropping them is *not* a supported
+configuration — the secure-setup and discovery flows the
+framework ships depend on those skills being callable.
+
+**Golden rule 9 — reload `setup-steward` in-flight after a
+self-update.** When a sub-action changes or creates the
+content of the committed `setup-steward` skill (in practice:
+`adopt` recovering an out-of-date bootstrap, or `upgrade`'s
+overwrite-from-snapshot step), the agent **re-reads the
+modified files of this skill before continuing** the rest of
+the current run. Concretely: after the copy lands on disk,
+re-load `SKILL.md` and the sub-action file you are
+currently executing (and any helper file you have already
+opened, such as `conventions.md` or `overrides.md`), then
+resume from the step after the overwrite. The reload runs as
+the **first thing** that happens after the overwrite, before
+any further reconciliation, symlink work, or doc updates.
+The reason: the snapshot's skill version may have renamed
+steps, added new sub-actions, or changed the symlink
+contract; finishing the run against the *old* in-memory
+copy of the skill would silently mis-apply the new
+framework version the project just pinned to.
+
## Sub-actions
The skill dispatches by the first positional argument:
@@ -259,6 +305,19 @@ automatically sees the refreshed snapshot once the main
runs
upgrade, because each worktree's `<snapshot-dir>` is a symlink to
the main's.
+**`adopt` and `upgrade` always chain into `worktree-init` on every
+linked worktree as their final pass.** The chain is unconditional
+— even on a fresh adoption with no linked worktrees yet (the pass
+becomes a no-op), even on an upgrade where every worktree already
+looks wired (`worktree-init` is idempotent, repairs broken
+symlinks, and adds new always-on-family entries the upgrade
+introduced). The user does not need to remember to `cd` into each
+worktree and re-run anything; the main-checkout sub-action
+propagates state outward to the worktrees by itself. See
+[`adopt.md` Step
12.2](adopt.md#step-12--post-install-sync--worktree-propagation--sanity-check)
+and
+[`upgrade.md` Step
6c](upgrade.md#step-6c--propagate-to-every-worktree-run-worktree-init-unconditionally).
+
If the snapshot is missing (no `<snapshot-dir>/`) and
`<committed-lock>` exists, the skill treats any sub-action as
the recover-snapshot path: re-install per the committed lock
@@ -270,7 +329,7 @@ first, then continue.
|---|---|
| `from:<git-ref>` / `from:<version>` | Adopt or upgrade from a specific
framework ref or version. Used during `adopt` (overrides the user prompt) and
`upgrade` (overrides the committed lock for *this run only* — does NOT update
the committed lock). |
| `method:<git-branch\|git-tag\|svn-zip>` | Pick the install method
explicitly. Default during `adopt`: prompt the user. |
-| `skill-families:<list>` | Comma-separated families to symlink (`security`,
`pr-management`). Default on `adopt`: prompt. Default on `upgrade`: read the
families list from `<committed-lock>` / `<local-lock>` and **ensure every
framework skill in those families has a valid symlink** — create or repair
missing / broken symlinks, not just add new ones. |
+| `skill-families:<list>` | Comma-separated **opt-in** families to symlink
(`security`, `pr-management`). Default on `adopt`: prompt. Default on
`upgrade`: read the families list from `<committed-lock>` / `<local-lock>` and
**ensure every framework skill in those families has a valid symlink** — create
or repair missing / broken symlinks, not just add new ones. The flag never
accepts the always-on families (`setup-*` minus `setup-steward` itself, and
`list-steward-*`); per [Golden rule 8 [...]
| `--purge-overrides` | *(unadopt only)* Also `git rm -r`
`.apache-steward-overrides/`. Default: preserve. |
| `dry-run` | Show what the skill would do without writing anything. |
diff --git a/.claude/skills/setup-steward/adopt.md
b/.claude/skills/setup-steward/adopt.md
index 01fcc94..f7a3183 100644
--- a/.claude/skills/setup-steward/adopt.md
+++ b/.claude/skills/setup-steward/adopt.md
@@ -35,8 +35,14 @@ between automatically:
version (overrides the prompt).
- `method:<git-branch | git-tag | svn-zip>` — explicit method
(overrides the prompt).
-- `skill-families:<list>` — comma-separated families to
- symlink (default: prompt).
+- `skill-families:<list>` — comma-separated **opt-in**
+ families to symlink (default: prompt). Valid values:
+ `security`, `pr-management`. The flag does **not** accept
+ the always-on families (`setup-*` minus `setup-steward`
+ itself, and `list-steward-*`); per
+ [`SKILL.md` Golden rule 8](SKILL.md#golden-rules) those
+ are wired up unconditionally on every adopt run and the
+ user is never asked about them.
## Step 0 — Pre-flight
@@ -131,8 +137,56 @@ fetch — the recipe ran first and left the snapshot in
place.
After the fetch (or skip), confirm
`<snapshot-dir>/.claude/skills/` lists the framework skills
-(`pr-management-*`, `security-*`, `setup-*`). If not, the
-fetch produced an unexpected layout — surface and stop.
+(`pr-management-*`, `security-*`, `setup-*`,
+`list-steward-*`). If not, the fetch produced an unexpected
+layout — surface and stop.
+
+## Step 3b — Reconcile the committed `setup-steward` with the new snapshot +
reload in-flight
+
+Per [`SKILL.md` Golden rule 9](SKILL.md#golden-rules), the
+adopter-side committed `setup-steward` skill must match the
+snapshot's version before the rest of this run executes —
+otherwise we finish adoption against the *old* bootstrap
+logic for a *new* framework version.
+
+1. Diff `<adopter-skills-dir>/setup-steward/` against
+ `.apache-steward/.claude/skills/setup-steward/`.
+2. If they match — skip the rest of this step.
+3. If they differ and the adopter has **no** local
+ modifications beyond what the snapshot ships — overwrite
+ the committed copy from the snapshot:
+
+ ```bash
+ # Flat layout:
+ rm -rf <adopter-skills-dir>/setup-steward
+ cp -r .apache-steward/.claude/skills/setup-steward \
+ <adopter-skills-dir>/setup-steward
+
+ # Double-symlinked layout: copy into .github/skills/ —
+ # the .claude/skills/setup-steward symlink already
+ # points at it.
+ ```
+
+4. If the adopter **does** have local modifications,
+ surface the diff and stop. The user either (a) confirms
+ the local mods can be discarded, (b) upstreams them as a
+ PR to `apache/airflow-steward` first, or (c) defers the
+ bootstrap-skill refresh — in (c) the rest of this run
+ continues against the in-flight (older) version with a
+ warning.
+5. **Reload in-flight.** Immediately after the copy lands,
+ re-read `<adopter-skills-dir>/setup-steward/SKILL.md`
+ and `<adopter-skills-dir>/setup-steward/adopt.md` (the
+ current sub-action file), plus any helper file already
+ open in this run (`conventions.md`, `overrides.md`),
+ before continuing to Step 4. The remaining steps run
+ against the just-loaded content.
+
+For a FRESH adoption where the bootstrap recipe placed the
+matching `setup-steward` content on disk before this skill
+was invoked, the diff in (1) is empty and this step is a
+no-op. For a SUBSEQUENT adoption against an old committed
+copy, the overwrite + reload is the common case.
## Step 4 — Write `<committed-lock>` (FRESH only)
@@ -153,19 +207,40 @@ ref: <branch | tag | version>
## Step 5 — Pick the skill families
-(SUBSEQUENT adoption: re-use the families currently
-symlinked, if any. Or re-prompt if none.)
+The framework's family set splits into two tiers:
+
+**Always-on (no prompt; per
+[`SKILL.md` Golden rule 8](SKILL.md#golden-rules)):**
+
+- **`setup-*`** *(minus `setup-steward` itself)* — every
+ `setup-*` skill in the snapshot. Today:
+ `setup-isolated-setup-install`,
+ `setup-isolated-setup-update`,
+ `setup-isolated-setup-verify`, `setup-override-upstream`,
+ `setup-shared-config-sync`.
+- **`list-steward-*`** — every `list-steward-*` skill in
+ the snapshot. Today: `list-steward-skills`.
+
+These are wired up unconditionally; the user is **not**
+asked about them and they cannot be opted out via the
+`skill-families:` flag. The lock files do not record them
+because they are framework-mandated, not user-selected.
+
+**Opt-in (prompt, or read from
+`skill-families:` / the locks):**
+
+(SUBSEQUENT adoption: re-use the opt-in families currently
+recorded in `<committed-lock>` / `<local-lock>`, if any. Or
+re-prompt if none.)
-If `skill-families:` was passed, use those. Otherwise,
-prompt the user:
+If `skill-families:` was passed, use those values verbatim
+for the opt-in set. Otherwise prompt the user with:
- **`security`** — eight skills for security-issue
handling. Maintainer-only; not useful unless the project
has a security tracker.
-- **`pr-management`** — three skills for maintainer-facing
+- **`pr-management`** — five skills for maintainer-facing
PR queue work.
-- **`setup`** *(implicit)* — always installed because the
- snapshot carries it.
**Prefer structured Q&A.** When the agent harness offers a
structured-question tool, use a *multi-select* prompt for
@@ -178,6 +253,10 @@ user named no family, default to selecting both for an
adopter that is a maintainer-driven repo, or to no
pre-selection otherwise. Free-form chat is the fallback.
+Do **not** offer `setup-*` or `list-steward-*` as
+selectable options in the prompt — they are wired up
+silently regardless of what the user picks here.
+
## Step 6 — Write `<local-lock>`
Always written, both FRESH and SUBSEQUENT. Records what
@@ -204,24 +283,54 @@ idempotent — re-add them if they're missing.
/.claude/skills/security-*
/.claude/skills/pr-management-*
/.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/setup-isolated-setup-*
+/.github/skills/setup-override-upstream
/.github/skills/setup-shared-config-sync
+/.github/skills/list-steward-*
```
+The `setup-override-upstream`, `setup-shared-config-sync`,
+`setup-isolated-setup-*`, and `list-steward-*` entries are
+the always-on families per
+[`SKILL.md` Golden rule 8](SKILL.md#golden-rules); they are
+gitignored on every adopter regardless of the opt-in
+family pick. `setup-steward` itself is **not** gitignored —
+it is the one committed framework skill.
+
Mirror under `.github/skills/` only if the adopter uses the
double-symlinked convention.
## Step 8 — Wire up the framework-skill symlinks
-For each skill family the user picked, walk
-`<snapshot-dir>/.claude/skills/` and create a gitignored
-symlink for every matching skill at
-`<adopter-skills-dir>/<skill>` → relative path into
+The skill walks `<snapshot-dir>/.claude/skills/` and creates
+a gitignored symlink for every framework skill the adopter
+should have callable, at `<adopter-skills-dir>/<skill>` →
+relative path into
`<snapshot-dir>/.claude/skills/<skill>/`.
+The set of skills to link is the **union** of:
+
+1. **The opt-in families the user picked in Step 5**
+ (`security`, `pr-management`, or both). Each contributes
+ every framework skill in the snapshot whose name starts
+ with that family's prefix.
+2. **The always-on families** (no user input — per
+ [`SKILL.md` Golden rule 8](SKILL.md#golden-rules)):
+ every `setup-*` skill *except* `setup-steward` itself,
+ and every `list-steward-*` skill.
+
+The always-on set is added on every run, even when the user
+picked no opt-in families, even when `skill-families:` was
+passed with a narrow value, and even on the SUBSEQUENT-
+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
@@ -229,10 +338,17 @@ snapshot, the outer `.claude/skills/` points at the
inner. Both gitignored.
**Never overwrite an existing committed skill** of the same
-name. Surface conflicts and stop.
-
-Show the symlinks the skill is about to create, ask the
-user to confirm, then create them.
+name. Surface conflicts and stop. `setup-steward` itself is
+the one committed skill — the symlink wiring step skips it
+by name; the committed copy is reconciled in
+[Step
3b](#step-3b--reconcile-the-committed-setup-steward-with-the-new-snapshot--reload-in-flight),
+not here.
+
+Show the symlinks the skill is about to create, grouped by
+*opt-in family* / *always-on family*, ask the user to
+confirm, then create them. Always-on entries are surfaced
+read-only — the prompt is "confirm this list" not "edit this
+list".
## Step 9 — Scaffold `.apache-steward-overrides/` (FRESH only)
@@ -541,10 +657,72 @@ Surface the rendered diff (`git diff README.md AGENTS.md`)
to the user before writing. The user confirms once for the
whole doc set; do not ask separately per file.
-## Step 12 — Sanity check
-
-Run [`verify.md`](verify.md)'s checklist as a final step.
-Every check should be ✓ before the skill reports success.
+## Step 12 — Post-install sync + worktree propagation + sanity check
+
+Three passes, in this order:
+
+1. **Sync hooks and config from the snapshot.** Walk every
+ hook or config file the framework ships that an adopter
+ is expected to carry locally — at minimum the
+ `post-checkout` hook installed in
+ [Step 10](#step-10--worktree-aware-post-checkout-hook-fresh-only),
+ plus any other adopter-side hook or config file the
+ framework adds in future. For each one, compare the
+ adopter's installed copy against the snapshot's expected
+ content; if drifted, re-install from the snapshot (after
+ surfacing the diff and asking for confirmation when the
+ local copy looks hand-edited). This is the "sync local
+ versions with the framework's latest" pass and runs
+ *every* time `/setup-steward` runs in either FRESH or
+ SUBSEQUENT adoption — it is the same pass `/setup-steward
+ upgrade` runs after a snapshot refresh.
+
+2. **Propagate to every worktree (run `worktree-init`
+ unconditionally).** The main is now adopted; any
+ pre-existing linked worktree of this repo still lacks
+ the snapshot symlink and the `<adopter-skills-dir>`
+ symlinks. `worktree-init` is **always run on every
+ worktree** at the end of adopt, even when none exist
+ yet, even when the worktree appears wired, because
+ `worktree-init` is idempotent and the cost of an
+ unnecessary run is trivially small. Conversely, *not*
+ running it leaves worktree state inconsistent with the
+ freshly-adopted main.
+
+ Procedure:
+
+ - Enumerate worktrees with
+ `git worktree list --porcelain`. Filter to linked
+ worktrees only — skip the main (already handled in
+ Steps 1–11 above) and skip any bare worktrees.
+ - If the list is empty, this pass is a no-op; record
+ "no linked worktrees" in the recap and continue.
+ - For each linked worktree, invoke
+ `/setup-steward worktree-init` with that worktree's
+ working directory as the `cwd`. The sub-action picks up
+ the family set from `<main>/.apache-steward.lock` plus
+ the always-on families per
+ [`SKILL.md` Golden rule 8](SKILL.md#golden-rules), and
+ reconciles both the snapshot symlink and the
+ `<adopter-skills-dir>` symlinks (see
+ [`worktree-init.md` Step 1 + Step 1b](worktree-init.md)).
+ - Collect each invocation's recap into a per-worktree
+ row in the adopt summary's `Worktrees:` section.
+
+ Do **not** abort adopt because one worktree failed — the
+ main is already adopted, and the failing worktree is
+ recorded in the summary for later resolution (typically:
+ the user `cd`s there and re-runs `/setup-steward
+ worktree-init` after merging the adoption commit
+ forward).
+
+3. **Run the verify checklist.** Invoke
+ [`verify.md`](verify.md)'s checks. Every check should be
+ ✓ before the skill reports success. The hook-content
+ drift check passes trivially because pass (1) just
+ refreshed the hook from the snapshot; the worktree
+ symlink checks pass trivially because pass (2) just
+ ran `worktree-init` everywhere.
## Output to the user
@@ -570,7 +748,10 @@ Committed (you'll see in `git status`):
Gitignored (do NOT commit):
.apache-steward/
.apache-steward.local.lock
-
.claude/skills/{security,pr-management,setup-isolated-setup,setup-shared-config-sync}-*
+ .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)
```
diff --git a/.claude/skills/setup-steward/upgrade.md
b/.claude/skills/setup-steward/upgrade.md
index 680bf1a..db22206 100644
--- a/.claude/skills/setup-steward/upgrade.md
+++ b/.claude/skills/setup-steward/upgrade.md
@@ -91,9 +91,11 @@ For each kind of drift, present:
- **`setup-steward` skill changed in the framework** —
surface as a separate note. The adopter's *committed*
copy of `setup-steward` will be auto-overwritten from the
- new snapshot in [Step
6b](#step-6b--overwrite-the-committed-setup-steward-skill-from-the-new-snapshot)
- so the bootstrap stays in sync with the framework version
- the project just pinned.
+ new snapshot in [Step
4b](#step-4b--overwrite-the-committed-setup-steward-from-the-new-snapshot--reload-in-flight)
+ and then the skill **reloads in-flight** before the rest
+ of the upgrade runs, so the bootstrap stays in sync with
+ the framework version the project just pinned and the
+ remaining steps execute against the new content.
Ask for explicit confirmation before deleting and re-
installing.
@@ -134,6 +136,66 @@ new `<local-lock>`:
HEAD` for git methods; the version for svn-zip.
- `fetched_at` — current ISO-8601 timestamp.
+## Step 4b — Overwrite the committed `setup-steward` from the new snapshot +
reload in-flight
+
+This step **must run before Steps 5+** so the remainder of
+this upgrade executes against the framework version the
+project just pinned to, not against the pre-upgrade
+bootstrap logic. It implements
+[`SKILL.md` Golden rule 9](SKILL.md#golden-rules).
+
+1. Compute the diff between the adopter-side
+ `<adopter-skills-dir>/setup-steward/` (committed copy)
+ and the snapshot's
+ `.apache-steward/.claude/skills/setup-steward/`.
+2. **If the adopter has local modifications** to their
+ committed copy beyond what the snapshot ships — surface
+ the diff and stop. Do **not** silently overwrite local
+ work. The user either (a) confirms the modifications can
+ be discarded, (b) decides to upstream them as a PR
+ against `apache/airflow-steward` first, or (c) defers
+ the bootstrap-skill update to a later upgrade run.
+3. **If there are no local modifications** (or the user
+ confirmed in 2), copy the snapshot's `setup-steward`
+ over the committed copy:
+
+ ```bash
+ # For the flat layout:
+ 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):
+ 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
+ # which is now the new content.
+ ```
+
+4. **Reload in-flight.** Immediately after the copy lands —
+ before doing anything else in this run — re-read the
+ updated `<adopter-skills-dir>/setup-steward/SKILL.md`,
+ the just-overwritten `upgrade.md` (this file), and any
+ helper file you have already opened in this run
+ (`conventions.md`, `overrides.md`, `verify.md`). Resume
+ the upgrade from the step *after* this one, executing
+ the reloaded content — not the version of this file
+ that was in memory when the upgrade started.
+
+5. The new bootstrap-skill content lands as **modified files
+ in `git status`** at the adopter's committed-skill path.
+ The user reviews the diff and commits it as part of the
+ upgrade PR; on merge, every other contributor's next
+ `/setup-steward` run loads the matching version.
+
+The adopter shouldn't modify the bootstrap copy locally —
+the framework's hard rule is *"local mods go in
+`.apache-steward-overrides/`, framework changes go via PR
+to `apache/airflow-steward`"*. But if they did, step (2)
+catches it before the overwrite would erase their work.
+
## Step 5 — Reconcile overrides
For each file in `<repo-root>/.apache-steward-overrides/`:
@@ -158,19 +220,37 @@ pattern-matching.
## Step 6 — Refresh framework-skill symlinks
-Read the chosen skill families from `<committed-lock>`
+Read the opt-in skill families from `<committed-lock>`
(falling back to `<local-lock>` if the committed lock is
-silent on families). The post-upgrade state must be:
-*every framework skill in the new snapshot that belongs to
-a chosen family has a valid symlink in
-`<adopter-skills-dir>`*, and *no symlink points at a
-framework skill that no longer exists in the snapshot*.
+silent on families). Compose the **effective family set**
+for this upgrade as:
+
+- **Opt-in families** the project recorded (`security`,
+ `pr-management`, or both).
+- **Always-on families** (always added — never read from
+ the lock, never user-configurable, per
+ [`SKILL.md` Golden rule 8](SKILL.md#golden-rules)):
+ - every `setup-*` skill in the new snapshot *except*
+ `setup-steward` itself, and
+ - every `list-steward-*` skill in the new snapshot.
+
+Compute the always-on set fresh from the snapshot contents
+on disk — it expands automatically when the framework adds
+a new `setup-*` or `list-steward-*` skill in a release, and
+contracts on a rename / removal without code changes here.
+
+The post-upgrade state must be: *every framework skill in
+the new snapshot that belongs to the effective family set
+has a valid symlink in `<adopter-skills-dir>`*, and *no
+symlink points at a framework skill that no longer exists
+in the snapshot*.
Run two passes:
1. **Ensure every family-member skill is linked.** For each
- framework skill in the new snapshot that belongs to a
- chosen family, check `<adopter-skills-dir>/<skill>`:
+ framework skill in the new snapshot that belongs to the
+ effective family set, check
+ `<adopter-skills-dir>/<skill>`:
- If the symlink exists and points at the matching
snapshot path, leave it alone.
- If it's missing, create it.
@@ -196,61 +276,103 @@ Run two passes:
For the double-symlinked convention, refresh both layers.
-## Step 6b — Overwrite the committed `setup-steward` skill from the new
snapshot
-
-The adopter-side committed `setup-steward` skill is the
-**only** framework skill that lives as a committed copy
-rather than a gitignored symlink (per
-[`SKILL.md` Golden rule 6](SKILL.md#golden-rules)). When the
-framework's `setup-steward` evolves — new sub-action, lock-
-format change, drift-detection refinement — the adopter's
-copy must follow, or the bootstrap on a fresh clone will run
-old logic against a new snapshot.
-
-This step keeps the two in sync **automatically on every
-upgrade**:
-
-1. Compute the diff between the adopter-side
- `<adopter-skills-dir>/setup-steward/` (committed copy)
- and the snapshot's
- `.apache-steward/.claude/skills/setup-steward/`.
-2. **If the adopter has local modifications** to their
- committed copy beyond what's in the snapshot — surface
- the diff and stop. Do **not** silently overwrite local
- work. The user either (a) confirms the modifications can
- be discarded, (b) decides to upstream them as a PR
- against `apache/airflow-steward` first, or (c) defers the
- bootstrap-skill update to a later upgrade run.
-3. **If there are no local modifications** (or the user
- confirmed in 2), copy the snapshot's `setup-steward`
- over the committed copy:
-
- ```bash
- # For the flat layout:
- 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):
- 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
- # which is now the new content.
- ```
-
-4. The new bootstrap-skill content lands as **modified files
- in `git status`** at the adopter's committed-skill path.
- The user reviews the diff and commits it as part of the
- upgrade PR; on merge, every other contributor's next
- `/setup-steward` run loads the matching version.
-
-The adopter shouldn't modify the bootstrap copy locally —
-the framework's hard rule is *"local mods go in
-`.apache-steward-overrides/`, framework changes go via PR
-to `apache/airflow-steward`"*. But if they did, this step
-catches it before the overwrite would erase their work.
+## Step 6b — Sync locally-installed hooks and configuration
+
+The framework ships hooks and config files an adopter
+*carries locally* (in the working tree or under `.git/`)
+rather than pulls in via symlink. Examples:
+
+- `<repo-root>/.git/hooks/post-checkout` (the worktree-aware
+ hook installed during adoption).
+- Any future hook or local config the framework adds.
+
+These can drift independently of the snapshot — an
+adopter who never re-runs `/setup-steward` after a
+framework upgrade keeps the old hook content even after the
+snapshot updates. This step closes that gap.
+
+For each hook / local config file the framework declares as
+"adopter-installed":
+
+1. Compute the snapshot's *expected* content for that file
+ (the framework ships the expected content under
+ `.apache-steward/.claude/skills/setup-steward/` or a
+ sibling location — locate the canonical source for each
+ file).
+2. Compare against the local copy.
+3. If unchanged — ✓, move on.
+4. If drifted and the diff is consistent with a stock
+ framework refresh (no operator hand-edits) — overwrite
+ silently.
+5. If the local copy looks hand-edited — surface the diff,
+ ask the user whether to overwrite, keep, or move-aside.
+
+Run this sync unconditionally on every upgrade and every
+adopt run, regardless of whether the snapshot changed. It
+catches the "local config drifted while the snapshot didn't"
+case (e.g. a contributor accidentally edited
+`.git/hooks/post-checkout`).
+
+## Step 6c — Propagate to every worktree (run `worktree-init` unconditionally)
+
+The main checkout drives the upgrade, but each worktree
+carries its own gitignored `<adopter-skills-dir>` symlinks.
+Those symlinks need refreshing too — otherwise a developer
+sitting in a worktree sees the new snapshot via the shared
+`<snapshot-dir>` symlink (per
+[`worktree-init.md`](worktree-init.md)) but their
+`<adopter-skills-dir>` may still point at *missing* skills
+(a family the upgrade added) or *renamed* ones (a framework
+rename).
+
+`worktree-init` is **always run on every worktree** at the
+end of an upgrade, even when the user did not ask for it,
+even when the worktree looks "already wired", because
+`worktree-init` is idempotent (a no-op when state is
+correct) and the cost of running it unnecessarily is
+trivially small. Conversely, *not* running it leaves worktree
+state inconsistent with the new framework version. The
+post-checkout hook covers the "next checkout" case, but
+upgrade re-aligns the existing worktrees **now**.
+
+Procedure:
+
+1. Enumerate worktrees with `git worktree list --porcelain`.
+ Filter to the linked worktrees only — skip the main
+ checkout (already handled above) and any bare worktrees.
+2. For each linked worktree, invoke
+ `/setup-steward worktree-init` with that worktree's
+ working directory as the `cwd`. The sub-action picks up
+ the family set from `<main>/.apache-steward.lock` (the
+ committed lock the worktree shares via git) plus the
+ always-on families per
+ [`SKILL.md` Golden rule 8](SKILL.md#golden-rules), and
+ reconciles both the snapshot symlink and the
+ `<adopter-skills-dir>` symlinks (see
+ [`worktree-init.md` Step 1 + Step 1b](worktree-init.md)).
+3. Collect each invocation's recap into a per-worktree row
+ for the upgrade summary's `Worktrees:` section
+ (Step 8 output block).
+
+**Failure handling per worktree:**
+
+- If a worktree is on a branch that does not carry the
+ adopter's committed `setup-steward` skill (e.g. a feature
+ branch from before adoption landed), the worktree-init
+ invocation refuses with "main checkout not adopted from
+ this branch's perspective". Surface as a ⚠ row in the
+ summary and continue with the next worktree — the user
+ resolves it later by merging the adoption commit forward.
+- If a worktree has a hand-maintained `<snapshot-dir>` that
+ is **not** a symlink to the main's, the move-aside flow
+ in [`worktree-init.md` Step 0 row 4](worktree-init.md)
+ asks for confirmation. Surface to the user; either
+ confirm and continue, or defer that worktree and move on.
+
+Do **not** abort the whole upgrade because one worktree
+failed — the main is already upgraded and the other
+worktrees still benefit from the propagation. The summary
+makes the skipped worktrees easy to come back to.
## Step 7 — Update `<local-lock>`
@@ -280,14 +402,30 @@ Drift remediated:
Local: <local.fetched-commit-or-version> → <new>
Snapshot: refreshed at .apache-steward/
-Symlinks:
+setup-steward (bootstrap):
+ ✓ in sync OR ↻ overwritten from snapshot (reloaded in-flight)
+
+Symlinks (main checkout):
+ Opt-in families: <security>, <pr-management> (from lock)
+ Always-on families: setup-*, list-steward-* (per Golden rule 8)
✓ <list of unchanged symlinks>
- + <list of newly-created symlinks (skill present in a chosen
- family but missing from <adopter-skills-dir>)>
+ + <list of newly-created symlinks (skill present in the
+ effective family set but missing from <adopter-skills-dir>)>
↻ <list of repaired symlinks (existed but broken / pointing
at the wrong path)>
- <list of removed stale symlinks>
+Hooks + local config:
+ ✓ <list of files in sync>
+ ↻ <list of files re-synced from the snapshot>
+ ⚠ <list of files with hand-edits that need operator review>
+
+Worktrees (worktree-init was run on each, idempotently):
+ ✓ <worktree-path> (snapshot symlink + family symlinks aligned)
+ ↻ <worktree-path> (refreshed by worktree-init)
+ ⚠ <worktree-path> (skipped — branch missing adopter's setup-steward)
+ - <none> (when this repo has no linked worktrees)
+
Overrides:
✓ <list of overrides whose target is unchanged>
⚠ <list of overrides flagged for re-anchoring> (open the
diff --git a/.claude/skills/setup-steward/verify.md
b/.claude/skills/setup-steward/verify.md
index 32a59bb..6025dfc 100644
--- a/.claude/skills/setup-steward/verify.md
+++ b/.claude/skills/setup-steward/verify.md
@@ -129,9 +129,28 @@ into `.apache-steward/.claude/skills/<name>/`:
or this same skill with `--auto-fix-symlinks`.
For each framework skill in the snapshot **not** symlinked
-in the adopter — surface as ⚠ with the family
-classification. The user may have intentionally not picked
-that family; the warning prompts a decision.
+in the adopter, classify it:
+
+- **Always-on family** (every `setup-*` *except*
+ `setup-steward` itself, and every `list-steward-*` — per
+ [`SKILL.md` Golden rule 8](SKILL.md#golden-rules)) →
+ surface as ✗. These families are not opt-in; missing
+ symlinks here indicate a broken install or a skipped
+ upgrade pass. Remediation:
+ `/setup-steward verify --auto-fix-symlinks` (cheap), or
+ `/setup-steward upgrade` (covers the family-wide pass).
+- **Opt-in family the project picked** (per
+ `<committed-lock>` / `<local-lock>`) → surface as ✗. The
+ project declared the family but the install is missing a
+ skill from it. Remediation as above.
+- **Opt-in family the project did NOT pick** → surface as
+ ⚠. The user may have intentionally not picked that
+ family; the warning prompts a decision.
+
+The `--auto-fix-symlinks` path repairs the first two
+classes in place without prompting; the ⚠ class needs an
+explicit `/setup-steward adopt` re-run with the family
+added to the pick.
### 6. `.apache-steward-overrides/` exists + has the README
@@ -160,11 +179,14 @@ snapshot's
`.apache-steward/.claude/skills/setup-steward/`.
case after a framework upgrade where the adopter has
not yet rerun `/setup-steward upgrade`). Run
`/setup-steward upgrade` — its
- [Step
6b](upgrade.md#step-6b--overwrite-the-committed-setup-steward-skill-from-the-new-snapshot)
+ [Step
4b](upgrade.md#step-4b--overwrite-the-committed-setup-steward-from-the-new-snapshot--reload-in-flight)
auto-overwrites the committed copy with the snapshot's
- version, surfaces local modifications first if any
- exist, and lands the change in `git status` for the
- user to commit.
+ version, **reloads the skill in-flight** so the rest of
+ the upgrade run executes against the new bootstrap
+ content (per
+ [`SKILL.md` Golden rule 9](SKILL.md#golden-rules)),
+ surfaces local modifications first if any exist, and
+ lands the change in `git status` for the user to commit.
- **Committed copy is newer than the snapshot** (the
adopter modified the bootstrap skill directly; an
anti-pattern per the framework's hard rule). The
@@ -173,16 +195,34 @@ snapshot's
`.apache-steward/.claude/skills/setup-steward/`.
is to revert the modifications and use
`.apache-steward-overrides/` instead.
-### 8. Post-checkout hook installed
-
-`<repo-root>/.git/hooks/post-checkout` exists, is
-executable, and contains the
-`/setup-steward verify --auto-fix-symlinks` recipe.
-
-- ⚠ if missing — strictly optional, but worktrees off this
- repo will need a manual
- `/setup-steward verify --auto-fix-symlinks` after
- checkout. Print the install recipe.
+### 8. Post-checkout hook installed *and content matches the framework's
expected*
+
+Two sub-checks on `<repo-root>/.git/hooks/post-checkout`:
+
+1. **Presence + executable.** File exists, is executable,
+ and contains the
+ `/setup-steward verify --auto-fix-symlinks` recipe.
+ - ⚠ if missing — strictly optional, but worktrees off
+ this repo will need a manual
+ `/setup-steward verify --auto-fix-symlinks` after
+ checkout. Print the install recipe.
+
+2. **Content drift vs the framework's expected.** Diff the
+ installed hook against the framework's expected hook
+ content (the canonical source is shipped under the
+ snapshot — locate it during the check). Same logic
+ applies for any other adopter-installed local hook or
+ config file the framework grows in future.
+ - ✓ if content matches.
+ - ⚠ if drifted and the diff looks like operator
+ hand-edits — surface the diff; remediation is to run
+ `/setup-steward` (adopt or upgrade), whose
+ hook+config-sync pass re-installs from the snapshot
+ after asking about hand-edits.
+ - ✗ if drifted and the installed content is clearly
+ stale (older framework version's recipe) — same
+ remediation, no operator prompt needed; the sync
+ pass overwrites silently.
### 9. Project documentation mentions the framework
diff --git a/.claude/skills/setup-steward/worktree-init.md
b/.claude/skills/setup-steward/worktree-init.md
index a82c1f6..6d9a71f 100644
--- a/.claude/skills/setup-steward/worktree-init.md
+++ b/.claude/skills/setup-steward/worktree-init.md
@@ -57,7 +57,7 @@ has the right symlink is a no-op.
| Symlink to **something else** | Step 1 with a move-aside warning. The
skill backs the existing link up, names what it pointed at, and asks the user
to confirm before replacing. |
| Regular directory (per-worktree snapshot from before this convention) |
Step 1 with a move-aside warning. Back up the directory to
`.apache-steward.bak.<timestamp>` and create the symlink. **Do not** `rm -rf`
without confirmation — the directory may hold uncommitted local edits the
operator wants to preserve before the framework standardised on
snapshot-from-main. |
-## Step 1 — Create the symlink
+## Step 1 — Create the snapshot symlink
```bash
ln -s <main>/.apache-steward <worktree>/.apache-steward
@@ -69,19 +69,61 @@ Then verify the chain end-to-end:
at `<main>/.apache-steward`.
- `ls <worktree>/.apache-steward/.claude/skills/` lists the
same skills as `ls <main>/.apache-steward/.claude/skills/`.
-- Pick any committed skill symlink (e.g.
- `<worktree>/.claude/skills/security-issue-sync/SKILL.md`) and
- confirm `readlink -f` resolves it into
- `<main>/.apache-steward/...` rather than dangling.
+
+## Step 1b — Wire up the worktree's `<adopter-skills-dir>` symlinks
+
+The snapshot symlink in Step 1 only makes the framework's
+*source* available to this worktree. The `<adopter-skills-dir>`
+symlinks (the gitignored per-skill entries the agent harness
+actually resolves) are **per-worktree** — each working copy
+needs its own. A worktree branched from before adoption
+landed, or branched from a state where the symlinks were
+cleaned, has none on disk.
+
+Compose the **effective family set** for this worktree:
+
+- **Opt-in families** the project recorded — read from
+ `<main>/.apache-steward.lock` (the committed lock; the
+ worktree shares it via git).
+- **Always-on families** — every `setup-*` skill in the
+ snapshot *except* `setup-steward` itself, and every
+ `list-steward-*` skill, per
+ [`SKILL.md` Golden rule 8](SKILL.md#golden-rules). These
+ are added unconditionally, never read from the lock.
+
+For each framework skill in the effective family set:
+
+- If `<worktree>/<adopter-skills-dir>/<skill>` is missing —
+ create it (gitignored).
+- If it exists and points at the correct snapshot path —
+ leave it alone.
+- If it exists but is broken or points at the wrong path —
+ 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.
+
+Pick any framework skill symlink that should now exist (e.g.
+`<worktree>/.claude/skills/security-issue-sync/SKILL.md`) and
+confirm `readlink -f` resolves it into
+`<main>/.apache-steward/...` rather than dangling — same
+sanity check as Step 1's bottom bullet, just now end-to-end
+from agent-harness path through the worktree's symlink
+through the snapshot symlink to the framework source.
## Step 2 — Recap
Print a short summary:
-- The symlink that was just created.
+- The snapshot symlink that was just created or confirmed.
- The main checkout's resolved path.
- The framework version the main is pinned at (read from
`<main>/.apache-steward.lock`).
+- The effective family set wired in Step 1b, split into
+ *opt-in* and *always-on*, with per-skill ✓ / + / ↻
+ counts.
- A reminder: `upgrade` from the main, not from the worktree.
## Inputs