The agent13 of 15
Skills
On this page
A skill is a Markdown file. Not code, not a plugin — a document she reads when the job matches it.
Two tools in the skills toolset: skill_list and skill_view. Both are read risk and both are
offered in a conversation as well as in work mode.
The three that ship
Read live from the repo:
| Name | requires_toolsets | What it is |
|---|---|---|
research | (none) | Plan sub-questions, search several angles, prefer authoritative sources, synthesise, and produce a .md report with citations |
operating-websites | browser | Drive a real website end-to-end — log in, search, navigate, fill forms |
discord-server | discord | Reading a conversation back, remembering people, and what she cannot do in a guild |
research.md is about 10,500 characters. A skill is allowed to be long, because it is loaded only
when it is needed.
Where they live
skills_dir() resolves in this order:
KOTOBA_SKILLS_DIR, if set<repo>/soul/skills— from a clone<package>/data/soul/skills— the read-only copy inside the wheel
From a wheel, position 2 becomes ~/.kotoba/soul/skills, so a skill you drop there wins over the
packaged one.
The frontmatter
Optional YAML at the top of the file:
---
name: operating-websites
description: Drive a real website end-to-end — log in, search, navigate, fill forms — with the browser tools.
when_to_use: any multi-step task inside a real website (Facebook, Gmail, a dashboard, a store)
requires_toolsets: [browser]
---
# Operating websites
…the body…
| Key | Effect |
|---|---|
name | The name skill_view takes, and the title in the list. Defaults to the filename stem. |
description | The one line she sees when scanning the list. Falls back to the first paragraph after the first # heading. |
requires_toolsets | A list (or a single string). The skill is eligible only when every named toolset is active. Empty or absent means always eligible. |
when_to_use | Read by humans. Nothing in the code parses it. |
No frontmatter at all still works: the title comes from the first # heading and the description
from the first non-heading line under it.
Broken YAML degrades to no frontmatter rather than failing.
requires_toolsets is also accepted nested under metadata.hermes.requires_toolsets, for
compatibility with that convention.
Measured:
eligible with no toolsets active: ['research']
eligible with browser + discord: ['discord-server', 'operating-websites', 'research']
How she picks one
Two different mechanisms, one per mode.
In a conversation, the system prompt lists name — description for every skill, with no
toolset filtering, under a rule about balance:
"for everyday things you already know (chat, a quick search, simple math, a basic page) just ACT — don't call
skill_viewas 'preparation' and don't loop on it … BUT if a skill matches, load it ONCE withskill_view(name)before you start. Load at most one, once, then act."
In work mode, the job is given a filtered list — only skills whose required toolsets are actually active — under an explicit instruction:
"SKILLS (playbooks you can load) — this list is your STEP ZERO. Scan it; if one matches the job, load that ONE with
skill_view(name)first and follow what it says. If none matches, there is no step zero: act straight away. Never a second skill, never a re-read."
Why "step zero" is named at all
This block and work mode's BIAS TO ACTION rule contradicted each other for months. One asked for
the skill; the other called a skill_view a forbidden "preparation" step. The prohibition won every
run.
What went with it was everything a skill knows that nothing else does. A Spanish research request came back as an English report, because the language rule lives in the one page the job was told not to open.
So both rules now name each other:
"BIAS TO ACTION, from step zero onward … These two are one rule with an entry point: step zero is a single read that ends the moment it returns, and everything past it acts. Each covers a failure the other cannot — do not delete either one to satisfy the other."
The cost is one extra model call, plus the skill's own text as input, before the first real action.
skill_list
No arguments. Returns - name: description per skill. Unfiltered — it lists everything in the
directory, regardless of which toolsets are active.
Returns nothing when the directory is empty, and she says "I don't have any skills loaded right now."
skill_view
Takes: name. Returns the file's full text, frontmatter included.
The name is slugified and jailed: lowercased, non-alphanumerics collapsed to -, then resolved
against the skills directory and checked to be inside it. Measured — skill_view("Research") works,
skill_view("../../etc/passwd") returns None.
A name that resolves to nothing is a plain failure and she says "I don't have a skill by that name yet."
Adding your own
Drop a .md file into the skills directory. There is nothing to register and nothing to restart —
list_skills() reads the directory on every call.
Three things worth knowing before you write one:
- Give it frontmatter. The fallback title-and-first-paragraph convention works, but a
descriptionis what she scans, and it is the only thing standing between your skill and being ignored. - Use
requires_toolsetsif it depends on tools. A browser playbook offered to a job with no browser is guidance she cannot follow. - The filename is the address.
skill_viewslugifies whatever it is given and looks for<slug>.md, so name the file the way you want it called.
A note on language: research.md is currently the only skill in the tree that carries a rule
about which language the deliverable is written in. Every non-research job would lose that rule if
the language note were removed from the work brief, which is why it is stated in both places.
