From 843477a41ea19357eca08b307d550af4da8f333d Mon Sep 17 00:00:00 2001 From: Tester Date: Fri, 3 Jul 2026 19:50:32 +0200 Subject: [PATCH] =?UTF-8?q?feat(skill-sources):=20Phase=20B=20=E2=80=94=20?= =?UTF-8?q?fetch/pin/symlink=20wiring=20for=20trusted=20external=20skill?= =?UTF-8?q?=20sources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the trusted-external-skill-sources feature runnable (Phase A landed the design, formats, and validator in #677): - New /magpie-setup skill-sources sub-action (skills/setup/skill-sources.md): read the adopter trust list, fetch+verify each source into the gitignored .apache-magpie-sources// reusing the framework install recipes, write the two source locks, and symlink the provided skills exactly like framework skills (canonical + relay, magpie- prefixed). - Two source locks: committed .apache-magpie.sources.lock (per-source pins) + gitignored .apache-magpie.sources.local.lock (per-machine fetch), same drift model as the framework snapshot. - Fold into adopt (Step 8b + .gitignore block), upgrade (Step 6f re-fetch + source drift), verify (check 10 source health), worktree-init (share the source snapshot). SKILL.md registers the sub-action, dispatch, locks. - symlink-lint prunes .apache-magpie-sources/ (a build artefact like the framework snapshot); regression test added. --- docs/skill-sources/README.md | 5 +- skills/setup/SKILL.md | 36 ++- skills/setup/adopt.md | 34 +++ skills/setup/skill-sources.md | 278 ++++++++++++++++++ skills/setup/upgrade.md | 35 +++ skills/setup/verify.md | 29 ++ skills/setup/worktree-init.md | 13 + .../symlink-lint/src/symlink_lint/__init__.py | 1 + tools/symlink-lint/tests/test_symlink_lint.py | 9 + 9 files changed, 434 insertions(+), 6 deletions(-) create mode 100644 skills/setup/skill-sources.md diff --git a/docs/skill-sources/README.md b/docs/skill-sources/README.md index b4b0f04c..faf5427b 100644 --- a/docs/skill-sources/README.md +++ b/docs/skill-sources/README.md @@ -125,8 +125,9 @@ directory draws no eval-coverage advisory because its evals are external). ## How a trusted skill is installed -The [`setup`](../../skills/setup/SKILL.md) skill drives the fetch (see -`/magpie-setup skill-sources`). In outline: +The [`setup`](../../skills/setup/SKILL.md) skill drives the fetch — the +[`skill-sources`](../../skills/setup/skill-sources.md) sub-action +(`/magpie-setup skill-sources`). In outline: 1. Read `/skill-sources.md` — the trust list. Sources not listed there are never fetched. diff --git a/skills/setup/SKILL.md b/skills/setup/SKILL.md index 4458c03a..ac8e394b 100644 --- a/skills/setup/SKILL.md +++ b/skills/setup/SKILL.md @@ -10,6 +10,7 @@ description: | `/magpie-setup upgrade` - refresh the gitignored snapshot per the committed lock (main-checkout only) `/magpie-setup worktree-init` - symlink a worktree's snapshot to the main's `/magpie-setup verify` - health check + drift detection + `/magpie-setup skill-sources` - fetch/pin/symlink skills from trusted external sources listed in the adopter trust list (main-checkout only) `/magpie-setup override ` - open or scaffold an agentic override in `.apache-magpie-overrides/` `/magpie-setup unadopt` - reverse the adoption (snapshot, locks, symlinks, hook, doc sections); preserves `.apache-magpie-overrides/` by default (main-checkout only) when_to_use: | @@ -163,6 +164,24 @@ The drift check on every framework-skill invocation compares this against `` and surfaces any mismatch as a proposed `/magpie-setup upgrade`. +### Source locks — the same split, for trusted external sources + +Skills pulled from [trusted external +sources](../../docs/skill-sources/README.md) use their **own** +pair of locks with the identical committed-pin / local-fingerprint +split, kept separate from the framework locks so a source re-pin +never entangles a framework upgrade: + +- **`.apache-magpie.sources.lock`** (committed) — the project's + per-source pins, one block per source keyed by `id` + (`method`/`url`/`ref` + `commit`|`sha512`). +- **`.apache-magpie.sources.local.lock`** (gitignored) — this + machine's per-source fetch fingerprint. + +They are written and reconciled by +[`skill-sources.md`](skill-sources.md) and re-fetched on +`upgrade`; the format and drift semantics live there. + ## Detail files in this directory | File | Purpose | @@ -170,6 +189,7 @@ proposed `/magpie-setup 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-magpie-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-magpie-overrides/` exists, drift status (committed vs local), the `setup` skill itself is current. | +| [`skill-sources.md`](skill-sources.md) | Fetch/verify skills from trusted external sources listed in `/skill-sources.md`, pin them in the committed `.apache-magpie.sources.lock`, and symlink the provided skills in exactly like framework skills. The runnable half of [trusted external skill sources](../../docs/skill-sources/README.md); the install gate is the adopter trust list. | | [`agents.md`](agents.md) | The agent-target registry — *which* directories framework-skill symlinks land in across vendors, and the **canonical-plus-relay** model: `.agents/skills/` is the one canonical home (links into the snapshot/source); every other target (`claude-code`, `github`, holdout natives like Windsurf / Goose) gets a per-skill relay symlink into `.agents/skills/`. Defines active-target selection, SKILL.md format portability, and the Claude-Code-only layer (sandbox/hooks). The source of truth every sub-action consults for the target set. | | [`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` skill itself. Preserves `.apache-magpie-overrides/` by default; `--purge-overrides` removes it too. Surfaces the full removal plan before any write. | @@ -217,7 +237,7 @@ accept, `upgrade`: 5. Updates `` to the new fetch. **Golden rule 4 — `.gitignore` keeps the adopter repo clean.** -Three things gitignored in the adopter repo: +Gitignored in the adopter repo: - `` (the entire framework snapshot — gigabytes potentially). @@ -228,12 +248,19 @@ Three things gitignored in the adopter repo: `.claude/skills/` / `.github/skills/` / holdouts (they target the canonical entries) — both would dangle in a fresh clone. The one exception un-ignored in each dir is `magpie-setup`. +- `.apache-magpie-sources/` (the gitignored fetch of every + trusted external skill source) and + `.apache-magpie.sources.local.lock` (per-machine source-fetch + fingerprint), when the adopter trusts any source. See + [`skill-sources.md`](skill-sources.md). **Committed**: this skill (`setup`, as the canonical `.agents/skills/magpie-setup/` plus its relays), the -``, the `.apache-magpie-overrides/` -directory, the `.gitignore` entries themselves, any -project-doc updates the `adopt` sub-action makes. +``, the **`.apache-magpie.sources.lock`** +per-source pins (the project's committed vouch for each trusted +source), the `.apache-magpie-overrides/` directory, the +`.gitignore` entries themselves, any project-doc updates the +`adopt` sub-action makes. **Golden rule 5 — `.agents/skills/` is canonical; everything else just relays into it.** Regardless of how an adopting @@ -334,6 +361,7 @@ The skill dispatches by the first positional argument: | `/magpie-setup upgrade` | [`upgrade.md`](upgrade.md) | Refresh snapshot per `` + reconcile overrides + refresh symlinks. **Main-checkout only** — worktrees pick up upgrades automatically via the symlink installed by `worktree-init`. | | `/magpie-setup worktree-init` | [`worktree-init.md`](worktree-init.md) | **Worktree-only.** Symlink the worktree's `` to the main checkout's so this worktree shares one framework state. No fetch, no lock files written; idempotent. | | `/magpie-setup verify` | [`verify.md`](verify.md) | Read-only health check + drift status report. Works in both main and worktrees. | +| `/magpie-setup skill-sources` (aka `skill-sources add `) | [`skill-sources.md`](skill-sources.md) | Fetch/verify/pin/symlink skills from the trusted external sources the adopter listed in `/skill-sources.md`. **Main-checkout only** — worktrees share the source snapshots via `worktree-init`. | | `/magpie-setup override ` | [`overrides.md`](overrides.md) | Open / scaffold an override file. | | `/magpie-setup unadopt` | [`unadopt.md`](unadopt.md) | Reverse the adoption. Removes snapshot, locks, symlinks, hook, doc sections, and this skill itself. Preserves `.apache-magpie-overrides/` unless `--purge-overrides` is passed. **Main-checkout only.** | diff --git a/skills/setup/adopt.md b/skills/setup/adopt.md index 927d7871..1bcf79c6 100644 --- a/skills/setup/adopt.md +++ b/skills/setup/adopt.md @@ -502,6 +502,8 @@ idempotent — re-add them if they're missing. ```text /.apache-magpie/ /.apache-magpie.local.lock +/.apache-magpie-sources/ +/.apache-magpie.sources.local.lock /.claude/settings.local.json /.claude/hooks/agent-guard.py /.claude/hooks/guards.d/ @@ -509,6 +511,19 @@ __pycache__/ *.pyc ``` +The `/.apache-magpie-sources/` and +`/.apache-magpie.sources.local.lock` lines keep the gitignored +fetch of every [trusted external skill +source](../../docs/skill-sources/README.md) and its per-machine +fetch fingerprint out of the tree — the source counterpart of +`/.apache-magpie/` + `/.apache-magpie.local.lock`. The committed +per-source pins (`.apache-magpie.sources.lock`) are **not** ignored; +they travel with the repo like ``. These two lines +are harmless when the adopter trusts no source (the paths simply +never appear); [`skill-sources.md`](skill-sources.md) also adds +them idempotently the first time a source is pinned on an older +adoption. + The `__pycache__/` and `*.pyc` lines (non-anchored — they match at any depth) keep the byte-compiled artefacts that framework skill scripts emit when run from the adopter checkout (e.g. @@ -643,6 +658,25 @@ confirm, then create them. Always-on entries are surfaced read-only — the prompt is "confirm this list" not "edit this list". +## Step 8b — Wire up trusted external-source skills + +If `/skill-sources.md` (the adopter trust list) +exists and lists any source, run the +[`skill-sources`](skill-sources.md) sub-action now as a content +pass: fetch + verify each trusted source into +`.apache-magpie-sources//`, write both source locks, and +create the canonical + relay `magpie-` symlinks for the +skills each source `provides` — the same wiring Step 8 does for +framework skills, just targeting the source snapshots. Nothing is +fetched if the trust list is absent or empty (the common case); +this step is then a no-op. + +Source skills are `magpie-`-prefixed and gitignored exactly like +framework skills, so the `.gitignore` block from +[Step 7](#step-7--gitignore-entries-fresh-only) already covers +their symlinks; the `.apache-magpie-sources/` snapshot dir and +`.apache-magpie.sources.local.lock` were added there too. + ## Step 9 — Scaffold `.apache-magpie-overrides/` (FRESH only) Create `/.apache-magpie-overrides/` (directory) diff --git a/skills/setup/skill-sources.md b/skills/setup/skill-sources.md new file mode 100644 index 00000000..bf9fd6e0 --- /dev/null +++ b/skills/setup/skill-sources.md @@ -0,0 +1,278 @@ + + + + +# setup — `skill-sources` (pull skills from trusted external sources) + +Wire a skill or a whole skill-family from a **trusted external +source** — a repo other than `apache/magpie` that ships +Magpie-shaped skills — into the adopter repo so it behaves +**exactly like an in-tree framework skill**: same +`magpie-`-prefixed canonical-plus-relay symlink, same override +layer, same eval binding. + +This is the runnable half of the [trusted external skill +sources](../../docs/skill-sources/README.md) feature. The +formats (source descriptor, `skills//source.md` pointer), +the trust model, and the §13 carve-out are defined there and in +[`RFC-AI-0006`](../../docs/rfcs/RFC-AI-0006.md); this sub-action +does the fetch, the pin, and the symlink. + +**The rule that governs everything here:** per +[`PRINCIPLES.md` §13](../../PRINCIPLES.md#13-snapshot-plus-override-never-vendored-copies), +a source is fetched **only if the adopter has listed it in +`` and committed its pin.** An org curating a source, +or the [registry](../../docs/skill-sources/registry.md) listing +one, never triggers an install. This sub-action reads the trust +list and does nothing for a source that is not on it. + +Invocations (registered in [`SKILL.md`](SKILL.md#sub-actions)): + +- `/magpie-setup skill-sources` — reconcile every trusted source: + fetch/verify, pin, symlink the provided skills. +- `/magpie-setup skill-sources add ` — the same flow + scoped to one source id (the id must already be present in + `` — this sub-action never edits the trust list; + vouching for a source is the adopter's committed act). + +**Main-checkout only**, like `adopt`/`upgrade` — a worktree picks +up the source snapshots through the same `` symlink +that `worktree-init` seeds (the source snapshots live beside the +framework snapshot and are shared the same way). Refuse to run in +a worktree; direct the user to run it in the main checkout. + +## The two source locks + +Sources reuse the framework's [two-lock drift +model](SKILL.md#the-two-lock-files) verbatim, in a **separate pair +of files** so a source re-pin is never entangled with a framework +upgrade: + +### `` — `.apache-magpie.sources.lock` (committed) + +The project's **per-source pins** — one block per trusted source, +keyed by `id`, carrying the same pin keys as the framework lock +(`method` / `url` / `ref` + the per-method verification anchor). +Edited only by this sub-action and `upgrade`; do not modify by +hand. + +```text +# .apache-magpie.sources.lock — committed; the project's per-source pins. + +- id: acme-security-skills + method: git-tag + url: https://github.com/acme/magpie-skills + ref: v2.1.0 + commit: + +- id: example-svn-source + method: svn-zip + url: https://downloads.example.org/skills/skills-1.4.0.zip + ref: 1.4.0 + sha512: +``` + +`git-branch` sources carry `method`/`url`/`ref` and **no** +cryptographic anchor (tip-tracking, WIP-only) — exactly as for a +`git-branch` framework snapshot. + +### `` — `.apache-magpie.sources.local.lock` (gitignored) + +The **per-machine fetch fingerprint** — what this checkout +actually pulled for each source, and when. One block per source, +keyed by `id`. + +```text +# .apache-magpie.sources.local.lock — gitignored; per-machine. + +- id: acme-security-skills + source_method: git-tag + source_url: https://github.com/acme/magpie-skills + source_ref: v2.1.0 + fetched_commit: + fetched_at: +``` + +**Drift** for a source is a committed-vs-local mismatch on its +block, read identically to the framework drift check: every +framework skill (and `verify`) compares the two and, on a gap, +proposes `/magpie-setup upgrade`. The committed lock is the pin +that travels with the repo; the local lock is per-machine truth. + +## Step 0 — Pre-flight + +1. **Main-checkout guard.** As `adopt`/`upgrade` + ([`SKILL.md` Sub-actions](SKILL.md#sub-actions)): if + `git rev-parse --git-dir` ≠ `git rev-parse --git-common-dir`, + this is a linked worktree — refuse and tell the user to run + the reconcile in the main checkout (the worktree already sees + the source snapshots via its shared ``). +2. **Framework adopted?** Require `` + (`.apache-magpie.lock`) and a live ``. If the + framework itself is not adopted yet, stop and point the user + at `/magpie-setup` first — trusted sources ride on the same + symlink relay the framework install establishes. +3. **Trust list present?** Read `` + (`/skill-sources.md`). If it is absent or + lists no sources, there is nothing to do — say so and exit + (this is the common, expected case: an adopter running only + in-tree framework skills). + +## Step 1 — Resolve the trusted sources + +For every entry in `` +([format](../../docs/skill-sources/README.md#source-descriptor)): + +1. **Resolve the descriptor.** An entry that is a bare + `id` (+ `ref` + anchor) **references** an org-curated + descriptor — look it up in + `organizations//skill-sources.md` for the org resolved by + the standard `project → organization → framework` precedence + (see [`AGENTS.md`](../../AGENTS.md#configuration-resolution-order)). + An entry that carries a full descriptor is used as-is. Merge: + the org descriptor supplies `method`/`url`/`layout`/`provides`; + the adopter entry supplies (and may override) `ref` + anchor — + the adopter's committed pin always wins. +2. **Validate before any fetch.** `organization:` must name a + directory under `organizations/`; `method` must be one of + `git-tag`/`git-branch`/`svn-zip`; a non-`git-branch` source + must carry its anchor (`commit` for git-tag, `sha512` for + svn-zip). A source that fails validation is reported and + **skipped** — never fetched on a partial pin. (The + [skill-and-tool-validator](../../tools/skill-and-tool-validator/) + enforces the same shape statically on the descriptor files.) +3. Present the resolved, validated source list (id, org, method, + ref, what it `provides`) and the exact fetch each will run. + This is the point of consent before any network egress. + +## Step 2 — Fetch + verify each source + +Into **`.apache-magpie-sources//`** (gitignored, +sibling to `` — kept separate on purpose so a +framework `upgrade`, which deletes `` outright, does +**not** wipe the source snapshots). Reuse the framework [install +recipes](../../docs/setup/install-recipes.md) **verbatim**, +parameterized by the source `url`/`ref`: + +- **`git-tag` / `git-branch`** — `git clone --depth=1 --branch + .apache-magpie-sources/`. For `git-tag`, resolve + `HEAD` after clone and confirm it equals the committed `commit` + anchor; a mismatch aborts that source (the tag moved — a + supply-chain signal). +- **`svn-zip`** — download the archive, `sha512sum -c` against the + committed `sha512`, optional `gpg --verify` against the source's + published `KEYS`, then unzip into + `.apache-magpie-sources//`. A checksum failure aborts that + source. + +A source that fails verification is left un-installed and +reported; the others proceed. Never fall back to an unverified +fetch. + +## Step 3 — Write both source locks + +1. **``** (committed) — for a first pin of a source, + write its block: `method`/`url`/`ref` + the resolved anchor + (`commit` from the clone for git-tag; `sha512` for svn-zip). + Bumping an existing pin is a deliberate project action and + shows up in the PR diff, exactly like a framework-lock bump. +2. **``** (gitignored) — write/refresh this + machine's fetch fingerprint for each source + (`source_*` + `fetched_commit` + `fetched_at`). + +The two source locks + the `.apache-magpie-sources/` snapshot dir +must be gitignored — see +[`adopt.md` Step 7](adopt.md#step-7--gitignore-entries-fresh-only), +which writes the block. If any line is missing (e.g. this is the +first source on an older adoption), add it here idempotently. + +## Step 4 — Select the provided skills + +Expand each source's `provides` against its **fetched** +`skills_root`: + +- `skill: ` → the single directory `skills//`. +- `family: -*` → every `skills/-*` directory in + the fetched snapshot — the **same prefix-glob** as framework + family selection ([`adopt.md` Step 8](adopt.md#step-8--wire-up-the-framework-skill-symlinks)), + computed fresh from disk, never a hard-coded list. + +Show the resolved skill set per source and confirm. A +**pointer file** (`skills//source.md`) may already sit in +the adopter's committed tree marking where a specific skill is +expected; a `provides` entry and a pointer are two views of the +same intent — reconcile them (a pointer with no matching +`provides` is surfaced as an unfulfilled redirect). + +## Step 5 — Wire the symlinks + +Identical to the framework's canonical-plus-relay model +([`adopt.md` Step 8](adopt.md#step-8--wire-up-the-framework-skill-symlinks), +[`agents.md`](agents.md)) — the target is the source snapshot +instead of the framework snapshot: + +- **Canonical (`.agents/skills/`)** — one gitignored symlink per + provided skill: `.agents/skills/magpie-` → + `../../.apache-magpie-sources//skills//`. +- **Relays (`.claude/skills/`, `.github/skills/`, any present + holdout)** — one gitignored symlink per skill: + `/skills/magpie-` → + `../../.agents/skills/magpie-` (back through the canonical + entry, never at the source snapshot). + +Every source skill is `magpie-`-prefixed, so the same +`magpie-*` gitignore glob and the same `symlink-lint` +relay-through-canonical rule that cover framework skills cover +source skills unchanged. **Never overwrite an existing committed +skill or a framework symlink of the same name** — a source that +`provides` a `magpie-` already claimed by the framework or +another source is a collision: surface it and stop, do not +silently shadow. + +The pulled skill's eval suite rides along in the source snapshot +at `.apache-magpie-sources//tools/skill-evals/evals//`; +because the source keeps the framework's two-tree layout (declared +in its descriptor `layout:` and required by the +[layout contract](../../docs/skill-sources/README.md#layout-contract--skills-evals-tests)), +the eval binding resolves after fetch with no extra wiring. + +## Step 6 — Chain worktree propagation + +As with `adopt`/`upgrade`, finish by running +[`worktree-init`](worktree-init.md) on every linked worktree so +each one's shared `` and agent-dir symlinks reflect +the new source skills. Unconditional and idempotent — a no-op when +there are no worktrees. + +## Relationship to `adopt`, `upgrade`, `verify` + +- **`adopt`** chains into this sub-action as its final content + pass when `` lists any source, so a fresh adoption + that already trusts a source wires it in the same run (see + [`adopt.md` Step 8b](adopt.md#step-8b--wire-up-trusted-external-source-skills)). +- **`upgrade`** re-fetches every source per its committed + `` pin, refreshes the symlinks, and extends drift + detection to the two source locks (see + [`upgrade.md` Step 6f](upgrade.md#step-6f--re-fetch-trusted-external-sources)). +- **`verify`** reports source-snapshot presence, source-symlink + health, and source drift (committed vs local) alongside the + framework checks (see + [`verify.md` check 10](verify.md#10-trusted-external-source-snapshots--symlinks)). + +## Failure modes + +| Symptom | Likely cause | Remediation | +|---|---|---| +| "nothing to do — no trusted sources" | `` absent or empty | Expected when running only in-tree skills. Add a source to `` (and commit its pin) to install one. | +| A source is skipped with a validation error | Unknown `organization:`, bad `method`, or a missing anchor on a non-`git-branch` source | Fix the descriptor / pin in `` (or the org's `skill-sources.md`) and re-run. | +| `git-tag` fetch aborts on a `commit` mismatch | The upstream tag was moved after it was pinned — a supply-chain signal | Re-review the source; only re-pin (bump `commit` in ``) once the new tag is trusted. | +| `svn-zip` fetch aborts on a checksum failure | The archive changed, or the pinned `sha512` is wrong | Re-verify the archive out-of-band before re-pinning. | +| A provided `magpie-` collides with a framework or another source skill | Two sources (or a source and the framework) claim the same prefixed name | Rename is the source's call; drop one `provides`/pointer to resolve locally. | +| Worktree can't see a source skill | Source snapshots are shared via the worktree's `` symlink, seeded by `worktree-init` | `/magpie-setup verify` in the worktree; `worktree-init` if the symlink is missing. | diff --git a/skills/setup/upgrade.md b/skills/setup/upgrade.md index fbb7586c..25353041 100644 --- a/skills/setup/upgrade.md +++ b/skills/setup/upgrade.md @@ -657,6 +657,41 @@ missing entirely, point the operator at [`adopt.md` Step 9c](adopt.md#step-9c--comdev-mcp-prerequisites-asf-projects) to (re-)install it. +## Step 6f — Re-fetch trusted external sources + +If `/skill-sources.md` lists any source, reconcile +the source snapshots the same way this upgrade reconciled the +framework one — but from the **committed +`.apache-magpie.sources.lock`** pins, not the framework lock, and +**without** deleting them alongside `` (source +snapshots are a separate, sibling tree — see +[Step 3](#step-3--delete-the-old-snapshot), which deletes only +`.apache-magpie/`): + +1. **Source drift.** For each source, compare its + `.apache-magpie.sources.lock` block (committed pin) against its + `.apache-magpie.sources.local.lock` block (what this machine + fetched). Report any gap in the upgrade summary, exactly like + the framework drift row. +2. **Re-fetch per the committed pin.** Run the + [`skill-sources`](skill-sources.md) fetch + verify for every + trusted source (git-tag `commit` / svn-zip `sha512` re-checked), + refresh `.apache-magpie-sources//`, and refresh the + canonical + relay `magpie-` symlinks — adding skills a + source newly `provides`, removing ones it dropped, repairing + broken links. This is the source counterpart of + [Step 6](#step-6--refresh-framework-skill-symlinks). +3. **Update the source local lock** to the new fetch fingerprint. + +Nothing happens when the trust list is absent or empty. Because +each worktree shares main's `.apache-magpie-sources/` through the +snapshot symlink [`worktree-init`](worktree-init.md) seeds +(alongside ``), the refreshed source **content** is +visible to every worktree immediately; a worktree that predates a +newly-`provides`-d source skill picks up its per-worktree symlink +on its next `worktree-init` or +`/magpie-setup verify --auto-fix-symlinks`. + ## Step 7 — Update `` Write the new local lock with the values captured in Step diff --git a/skills/setup/verify.md b/skills/setup/verify.md index 111d9a76..1d086200 100644 --- a/skills/setup/verify.md +++ b/skills/setup/verify.md @@ -700,6 +700,35 @@ as ⚠ overall only, never ✗. `CONTRIBUTING.md` counts as a fallback for `README.md` if the adopter declared it so during adoption. +### 10. Trusted external source snapshots + symlinks + +Only when `/skill-sources.md` (the trust list) +lists at least one source — otherwise skip this check silently +(the adopter runs in-tree skills only). For each trusted source +([`skill-sources.md`](skill-sources.md)): + +- **Committed pin present.** The source has a block in + `.apache-magpie.sources.lock` (`method`/`url`/`ref` + anchor). + Missing ⇒ ✗: the trust list vouches for a source that was never + pinned — run `/magpie-setup skill-sources`. +- **Snapshot present.** `.apache-magpie-sources//` exists on + disk with the source's `skills_root`. Missing ⇒ ✗ with the + remediation `/magpie-setup skill-sources` (the fetch is + gitignored, so a fresh clone has none — expected, same as the + framework snapshot). +- **Source drift.** The source's committed block vs its + `.apache-magpie.sources.local.lock` block — a mismatch ⇒ ⚠ and + proposes `/magpie-setup upgrade`, exactly like framework drift + (check 3). +- **Symlinks live.** Every `magpie-` the source `provides` + resolves through the canonical + `.agents/skills/magpie-` → `../../.apache-magpie-sources//skills//` + and its relays (same rule as check 5). Dangling / misdirected ⇒ + ✗ with `/magpie-setup verify --auto-fix-symlinks`. +- **No name collision.** No `magpie-` provided by a source + shadows a framework skill or another source's skill. Collision + ⇒ ✗ (surface, do not auto-resolve). + ## After the report If every check is ✓ (or ⚠ on items the adopter has diff --git a/skills/setup/worktree-init.md b/skills/setup/worktree-init.md index 2cee737a..39729b9f 100644 --- a/skills/setup/worktree-init.md +++ b/skills/setup/worktree-init.md @@ -63,12 +63,25 @@ has the right symlink is a no-op. ln -s
/.apache-magpie /.apache-magpie ``` +**Trusted external skill sources.** If the main checkout has a +`.apache-magpie-sources/` directory (the adopter trusts at least +one [external source](../../docs/skill-sources/README.md)), share +it the same way so this worktree's source-skill symlinks resolve +against one snapshot on disk: + +```bash +# only when
/.apache-magpie-sources exists: +ln -s
/.apache-magpie-sources /.apache-magpie-sources +``` + Then verify the chain end-to-end: - `ls -la /.apache-magpie` returns a symlink pointing at `
/.apache-magpie`. - `ls /.apache-magpie/skills/` lists the same skills as `ls
/.apache-magpie/skills/`. +- when sources are in use, `ls -la /.apache-magpie-sources` + is likewise a symlink to `
/.apache-magpie-sources`. ## Step 1b — Wire up the worktree's per-target symlinks diff --git a/tools/symlink-lint/src/symlink_lint/__init__.py b/tools/symlink-lint/src/symlink_lint/__init__.py index 757e0a1f..3a4e7c5d 100644 --- a/tools/symlink-lint/src/symlink_lint/__init__.py +++ b/tools/symlink-lint/src/symlink_lint/__init__.py @@ -45,6 +45,7 @@ ".venv", "node_modules", ".apache-magpie", + ".apache-magpie-sources", ".mypy_cache", ".pytest_cache", ".hatch", diff --git a/tools/symlink-lint/tests/test_symlink_lint.py b/tools/symlink-lint/tests/test_symlink_lint.py index e60c8862..2818fc0e 100644 --- a/tools/symlink-lint/tests/test_symlink_lint.py +++ b/tools/symlink-lint/tests/test_symlink_lint.py @@ -92,6 +92,15 @@ def test_pruned_directory_ignored(tmp_path: Path) -> None: assert symlink_lint.find_cyclic_symlinks(tmp_path) == [] +def test_source_snapshot_dir_pruned(tmp_path: Path) -> None: + # The gitignored fetch of a trusted external skill source is a build + # artefact like the framework snapshot — never scanned. A stray link + # inside it must not be flagged. + _symlink(tmp_path, ".apache-magpie-sources/acme/loop", target=".") + assert symlink_lint.find_cyclic_symlinks(tmp_path) == [] + assert symlink_lint.find_misdirected_relays(tmp_path) == [] + + def test_symlink_name_with_spaces_detected(tmp_path: Path) -> None: _symlink(tmp_path, "weird dir/loop with space", target=".") assert offending_paths(symlink_lint.find_cyclic_symlinks(tmp_path), tmp_path) == {