skills: Add project-specific skill library.

This commit is contained in:
2026-07-05 15:16:53 -04:00
parent 47241b8421
commit b233fe41e7
6 changed files with 363 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
---
name: nbdev
description: nbdev conventions for notebooks — directives, cell structure, docments, tests, execution. Use for any .ipynb operation — including reads — in an nbdev project.
---
# nbdev
## Tool Preference
- Use the **Jupyter MCP** for all `.ipynb` operations — read, edit, insert, delete, execute
- Do **not** use the built-in `NotebookEdit` tool; it writes cell source as a single JSON string which breaks standard Jupyter formatting and produces noisy diffs
- Re-read the notebook before editing if it may have changed since your last read — cell indices/IDs can shift under concurrent edits (e.g. via JupyterLab's real-time collaboration), and editing by a stale index can hit the wrong cell
## nbdev Directives
Directives are comments at the top of a cell that control how nbdev processes it:
- `#| export` — include this cell in the exported Python module and in the docs
- `#| hide` — exclude this cell from both the module and the docs
- `#| hide_input` — show cell output in docs but hide the source code
- `#| default_exp module_name` — set which module this notebook exports to (second cell)
- `#| exporti` — export to module but do not show in docs (for internal helpers)
- `#| eval: false` — include in docs but do not execute during `nbdev-test`
Imports needed only for tests or examples should **not** be exported.
Never hand-edit the exported `.py` module files — they're build artifacts regenerated from the notebook by `nbdev_export`. All edits go through the source notebook in `nbs/`.
## Notebook Structure
Every notebook must follow this structure:
**Cell 1 — Markdown frontmatter:**
```markdown
# Module Title
> A one-line description of what this module does
```
The H1 becomes the page title in docs. The blockquote becomes the subtitle.
**Cell 2 — Default export:**
```python
#| default_exp module_name
```
**Body cells** — alternating between exported code, demonstrations, and markdown explanations (see Cell Structure below).
**Last cell:**
```python
#| hide
import nbdev; nbdev.nbdev_export()
```
Before declaring any notebook task complete, restart the kernel and run all cells top-to-bottom to verify it is fully reproducible.
## Cell Structure
Keep cells short. Each exported function gets its own cell, immediately followed by a demonstration. Do not write long functions with comments interspersed — split them into small separate cells with explanations and working examples after each.
The pattern per concept:
1. *(Optional)* A markdown cell explaining what comes next
2. A `#| export` code cell with the function
3. One or more plain code cells demonstrating usage
4. Assertions that double as tests
Example:
```python
#| export
def slugify(text: str) -> str:
"Convert text to a URL-safe slug"
return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
```
```python
slug = slugify("Hello, World!")
assert slug == "hello-world"
slug
```
## Docstrings and Parameter Documentation
Keep docstrings short — a single-line summary is sufficient for most functions. Elaborate in separate markdown or code cells below, where you can use real examples.
Use **docments** (inline parameter comments) instead of verbose docstring parameter sections:
```python
#| export
def greet(
name: str, # Person to greet
greeting: str="Hi", # Greeting word to use
) -> str: # The composed greeting
"Compose a greeting for name"
return f"{greeting}, {name}!"
```
This renders as a clean parameter table in the docs automatically — no need to repeat type information in the docstring body.
Use backticks around symbol names in docstrings and markdown — nbdev automatically converts these to hyperlinks to the relevant reference page.
## Code Style
- **Prefer composition**: write small functions that do one thing well
- Each exported function should be focused enough to fit naturally in a single notebook cell — one cell, one idea
- Use type hints on all exported functions
- Avoid classes unless state is genuinely needed — prefer functions that take and return data
- If you do write a class, use `fastcore`'s `@patch` decorator to define each method in its own cell, immediately followed by a demonstration. This avoids long class definitions and keeps examples close to the code
When a class is needed, document its methods with `show_doc`:
```python
from nbdev.showdoc import show_doc
show_doc(MyClass.my_method)
```
## Tests
Every code cell is run as a test by nbdev unless explicitly marked otherwise — any exception fails the test.
- Turn demonstrations into tests by adding `assert` statements
- Use `fastcore.test` helpers for better error messages:
```python
from fastcore.test import test_eq, test_fail
test_eq(slugify("Hello World"), "hello-world")
```
- Document expected error cases with `test_fail`:
```python
test_fail(lambda: slugify(""), contains="empty")
```
- Each test/demo cell should import what it needs directly — don't rely on a name imported in a later cell just because it happened to be in scope during a prior run
## Execution
- Always execute cells after writing them to verify they work
- If a cell errors, read the full traceback before attempting a fix — do not guess
- When installing packages, use `%pip install` inside the notebook (not `!pip install`) so they install into the running kernel
- Use autoreload at the top of notebooks that import from other modules in the project:
```python
%load_ext autoreload
%autoreload 2
```
## Documentation
- Use H2 (`##`) markdown cells to group related symbols within a notebook
- Use H4 (`####`) markdown cells to split long explanations within a symbol's section (notes, examples, edge cases, etc.)
- Add rich representations to classes via `_repr_markdown_` where it aids understanding
- Include real code examples, plots, and diagrams — notebooks support rich output, use it
## Outputs
- Never print secrets, tokens, passwords, or API keys into cell output — notebook outputs get committed to git and published in docs, unlike transient script output
- Prefer summaries over dumping large data structures (`.head()`, `len()`, `[:5]`, etc.)
- Large outputs consume context window — keep them concise

View File

@@ -0,0 +1,37 @@
---
name: remove-skills
description: Remove one or more previously added library skills from the current project.
disable-model-invocation: true
---
Removes a skill that [`setup-skills`](../setup-skills/SKILL.md) previously
copied into the current project, deleting both its files and its entry in
`.claude/skills-lock.yaml` (see [LOCKFILE.md](../setup-skills/LOCKFILE.md)
for its schema).
## Steps
1. Read `.claude/skills-lock.yaml`. If it doesn't exist or is empty, tell
the user there's nothing installed to remove and stop.
2. Determine which skill(s) to remove:
- If the user's invocation already named a specific skill, use that —
if it isn't in the lockfile, say so and stop.
- Otherwise, list every skill currently in the lockfile and ask the
user to pick one (or more).
3. For each skill to remove, compute its current hash
(`~/.claude/skills/setup-skills/hash-dir.sh .claude/skills/<name>`)
and compare it to the hash stored in the lockfile:
- If it matches (never modified since it was installed), delete
`.claude/skills/<name>/` and remove its lockfile entry immediately —
no confirmation needed, since nothing of the user's is being lost.
- If it differs (locally customized), tell the user it has local
changes that will be permanently lost and ask for confirmation
before deleting. If they decline, leave that skill installed and
move on to the next.
4. Finish with a summary of what was removed and what was left in place.
Done when every skill to remove has been either deleted (with its lockfile
entry removed) or explicitly left in place with a stated reason.

View File

@@ -0,0 +1,53 @@
# Skills Lockfile
`.claude/skills-lock.yaml`, at the root of a project, tracks which library
skills (from `~/.claude/skills/library/`) have been copied into that
project's `.claude/skills/`, so [`setup-skills`](SKILL.md),
[`update-skills`](../update-skills/SKILL.md), and
[`remove-skills`](../remove-skills/SKILL.md) all agree on what's installed
without re-deriving it from the filesystem.
## Schema
A YAML list of entries, one per installed skill:
```yaml
- name: nbdev
hash: 3f2a9b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a
- name: terraform-conventions
hash: 9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a3f2a9b
```
- `name` — matches both the skill's directory name in the library
(`skills/library/<name>`) and its copied directory name in the project
(`.claude/skills/<name>`).
- `hash` — the output of `hash-dir.sh` run against that one skill's
directory contents, recorded at the moment it was last copied or
confirmed up to date. Never a hash of anything else — not the whole
project, not the whole library, just that one skill's own directory
tree.
## What a mismatch means
To classify a skill's state, compare three values: the lockfile's stored
`hash`, `hash-dir.sh` on the project's current copy
(`.claude/skills/<name>`), and `hash-dir.sh` on the library's current
source (`~/.claude/skills/library/<name>`).
| stored vs. project copy | stored vs. library source | meaning |
|--------------------------|----------------------------|--------------------------------------|
| match | match | nothing to do |
| match | differs | library moved on — safe to update |
| differs | match | project customized on purpose — leave it |
| differs | differs | conflict — report, don't touch |
## Writing to the lockfile
- Adding a skill: append a new `{name, hash}` entry.
- Applying a safe update: overwrite that entry's `hash` in place with the
library's current hash.
- Removing a skill: delete its entry entirely.
Never reorder or restructure existing entries beyond what an add, update,
or remove requires — this file is meant to diff cleanly in a project's
git history.

View File

@@ -0,0 +1,46 @@
---
name: setup-skills
description: Add relevant skills from the shared skills library to the current project.
disable-model-invocation: true
---
Adds opt-in, project-specific skills from `~/.claude/skills/library/` into
the current project's `.claude/skills/`, tracked in
`.claude/skills-lock.yaml` (see [LOCKFILE.md](LOCKFILE.md) for its schema).
Only ever adds — checking already-installed skills for updates is
[`update-skills`](../update-skills/SKILL.md)'s job, not this one's.
## Steps
1. Read `.claude/skills-lock.yaml` in the current project, if it exists.
Note every skill name already listed — these are already installed and
must not be re-proposed.
2. List every skill under `~/.claude/skills/library/*/SKILL.md` and read
each one's `name` and `description`.
3. Inspect the current project (file tree, manifests like
`pyproject.toml`/`package.json`, file extensions present, etc.) and
judge which library skills — excluding ones already installed — seem
relevant, the same way you'd reason about any unfamiliar codebase.
Propose that shortlist to the user with your reasoning, one line per
skill. If the user asks to see the full catalog instead, list every
library skill (minus already-installed ones) with its description.
4. Let the user confirm, adjust, or pick freely from the full list.
5. For each confirmed skill:
- If `.claude/skills/<name>/` already exists in the project and is
*not* in the lockfile, skip it and tell the user why (a same-named
skill already lives there and isn't tracked — remove or rename it
first if they want the library version).
- Otherwise, copy `~/.claude/skills/library/<name>/` to
`.claude/skills/<name>/` in the project, run
`~/.claude/skills/setup-skills/hash-dir.sh .claude/skills/<name>`,
and append `{name, hash: <output>}` to `.claude/skills-lock.yaml`
(create the file, an empty YAML list, if it doesn't exist yet).
6. Report what was added and what was skipped, and why.
Done when every confirmed skill is either copied and recorded in the
lockfile, or explicitly skipped with a stated reason.

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Deterministic recursive hash of a directory's file contents.
#
# Hashes relative paths, not absolute ones, so two directories with
# identical contents hash identically regardless of where they live on
# disk (needed to compare a project's copied skill against the library
# source it was copied from).
#
# Usage: hash-dir.sh <directory>
set -euo pipefail
if [ $# -ne 1 ]; then
echo "Usage: hash-dir.sh <directory>" >&2
exit 1
fi
dir="$1"
if [ ! -d "$dir" ]; then
echo "Not a directory: $dir" >&2
exit 1
fi
(cd "$dir" && find . -type f -print0 | sort -z | xargs -0 -r sha256sum) | sha256sum | awk '{print $1}'

View File

@@ -0,0 +1,52 @@
---
name: update-skills
description: Check the current project's installed library skills for upstream changes and apply the safe ones.
disable-model-invocation: true
---
Compares every skill listed in the current project's
`.claude/skills-lock.yaml` (see [LOCKFILE.md](../setup-skills/LOCKFILE.md)
for its schema) against both the project's own copy and the current
library source, and decides what to do about each one. Never installs a
skill that isn't already there — that's
[`setup-skills`](../setup-skills/SKILL.md)'s job.
## Steps
1. Read `.claude/skills-lock.yaml`. If it doesn't exist or is empty, tell
the user there's nothing to check and stop.
2. For each `{name, hash}` entry, compute:
- `project_hash`: `~/.claude/skills/setup-skills/hash-dir.sh .claude/skills/<name>`
- `library_hash`: `~/.claude/skills/setup-skills/hash-dir.sh ~/.claude/skills/library/<name>`
If either path is missing entirely, report that anomaly for this skill
(don't try to classify it) and move on to the next entry.
3. Classify each entry against the table in
[LOCKFILE.md](../setup-skills/LOCKFILE.md#what-a-mismatch-means),
using `project_hash` in place of "project copy" and `library_hash` in
place of "library source". The two outcomes that need action below are
**safe update** (stored matches project, differs from library) and
**conflict** (stored differs from both). "Locally customized" needs no
message beyond the summary.
4. If there are any safe updates, list them by name and ask for one
confirmation to apply all of them — unless the user's invocation
already included an explicit go-ahead argument (e.g. `-y`, `yes`), in
which case apply them without asking. Applying means: delete
`.claude/skills/<name>/` entirely and copy
`~/.claude/skills/library/<name>/` in its place, so no file the project
copy had but the library no longer has can survive — then recompute its
hash and overwrite that entry's `hash` in `.claude/skills-lock.yaml` in
place.
5. For every conflict, report it and show a recursive diff between the
project's copy and the library's current version
(`diff -ru .claude/skills/<name> ~/.claude/skills/library/<name>`).
Do not modify the project's copy or the lockfile entry for a
conflicted skill under any circumstances — surfacing it is the whole
job here.
6. Finish with a summary: updated, left alone (customized), conflicted,
already current, and any anomalies from step 2.