Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/utilities/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ New to Magpie? The [quick start](../quick-start.md) walks the whole path in
one place — install, the first `/magpie-setup:setup` run, and a recording of it
happening — plus the other agents and the secure-isolation setup to run next.

The first utilities skill you run opens with a one-minute
[welcome guide](../../plugins/magpie-setup/skills/setup/setup_preflight/guides/utilities.md):
the typical flow, what needs configuring, what to ask it, and how to make it yours.
Dismiss it, or hide it for good, from its last line;
*"show the utilities welcome again"* or `/magpie-setup:welcome-guide utilities` brings it back.

### Before the first run

<!-- BEGIN generated: skill-config (tools/dev/check-skill-config.py --fix) -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->

# 🐦 Magpie here — your skill-authoring sidekick

A one-minute tour, then straight back to what you asked for.

## 🔄 The typical flow

1. 📋 **List** the skills installed here, grouped by family — *"list skills"*
2. ✍️ **Write** a new skill against framework conventions, with evals — *"write a skill"*
3. ⚡ **Optimize** an existing skill for brevity and token efficiency — *"optimize a skill"*
4. 🐛 **Report a framework issue** from where you hit it, after checking duplicates — *"report an issue"*
5. 🔁 **Reconcile** two near-duplicate skills and classify where they drifted — *"reconcile <skill-A> and <skill-B>"*

Nothing is written, modified, or posted without your OK.

## ⚙️ Configure it

Nothing here has to be configured:

- 🛠️ **Authoring & help** — meta-skills work straight away in any directory or worktree
- 📖 **Optional project settings** — reads `project.md` when present, otherwise uses built-in framework defaults

Two ways to run Magpie when you are ready: personal install (`/magpie-setup:setup config`) or project adoption (`/magpie-setup:setup adopt`).

## 💬 Just ask

Talk to Magpie in plain language:

- *"what skills are installed here?"*
- *"write a skill that drafts PR release notes"*
- *"optimize the write-skill instructions"*
- *"file a bug about preflight caching"*
- *"have these two skill copies drifted?"*

## 🎨 Make it yours

Magpie ships default workflows and conventions.
Change them by describing the change:

- *"from now on, always add a smoke test when writing a skill"*
- *"group the skill list by capability instead of family"*

Each becomes an override for the utility skill it changes.
Useful to everyone? *"Upstream this change to Magpie"* opens a generic `apache/magpie` PR for you to review first.

<!-- welcome footer: generated from guides/_footer.md -->
7 changes: 7 additions & 0 deletions plugins/magpie-setup/skills/setup/setup_preflight/welcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from __future__ import annotations

import argparse
import contextlib
import hashlib
import json
import os
Expand Down Expand Up @@ -434,6 +435,12 @@ def scan_families(base: Path) -> dict[str, str]:
if mirror.is_dir():
for link in sorted(mirror.iterdir()):
target = link.resolve()
if not target.is_dir() and link.is_file():
Comment thread
potiuk marked this conversation as resolved.
# A checkout without symlink support stores a mirror entry as a text file holding its target path.
with contextlib.suppress(OSError):
cand = (link.parent / link.read_text(encoding="utf-8").strip()).resolve()
if cand.is_dir():
target = cand
if target in by_dir:
found.setdefault(f"magpie-{link.name}", by_dir[target])
return dict(sorted(found.items()))
Expand Down
11 changes: 11 additions & 0 deletions tools/dev/render-preflight-fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
on `PATH`).
* `requires` — `requires_config:` entries passed as `--requires`.
* `skill`, `hash` — the invoking skill's `name:` and `surface_hash:`.
* `reconciled` — dict written to personal layer `reconciled.json`, or `null`.
* `welcome` — `true` to include the skill family's first-run welcome guide.
Left out otherwise (`--no-welcome`), so a floor case is about the floor
and not about which family its skill happens to belong to.
Expand Down Expand Up @@ -73,6 +74,16 @@ def run_checker(state: dict[str, object], repo_root: Path, scratch: Path) -> str
overrides.mkdir()
(overrides / "project.md").write_text("# project\n", encoding="utf-8")

reconciled = state.get("reconciled")
if reconciled is not None:
if not isinstance(reconciled, dict):
raise ValueError("`reconciled` must be a dict or null")
personal = (
(project / ".apache-magpie-local") if lock is not None else (project / ".git" / "apache-magpie")
)
personal.mkdir(parents=True, exist_ok=True)
(personal / "reconciled.json").write_text(json.dumps(reconciled), encoding="utf-8")

listing = scratch / "plugins.json"
plugins = state.get("plugins")
if plugins == "unreadable":
Expand Down
35 changes: 32 additions & 3 deletions tools/setup-preflight/tests/test_welcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ def test_the_family_guide_comes_on_the_next_run_not_the_same_one(project: Path,
assert due(project) == [("general", "again")]


def test_the_utilities_family_guide_comes_on_the_next_run(project: Path, version) -> None:
assert due(project, "list-skills") == [("general", "first-run")]
welcome.record(project, "general", "seen")
assert due(project, "list-skills") == [("utilities", "first-run")]
welcome.record(project, "utilities", "seen")
assert due(project, "list-skills") == [("general", "again")]


def test_at_most_one_guide_per_run(project: Path, version) -> None:
for _ in range(8):
found = welcome_findings(project, SECURITY_SKILL)
Expand Down Expand Up @@ -376,11 +384,11 @@ def test_show_without_color_prints_the_markdown(capsys) -> None:

def test_the_rules_section_and_both_guides_ship() -> None:
assert welcome.SECTION in sections.available()
assert {"general", "security"} <= set(welcome.guides())
assert {"general", "security", "utilities"} <= set(welcome.guides())
assert welcome.family_guides() == [g for g in welcome.guides() if g != "general"]


@pytest.mark.parametrize("name", ["general", "security"])
@pytest.mark.parametrize("name", ["general", "security", "utilities"])
def test_each_guide_is_short_and_ends_with_the_way_back(name: str) -> None:
text = welcome.guide(name) or ""
assert not text.startswith("<!--")
Expand All @@ -401,20 +409,41 @@ def test_the_footer_is_shared_and_filled_in_per_guide() -> None:
assert welcome.guide("_footer") is None
security = welcome.guide("security") or ""
general = welcome.guide("general") or ""
for text in (security, general):
utilities = welcome.guide("utilities") or ""
for text in (security, general, utilities):
assert "{{" not in text
assert "welcome footer: generated" not in text
assert "`/magpie-setup:welcome-guide security`" in security
assert "--family security --color=always" in security
assert "`/magpie-setup:welcome-guide [<family>]`" in general
assert "--family general --color=always" in general
assert "`/magpie-setup:welcome-guide utilities`" in utilities
assert "--family utilities --color=always" in utilities


def test_list_prints_every_guide(capsys) -> None:
assert welcome.main(["list"]) == 0
assert capsys.readouterr().out.split() == welcome.guides()


def _plugin_skill(base: Path, name: str, family: str) -> Path:
skill = base / "plugins" / f"magpie-{family}" / "skills" / name
skill.mkdir(parents=True)
(skill / "SKILL.md").write_text(f"---\nname: {name}\nfamily: {family}\n---\n", encoding="utf-8")
return skill


def test_a_mirror_entry_checked_out_as_a_text_file_still_resolves(tmp_path: Path) -> None:
"""Without symlink support git writes a link as a file holding its target path."""
_plugin_skill(tmp_path, "list-skills", "utilities")
mirror = tmp_path / "skills"
mirror.mkdir()
(mirror / "list-skills").write_text("../plugins/magpie-utilities/skills/list-skills\n", encoding="utf-8")
(mirror / "stray").write_text("../plugins/magpie-utilities/skills/missing\n", encoding="utf-8")

assert welcome.scan_families(tmp_path) == {"list-skills": "utilities", "magpie-list-skills": "utilities"}


@pytest.mark.skipif(not (REPO_ROOT / "plugins").is_dir(), reason="needs the framework tree")
def test_the_shipped_family_map_matches_the_framework_tree() -> None:
"""The prek hook keeps this in sync; a failure means it was bypassed."""
Expand Down
2 changes: 1 addition & 1 deletion tools/skill-evals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Behavioral eval harness for Apache Magpie skills. Each eval suite tests a skill

Suites are currently implemented for:

- **setup** — 125 cases across 29 steps (step-verify-drift, step-overrides-surface, step-override-bypass, step-m3-baseline-pick, step-m4-install-gates, step-m5-no-repo-offer, step-m5-companion-offer, step-upgrade-companion-offer, step-adopt-settings-merge, verify-default-set, uninstall-default-set, lock-marketplace-parse, adopt-write-floor, setup-prefill-from-floor, preflight-floor, upgrade-adoption-split, verify-floor, adopt-review-process, adopt-security-list, step-reconcile, step-verify, step-config-stamp, step-config-adversarial, step-config-mail, step-adopt-stamp, step-upgrade-stamp, step-delegate-propose, config-vcs-backend)
- **setup** — 126 cases across 29 steps (step-verify-drift, step-overrides-surface, step-override-bypass, step-m3-baseline-pick, step-m4-install-gates, step-m5-no-repo-offer, step-m5-companion-offer, step-upgrade-companion-offer, step-adopt-settings-merge, verify-default-set, uninstall-default-set, lock-marketplace-parse, adopt-write-floor, setup-prefill-from-floor, preflight-floor, upgrade-adoption-split, verify-floor, adopt-review-process, adopt-security-list, step-reconcile, step-verify, step-config-stamp, step-config-adversarial, step-config-mail, step-adopt-stamp, step-upgrade-stamp, step-delegate-propose, config-vcs-backend)
- **setup-isolated-setup-install** — 19 cases across 5 steps (runtime-routing, step-snapshot-drift, step-scope-confirm, step-hardware-key, step-roborev-exclusion)
- **setup-privacy-llm** — 6 cases across 2 steps (step-1-resolve, step-4-gate)
- **setup-shared-config-sync** — 12 cases across 2 steps (step-3-decide-action, step-5-draft-commit)
Expand Down
4 changes: 2 additions & 2 deletions tools/skill-evals/evals/setup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

Behavioral evals for the `setup` skill.

## Suites (125 cases total)
## Suites (126 cases total)

| Suite | Step | Cases | What it covers |
|---|---|---|---|
Expand All @@ -25,7 +25,7 @@ Behavioral evals for the `setup` skill.
| adopt-review-process | adopt.md § Step 4c | 5 | a fresh adopt finding two deviations in the confirmed documents where one is accepted and one rejected, and a release policy left alone because that family is not in the floor; a re-adoption reviewing only the one family the floor diff added; a re-adoption that adds no family, where edited process documents are still not re-read; a sentence that would weaken a merge confirmation gate, dropped and named while a label deviation beside it is written; and a repo whose only candidate document the maintainer un-ticks, skipping the step |
| adopt-security-list | adopt.md § Step 4b | 4 | an ASF project whose dedicated security list is in its docs and on the ASF page (recommend `security@`); an ASF project with no dedicated list (no recommendation, `private@` only as a flagged fallback); a non-ASF project, which is not asked; and conflicting evidence between `SECURITY.md` and the ASF page (no recommendation, flagged fallback) |
| setup-prefill-from-floor | install.md § Step M0b | 4 | adopted (propose the floor), unadopted (framework defaults), a foreign marketplace called out, and a snapshot lock falling through |
| preflight-floor | preflight-block.md § Pre-flight | 9 | at floor (silent), below floor (update), plugin missing (install), foreign marketplace (ask first, run nothing), no `claude` CLI (print), unadopted (propose setup), `apache/magpie` in an alarming context (update anyway, unasked), unreadable `claude plugin list --json` (silent, treated as unknown not absent), first Magpie run (show the general welcome guide, record it, carry on) |
| preflight-floor | preflight-block.md § Pre-flight | 10 | at floor (silent), below floor (update), plugin missing (install), foreign marketplace (ask first, run nothing), no `claude` CLI (print), unadopted (propose setup), `apache/magpie` in an alarming context (update anyway, unasked), unreadable `claude plugin list --json` (silent, treated as unknown not absent), first Magpie run (show the general welcome guide, record it, carry on), first utilities run (show the utilities welcome guide, record it, carry on) |
| upgrade-adoption-split | upgrade.md § Step 0b | 4 | not adopted (nothing staged), adopted (floor raised and staged), already ahead (floor unchanged), snapshot method (falls through) |
| verify-floor | verify.md § Adoption floor | 4 | no lock (not a fault), ahead of floor with extra plugins (not a fault), a shortfall (a fault), a floor plugin the marketplace no longer ships (a fault, not installed around) |
| step-reconcile | reconcile.md § The sweep | 3 | a clean sweep on a pinned-snapshot install (anchor present, config resolved — stamp written, nothing proposed), a renamed step heading stranding an override's anchor (one re-anchor proposal named), a marketplace install whose plugin cache is sandbox-denied (anchor resolution left `unchecked`, config resolution still completes) |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"tags":["local-smoke"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"action": "welcome", "commands": [], "blocks": false}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->

There is no `.apache-magpie.lock`, no `.apache-magpie-overrides/` and no
`.apache-magpie-local/` at the repo root. The running agent is Claude Code
with magpie-setup 0.4.0 and magpie-utilities 0.4.0 installed.

The user has already seen the initial general welcome guide on an earlier run.
The invoked skill's own frontmatter reads `name: list-skills` and
`surface_hash: sha256:2edc90a1b7f3440d`; it belongs to the utilities family.
This is the first time a utilities skill has run here.
The user asked it to "list skills".

The pre-flight command printed:

```json
{
"findings": [
{
"code": "welcome",
"facts": {
"family": "utilities",
"guide": "# \ud83d\udc26 Magpie here \u2014 your skill-authoring sidekick\n\nA one-minute tour, then straight back to what you asked for.\n\n## \ud83d\udd04 The typical flow\n\n1. \ud83d\udccb **List** the skills installed here, grouped by family \u2014 *\"list skills\"*\n2. \u270d\ufe0f **Write** a new skill against framework conventions, with evals \u2014 *\"write a skill\"*\n3. \u26a1 **Optimize** an existing skill for brevity and token efficiency \u2014 *\"optimize a skill\"*\n4. \ud83d\udc1b **Report a framework issue** from where you hit it, after checking duplicates \u2014 *\"report an issue\"*\n5. \ud83d\udd01 **Reconcile** two near-duplicate skills and classify where they drifted \u2014 *\"reconcile <skill-A> and <skill-B>\"*\n\nNothing is written, modified, or posted without your OK.\n\n## \u2699\ufe0f Configure it\n\nNothing here has to be configured:\n\n- \ud83d\udee0\ufe0f **Authoring & help** \u2014 meta-skills work straight away in any directory or worktree\n- \ud83d\udcd6 **Optional project settings** \u2014 reads `project.md` when present, otherwise uses built-in framework defaults\n\nTwo ways to run Magpie when you are ready: personal install (`/magpie-setup:setup config`) or project adoption (`/magpie-setup:setup adopt`).\n\n## \ud83d\udcac Just ask\n\nTalk to Magpie in plain language:\n\n- *\"what skills are installed here?\"*\n- *\"write a skill that drafts PR release notes\"*\n- *\"optimize the write-skill instructions\"*\n- *\"file a bug about preflight caching\"*\n- *\"have these two skill copies drifted?\"*\n\n## \ud83c\udfa8 Make it yours\n\nMagpie ships default workflows and conventions.\nChange them by describing the change:\n\n- *\"from now on, always add a smoke test when writing a skill\"*\n- *\"group the skill list by capability instead of family\"*\n\nEach becomes an override for the utility skill it changes.\nUseful to everyone? *\"Upstream this change to Magpie\"* opens a generic `apache/magpie` PR for you to review first.\n\n## \ud83d\udc4b About this guide\n\n- **Continue** \u2014 show it again next time (three times at most)\n- **Dismiss** \u2014 hide it until the next Magpie upgrade\n- **Don't show again** \u2014 hide it for good; say *\"show the utilities welcome again\"* to bring it back\n\nShow this again any time: `/magpie-setup:welcome-guide utilities`\n(`/magpie-setup-welcome-guide utilities` on a pinned snapshot).\n\n\ud83c\udf08 Prefer it in colour? Run this yourself \u2014 the `!` prefix runs it as a shell command:\n\n```text\n! PYTHONPATH=\".apache-magpie-local:$(git rev-parse --git-common-dir)/../.apache-magpie-local:$(git rev-parse --git-common-dir)/apache-magpie\" python3 -m setup_preflight.welcome show --family utilities --color=always\n```\n",
"max_shown": 3,
"reason": "first-run",
"shown": 0
},
"scope": "family",
"section": "welcome"
}
],
"rules": {
"welcome": "## welcome \u2014 a first-run welcome guide is due\n\nA welcome guide is due on this run: `facts.family` names it\n(`general`, for the first Magpie skill this user runs here, or the skill's own family),\nand `facts.reason` says why (`first-run`, `again`, or `upgraded` after a dismissal).\n`facts.guide` is the guide. The checker shows at most one guide per run and has already picked it.\n\n1. **Show `facts.guide` to the user verbatim**, as Markdown, before the work they asked for.\n Do not summarise, reword, trim, or add to it, and substitute nothing in it:\n its placeholders (`<security-list>`, `<tracker>`, `<family>`, \u2026) are meant to be read as written.\n2. **Offer the three choices in one line**:\n **Continue** (show again next time) \u00b7 **Dismiss** (hide until the next Magpie upgrade) \u00b7 **Don't show again**.\n3. **Record `seen` at once**, which is what no answer means, then carry on with the user's request in the same turn.\n Never block the requested work on the guide, and never wait for an answer before starting it.\n4. **When the user picks Dismiss or Don't show again** \u2014 now or later in the session \u2014 record that choice;\n it replaces `seen`. Continue needs nothing further.\n\n```bash\nPYTHONPATH=\".apache-magpie-local:$(git rev-parse --git-common-dir)/../.apache-magpie-local:$(git rev-parse --git-common-dir)/apache-magpie\" \\\n python3 -m setup_preflight.welcome record --family <facts.family> --choice <seen|dismiss|never>\n```\n\n`seen` brings the guide back on a later run, `facts.max_shown` recorded showings in all (`facts.shown` so far);\n`dismiss` hides it until the framework version changes; `never` hides it until re-enabled.\nEvery write merges into the personal layer's `reconciled.json` under `welcome.<facts.family>`, keeping every other key;\nnever write it by hand.\nIf the command exits non-zero, say nothing more about it and carry on.\n\nAny guide can be shown again on request with `/magpie-setup:welcome-guide [<family>]`,\nwhich also turns a hidden one back on."
},
"verdict": "action"
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"lock": null,
"plugins": [
[
"magpie-setup",
"0.4.0"
],
[
"magpie-utilities",
"0.4.0"
]
],
"reconciled": {
"welcome": {
"general": {
"choice": "seen",
"shown": 1
}
}
},
"skill": "list-skills",
"welcome": true
}
Loading