Files
Nico Bellante d84e3344da Replace developing-with-streamlit with meta-skill for bundled skill discovery (#26)
## Summary

Replaces the old `developing-with-streamlit` skill (21 sub-skills + 17
templates) with a single lightweight **meta-skill** that discovers
Streamlit's bundled skills inside the pip package (>=1.57). One
user-level install works across every project; the discovered skills
always match each project's pinned Streamlit version.

Net diff: **+1,171 / −12,738** across 99 files.

## The contract

`developing-with-streamlit/scripts/discover.py` is the actual logic —
stdlib-only Python, cross-platform via `pathlib` + the `venv` module's
Windows/POSIX awareness. It detects the active Python interpreter
(`$VIRTUAL_ENV` → `./.venv` → `../.venv` → `<git-root>/.venv` →
`$CONDA_PREFIX` → `pipenv` if `Pipfile` → `poetry` if `poetry.lock` →
`pdm` if `pdm.lock` → `uv` if `uv.lock` → system), runs `import
streamlit; print(streamlit.__path__[0])`, and emits one of six exit
codes:

| Exit | Meaning | Agent action |
|---|---|---|
| 0 | Bundled SKILL.md found | Read printed path, follow its routing |
| 1 | Streamlit not installed | Confirm with user, then install with the
**single** package-manager-matched command in the error (`poetry add
streamlit` for poetry, `uv add streamlit` for uv, `<venv-python> -m pip
install streamlit` for venv, etc.) — no buffet |
| 2 | Streamlit < 1.57 | Suggest `pip install --upgrade streamlit` for
version-matched bundled skills; fall back to
`docs.streamlit.io/llms-full.txt` if user can't upgrade |
| 3 | No Python interpreter | Ask user to install Python 3.10+ (suggest
`uv` as the easy path) |
| 4 | Bundled skills exist but expected sub-path missing | Read listed
skills directly |
| 5 | Invalid argument | Fix invocation |

`SKILL.md` is a thin wrapper that documents this contract and tells the
agent to invoke `python <SKILL_DIR>/scripts/discover.py --project-dir
<USER_PROJECT_DIR>`. The `--project-dir` arg makes resolution
deterministic regardless of the agent's CWD at invocation time.

`detect_interpreter()` returns `(cmd, tag)` where `tag` identifies which
detection branch fired. `install_advice(cmd, tag)` maps the tag to one
matching install command, so a poetry project's exit-1 message points at
`poetry add streamlit` instead of dumping pip + uv + poetry + pipenv +
conda and asking the agent to guess.

Why a script instead of prose: same logic runs in production AND in
tests, so no parallel reference impl can drift. Pure-prose meta-skills
need LLM-in-the-loop evals to validate; this gets the bulk of realistic
failure modes from cheap pytest at zero LLM cost — modulo the
description-triggering question, which only LLM eval can answer.

## Tier 1 test matrix

19 pytest tests (26 cases including parametrized) in
`tests/discovery/test_discovery.py`. Each E2E test creates an isolated
`tmp_path` venv via the stdlib `venv` module, installs Streamlit fresh,
runs `discover.py`, and asserts the contract. Coverage:

- **Detection priority**: each interpreter source in our priority order,
including `<git-root>/.venv` (helpful for deep monorepo layouts)
- **Conflict**: `$VIRTUAL_ENV` wins over `./.venv`
- **Fallbacks**: missing-Streamlit (exit 1), pre-1.57 (exit 2),
upstream-restructured (exit 4)
- **Invocation contract**: `--project-dir` argument from outside the
project
- **Tool-marker detection**: `Pipfile` (pipenv), `poetry.lock` (poetry),
`pdm.lock` (pdm), `uv.lock` (uv)
- **uv branch isn't too eager**: doesn't fire on bare `pyproject.toml`
without `uv.lock`
- **Tool-aware install advice**: `install_advice` parametrized over all
10 tags (virtual-env, venv-local, venv-parent, venv-git-root, conda,
pipenv, poetry, pdm, uv, system) — guarantees the per-tag mapping
doesn't regress

Negative-test verified: temporarily reversing priority order in
`discover.py` causes `test_priority_venv_over_local` to fail with a
clear diagnostic; reverting the `uv.lock` marker causes
`test_uv_no_lockfile` to fail similarly. Both prove the tests catch the
regressions they're designed for.

CI runs on `ubuntu-latest` AND `windows-latest`, on PR + push to main +
weekly cron. Tests install whatever `streamlit` is currently latest on
PyPI — failures here are an honest signal that something broke, either
in our code or in upstream's bundled-skill layout.

### Latest passing run

[Run
25827312532](https://github.com/streamlit/agent-skills/actions/runs/25827312532)
on commit `7fd4fae`:

| Job | Result | Duration |
|---|---|---|
|
[`ubuntu-latest`](https://github.com/streamlit/agent-skills/actions/runs/25827312532/job/75883639893)
| 26 passed | 4m 33s |
|
[`windows-latest`](https://github.com/streamlit/agent-skills/actions/runs/25827312532/job/75883639903)
| 26 passed | 9m 13s |

All 26 cases pass on both runners — no skips. Every Windows-specific
concern (Scripts vs bin, `python.exe` vs `python`, path separators,
`Pipfile` / `poetry.lock` / `pdm.lock` / `uv.lock` markers) is
empirically validated. Each E2E test pins the resolved path to its own
venv via `assert_resolves_bundled(..., inside=...)`, so a
wrong-but-valid Streamlit install elsewhere on the runner can't silently
satisfy the assertion.

## End-to-end verification (LLM-in-the-loop)

Hands-on smoke test in a fresh Claude Code v2.1.128 session — done by me
before opening this for review, not by an automated script.

**Setup:** Built a `streamlit==1.57.0` venv at
`/tmp/test-streamlit-install-2/`, installed the skill at
`~/.claude/skills/developing-with-streamlit/`. Started Claude Code in
the project directory with no prior context, no `CLAUDE.md`, no skill
pre-selected.

**Prompt:**

> *I want to add a small KPI dashboard to a new app.py in this directory
— three metric cards showing total revenue, active users, and conversion
rate. Use placeholder values for now. Make it look polished.*

**Observed (full trajectory captured in the session JSONL — 14 tool
calls):**

1. Description match → `Skill(developing-with-streamlit)` fires
automatically. The prompt never names the skill or mentions `discover`.
2. `Bash` runs `discover.py --project-dir <project>` with **no
permission prompt** — the `allowed-tools` frontmatter pre-approves the
bundled script.
3. Stdout returns the bundled `SKILL.md` path inside the venv.
4. `Read` bundled SKILL.md, then `references/dashboards.md` and
`references/design.md`.
5. `ls` + `Read` of the `dashboard-metrics` template.
6. `Write` `app.py` using bundled patterns: `st.metric(border=True)`,
horizontal container, Material icons, sparkline trends.
7. Step 4 of the bundled SKILL.md respected — agent offered to run the
app rather than auto-running.

Whole chain held end-to-end. No skill misroutes, no silent fallthroughs,
no freelancing of Streamlit code without first reading the bundled refs.

## Acknowledged gaps

- **Multi-env disambiguation** — user with both conda + venv pinning
different Streamlit versions gets whichever the priority order picks.
Acceptable without a `cortex env detect`-style CLI.
- **Cross-agent `allowed-tools` support** — the pre-approval works in
Claude Code but is ignored by Cursor / Copilot / Gemini / Codex. They
fall back to their own permission prompts, which is the safe default —
just slightly more friction on first invocation in those agents.
- **Cross-agent end-to-end validation** — the smoke test above was
Claude Code only. Other agents follow the same Agent Skills spec and
should work via `npx skills add`, but individual agents haven't been
verified hands-on.

## Manual verification

All three confirmed by the cold-start hands-on test above:

- [x] Description's trigger phrases auto-fire the skill (no prompt
mentioned `developing-with-streamlit` or `discover`).
- [x] Agent invokes `scripts/discover.py` rather than winging it from
prose.
- [x] `allowed-tools` frontmatter pre-approves the bundled `discover.py`
(no bash permission prompt on first invocation in Claude Code).
2026-05-13 15:06:46 -07:00
..

Discovery tests

End-to-end tests for developing-with-streamlit/scripts/discover.py.

Each test exercises one environment shape from the meta-skill's documented priority order, or one of the fallback codepaths. Tests are pytest-based, stdlib-only (no third-party deps beyond pytest itself), and cross-platform — the same source runs on Linux, macOS, and Windows.

Layout

tests/discovery/
  conftest.py             # shared helpers: make_venv, run_discover, assert_resolves_bundled
  test_discovery.py       # all tests, 19 functions covering the documented matrix
  run-local.sh            # convenience wrapper around `pytest tests/discovery/`
  README.md

Running locally

pip install pytest                              # one-time
bash tests/discovery/run-local.sh               # full suite
bash tests/discovery/run-local.sh -k pipenv     # pytest filter
bash tests/discovery/run-local.sh -x            # stop at first failure

Tests that need third-party tools (uv, pipenv, poetry, pdm, conda) skip cleanly when those tools aren't installed — pytest reports them as SKIPPED rather than errors. CI installs uv, pipenv, poetry, and pdm so those tests run; conda is intentionally not installed on CI runners, so test_conda is reported as SKIPPED there and only exercised locally.

Each test takes ~515 seconds (most of which is pip install streamlit into a fresh venv). A full local run is under 2 minutes.

Running in CI

Triggered by .github/workflows/test-discovery.yml on PRs that touch developing-with-streamlit/** or tests/**, on push to main, and weekly via cron. Two OS jobs:

  • Linux (ubuntu-latest): runs the full suite; test_conda skips.
  • Windows (windows-latest): runs the full suite; test_conda skips.

Both jobs install the same toolchain (Python 3.12, pytest, uv, pipenv, poetry, pdm) before running the suite, so coverage is identical across OSes. conda is deliberately not installed — its tests run locally on machines that already have it.

What this catches

  • Regressions in the priority order (e.g. ./.venv silently winning over $VIRTUAL_ENV).
  • Breakage when Streamlit upstream moves .agents/skills/.
  • Broken fallback messages for missing Streamlit or pre-1.57 versions.
  • Cross-platform regressions (Windows-specific path handling, Scripts/python.exe vs bin/python).
  • discover.py bugs that wouldn't surface on the author's single machine.

What this does NOT catch

  • LLM misinterpretation of SKILL.md prose (Tier 2 / cold-start eval — out of scope).
  • Bugs in the bundled skills themselves (upstream repo's concern).
  • Environment shapes we don't document: hatch-managed envs without activation, pyenv-virtualenv without activation.

Adding a test

  1. Add a function to test_discovery.py. Use the tmp_path fixture for an isolated working directory, and make_venv() / run_discover() from conftest.py for setup.
  2. If the test depends on a tool that may not be installed, add @pytest.mark.skipif(shutil.which("toolname") is None, reason="...").
  3. CI matrix is OS-only (ubuntu-latest, windows-latest); pytest discovers new tests automatically — no workflow changes needed.