Layered sources
Skills layer cleanly across system / user / project sources. Pass a
list of paths to skills=; later sources override earlier ones by
skill name.
from loomflow import Agent
agent = Agent(
"...",
model="claude-opus-4-7",
skills=[
"~/.jeeves/skills/system/", # base layer
"~/.jeeves/skills/user/", # user customizes
"./.jeeves-skills/", # project-local override
],
)If ~/.jeeves/skills/system/git-discipline/SKILL.md defines a
git-discipline skill, and ./.jeeves-skills/git-discipline/SKILL.md
also defines one with the same name, the project’s wins. The system
version is shadowed entirely. Body, bundled files, and any tools.
Labelled sources
Any source can be tagged with a human-friendly label by passing a
(path, label) tuple. The label shows up in skill listings and tool
prefixes:
agent = Agent(
skills=[
("~/.jeeves/skills/system/", "System"),
("~/.jeeves/skills/user/", "User"),
("./.jeeves-skills/", "Project"),
],
)Auto-namespacing prevents collisions
Tool names get prefixed with the skill name automatically:
| Skill ships | Registered as |
|---|---|
add (Mode C, in calc skill) | calc__add |
say_hi (Mode B, in greeter skill) | greeter__say_hi |
search (in two skills a and b) | a__search and b__search. No clash |
The double underscore (__) is the namespace separator. Inside the
skill, you write the un-prefixed name; the prefix is added at
registration.
Source order matters
The framework loads sources left-to-right. For each source, every
discovered skill (one directory per skill, plus inline Skill
instances) is added to the registry. When a name collides with a
previously-loaded skill, the new one replaces the old.
This means:
- First-loaded source = lowest priority (gets overridden).
- Last-loaded source = highest priority (wins on collision).
Standard convention:
system/. Defaults shipped with the framework / your platform.user/. What the operator customizes.project/. What the codebase pins.
Inline skills mix freely
You can combine path-based sources with inline Skill instances in
the same list:
from loomflow import Agent
from loomflow.skills import Skill
agent = Agent(
"...",
model="claude-opus-4-7",
skills=[
"~/.jeeves/skills/system/",
Skill.from_text("""
---
name: tone
description: Always reply in a brief, factual tone.
---
Stay under 100 words. No filler.
"""),
"./.jeeves-skills/",
],
)The inline skill obeys the same override rules. If ./.jeeves-skills/
also ships a tone skill, the project version wins.
Override is by NAME, not path. Two skills with different names in different sources both load. Two skills with the same name in different sources. The later source wins, the earlier is dropped.