file-system-object

SkillFiles & storage

The grammar rules that make MOOLLM's file system object-oriented. Plural directory names declare element type; UPPERCASE marker files declare interface exports (COM-style, minus the UUIDs); directories are implementation classes exporting every interface whose marker file sits at their root.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the file-system-object skill

What this skill tells your AI

The instructions your AI receives, as published by simhacker/moollm in skills/file-system-object/SKILL.md and read by ahel’s review.

"A directory named <things>/ is saying: the things in here are of the type <thing>. A file named FOO.md at a directory's root is saying: this directory exports the FOO interface. The directory IS an implementation class; the UPPERCASE marker files ARE its interface declarations; no UUIDs needed because the filename IS the class name."

This skill formalizes a grammar that already exists throughout MOOLLM. Every skill you've read — skill, prototype, card, schema, adventure, room, character, incarnation — uses these rules implicitly. The other meta-skills USE the grammar; this skill NAMES it so newcomers (human or LLM) can read MOOLLM directory trees fluently without pattern-matching each one from scratch.


The Three Rules

Rule 1: Plural-named directories declare element type

A directory named <things>/ declares "the things in here are of type <thing>."

skills/          → each child is (presumably) a skill
biomes/          → each child is a biome
mechanisms/      → each child is a schema mechanism
recipes/         → each file is a recipe
gotchas/         → each file is a gotcha
runbooks/        → each file is a runbook
instances/       → each child is an instance
stacks/          → each child is a stack
protocols/       → each file is a protocol
rooms/           → each child is a room
characters/      → each child is a character

"Presumably" is doing real work here: the plural name is a declaration of intent. To confirm a specific child actually IS of that type, look for its interface marker file (see Rule 3). Auxiliary content (templates, indexes, READMEs) can also live in a plural container without being instances — their absence of a marker file is the tell.

Rule 2: Singular-named directories are instances

A directory named <thing>/ (singular, kebab-case or single word) is an instance. The directory name is the instance's identifier.

skills/skill/                    → instance named "skill" (a specific skill, happens to BE the meta-skill about skills)
skills/biome/                    → instance named "biome"
skills/gcs/                      → instance named "gcs"
mechanisms/json-schema/          → instance named "json-schema"
instances/leela-zion2-dev-0/     → instance named "leela-zion2-dev-0"

This reads as: <plural-container>/<instance-id>/.

Rule 3: UPPERCASE marker files declare interface exports

A file named FOO.md or FOO.yml (UPPERCASE filename) at a directory's root declares that the directory EXPORTS the FOO interface.

skills/gcs/
├── SKILL.md         ← "I export the SKILL interface"
├── CARD.yml         ← "I export the CARD interface"
├── GLANCE.yml       ← "I export the GLANCE interface"
├── README.md        ← "I export the README interface (human landing)"
├── recipes/         ← plural: "I contain recipes"
├── gotchas/         ← plural: "I contain gotchas"
└── ...

The UPPERCASE convention visually distinguishes interface declarations from ordinary content. The presence of the file IS the declaration — no separate registry required.

One directory exports many interfaces. Just like a COM class implements many interfaces, a MOOLLM skill directory typically exports SKILL + CARD + GLANCE + README + sometimes CHARACTER + EXPORTS + PROTOTYPES — all simultaneously, via multiple marker files.


The COM Analogy

This is Henry's framing, which anchors the whole grammar:

COM conceptFile-system-object equivalent
Implementation class (C++ / IDL class)Directory — the <thing>/ singular name
Interface declaration (IDL interface IFoo)UPPERCASE marker file — FOO.md or FOO.yml
Interface UUID (IID_IFoo = {01234567-89ab-...})Not needed — the filename IS the identifier
Multiple interfaces per classMultiple UPPERCASE marker files per directory
QueryInterface(IID_IFoo, &ptr)cat <dir>/FOO.md (or "does this dir export FOO?" = test -e <dir>/FOO.md)
Class factory (CoCreateInstance)Prototype instantiation (see skill-instantiation-protocol.md, delegation-object-protocol.md)
Aggregation (one class delegating to another)DOP prototype chain via inherits: or PROTOTYPES.yml
TypeLib registryschemapedia/registry.yml (for schema mechanisms); skills/biome/biomes/registry.yml (for biomes)

COM's big wins were: (a) multiple interfaces per implementation, (b) language-neutral contracts, (c) strong versioning via UUIDs. MOOLLM keeps (a) and (b), swaps (c) for readable UPPERCASE names because we want humans and LLMs to read the tree, not a binary-stamped registry.

COM's big losses were: opaque UUIDs, fiddly registration, reliance on a global registry, Windows-centric tooling. MOOLLM avoids all four by putting the declarations in the filesystem itself.


Why UPPERCASE

The UPPERCASE-filename convention (SKILL.md, CARD.yml, MECHANISM.yml, ALERT.yml, PROTOTYPES.yml) does several things at once:

  1. Visual grep. In any ls output, UPPERCASE files leap out. Structural signposts pop; ordinary content recedes.
  2. Class-vs-content distinction. Lowercase filenames (camera-configs.json, my-recipe.md) are content; UPPERCASE is meta — "I am telling you what this directory IS."
  3. Namespace cleanliness. foo.md (lowercase) is a note about foo; FOO.md (uppercase) is a declaration that this directory implements FOO. They don't collide, and their roles are clear at a glance.
  4. Future-proof tooling. A validator script can simply glob for [A-Z]*.{md,yml} at a directory root to find all exported interfaces. No manifest file, no parsing.
  5. Big-endian naming. The UPPERCASE prefix groups structural files together alphabetically at the top of ls output.

When NOT to use uppercase. Normal content, per-instance data (state.yml, history.md), templates (RUN.yml.tmpl — the .tmpl is the tell that it's a template, not a declaration), ordinary documentation (troubleshooting.md).


Serialization — YAML Preferred, JSON Welcome, Comments Encouraged

Interface marker files come in three flavors, each picked on purpose:

ExtensionUse whenWhy
.ymlThe interface is data-shaped (fields, structure, lists). Default choice.Comments are first-class and encouraged (see yaml-jazz). The same file can carry structure, semantic hints, lineage notes, and authorial voice simultaneously.
.mdThe interface is narrative (explains, teaches, guides).Markdown supports prose + structure + code blocks. SKILL.md, README.md are the canonical narrative interfaces.
.jsonA downstream tool needs it (jq pipelines, ajv validators, frontend code reading an API) and no human is expected to author it by hand.Universal tool support. Smaller parser footprint. But: no comments → strips human intent.

The rule: YAML is preferred, JSON is welcome, comments are encouraged.

When to prefer which

  • Authoring by hand? → YAML. Every time.
  • Tool reads it, humans rarely? → JSON is fine.
  • Both? → YAML is canonical; emit JSON on a hook if needed.
  • Narrative / teaching / guiding? → Markdown.

Why YAML over JSON for MOOLLM interface declarations

YAML gives us three-audiences support (humans, LLMs, machines — see the yaml-jazz skill):

# CARD.yml — this is a comment — it carries semantic information for
# humans AND LLMs, even though machines technically ignore it.
card:
  id: example
  tagline: "The tagline is the elevator pitch."
  # ↑ Next audience note: rule-of-thumb is one sentence, present tense.

Strip the comments and you have the same data. Keep them and you have intent alongside the data — why the fields are there, what alternatives we considered, which examples are load-bearing. That's the yaml-jazz difference, and it's why every UPPERCASE marker file in MOOLLM defaults to YAML.

Canonical patterns observed in MOOLLM

PatternExampleNotes
Data-shaped interfaceCARD.yml, GLANCE.yml, MECHANISM.yml, ALERT.yml, CHARACTER.yml, PROTOTYPES.yml, EXPORTS.ymlYAML. Comments throughout.
Narrative interfaceSKILL.md, README.md, CHANGELOG.mdMarkdown with YAML frontmatter.
Generated-for-tool interfacerare in MOOLLM proper; common in build outputs.json next to .yml canonical, regenerated on change.
Template interfacesCARD.yml.tmpl, SKILL.md.tmpl.tmpl suffix signals placeholder content; not a real declaration.

If you're introducing a new UPPERCASE interface type and you have to choose: FOO.yml is the default. Reach for .md if the content is prose-heavy; reach for .json only when a tool demands it.

The yaml-jazz skill is a hard dependency

This skill inherits conceptually from yaml-jazz: "Semantic YAML — comments as data; three audiences." The two skills together give MOOLLM its authoring grammar:

  • file-system-object says: directories are classes, UPPERCASE files are interfaces.
  • yaml-jazz says: inside those interface files, comments are first-class semantic carriers.

Together they turn an ls + cat session into a legitimate class browsing experience.


Nesting — Sub-skills and Categorization

Following the grammar, a directory can nest further plurals inside itself:

skills/biome/                  ← singular (instance of skill type)
├── SKILL.md                   ← exports SKILL interface
├── CARD.yml                   ← exports CARD interface
├── biomes/                    ← plural: "this skill contains biomes"
│   ├── registry.yml           ← (not UPPERCASE — data, not interface)
│   ├── gateways.yml           ← (not UPPERCASE — data, not interface)
│   ├── convention.yml         ← (not UPPERCASE — data, not interface)
│   └── families.yml           ← (not UPPERCASE — data, not interface)
└── templates/
    └── BIOME/                 ← the template IS named UPPERCASE because it's a TYPE template
        ├── SKILL.md.tmpl
        ├── CARD.yml.tmpl
        └── ...

A skill can also contain sub-skills:

skills/parent-skill/
├── SKILL.md
├── CARD.yml
└── skills/                    ← plural: sub-skills live here
    └── child-skill/
        ├── SKILL.md
        └── CARD.yml

And so on recursively. The grammar holds at any depth.

Skills as categories

When a skill contains a skills/ subdirectory, the parent skill plays two roles at once:

  1. It's still a skill itself (via its own SKILL.md / CARD.yml).
  2. It's also a CATEGORY for the sub-skills — the parent directory name is the category name; its child skills belong to that category by virtue of their path.
skills/gardening/                    ← the gardening skill AND the gardening category
├── SKILL.md                         ← the parent skill's full protocol
├── CARD.yml
└── skills/                          ← sub-skills (i.e., members of the gardening category)
    ├── composting/
    │   ├── SKILL.md
    │   └── CARD.yml
    ├── pruning/
    │   ├── SKILL.md
    │   └── CARD.yml
    └── seed-starting/
        └── SKILL.md

No separate "category" concept is needed — the parent skill IS the category. A sub-skill participates in the category simply by living under skills/<parent>/skills/<child>/. Cross-category sub-skills (a skill that belongs to two categories) are handled by DOP inheritance, not by duplicating directories: child-skill lives in one canonical path, and other categories reference it via inherits: chains.

When to nest vs flatten. Put a skill under a parent when:

  • It's only meaningful inside the parent's context (e.g., biome's biomes/ stubs — each stub is only a reference biome for this mother skill).
  • It's a sub-concern of the parent that isn't reused elsewhere yet.

Pull it up to a top-level skill (lift it) when:

  • It becomes reusable across unrelated contexts.
  • Other skills start wanting to inherit from it.
  • Its audience broadens beyond users of the parent skill.

This is the same PLAY-LEARN-LIFT arc applied to skill hierarchy: sub-skills start nested (low-cost), get promoted when they earn independence.


Skill Lookup and Inheritance Through Tree Containment

When one skill references another — via inherits:, related:, or a prose mention — the reference can take two forms:

FormExampleWhen to use
Bare nameinherits: [biome]Most authoring. Short, Postel-friendly, resolver-driven.
Unambiguous path fragmentsee: moollm/skills/biome/SKILL.mdWhen you need to disambiguate, cross-repo-reference, or help a reader find the exact file.

Both are valid. MOOLLM's resolver is expected to accept either and find the right skill, following Postel's robustness principle: be liberal in what you accept, conservative in what you emit. When authoring by hand, reach for the unambiguous form when you easily can; reach for the bare name when the context makes it obvious.

What counts as a skill — duck typing on marker files

A directory participates in the MOOLLM object system to the extent its UPPERCASE marker files say it does. There are three distinct levels of participation, and a directory can sit at any of them:

LevelMinimum markersWhat worksWhat doesn't
📜 Full skillSKILL.md + CARD.yml (+ usually GLANCE.yml, README.md)Everything: dispatch, narrative, semantic-image-pyramid reading, ambient inheritance when contained
📇 Dispatchable objectCARD.yml aloneDispatch: methods, advertisements, k-lines, state, navigation all work. queryInterface(dir, 'card') returns the sniffable interface. Inheritance declared in CARD.yml's inherits: chain resolves normally.No narrative/protocol doc; no SKILL.md-level reading-order entry point (GLANCE → CARD → SKILL → README collapses to GLANCE → CARD)
🦆 Scope-declaredskills/<name>/ container (or biomes/<name>/, characters/<name>/, etc.) declares the TYPE of the directoryType-level discovery works (enumeration recognizes it as of the plural's type); becomes dispatchable the moment a CARD.yml appearsWithout CARD or SKILL, there's nothing to dispatch yet — it's a stub, not an error

A CARD-only directory is a first-class object. It isn't a "degraded skill" — it's a lightweight form for when narrative isn't warranted. A directory with just CARD.yml can still:

  • Be invoked via its methods: block
  • Advertise capabilities via advertisements:
  • Activate k-lines via k_lines:
  • Declare inheritance via inherits: (resolver walks the chain the same way)
  • Participate in DOP delegation
  • Be the target of related: from other skills
skills/my-thing/                  ← dispatches just fine
└── CARD.yml                      ← this alone carries methods, ads, k-lines, navigation
                                  ← queryInterface(., 'card') → CARD.yml
                                  ← queryInterface(., 'skill') → null (no SKILL.md — that's OK)

This matters because it removes a friction: you don't need to write a 300-line SKILL.md just to make something dispatchable. Write the CARD when you have methods to declare; write the SKILL when you have a protocol to teach. The two files answer different questions ("what can this DO?" vs "how does this WORK?"), and not every object needs a teaching narrative.

🦆 Quack-quack duck typing — a directory is an object to the degree its marker files quack. SKILL.md is the full orchestra; CARD.yml is a solid string quartet; skills/<dir>/ with no markers is a stage with a name on it. All three are legitimate; each does a different amount of work.

Any additional UPPERCASE marker file adds another interface: CHARACTER.yml makes it incarnation-ready; MECHANISM.yml registers it with schemapedia; ALERT.yml makes it a branch-as-object alert. The directory exports all of them simultaneously, in classic COM-style multiple-interface fashion.

🎴 The CARD is a rich pun. CARD.yml names a surface that is — simultaneously — an interface definition (IDL / OLE Control / ActiveX TypeLib), a HyperCard (Atkinson-style navigable unit with fields/buttons/scripts), an actor (Hewitt-style message-receiver with local state), a thread (a dispatchable unit of control), a message dispatching surface (Smalltalk/Self-style), and a portable token (trading card + business card). The richness is load-bearing; see skills/card/SKILL.md § "The Card Pun Stack" for the full lineage.

The lookup scope — two searches, one upward walk

A bare reference like inherits: [biome] is resolved by walking outward from the referring skill's location. The walk performs two independent searches at every level. Both use the same parent-walking loop; they return different things.

referring_skill_dir/
  ↑  up one level
referring_skill_dir's parent/
  ↑  up one level
...

At each step, check two things:

Search 1 — scoped skills/ directory at this level

If the current directory contains a skills/ subdirectory, it declares scoped sub-skills — skills visible by bare name from anywhere under this directory. The containing directory does not itself have to be a skill — any directory can host a scoped skills/ subdirectory (a project root, a repo root, an adventure, a biome, a runbook bundle, anything).

some-project/                 ← not a skill (no SKILL.md), but...
├── data/
├── README.md
└── skills/                   ← scopes skills to this project
    ├── biome/                ← resolves when anything under some-project/ says `biome`
    └── ingest/

This is "I bring my own skill library" — a directory can define its own private skills without publishing them, and things inside it pick them up automatically. The same pattern at the repo root (<repo>/skills/), the MOOLLM root (moollm/skills/), and even sibling repos mounted nearby (~/.../Leela/git/*/skills/) all follow the same rule — skills/ is a scoping container wherever it appears.

Search 2 — this directory IS an ambient parent skill

If the current directory has a SKILL.md (or other UPPERCASE marker file) at its root, it is itself a skill, and its conventions become ambient for anything inside it. A skills/ subdirectory is not required — any skill directory can contain arbitrary nested content, and that content inherits the parent's conventions.

moollm/skills/biome/          ← SKILL.md present → this dir is a skill
├── SKILL.md
├── CARD.yml
└── biomes/
    └── gcp/                  ← inside `biome`'s tree → biome is an ambient parent;
        └── GLANCE.yml        ← gcp's conventions fall back to biome's conventions

gcp doesn't need to declare inherits: biome for ambient containment inheritance — being located under moollm/skills/biome/ is already the signal. (Explicit inherits: biome is still encouraged when authoring for clarity.)

Putting the two searches together

# A directory "quacks like a skill" if it has any recognized marker:
is_skill_like(d) := exists(d / "SKILL.md") or exists(d / "CARD.yml")
                    # (more broadly: any UPPERCASE.{md,yml} at root makes d a
                    #  dispatchable object of that interface's type — this is
                    #  the general rule; SKILL/CARD are the common skill pair.)

resolve_skill(name, from_dir):
    # Phase 1 — local tree walk (Search 1 + Search 2 at each step)
    d = from_dir
    while d is not filesystem-root:
        # Search 1: scoped `skills/` subdir at this level
        if is_skill_like(d / "skills" / name):
            return d / "skills" / name
        # Search 2 / self-match: this dir IS the skill we're looking for
        if basename(d) == name and is_skill_like(d):
            return d
        d = parent(d)

    # Phase 2 — mounted-repo search (Cursor workspace roots, etc.)
    for repo in mounted_workspace_roots():
        if is_skill_like(repo / "skills" / name):
            return repo / "skills" / name

    return NOT_FOUND


ambient_parents(dir):
    # Independent of resolve — these are conventions inherited by tree containment.
    # Any ancestor that quacks like a skill counts as an ambient parent.
    d = parent(dir)
    while d is not filesystem-root:
        if is_skill_like(d):
            yield d       # d is an ambient parent skill (SKILL.md OR CARD.yml at root)
        d = parent(d)

The two searches compose: resolve_skill finds the skill a name points at; ambient_parents yields the chain of enclosing skills whose conventions apply regardless of any explicit declaration.

Mounted workspace roots — the distributed skill path

When MOOLLM runs under Cursor (or any editor with multiple mounted workspace roots), the set of mounted repos becomes the final search path for skill resolution. Each mounted repo contributes its skills/ subdirectory to the resolver — treat the whole set as one flat union, walked in a stable order after the local-tree walk fails.

Typical sources that get mounted and contribute skills:

CategoryExampleShape
MOOLLM coremoollm/skills/The baseline skill library (biome, schema, card, skill, yaml-jazz, …)
Customer-specificcentral/skills/, <customer-repo>/skills/Private per-customer overlays that inherit from MOOLLM mothers
Distributed / team<org-infra>/skills/, <team-shared>/skills/Organization-wide libraries shared across projects
Publicmoollm-contrib/skills/, <community-pack>/skills/Third-party skill packs — open-source MOOLLM ecosystem
Private / personal<my-skills>/skills/A developer's own scratch library of work-in-progress skills
Per-project<some-project>/skills/Skills scoped to just this project (same shape as any Search-1 hit)

The resolver treats them all as equal-citizen skill roots. There's no distinction in the grammar between "official MOOLLM" and "third-party" — they're all just <some-repo>/skills/<some-name>/. The only difference is what the user mounted; the grammar doesn't care.

Order and shadowing across repos

Within Phase 2, the order is:

  1. The repo containing the referring skill (if any) — checked first, because "my own repo" is closer.
  2. Other mounted repos, in a stable order (e.g., alphabetical, or Cursor's workspace order, or explicitly configured).
  3. MOOLLM core last — it's the fallback, the most general library.

Closer still shadows farther. If your own repo's skills/biome/ exists, it wins over moollm/skills/biome/. This is how overlays work: a customer repo can locally customize a MOOLLM skill just by defining a skill of the same name in its own skills/ directory, with inherits: [moollm/skills/biome] if it wants to extend rather than replace.

Cursor-specific behavior

Cursor's workspace model is ideal for this pattern. The user mounts central/, moollm/, autotest/, leela-alerts/, and some customer-specific repo; MOOLLM running as an agent sees all of them at once and can resolve skills across the entire mounted set. The user didn't have to publish anything anywhere — mounting the repo made its skills findable.

Graceful degradation when a repo isn't mounted

If a skill name resolves to <some-repo>/skills/<name>/ on developer A's machine (because they have the repo mounted) but not on developer B's (who doesn't), MOOLLM should:

  1. Treat the bare name as unresolved when the repo is missing.
  2. Fall through to whatever else is reachable.
  3. Emit a one-line warning identifying the expected repo, so the developer can mount it if needed.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
52
Forks
5
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
file-system-object
Source
github.com/simhacker/moollm