Course · Pi · Skills & packages
Package the harness and hand it to a teammate
Everything you’ve built for stash (the course’s running example, a small FastAPI feed reader) across the last two modules lives as separate
files: stash-tools.ts from your first extension, damage-control.ts,
tilldone.ts, plan-injector.ts, and statusline.ts from the rebuilt-defaults
module, and now add-feed-source/SKILL.md and triage-fetch.md from this one.
Count them - that’s seven files, spread across three directories: five in
.pi/extensions/, one in .pi/skills/, one in .pi/prompts/. That’s fine as long
as it’s just you working the codebase. It stops being fine the moment a teammate
clones stash and asks how to get the same setup: right now the honest answer is
“copy these seven files from these three directories,” which is the kind of
instruction that gets followed once and then quietly drifts.
A Pi package is the fix: the distribution unit that bundles Extensions, Skills,
Prompt Templates, and Themes together and ships them as one installable thing. It
ships over infrastructure that already exists - npm or git. There’s no bespoke Pi
registry to publish to: a package is just an npm package (or a git repo) that
happens to carry a pi key in its package.json. It doesn’t even need to, as it
turns out - watch what Pi finds with no manifest at all before you reach for one.
What convention finds, before you write a manifest
Section titled “What convention finds, before you write a manifest”Predict this before reading on: publish those seven files with no pi key anywhere
in package.json, just the files sitting in directories named extensions/,
skills/, and prompts/. How many does Pi actually pick up on install?
All seven. This is convention-based auto-discovery, and it runs whenever a
package skips the pi key entirely: Pi looks for an extensions/ directory and
takes every .ts/.js file inside it - your five extensions, matched. It looks
for a skills/ directory and takes every folder inside it that contains a
SKILL.md - your one Skill, matched, sitting in its own add-feed-source/ folder
exactly as the last lesson built it. It looks for a prompts/ directory and takes
every .md file inside it - triage-fetch.md, matched. There’s also a themes/
directory convention, scanned for .json files, and it would pick up
stash-dark.json too - if you chose to bundle it. This harness deliberately
doesn’t: a theme buys nothing the model needs, so there’s no reason to force your
color choices on a teammate who installs the rest. That’s why the count above
stayed seven.
Name the directories the way Pi expects, and the bundle needs no manifest at all.
The manifest: a pi key in package.json, when you need precision
Section titled “The manifest: a pi key in package.json, when you need precision”Convention works because this package happens to be single-purpose - every file in
extensions/ belongs to the stash harness, nothing else is mixed in. The moment
that stops being true - say extensions/ also holds an unrelated script, or your
skills live somewhere convention won’t find - you need to say so explicitly, in the package’s own package.json:
{ "name": "@you/stash-harness", "keywords": ["pi-package"], "pi": { "extensions": ["./extensions"], "skills": ["./skills"], "prompts": ["./prompts"], "themes": ["./themes"] }}The pi key is opt-in - its paths (which support globs and exclusions) tell Pi
exactly where to find each resource type inside the package, overriding whatever
convention would have guessed. For a package built around one coherent purpose,
like this harness, convention already gets the right answer; you’d reach for the
explicit pi key mainly to be precise about paths or to exclude something a bare
directory scan would wrongly sweep in.
The manifest buys precision; convention buys not needing one. Most single-purpose bundles, including this one, don’t need the manifest at all.
A separate detail, worth knowing but not part of that tradeoff:
keywords: ["pi-package"] is the one line that matters for discoverability - it’s
the tag that gets a package listed in the gallery and in npm keyword search. You
don’t need it for a private team package installed straight from git; it only
matters once you’re publishing for strangers to find.
Installing one
Section titled “Installing one”pi install npm:@foo/bar@1.0.0pi install git:github.com/user/repo@v1pi install https://github.com/user/repopi install ./relative/path/to/packagepi install /absolute/path/to/packageCompanion commands round out the lifecycle: pi remove, pi list, and pi update - bare pi update refreshes the pi CLI itself, --all also updates
packages and reconciles pinned git refs, and --extensions updates only the
installed packages, leaving the CLI alone (the flag’s name is narrower than its
scope: packages were the extension-distribution mechanism first, and the name
stuck). By default install and remove write to your user settings; -l writes
them to .pi/settings.json instead, which means the install line can be committed
with the repo - and once a project is trusted, Pi installs any missing packages
from it on startup. Commit the -l line and a teammate who trusts stash gets the
harness without even running the install themselves.
None of this is stash-specific machinery - it’s the same install surface for
anything in the ecosystem, which is exactly why packaging your own stash harness
this way means a teammate’s onboarding step becomes one command instead of a
conversation.
Once your harness is pushed to a git remote your teammate can reach, the whole thing installs in one line:
pi install git:github.com/your-org/stash-harness@v1That @v1 isn’t decorative. Drop it - pi install git:github.com/your-org/stash-harness
with no ref - and you get whatever commit happens to sit at the tip of the default
branch the moment the command runs. That’s also whatever commit is there the next
time a teammate installs, or the next time someone runs pi update. Those can be
two different commits, and this package’s extensions run with full system access -
the damage-control gate alone can read and write anything stash can touch.
Pinning to a tag freezes the exact code you reviewed; an unpinned install is a
standing bet that whoever controls that repo never ships something worse between
now and the next install. Review what you’re pinning to, not just that you
remembered to pin it.
What installing doesn’t give you: sync. pi install copies a package’s files
onto your machine at that moment - it’s not a live link back to the source. Push a
fix to stash-harness tomorrow and every already-installed copy is silently
behind, yours included, until someone runs pi update - which isn’t automatic
either. Two teammates who installed the same git: URL a week apart can be running
two different versions of your own damage-control rules without either of them
realizing it. For a harness carrying rules that gate writes to a real stash.db,
that’s worth saying out loud in your own onboarding doc: pin the version, and write
down that pi update is a step someone has to remember, because Pi won’t remember
it for you.
One honest wrinkle in the bundle itself: .pi/damage-control-rules.yaml isn’t an
extension, skill, prompt, or theme, so it isn’t one of the four resource types and
the package doesn’t carry it. That’s correct by accident - the file encodes
stash’s own paths (stash.db, .pi), which belong to the repo, not to a bundle.
The extension ships in the package; its rules stay home where the database is.
And because packages support per-resource filtering - the packages array in
settings takes an object form ("extensions": [] loads none of them), and
pi config toggles resources interactively - a teammate who wants your Skill and
template but not your particular damage-control rules can install the package and
switch off just the extensions piece.
Here’s how that bundling maps across five tools that ship a similar idea. Pi bundles all four resource types by design - that’s this whole lesson. The others reach the same goal by different roads, and not all of them get there at all. Before you click through: guess which one of the five bundles nothing whatsoever - no plugin format, every piece a loose file a team copies by hand, the exact “seven scattered files” problem this lesson just solved for Pi.
Cursor’s the outlier: skill, subagent, hook, MCP config, and command all ship
loose, because there’s no plugin format to bundle them into yet. If your team is
coordinating a shared setup there, you’re doing by hand - copy these files, tell
people where they go - exactly what a pi install does in one line here.
What’s actually out there
Section titled “What’s actually out there”Discovery starts at the package gallery -
pi.dev/packages, a searchable catalog of everything published with the
pi-package keyword: filter by name, description, or author; narrow by resource
type (extension, skill, theme, prompt); sort by downloads or recency. Each listing
shows a copyable pi install command plus links to the npm package and the source
repo, so “found it” and “can read the code” are one click apart. The gallery is a
catalog, not a registry - every entry is an npm package underneath, and getting
listed means npm publish with the keyword, not signing up for a Pi-specific
service. Below the gallery the rawer surfaces are npm’s pi-package keyword
search and the Pi community
on Discord -
then inspect source before installing, always.
A few packages worth knowing about for landscape, not as an endorsement to install
any of them blindly: a web-access bundle that adds search/fetch/GitHub-clone tools,
a subagent task-delegation package, MCP-related packages (Pi itself gained built-in MCP support in 0.99, referenced
back when you built your first extension, so check
what a package adds beyond that), and a cross-tool context/MCP plugin that also targets Claude Code and
Gemini CLI. The shape of that list is itself informative: on Pi, “add subagent
delegation” is just a pi install away, not a built-in feature you’re missing - a fact worth remembering, because the next module builds
delegation from first principles and you’ll want to compare.
The same caution, one more time
Section titled “The same caution, one more time”The docs put this plainly, and it applies to a package exactly as much as it
applied to a single Skill a lesson ago: a Pi package runs with full system
access, and its extensions execute arbitrary code. Installing someone’s
pi-package is installing their code onto your machine with the same reach Pi
itself has - no sandboxing layer specific to packages, and no version pin means no
guarantee the code you reviewed is the code that runs next time. Review before you
install, especially anything pulled straight from a git URL rather than a
name-checked npm publish. The convenience of pi install git:github.com/whoever/whatever
is real; so is the fact that it will run whatever’s in that repo, today and every
time it’s reinstalled after.
Which layer for which job
Section titled “Which layer for which job”Seven files, three directories, one command - the ratio promised at the start of this module holds. The four no-code layers line up cleanly by when they spend and who fires them: a Skill pays its frontmatter at startup and its body only on a matching request (you wrote the description; the model decides when it matches); a template pays nothing until you type it, with no model discretion at all; a theme never touches the window - it is rendering cost and human comfort, nothing the loop sees; and a package inherits all three of those price tags for whatever it bundles, costing nothing at install time beyond them - with one exception this page’s own bundle makes important: extensions. An installed package’s extension code loads at session start (its tool schemas join the window then, called or not, and the code runs with full reach - the security section’s whole point), so a bundle carrying extensions spends from the first session, not only when a job calls. Skills, templates, and themes hold to the nothing-until-fired rule, and every firing decision traces back to a name - yours, or a rule you wrote.
You’ve now got a stash harness that isn’t just yours anymore - it’s one
pi install away for anyone on the project. But notice what didn’t change across
any of the four rows: everything still loads into one window. Yours. A Skill’s
body, a template’s expansion, a whole package’s bundle - wherever they come from,
they all land in the same context you’ve been managing since the core
chapter first showed you the window. That’s fine
for every job you’ve handed it so far. It stops being fine the day the job itself
stops fitting in one window - and that day arrives in the next module, delivered
by email.
Next: build subagents from first principles.