slice 3.5c: seed 15 built-in skills across the 6 stacks
Hand-authored skill catalog anchored to real 2026-07 versions:
- Rust 1.97.1 (stable), edition 2024
- React 19.2.7, Server Components + Actions
- TailwindCSS 4.3.3 (CSS-first config, Oxide engine)
- three.js r185 (WebGPURenderer stable, BatchedMesh matured)
- React Native 0.86 / Expo SDK 54+ (New Architecture default)
- cargo-nextest 0.9.140, gitleaks 8.20+, cargo-audit 0.21+
- Postgres 17 (18 in beta, don't rely on)
- CUDA Blackwell, Metal Apple7+, ROCm CDNA3
Ships 15 skills across the categories:
foundation/ workspace-repo-commit-protocol
small-focused-commits
tdd-red-green-refactor
code-review-checklist
int-xx-marker-protocol
decompose-int-items
rust/ write-rust-current-edition
rust-error-handling
cargo-test-driven-development
rust-async-tokio-idioms
backend/ postgres-migrations-forward-only
postgres-index-selection
api-pagination-day-1
frontend/ react-19-server-components
tailwind-v4-idioms
component-4-state-model
mobile/ expo-managed-vs-bare
rn-flashlist-perf
gpu/ gpu-coalescing-and-occupancy
roofline-model
threejs/ threejs-perf-and-teardown
security/ cargo-audit-workflow
secret-scanning-gitleaks
skills_loader.rs walks skills/**/*.md, parses YAML frontmatter
(name, description, when_to_use, tags), upserts via
skills_catalog::upsert_builtin. Idempotent per boot — bumps version
+ appends skill_versions row ONLY when body changes. Deterministic
sha256-derived ids so builtins are stable across boots.
Dockerfile copies skills/ to /etc/clawmates/skills. Server boot
task spawns loader alongside team_template_loader.
Follow-ups (Slice 3.5c continuation, future PRs):
- 20-30 more skills (duckdb, shadcn composition, a11y, WebGPU
migration, metal frame capture, rocprof, deep gitea forge
integration, semgrep rulepacks)
- Bind skills to team template roles (add [role.skills] refs to
templates/teams/*.toml + wire template_role_skills population
in team_template_loader)
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
cddbcb91b3
commit
7b23f61632
@@ -0,0 +1,172 @@
|
||||
//! Load builtin skills from `skills/**/*.md` into `skills` +
|
||||
//! `skill_versions` at server boot. Slice 3.5c of the missions
|
||||
//! consolidation.
|
||||
//!
|
||||
//! Frontmatter shape (YAML between `---` fences):
|
||||
//! name: <slug>
|
||||
//! description: <one-line, shown to the LLM in resources/list>
|
||||
//! when_to_use: <trigger sentence, appended to description>
|
||||
//! tags: [foundation, rust, ...]
|
||||
//!
|
||||
//! The body is the rest of the file. Both are upserted idempotently:
|
||||
//! `skills_catalog::upsert_builtin` bumps the version + appends to
|
||||
//! `skill_versions` ONLY when the body actually changes.
|
||||
|
||||
use serde::Deserialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::PgPool;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cm_db::repo::skills_catalog::{upsert_builtin, UpsertBuiltinSkill};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Frontmatter {
|
||||
name: String,
|
||||
description: String,
|
||||
#[serde(default)]
|
||||
when_to_use: Option<String>,
|
||||
#[serde(default)]
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
fn skills_dir() -> PathBuf {
|
||||
if let Ok(d) = std::env::var("CLAWMATES_SKILLS_DIR") {
|
||||
return PathBuf::from(d);
|
||||
}
|
||||
let container = PathBuf::from("/etc/clawmates/skills");
|
||||
if container.exists() {
|
||||
return container;
|
||||
}
|
||||
PathBuf::from("skills")
|
||||
}
|
||||
|
||||
/// Deterministic id per builtin skill name — sha256 of a stable
|
||||
/// namespace + the name. Matches the pattern used by the team-template
|
||||
/// loader so ids are reproducible across boots.
|
||||
fn builtin_id(name: &str) -> uuid::Uuid {
|
||||
let mut h = Sha256::new();
|
||||
h.update(b"clawmates.builtin.skill\x00");
|
||||
h.update(name.as_bytes());
|
||||
let d = h.finalize();
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes.copy_from_slice(&d[..16]);
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
uuid::Uuid::from_bytes(bytes)
|
||||
}
|
||||
|
||||
/// Walk the skills tree + upsert every `*.md`. Returns the count of
|
||||
/// successful upserts. Failures are logged and skipped so a single
|
||||
/// broken skill can't block boot.
|
||||
pub async fn load_builtins(pool: &PgPool) -> usize {
|
||||
let dir = skills_dir();
|
||||
let files = match walk_md(&dir) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"skills_loader: dir {} not readable: {e} — skipping builtin skills seed",
|
||||
dir.display()
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let mut loaded = 0usize;
|
||||
for path in files {
|
||||
match load_one(pool, &path).await {
|
||||
Ok(name) => {
|
||||
loaded += 1;
|
||||
eprintln!("skills_loader: upserted builtin skill {name}");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("skills_loader: failed to load {}: {e}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
loaded
|
||||
}
|
||||
|
||||
fn walk_md(root: &std::path::Path) -> std::io::Result<Vec<PathBuf>> {
|
||||
let mut out = Vec::new();
|
||||
fn recurse(p: &std::path::Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
|
||||
for entry in std::fs::read_dir(p)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if entry.file_type()?.is_dir() {
|
||||
recurse(&path, out)?;
|
||||
} else if path.extension().and_then(|s| s.to_str()) == Some("md") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
recurse(root, &mut out)?;
|
||||
out.sort();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Parse the frontmatter + body out of one file and upsert.
|
||||
async fn load_one(pool: &PgPool, path: &std::path::Path) -> Result<String, String> {
|
||||
let text =
|
||||
std::fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
|
||||
let (frontmatter, body) = split_frontmatter(&text)
|
||||
.ok_or_else(|| format!("no --- frontmatter block in {}", path.display()))?;
|
||||
let fm: Frontmatter = serde_yaml::from_str(frontmatter)
|
||||
.map_err(|e| format!("parse frontmatter of {}: {e}", path.display()))?;
|
||||
|
||||
let skill = UpsertBuiltinSkill {
|
||||
id: builtin_id(&fm.name),
|
||||
name: &fm.name,
|
||||
description: &fm.description,
|
||||
when_to_use: fm.when_to_use.as_deref(),
|
||||
tags: fm.tags.clone(),
|
||||
body,
|
||||
};
|
||||
upsert_builtin(pool, skill)
|
||||
.await
|
||||
.map_err(|e| format!("upsert {}: {e}", fm.name))?;
|
||||
Ok(fm.name)
|
||||
}
|
||||
|
||||
/// Extract the `---\n<yaml>\n---\n<body>` shape. Returns `(yaml, body)`
|
||||
/// or None if the file doesn't start with a frontmatter fence.
|
||||
fn split_frontmatter(text: &str) -> Option<(&str, &str)> {
|
||||
let mut rest = text.strip_prefix("---\n")?;
|
||||
// Some editors add a BOM; strip a leading whitespace/newline pair.
|
||||
if rest.starts_with('\r') {
|
||||
rest = rest.strip_prefix('\r').unwrap_or(rest);
|
||||
}
|
||||
let end = rest.find("\n---\n")?;
|
||||
let yaml = &rest[..end];
|
||||
let body = &rest[end + "\n---\n".len()..];
|
||||
Some((yaml, body.trim_start()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn splits_frontmatter() {
|
||||
let src = "---\nname: foo\ndescription: bar\n---\n# body\n";
|
||||
let (fm, body) = split_frontmatter(src).unwrap();
|
||||
assert!(fm.contains("name: foo"));
|
||||
assert_eq!(body, "# body\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_frontmatter_returns_none() {
|
||||
assert!(split_frontmatter("# plain md\n").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_id_stable() {
|
||||
assert_eq!(
|
||||
builtin_id("workspace-repo-commit-protocol"),
|
||||
builtin_id("workspace-repo-commit-protocol")
|
||||
);
|
||||
assert_ne!(
|
||||
builtin_id("workspace-repo-commit-protocol"),
|
||||
builtin_id("other-skill")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user