Skills Architecture
How MeetLoyd implements the Anthropic Agent Skills specification — from file-based skill packages to runtime agent queries.
Overview
The skills system has two layers:
┌─────────────────────────────────────────────────────────┐
│ Management Layer (Database) │
│ src/routers/skills.ts │
│ ┌─────────┐ ┌────────────────┐ ┌──────────────────┐ │
│ │ Skills │ │ Agent-Skill │ │ Store Listings │ │
│ │ CRUD │ │ Assignments │ │ & Purchases │ │
│ └─────────┘ └────────────────┘ └──────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Content Layer (Filesystem) │
│ src/skills/ (parser, loader, registry) │
│ ┌─────────┐ ┌────────────────┐ ┌──────────────────┐ │
│ │ SKILL.md│ │ references/ │ │ scripts/ │ │
│ │ Parser │ │ Loader │ │ assets/ │ │
│ └─────────┘ └────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
▲ │
│ skills/ directory │
└────────────────────────────────────┘
- Content Layer (
src/skills/): Parses, validates, and loads SKILL.md files following the Anthropic spec. Handles progressive disclosure. - Management Layer (
src/routers/skills.ts): Database-backed CRUD for multi-tenant skill ownership, agent assignments, store purchases.
Progressive Disclosure
The core design principle: load only what's needed, when it's needed.
┌──────────────────────────────┐
Level 1 (~100 tok) │ name: ticket-triage │ ← Loaded at startup
System Prompt │ description: ITIL-based... │ for ALL skills
└──────────────────────────────┘
│
Agent encounters support ticket
│
▼
┌──────────────────────────────┐
Level 2 (<5k tok) │ # Support Ticket Triage │ ← Loaded on activation
skill_query │ ## Decision Tree │ via skill_query tool
│ ## Priority Definitions │
│ ## Response Templates │
└──────────────────────────────┘
│
Agent needs escalation details
│
▼
┌──────────────────────────────┐
Level 3 (variable) │ references/ESCALATION.md │ ← Loaded on demand
skill_query +ref │ scripts/classify-ticket.py │ specific file only
└──────────────────────────────┘
Why Progressive Disclosure?
| Approach | Tokens Used | Agent Quality |
|---|---|---|
| Dump everything in system prompt | 50,000+ | Diluted attention |
| Only names in prompt, load on query | ~100 + on-demand | Focused, efficient |
With 80+ skills, dumping all instructions into the system prompt would consume 200k+ tokens and severely degrade agent performance. Progressive disclosure keeps the system prompt lean while giving agents full access to deep knowledge when needed.
Component Architecture
Parser (src/skills/parser.ts)
Handles SKILL.md file parsing and validation.
SKILL.md content
│
▼
┌─────────────────┐ ┌──────────────────┐
│ extractFrontmatter│ ──→ │ YAML parse │
│ (--- delimiters) │ │ (name, desc, ...) │
└─────────────────┘ └──────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Markdown body │ │ validateSkill() │
│ (instructions) │ │ - name format │
└─────────────────┘ │ - description len │
│ - dir name match │
│ - instruction len │
└──────────────────┘
Validation rules (per Anthropic spec):
name: 1-64 chars, lowercasea-z0-9-, no consecutive hyphens, matches directorydescription: 1-1024 chars, non-emptycompatibility: max 500 chars if present- Instructions: warning if > 500 lines (recommends splitting to references)
Key functions:
parseSkillMd(content)→{ frontmatter, instructions, raw }validateSkill(parsed, directoryName)→{ valid, errors, warnings }extractReferenceLinks(markdown)→string[](findsreferences/links)extractScriptReferences(markdown)→string[](findsscripts/links)
Loader (src/skills/loader.ts)
Implements the progressive disclosure loading strategy.
┌─────────────────────────────────────────────────┐
│ loadSkillSummary(path) [Level 1] │
│ Returns: { id, name, description, category } │
│ Cost: ~100 tokens │
├─────────────────────────────────────────────────┤
│ loadSkill(path) [Level 2] │
│ Returns: { ...summary, instructions, │
│ references[], scripts[], assets[] } │
│ Note: references/scripts discovered but NOT │
│ loaded (loaded: false) │
│ Cost: <5,000 tokens (instructions only) │
├─────────────────────────────────────────────────┤
│ loadReference(skill, filename) [Level 3] │
│ loadScript(skill, filename) │
│ Returns: file content string │
│ Cost: varies per file │
└─────────────────────────────────────────────────┘
Batch loading:
loadSkillsFromDirectory(dir)→Map<string, Skill>— loads all skills (Level 2)loadSkillSummariesFromDirectory(dir)→SkillSummary[]— summaries only (Level 1)loadReferences(skill, filenames)— batch load multiple references
Registry (src/skills/registry.ts)
In-memory index of all loaded skills with lookup and search.
┌──────────────────────────────────────────────────┐
│ SkillRegistry (singleton) │
│ │
│ skills: Map<string, Skill> ← Full skill data │
│ summaries: SkillSummary[] ← Quick lookup │
│ lastRefresh: Date ← Cache control │
├──────────────────────────────────────────────────┤
│ Lookup │
│ getSkill(id) → Skill │
│ getSkillSummary(id) → SkillSummary │
│ getAllSkillSummaries() → SkillSummary[] │
│ searchSkills(query) → SkillSummary[] │
│ getSkillsByCategory(cat) → SkillSummary[] │
├──────────────────────────────────────────────────┤
│ Agent Assignments (in-memory) │
│ assignSkillToAgent(agentId, skillId) │
│ unassignSkillFromAgent(agentId, skillId) │
│ getAgentSkills(agentId) → Skill[] │
│ getAgentSkillSummaries(agentId) → Summary[] │
├──────────────────────────────────────────────────┤
│ System Prompt Generation │
│ generateSkillsPromptSection(agentId) → markdown │
│ generateSkillsXML(agentId) → XML │
└──────────────────────────────────────────────────┘
Initialization flow:
Server startup
│
▼
initializeRegistry()
│
▼
loadSkillsFromDirectory('skills/')
│
├── skills/terraform/SKILL.md → parse → validate → Skill
├── skills/ticket-triage/SKILL.md → parse → validate → Skill
├── skills/sales-objection-handling/SKILL.md → ...
└── ... (80+ skills)
│
▼
Registry ready (Map<id, Skill> + SkillSummary[])
Data Flow: Agent Uses a Skill
End-to-end flow when an agent encounters a task that matches a skill:
1. Agent starts conversation
│
▼
2. System prompt includes skill summaries (Level 1)
"Available Skills:
- terraform: Expert Terraform/OpenTofu IaC...
- ticket-triage: ITIL-based ticket triage..."
│
▼
3. User asks: "How should I triage this production outage?"
│
▼
4. Agent recognizes skill match → calls skill_query tool
{ skillId: "ticket-triage", query: "production outage triage" }
│
▼
5. MCP tool handler:
a. getSkill("ticket-triage") from registry
b. buildSkillQueryResult(skill) → Level 2 instructions
c. Return: instructions + available references list
│
▼
6. Agent reads instructions, applies decision tree
"Is system completely down? → YES → P1 Critical"
│
▼
7. Agent needs escalation details → calls skill_query again
{ skillId: "ticket-triage", referenceFile: "ESCALATION-MATRIX.md" }
│
▼
8. loadReference(skill, "ESCALATION-MATRIX.md") → Level 3
│
▼
9. Agent applies escalation rules, responds to user
System Prompt Integration
When an agent has skills assigned, the registry generates a skills section for the system prompt.
Markdown format (generateSkillsPromptSection):
## Available Skills
You have access to the following skills. Use the `skill_query` tool to load instructions when needed.
- **terraform**: Expert Terraform/OpenTofu Infrastructure as Code...
- **ticket-triage**: Triage and categorize support tickets using ITIL-based framework...
- **sales-objection-handling**: Handle B2B SaaS sales objections with proven frameworks...
XML format (generateSkillsXML):
<available_skills>
<skill name="terraform">
<description>Expert Terraform/OpenTofu Infrastructure as Code...</description>
<category>engineering</category>
<version>1.0.0</version>
</skill>
...
</available_skills>
Database Layer
The management layer stores skill ownership and assignments in PostgreSQL:
skills Table
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
tenant_id | UUID | FK → tenants |
name | VARCHAR | Skill name |
slug | VARCHAR | URL-safe identifier |
description | TEXT | Skill description |
category | VARCHAR | Category tag |
tool_ids | JSONB | Associated MCP tool IDs |
config | JSONB | Skill configuration |
listing_ref | UUID | FK → store_listings (if purchased) |
listing_version | VARCHAR | Store listing version |
locked | BOOLEAN | Prevent edits (store skills) |
current_version | VARCHAR | Current version string |
status | VARCHAR | active / inactive / archived |
agent_skill_assignments Table
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
tenant_id | UUID | FK → tenants |
skill_id | UUID | FK → skills |
agent_id | UUID | FK → agents |
skill_version | VARCHAR | Version at time of assignment |
listing_ref | UUID | Store listing reference |
enabled | BOOLEAN | Toggle on/off without removing |
config | JSONB | Per-assignment configuration |
priority | INTEGER | Higher = more likely to use |
assigned_at | TIMESTAMP | When assigned |
assigned_by | UUID | FK → users |
Vertical Skills
Verticals (like MFO) can bundle their own skills that reference vertical-specific MCP tools:
mfo/skills/
├── tax-optimization/SKILL.md → references mfo_income_tax_calculate, mfo_ifi_calculate
├── bspce-strategy/SKILL.md → references mfo_bspce_calculate, mfo_bspce_simulate
├── patrimony-review/SKILL.md → references getProfile, listAssets
├── real-estate-timing/SKILL.md → references mfo_cdhr_calculate
└── succession-planning/SKILL.md → references mfo_av_compare_options
Vertical skills are loaded alongside platform skills but scoped to tenants using the vertical.
Key Files
| File | Purpose |
|---|---|
src/skills/types.ts | Type definitions (SkillFrontmatter, Skill, SkillSummary, etc.) |
src/skills/parser.ts | SKILL.md parsing and validation |
src/skills/loader.ts | Progressive disclosure loading |
src/skills/registry.ts | In-memory skill index, search, assignments |
src/skills/index.ts | Public API re-exports |
src/routers/skills.ts | REST API for skill CRUD and assignments |
skills/ | 80+ first-party SKILL.md files |
Spec Compliance
MeetLoyd implements the full Agent Skills specification:
| Spec Requirement | Implementation |
|---|---|
| SKILL.md with YAML frontmatter | parser.ts → parseSkillMd(), extractFrontmatter() |
| Name validation (lowercase, hyphens, matches dir) | parser.ts → SKILL_NAME_PATTERN, validateSkill() |
| Description (max 1024 chars) | types.ts → SKILL_DESCRIPTION_MAX_LENGTH |
Optional references/, scripts/, assets/ | loader.ts → discoverReferences(), discoverScripts(), discoverAssets() |
| Progressive disclosure (3 levels) | loader.ts → loadSkillSummary(), loadSkill(), loadReference() |
| Under 500 lines recommendation | types.ts → SKILL_INSTRUCTIONS_MAX_LINES, warning in validation |
Next: See the Skills API Reference for endpoint documentation, or the Creating Skills Guide for a step-by-step tutorial.