Skills
Self-modifying skill registry. SQLite-backed metadata + markdown bodies on disk + auto-curator. The system werbos uses to inject relevant knowledge into the system prompt without you having to re-explain it every session.
A skill is a markdown file with a body the runtime injects into the
system prompt when the user’s message matches its triggers. Skills
fire automatically — the model doesn’t choose them, the
SkillProvider ranks them per turn, reads the top matches, and
appends them to the prompt as a SystemMessage after the persona.
Sprint Sk.1 (closed 2026-05-09) shipped this end-to-end: SQLite store, on-disk bodies, three-way autonomy mode, a detector that clusters chat-archive patterns, and an auto-curator that creates + purges skills.
Where skills live
~/werbos/skills.db # SQLite — metadata + integrity hashes
~/werbos/skills/<name>.md # markdown body for each skill
The store carries one row per skill with name, description,
triggers, body_sha256, enabled, invocations, plus the
multi-tenant tenant_id / user_id columns that mirror the
tasks/budgets stores. The body file’s SHA-256 is checked on every
read; mismatched bodies raise SkillFileError so a tampered file
never gets silently injected.
Skill name rules
Tight slug constraints — these matter because the name maps directly to a filename without sanitization:
- lowercase ASCII letters, digits, hyphen, underscore
- 1-64 characters
- first character must be a letter
A skill named ../etc/passwd cannot reach a path concat. The check
runs at create time.
Skill file format
<!--
name: code-review
description: Review the diff on the current branch against quality + security standards.
triggers: [code review, gut check, pre-merge audit, look at my diff]
created: 2026-05-09
-->
When the user is asking for a code review, prefer:
1. Walk the diff scope (current branch vs main) before commenting on
any single file.
2. Name security issues first, performance issues second, style last.
3. Cite specific line numbers; do not make claims you can't point at.
4. If you would change the design rather than the code, say so
explicitly.
The HTML-comment block is human-readable metadata; the runtime stores the same fields as columns in the SQLite row at create time. The body — everything after the comment — is what gets injected.
Skills are at most ~2-3 KB of body text. The SkillProvider reads
the top-N matches per turn (default 3); beyond that the system
prompt grows linearly without proportionate quality benefit.
How skills fire
Per-turn flow inside ChatSession.step:
- Rank.
werbos.skills.discovery.rank_skillsdoes a Jaccard match of the user-message keywords against each enabled skill’striggerslist. - Read. Top-3 matches’ bodies get read from disk; SHA-256 is verified.
- Inject.
SkillProvider.inject_forreturns anInjectionResult(prompt_block, fired, matches). The runtime appendsprompt_blockas a SystemMessage AFTER the persona + project_context + user.md, but BEFORE history. - Bump. Each fired skill’s
invocationscounter increments — data the auto-curator uses to identify stale skills.
The model doesn’t see the ranking — it sees one SystemMessage with the relevant skill bodies concatenated. This is intentional: the runtime owns the routing, not the model.
The 3-way autonomy mode
Skills have an operator-controlled mode persisted in the store:
| Mode | Behavior |
|---|---|
off | Auto-curator stays silent. Manually-created skills still fire. |
propose | Auto-curator finds candidates and lists them; writes nothing. |
auto | Auto-curator creates new skills + purges stale ones. Loud: every flip emits a warden Alert. |
Set the mode via the /skill slash inside the REPL:
werbos> /skill # show current mode + list enabled skills
werbos> /skill mode propose # change mode
werbos> /skill list # all skills with status
werbos> /skill show code-review # print the body
werbos> /skill enable foo # toggle enabled flag
werbos> /skill disable foo
auto is loud by design. Silent automation is the failure mode for
a system that can delete the operator’s own skills — the
LoggingAlertSink default ensures every mode flip lands somewhere
visible.
The detector + auto-curator (Sk.1 day 3)
werbos skills scan
Two passes:
- Detect.
werbos.skills.detector.detect_from_archiveswalks~/werbos/chats/*.pfm, extracts user-message keywords, and clusters them by Jaccard similarity. Clusters above the size threshold (default 3) become candidate skills with proposed names + triggers. - Curate.
werbos.skills.autocurator.AutoCurator.apply_candidatescreates new skills from candidates if the persisted mode isauto; otherwise prints what it would do. The body’s first draft is a literal “see triggers” placeholder — you fill it in. - Purge.
apply_purgesfinds stale skills (zero invocations + age >--stale-days, default 30) and deletes them inautomode.
Flags worth knowing:
werbos skills scan --apply # bypass propose, write right away
werbos skills scan --no-write # dry-run even in auto mode
werbos skills scan --include-purges # also surface stale candidates
werbos skills scan --jaccard 0.5 # tighter cluster similarity
werbos skills scan --min-cluster-size 5 # require more evidence
werbos skills scan --stale-days 60 # broader purge window
werbos skills scan --archive-dir PATH # custom chat archive root
The future shape is a scheduled background sweep inside werbos serve
once the curator’s accuracy is calibrated. Today the operator runs
the scan when they want it.
Writing your own
The fastest path:
mkdir -p ~/werbos/skills
cat > ~/werbos/skills/standup.md <<'EOF'
<!--
name: standup
description: Generate a one-paragraph summary of yesterday's chat archives.
triggers: [standup, daily summary, what did i do yesterday]
created: 2026-05-09
-->
When the user asks for a standup, prefer:
1. List yesterday's PFM archives from ~/werbos/chats/ (file mtime).
2. For each one, extract the title + a one-line outcome from the
final assistant message.
3. Format as:
> Yesterday: <N> chats. Highlights:
> - <one-line per labeled-good archive>
> Lowlights:
> - <one-line per labeled-bad archive>
4. End with one TODO based on patterns you noticed.
EOF
Then register it in the store via Python (no werbos skills create
CLI yet):
from werbos.skills import SkillStore
from pathlib import Path
store = SkillStore(Path.home() / "werbos" / "skills.db")
store.add(
name="standup",
description="Generate a one-paragraph summary of yesterday's chat archives.",
body=(Path.home() / "werbos" / "skills" / "standup.md").read_text(),
triggers=["standup", "daily summary", "what did i do yesterday"],
source="manual",
)
source="manual" distinguishes operator-authored skills from
auto-curator output. Manual skills fire regardless of mode (you
wrote them; you ARE the authority).
Skill vs persona vs tool
| Concept | What it is | Lifetime |
|---|---|---|
| Tool | One verb. read_file(path). Atomic. Defined in Python. | Always available; gated by --tools |
| Skill | Markdown body injected per-turn when triggers match. Workflow + knowledge. | Auto-fires per-message |
| Persona | The voice/character of the session. | Whole session |
| user.md | Who the operator is. | Whole session |
Skills are written by the user (or the auto-curator). Tools are written in Python. Personas are voice-layer system prompts. user.md is the operator’s profile. They compose: persona + project_context + user.md + skills + history = the full system prompt.
Multi-tenant note
Every row carries tenant_id + user_id (defaults default /
local). Same shape as the tasks store. Single-user mode today; the
gatekeep multi-tenant seam is baked in so a hosted SKU is a
default-value change, not a refactor.
See also
- Personas — the voice layer that runs every turn; skills append AFTER personas in the system prompt.
- Chat surfaces — every slash inside the
REPL, including
/skill. - How to nudge — chat-archive labeling, which feeds the detector’s cluster signal.