Files
poimen-memory/tasks/M3.5.5-skills-endpoint.md
T
Story Crater Bot 603c2b681f feat: M3.5.8 complete - all endpoints, rate limiting, and deployment (253 tests)
Changes:
- Queue cleanup: Deleted 17 poisoned CI runs from database
- Code: All M3.5 endpoints implemented and tested
- Tests: 253 total, all passing
- Deployment: K8s manifests and ArgoCD configured
- CI: Forgejo Actions dispatcher issue (image not built yet)

Next: Manual image build or CI dispatcher fix
2026-08-23 17:19:42 -07:00

167 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M3.5.5 — GET /skills and /skills/{name}: loadable skills catalog
| Field | Value |
|---|---|
| Phase | M3.5 — Distributed API Layer |
| Size | M — 13 days |
| Status | ✅ Done |
| Flags | — |
| Spec | inlined below |
| Blocks | M3.5.8 |
| Depends | M3.5.1, M4.1 (skill drafts exist locally) |
## Goal
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/`)
- For each `*/SKILL.md`, parse frontmatter
- Extract: `name`, `description`, `when_to_use`, `argument_hint`, `promoted_at` (file mtime)
- Parse `generated_from` field to show provenance
- Return array
2. `GET /memory/skills/{name}` handler:
- Load `vault/skills/{name}/SKILL.md`
- Parse frontmatter and body
- If `include_body=false` (default), return metadata only
- If `include_body=true`, include markdown body
3. `loadable` query param (admin-only feature):
- Default: exclude `_drafts/`
- `loadable=false` with admin apikey: include `_drafts/` in listing
- Non-admin key requesting `loadable=false` → 403 Forbidden
4. Error handling:
- Skill not found → 404 with `{"error":"not_found","reason":"skill 'xyz' not promoted"}`
- Malformed SKILL.md (frontmatter parse fails) → 500 with error (admin debug only)
- Admin check: apikey must be in a whitelist (env var `MEM_ADMIN_APIKEYS` or config)
## Acceptance
- List endpoint returns all promoted skills
- Individual skill fetch works
- Drafts are excluded by default
- Admin with `loadable=false` sees drafts
- Skill body is optional (include_body param)
- Promoted_at field reflects file mtime
## Verify
**Harness:** Integration tests + filesystem fixtures.
**Setup:** Create test `vault/skills/` with:
- `vault/skills/test-skill-1/SKILL.md` (promoted)
- `vault/skills/test-skill-2/SKILL.md` (promoted)
- `vault/skills/_drafts/draft-skill/SKILL.md` (unpromoted)
**Integration test**`tests/it_skills_endpoint.rs`:
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)