Read-only endpoints for skill catalog. List all promoted skills (exclude `_drafts/`), fetch individual skill metadata and body. Skills are Obsidian notes; expose them over HTTP for agent discovery.
## Design
**List all loadable skills:**
```
GET /memory/skills?loadable=true
→ 200 {
"skills": [
{
"name": "infra-root-causes",
"description": "Identify root causes of infrastructure failures",
"when_to_use": "When troubleshooting cluster or service outages",
"argument_hint": "--project <name>",
"promoted_at": "2026-08-20T10:30:00Z",
"generated_from": null
},
...
]
}
```
**Get one skill (metadata only):**
```
GET /memory/skills/infra-root-causes
→ 200 {
"name": "infra-root-causes",
"description": "...",
"when_to_use": "...",
"argument_hint": "...",
"promoted_at": "2026-08-20T...",
"generated_from": null
}
```
**Get skill with body (full content):**
```
GET /memory/skills/infra-root-causes?include_body=true
→ 200 {
"name": "infra-root-causes",
"description": "...",
"body": "# Infra root causes\n\n..."
}
```
**Filters:**
-`loadable=true` (default): exclude `_drafts/`, return only promoted skills
-`loadable=false`: include everything (admin only — must have special apikey, documented in code)
## Technical
**Source:** Vault at `vault/skills/` contains skill markdown files. Each skill is a directory:
```
vault/skills/
infra-root-causes/
SKILL.md <- frontmatter + body
```
**Frontmatter (YAML in SKILL.md):**
```yaml
---
name:infra-root-causes
description:Identify root causes of infrastructure failures
when_to_use:When troubleshooting cluster or service outages
argument_hint:--project <name>
generated_from:null| <L2 sha256>
---
```
**Drafts are in `vault/skills/_drafts/`:**
```
vault/skills/
_drafts/
new-skill/
SKILL.md
```
Only load from `vault/skills/*/SKILL.md` (not `_drafts`), unless `loadable=false` is passed with an admin key.
## Steps
1.`GET /memory/skills` handler:
- List `vault/skills/` directory (skip `_drafts/`)
1.`a1_list_skills_returns_promoted` — GET /skills returns array with test-skill-1 and test-skill-2.
2.`a2_drafts_excluded_by_default` — GET /skills does not include draft-skill.
3.`a3_drafts_included_with_admin_key` — GET /skills?loadable=false with admin apikey includes draft-skill.
4.`a4_non_admin_denied_drafts` — GET /skills?loadable=false with regular apikey returns 403.
5.`a5_get_single_skill_metadata` — GET /skills/test-skill-1 returns 200 with frontmatter fields.
6.`a6_include_body_true` — GET /skills/test-skill-1?include_body=true returns body field with markdown.
7.`a7_include_body_false` — GET /skills/test-skill-1?include_body=false (or omitted) does not include body field.
8.`a8_skill_not_found` — GET /skills/nonexistent returns 404.
9.`a9_promoted_at_is_file_mtime` — GET /skills/test-skill-1, assert promoted_at is a valid ISO timestamp close to SKILL.md's modification time.
10.`a10_generated_from_field` — SKILL.md with `generated_from: sha256xyz` is parsed and returned as-is.
**Command:**`cargo test -p mem-cli skills_endpoint`
**False pass:**
- Drafts never created in test fixtures. The default exclude-drafts logic is untestable without a draft.
- Admin key never tested. Non-admin path and admin path can be identical in code.
- Promoted_at never validated. Can return a fake date; file mtime is the only source.
- Frontmatter parsing doesn't validate required fields (name, description). A malformed SKILL.md is silently returned with null values.
## Traps
- Vault directory may not exist locally (only in deployed cluster). Start with a default empty list if vault/ is missing.
- YAML frontmatter parsing is fussy. A tab instead of spaces breaks YAML. Use a YAML parser (serde_yaml) and validate on load.
- File mtime precision: Unix mtime is seconds; SKILL.md edits may not increment it if done within the same second. Use actual write timestamp if available.
- Admin key stored in env var. If unset, default to deny (safer than default allow).
---
Background: [DESIGN.md § Skills — the procedural projection](../DESIGN.md#skills--the-procedural-projection)