Neo Skills — self-learning loop
Skills are markdown "playbooks" (SKILL.md) that a sub-agent runs under — scoped instructions, an optional tool allowlist, and an optional model override. This doc covers the skill lifecycle: where skills come from, how a draft becomes a live, model-visible skill, and how to inspect the pipeline from the CLI. For the loader contract itself (frontmatter fields, source tiers, body expansion) see src/skills/types.ts and src/skills/loader.ts.
The three ways a skill is born
| Path | Who authors it | Where it lands first | Provenance tag |
|---|---|---|---|
| Hand-authored | The owner (or a template pack) | Directly into ~/.neo/skills/<name>/ (or templates/skills/ for bundled packs) | owner |
| Dream-pass distillation | The daemon, unattended, from repeated telemetry patterns | ~/.neo/skills-candidates/<name>/ | agent |
| Agent-drafted (on request) | The agent itself, mid-conversation, via the skill-manager tool | ~/.neo/skills-candidates/<name>/ | agent |
Only the first path writes directly to a live location. Both machine-authored paths land as a candidate — never live — and require the same explicit owner approval step before they can run. This is a hard invariant, not a convenience default: a skill's body becomes a sub-agent's system prompt, so an unreviewed machine-authored skill is an unreviewed system prompt.
Provenance
Every skill candidate's SKILL.md frontmatter can carry a provenance field (src/skills/provenance.ts):
owner— hand-authored or hand-edited by the owner.agent— drafted or refined by the agent, whether via the on-requestskill-managertool or the unattended dream-pass distiller. Both are machine-authored with no owner keystroke in the loop, so they share one value; if a future need arises to tell "dreamed" apart from "on-request", extend the union then.ported— translated from an external reference (e.g. an OpenClaw/Hermes skill brought intotemplates/skills/). Reserved for that use; not currently written by any Wave C code.
Provenance is informational, not a security boundary — every candidate, regardless of provenance, still requires the same explicit approval step before it can ever run. neo skills list surfaces it (see below) purely so the owner can see "this was machine-drafted" vs. "I (or a template pack) put this here" while reviewing the queue. A candidate with no provenance key at all (e.g. dropped in by hand before this field existed) shows as unknown — never guess a default that implies more trust than is warranted.
End-to-end loop
┌──────────────────┐ ┌──────────────────┐ ┌───────────────────────┐
│ Dream-pass │ │ Agent-drafted │ │ Hand-authored │
│ distillation │ │ (skill-manager │ │ (owner writes │
│ (daemon job, │ │ tool, mid- │ │ SKILL.md directly, │
│ repeated-pattern │ │ conversation) │ │ or a template pack) │
│ telemetry scan) │ │ │ │ │
└─────────┬─────────┘ └─────────┬─────────┘ └───────────┬───────────┘
│ provenance: agent │ provenance: agent │ provenance: owner
▼ ▼ │
~/.neo/skills-candidates/<name>/SKILL.md │
│ │
│ neo skills list (see provenance + description)│
│ neo skills show <name> (read the full draft) │
│ neo skills approve <name> --yes │
▼ ▼
~/.neo/skills/<name>/SKILL.md ◄────────────────────────┘
│
│ picked up by loadSkills() on next
│ process start / registry refresh
▼
Live — visible to the model via the `skill` tool,
shown in `neo skills active` (source: user)1. Dream-pass distillation (unattended)
A daemon job (memory/distillation/skill-distiller.ts) periodically scans telemetry for repeated multi-step patterns and proposes a candidate SKILL.md describing the reusable procedure. This never touches a live skill directory — it writes only into ~/.neo/skills-candidates/<name>/, tagged provenance: agent.
2. Agent-drafted, on request (skill-manager tool)
When the owner asks mid-conversation ("turn what we just did into a skill", "write a skill for X"), the agent can draft one itself via the skill-manager tool (src/tools/skill-manager.ts, Wave C chip 3). Two actions:
draft— create a brand-new candidate. Fails with a clear conflict error if a candidate or live skill of that name already exists.refine— write an updated draft for an existing candidate or an existing live skill. Refining a live skill does not touch the live file — it writes a new candidate (<name>-refinement/) with arefines: <name>frontmatter field, so the live skill is untouched until the owner explicitly approves the refinement.
Every write goes through a security scan (size ceilings, name-charset validation, content scan) before landing — defense-in-depth on top of the "never writes live" invariant, not a substitute for it. Both actions tag the result provenance: agent.
3. Review and approval — the only path to live
Regardless of origin, a candidate only ever becomes live through:
neo skills list # see everything pending review, with provenance
neo skills show <name> # read the full draft (frontmatter + body)
neo skills approve <name> --yes # move it into ~/.neo/skills/<name>/
neo skills reject <name> # or: delete it permanently--yes is the confirmation — there's no interactive TTY prompt, so this is safe to script but never accidental (omitting it still validates the candidate and tells you to review with neo skills show first). Approving clears this process's skill-loader cache; a running daemon or a different session picks up the new skill on its own next restart or registry refresh — not instantly, and not automatically. There is currently no cross-process hot-reload for skills.
4. Live-skill management
Once live, a skill is managed with a second, disjoint set of verbs — these operate on loadSkills()'s actual disk-scanned output, not the review queue:
neo skills active # list everything loadSkills() would surface,
# including disabled skills (shown, not hidden)
neo skills disable <name> # reversible — filters the skill out of
# loadSkills() entirely; costs zero tokens
neo skills enable <name> # undo a disable
neo skills remove <name> --yes # destructive — deletes a user-scope skill
# directory; refuses project/plugin/built-inPrefer disable over remove whenever the goal is just "stop sending this to the model" — it's reversible, remove is not, and only user-scope skills can be removed at all (built-in skills have no on-disk directory to delete; project/plugin skills belong to their own tier).
Discovering skills
neo skills active is today's discovery surface: it lists every live skill with its source tier (built-in / user / plugin / project), description, when_to_use guidance, and disabled state — a single grep-friendly table covering everything the model can currently reach via the skill tool.
Neo's ToolSearch mechanism (src/tools/tool-search.ts) is a separate, tool-only index — it operates strictly over Tool objects (name + description + JSON Schema) for deferred-tool schema loading, and has no notion of a Skill (skills carry no JSON Schema, no shouldDefer flag — they're a different registry entirely, see src/skills/registry.ts). Wave C scoped skill discovery down to the CLI listing above rather than threading skills through ToolSearch's tool-shaped machinery. Follow-on candidate: either wrap skills as lightweight Tool-shaped entries purely for search purposes, or give the skill registry its own keyword-search helper mirroring tool-search.ts's scoring logic — neither was attempted this wave; flagged here rather than half-built.
See also
src/skills/types.ts—Skillshape, frontmatter schema, source tiers.src/skills/loader.ts— howSKILL.mdfiles are parsed and merged (last-wins by tier: built-in < user < plugin < project).src/skills/provenance.ts— the provenance tag semantics in full.src/actions/skills.ts— the action layer behind everyneo skills *command (candidate review + live-skill management).src/tools/skill-manager.ts— the agent-facing draft/refine tool.docs/routing.md— how Neo decides self vs. delegate vs. skill vs. team for an incoming request (skills sit inside the "self" branch, invoked via theskilltool).