Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Developing skills

For contributors adding or modifying plugins in this repo. (Using them: Using QuantEcon skills.)

Repo layout

.claude-plugin/marketplace.json   # the catalogue — every plugin registers here
scripts/                          # manifest + frontmatter validation, version-bump guard (CI runs these)
docs/                             # these guides
<plugin>/                         # one directory per plugin
  .claude-plugin/plugin.json      # name, description, version
  CHANGELOG.md                    # required: one entry per released version
  README.md                       # the plugin's user guide
  skills/<skill-name>/SKILL.md    # one directory per skill
  scripts/                        # deterministic scripts the skills drive
  references/                     # rule/rubric content the skills read

A plugin needs its manifest and its changelog; a skill needs only SKILL.md. A skill that is purely a procedure — nothing deterministic to run, no long reference material to point at — is one file in one directory, and should stay that way. scripts/ appears when there is something mechanical worth doing in code; references/ when the skill needs more context than belongs in its body. Adding either before you need it just makes the skill harder to read.

The three live plugins show some of the range: qe (an umbrella skill plus thin per-category sub-skills sharing plugin-level rules and scripts, alongside an unrelated standalone procedure with one script of its own), benchmark (one skill driving a deterministic engine, with worked examples as its regression baseline), and audit (sibling procedures sharing a method document). None of these is the house style — they are what three problems happened to need.

Conventions

Guidance rather than gates. The repo is early, and most of what follows generalises from one or two worked examples; where something is genuinely load-bearing it says so and gives the reason. Departing from the rest is fine when you have a reason — and worth mentioning in the PR, since a second example is how any of this eventually becomes a real convention (CATALOG.md § Principles).

Development loop

# validate everything the marketplace serves
python scripts/validate.py

validate.py checks: every catalogue entry resolves to a real directory; plugin.json agrees with marketplace.json on name/version/description; every SKILL.md has frontmatter whose name matches its directory. Negative-test your changes (break something on purpose; the validator must fail loudly) — a malformed manifest breaks installation silently in every consuming repo.

Testing locally

Two tiers, fastest first. Either way, test from a real consuming project (a lecture repo checkout), not from inside this repo — the whole class of path-resolution bugs (${CLAUDE_PLUGIN_ROOT}, workspace-vs-plugin working directories) only surfaces when the plugin runs read-only from an install location while the working directory is somewhere else.

Tier 1 — skill iteration, no install. Load one plugin directly into a session:

claude --plugin-dir /path/to/skills/<plugin>    # e.g. .../skills/benchmark

Nothing is installed and no marketplace state is touched. Best while editing SKILL.md or scripts; restart the session to pick up changes.

Tier 2 — full install simulation, before merging to main. Add your checkout as a local-path marketplace so you exercise exactly what users get (marketplace metadata, install, versioning, plugin-root resolution). In a Claude Code session in the consuming project:

/plugin marketplace add /path/to/your/skills-checkout
/plugin install benchmark@quantecon

Two things to know:

Switching back to production once the PR has merged:

/plugin marketplace remove quantecon
/plugin marketplace add QuantEcon/skills
/plugin install benchmark@quantecon

Confirm with /plugin marketplace list (the source should read QuantEcon/skills, not your local path) and /plugin list (the version should match the merged plugin.json). Routine setup and updating for end users is covered in Using QuantEcon skills.

The documentation site

These guides, the plugin guides, and the shared reference material are published to quantecon.github.io/skills by .github/workflows/docs.yml on every push to main. Preview it before you push:

npm install -g mystmd@1.10.1   # same version CI pins in docs.yml
myst start                     # live-reloading preview

Two things follow from how it is wired. Pages are rendered from the files where they already livemyst.yml points at benchmark/README.md and audit/references/*.md in place, never a copy — so editing a plugin’s docs updates the site, and no plugin loses documentation it needs to ship with it. And the sources have to stay readable on GitHub, since the repo view is the other half of the audience: prefer plain Markdown, and keep relative links relative, because CI checks that every one of them resolves.

Versioning and releases

The plugin is the released artifact and its version string is how a release is delivered, so a bump is a shipping decision rather than bookkeeping. Bump it in both <plugin>/.claude-plugin/plugin.json and the plugin’s marketplace.json entry — validate.py enforces that the two agree, and plugin.json is the one that wins at install time.

The changelog

Every plugin keeps its own <plugin>/CHANGELOG.md, and the version bump and its entry land in the same PR. qe/CHANGELOG.md is the worked example; the format and its reasons are stated in the file itself.

Why the file exists is worth stating honestly, because inside this repo it adds little: squash-merge already makes git log --oneline -- qe/ one clean line per PR, and the commit-subject convention already reads as a changelog. It earns its place on two grounds. The installed user has no git log — the cache is an extracted directory with no .git, no remote to query, and for someone in the VS Code extension no practical path to one — so a file shipped inside the plugin directory is the only way to tell them what changed between the version they have and the one they would get. That is also why it is per-plugin rather than at the repo root: self-contained plugins means an installed plugin ships only its own directory, so a root changelog is unreachable from the thing it describes, and a plugin’s changelog links out with absolute GitHub URLs only. And the entry is what makes the bump happen: a reviewer looking at a diff that touches qe/skills/… and carries no new version heading can see the omission, which is what #27 walked into.

Keep entries short and user-facing — what someone can now do, or what behaves differently. An entry that only restates the PR title is the right length for a small change; an entry that recapitulates the diff is not useful to anyone. Where the changelog and a PR title are word for word identical, that is fine: a changelog entry is frozen at release, so the two copies cannot drift, and they are aimed at different readers.

There is no Unreleased section. It would park the description of a change away from the bump that delivers it, which is the one coupling that has to hold, and squash-merge leaves nothing for it to hold anyway: one PR is one commit, one version, one entry. Dates are the date you open the PR — you cannot know the merge date while writing, and a day’s drift does not matter. Two PRs against the same plugin will conflict at the top of the file; that conflict is the mechanism, not a cost, since it is what stops both branches claiming the same version. Resolve it the ordinary way: rebase onto main and take the next version, in plugin.json, the marketplace.json entry, and the heading.

The CI guard

validate.yml runs scripts/check-version-bump.py on every pull request into main. If any file under <plugin>/ differs from the merge base, that plugin’s plugin.json version must differ too — and must not be one already published on main — and <plugin>/CHANGELOG.md must carry a heading naming it. Any Markdown heading containing the version satisfies the last check; the file’s format is not a CI contract.

It compares against the merge base and the base tip, so a branch that is behind main cannot land a version somebody else already shipped. It reads committed history only, so python scripts/check-version-bump.py run locally before you commit will say so rather than pretend to have checked your working tree. It needs full history (fetch-depth: 0) and exits 2 rather than 0 when it cannot see the base — a guard that silently passes when it cannot run manufactures confidence.

There is no override label. The escape hatch, on the rare occasion a bump feels disproportionate, is a patch bump: one line in two files, semantically honest, and unlike an exemption it actually delivers the change.

A second job runs claude plugin validate --strict against each plugin and the marketplace, using the runtime’s own parser. It is separate because it installs an npm toolchain and validate.py should stay fast, and it catches what a regex frontmatter reader cannot — real YAML errors, and unknown plugin.json keys. The CLI is pinned there deliberately: under --strict an upstream warning becomes an error, and an unpinned install would let someone else’s release fail an unrelated PR.

Tags

Each release is tagged {name}--v{version}, so three independently-versioned plugins share one tag namespace. Tag from a clean checkout of main after the release merges:

claude plugin tag ./<plugin> --push -m "<plugin> %s"

It takes the version from plugin.json, refuses unless the marketplace entry agrees, refuses if the plugin fails validation, and refuses on a dirty working tree — so a tag cannot point at a version you did not mean to release. --dry-run prints what it would do.

Nothing installs from a tag. The marketplace serves main, so the merge is still the release and the tag is a bookmark for human archaeology — which is also why only current versions were tagged when the scheme was adopted, rather than backfilling history. Several historical versions could not have been tagged honestly: some name two different published trees, because content shipped twice under one version before the guard existed, and others sit at commits that fail today’s validation. Each CHANGELOG.md records that ambiguity in prose, with PR numbers, which is more than a tag can carry.

PR flow