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:

  1. Rank. werbos.skills.discovery.rank_skills does a Jaccard match of the user-message keywords against each enabled skill’s triggers list.
  2. Read. Top-3 matches’ bodies get read from disk; SHA-256 is verified.
  3. Inject. SkillProvider.inject_for returns an InjectionResult(prompt_block, fired, matches). The runtime appends prompt_block as a SystemMessage AFTER the persona + project_context + user.md, but BEFORE history.
  4. Bump. Each fired skill’s invocations counter 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:

ModeBehavior
offAuto-curator stays silent. Manually-created skills still fire.
proposeAuto-curator finds candidates and lists them; writes nothing.
autoAuto-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_archives walks ~/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_candidates creates new skills from candidates if the persisted mode is auto; otherwise prints what it would do. The body’s first draft is a literal “see triggers” placeholder — you fill it in.
  • Purge. apply_purges finds stale skills (zero invocations + age > --stale-days, default 30) and deletes them in auto mode.

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

ConceptWhat it isLifetime
ToolOne verb. read_file(path). Atomic. Defined in Python.Always available; gated by --tools
SkillMarkdown body injected per-turn when triggers match. Workflow + knowledge.Auto-fires per-message
PersonaThe voice/character of the session.Whole session
user.mdWho 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.