slice 3.5a: skills catalog — the "how to think" layer
ci / gates (push) Successful in 4s
ci / frontend (push) Successful in 38s
ci / rust (push) Failing after 1m28s
ci / e2e (push) Skipped
ci / publish (push) Skipped

Introduce the skills catalog: the second half of the two-layer agent
model (skills teach agents HOW to think about a problem; MCP servers
give them the ABILITY to act). Delivery via MCP resources lands in
Slice 3.5b; this slice ships the data model + API surface.

Migration 0049 extends the legacy `skills` table (from 0001_init.sql,
originally a workspace catalog of markdown snippets) with the richer
typing we need — name, when_to_use, tags, source_kind, current_version
— rather than duplicating tables. Also adds:
  - skill_versions       (version history for level-up promotions +
                          rollback; back-pointer via promoted_from
                          JSONB records agent_id / research artifact /
                          brain memory that produced it)
  - template_role_skills (m2m binding skills to team-template roles
                          with pin_in_context + order_idx)
  - agent_skills_ext     (per-agent overlay: include=true adds a skill
                          to the bundle; include=false prunes a
                          template default for this specific agent)

Rust surface:
  - cm_db::repo::skills_catalog with typed Skill/SkillVersion/
    AgentSkillBinding structs + upsert_builtin (idempotent — bumps
    version + appends to skill_versions ONLY when body changes) +
    list_visible/get/get_by_name reads + template + agent binding
    helpers + effective_for_agent (merges template defaults with
    agent overrides, applies exclude precedence, batch-fetches skill
    bodies)
  - cm_api::routes::skills_catalog with:
      GET /api/skills             — list visible
      GET /api/skills/{id}        — detail
      GET /api/claws/{id}/skills  — effective binding (accepts
        template_id + role_slot as query args to merge in template
        defaults)

Follow-ups:
  - Slice 3.5b: clawmates_skills MCP server exposes catalog as MCP
    resources, honoring pin_in_context for auto-injection
  - Slice 3.5c: seed ~40-60 builtin skills across the 6 stacks

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-19 13:00:56 -07:00
co-authored by Claude Opus 4.7
parent 3f77370a6e
commit 1153d72d00
6 changed files with 577 additions and 0 deletions
+1
View File
@@ -26,6 +26,7 @@ pub mod research_setup;
pub mod routines;
pub mod sessions;
pub mod skills;
pub mod skills_catalog;
pub mod slack;
pub mod structure;
pub mod tailscale;
@@ -0,0 +1,57 @@
//! `/api/skills/*` — the skills catalog surface (Slice 3.5a).
//! Slice 3.5b's `clawmates_skills` MCP server layers on top.
use axum::{
extract::{Path, Query, State},
Json,
};
use cm_db::repo::skills_catalog::{AgentSkillBinding, Skill};
use serde::Deserialize;
use uuid::Uuid;
use crate::{ApiError, AppState, Authed};
pub async fn list(
State(state): State<AppState>,
Authed(user): Authed,
) -> Result<Json<Vec<Skill>>, ApiError> {
let rows =
cm_db::repo::skills_catalog::list_visible(&state.pool, user.workspace_id.as_uuid()).await?;
Ok(Json(rows))
}
pub async fn get(
State(state): State<AppState>,
Authed(_user): Authed,
Path(id): Path<Uuid>,
) -> Result<Json<Skill>, ApiError> {
let skill = cm_db::repo::skills_catalog::get(&state.pool, id)
.await?
.ok_or(ApiError::NotFound)?;
Ok(Json(skill))
}
#[derive(Debug, Deserialize)]
pub struct AgentBindingQuery {
pub template_id: Option<Uuid>,
pub role_slot: Option<String>,
}
/// Effective skill bundle for a specific agent. When `template_id` +
/// `role_slot` are provided, template defaults are merged with the
/// agent's per-agent overrides; otherwise only overrides are returned.
pub async fn effective_for_agent(
State(state): State<AppState>,
Authed(_user): Authed,
Path(agent_id): Path<Uuid>,
Query(q): Query<AgentBindingQuery>,
) -> Result<Json<Vec<AgentSkillBinding>>, ApiError> {
let rows = cm_db::repo::skills_catalog::effective_for_agent(
&state.pool,
agent_id,
q.template_id,
q.role_slot.as_deref(),
)
.await?;
Ok(Json(rows))
}