slice 3.5c: seed 15 built-in skills across the 6 stacks
ci / gates (push) Successful in 4s
ci / frontend (push) Successful in 25s
ci / rust (push) Failing after 3m41s
ci / e2e (push) Skipped
ci / publish (push) Skipped

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:
Omar Sobh
2026-07-19 13:55:44 -07:00
co-authored by Claude Opus 4.7
parent cddbcb91b3
commit 7b23f61632
28 changed files with 1657 additions and 0 deletions
+172
View File
@@ -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")
);
}
}