slice 3: 6 team templates seeded from TOML recipes
Team templates are the canonical rosters + tool bundles that mint
concrete teams for a mission. Every builtin ships as a TOML recipe
under templates/teams/*.toml, loaded into the DB at server boot.
Migration 0048 adds:
- team_templates (id, key, name, stack, default_topology,
risk_profile, mcp_bundles, version, source,
workspace_id)
- template_roles (m2m: template_id + slot; system_prompt,
skills[], brain_seed)
- teams gets template_id + template_version for level-up lineage
Ships 6 builtins:
- rust_sdlc — planner/coder/tester/reviewer/committer for Rust
- backend — api_designer/db_engineer/coder/tester/committer
(Postgres, DuckDB, graph DBs, wire protocols)
- frontend — designer/coder/tester/committer (React + Tailwind + ShadCN)
- mobile — designer/coder/tester/committer (Expo, RN, iOS, Android)
- gpu — arch_analyst/kernel_author/bench_engineer/coder/committer
(CUDA, Metal, ROCm from Rust)
- threejs — scene_designer/coder/shader_author/perf_engineer/
committer (three.js, WebGL, WebGPU)
Each role has a versioned system_prompt + skill list + brain_seed
markdown. Skills column is a name array today; Slice 3.5a promotes it
to a typed m2m join with the real skills catalog.
Server boot:
- team_template_loader::load_builtins reads TOML from
/etc/clawmates/templates/teams (container) or templates/teams (dev),
upserts idempotently. Deterministic uuid per template key (sha256
of a fixed namespace + key) so ids are stable across boots.
- Dockerfile copies templates/ to /etc/clawmates/templates.
Read API:
- GET /api/team-templates — list all
- GET /api/team-templates/{id} — detail with roles
Wizard:
- Step 3 rewired from a raw team_id text field to a template picker
with "LLM auto-provision" as the default option + one card per
builtin, showing stack, topology, risk profile, and description.
- Mission create now passes team_template_id (not team_id) so phase
execution knows which template to mint from.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
fc67936e33
commit
9ba5c06a1a
@@ -271,6 +271,16 @@ async fn run() -> Result<(), String> {
|
|||||||
runtime.clone(),
|
runtime.clone(),
|
||||||
std::time::Duration::from_secs(3),
|
std::time::Duration::from_secs(3),
|
||||||
);
|
);
|
||||||
|
// Load builtin team templates from disk into team_templates +
|
||||||
|
// template_roles. Idempotent per boot; edits to templates/teams/*.toml
|
||||||
|
// go live on the next deploy.
|
||||||
|
{
|
||||||
|
let pool = pool.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let n = cm_api::team_template_loader::load_builtins(&pool).await;
|
||||||
|
eprintln!("team_template_loader: loaded {n} builtin team template(s)");
|
||||||
|
});
|
||||||
|
}
|
||||||
// Outbound-email delivery: drains the §15-gated `outbox` over SMTP. Inert
|
// Outbound-email delivery: drains the §15-gated `outbox` over SMTP. Inert
|
||||||
// until CLAWMATES_SMTP_* is set, so it ships safely before credentials exist.
|
// until CLAWMATES_SMTP_* is set, so it ships safely before credentials exist.
|
||||||
cm_runtime::spawn_drainer(pool.clone(), std::time::Duration::from_secs(10));
|
cm_runtime::spawn_drainer(pool.clone(), std::time::Duration::from_secs(10));
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
getrandom = "0.2"
|
getrandom = "0.2"
|
||||||
|
toml = "0.8"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub mod research_container;
|
|||||||
mod routes;
|
mod routes;
|
||||||
mod runtime_provision;
|
mod runtime_provision;
|
||||||
pub mod swarm;
|
pub mod swarm;
|
||||||
|
pub mod team_template_loader;
|
||||||
pub mod tool_versions;
|
pub mod tool_versions;
|
||||||
mod topology_exec;
|
mod topology_exec;
|
||||||
pub mod topology_worker;
|
pub mod topology_worker;
|
||||||
@@ -401,6 +402,14 @@ pub fn router(state: AppState) -> Router {
|
|||||||
axum::routing::patch(routes::orgs::rename_org),
|
axum::routing::patch(routes::orgs::rename_org),
|
||||||
)
|
)
|
||||||
.route("/api/orgs/{id}/run", post(routes::orgs::run_org))
|
.route("/api/orgs/{id}/run", post(routes::orgs::run_org))
|
||||||
|
// Team templates — Slice 3 read-only surface. Server upserts
|
||||||
|
// builtins from TOML on boot; workspace-authored templates
|
||||||
|
// land here in a later slice.
|
||||||
|
.route("/api/team-templates", get(routes::team_templates::list))
|
||||||
|
.route(
|
||||||
|
"/api/team-templates/{id}",
|
||||||
|
get(routes::team_templates::get),
|
||||||
|
)
|
||||||
// Missions — Slice 1 skeleton. Runs in parallel with the
|
// Missions — Slice 1 skeleton. Runs in parallel with the
|
||||||
// legacy research/loops routes until Slice 9's cutover.
|
// legacy research/loops routes until Slice 9's cutover.
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub mod health;
|
|||||||
pub mod identity;
|
pub mod identity;
|
||||||
pub mod loops;
|
pub mod loops;
|
||||||
pub mod missions;
|
pub mod missions;
|
||||||
|
pub mod team_templates;
|
||||||
pub mod nodes;
|
pub mod nodes;
|
||||||
pub mod oauth;
|
pub mod oauth;
|
||||||
pub mod orgs;
|
pub mod orgs;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//! `/api/team-templates/*` — expose builtin + workspace team templates
|
||||||
|
//! to the wizard's team-picker step.
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, State},
|
||||||
|
};
|
||||||
|
use cm_db::repo::team_templates::{TeamTemplate, TeamTemplateDetail};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{ApiError, AppState, Authed};
|
||||||
|
|
||||||
|
pub async fn list(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authed(_user): Authed,
|
||||||
|
) -> Result<Json<Vec<TeamTemplate>>, ApiError> {
|
||||||
|
// Builtins are cross-workspace; workspace-authored templates are
|
||||||
|
// filtered by the repo layer (this route only exposes builtins for
|
||||||
|
// now — Slice 3 doesn't ship a workspace template editor yet).
|
||||||
|
let rows = cm_db::repo::team_templates::list_all(&state.pool).await?;
|
||||||
|
Ok(Json(rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authed(_user): Authed,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> Result<Json<TeamTemplateDetail>, ApiError> {
|
||||||
|
let d = cm_db::repo::team_templates::get(&state.pool, id)
|
||||||
|
.await?
|
||||||
|
.ok_or(ApiError::NotFound)?;
|
||||||
|
Ok(Json(d))
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
//! Load builtin team template TOML recipes from disk into
|
||||||
|
//! `team_templates` + `template_roles` at server boot.
|
||||||
|
//!
|
||||||
|
//! Templates ship under `templates/teams/*.toml` (repo-root path,
|
||||||
|
//! bundled into the server container image). The loader is idempotent:
|
||||||
|
//! re-upserts every boot so template edits go live on the next
|
||||||
|
//! deploy without needing a manual migration.
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use cm_db::repo::team_templates::{
|
||||||
|
upsert_builtin, UpsertBuiltin, UpsertBuiltinRole,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Deterministic id per builtin template key. Rolling to sha256 of a
|
||||||
|
/// stable namespace + the key gives us a v4-shaped id that never
|
||||||
|
/// changes across boots (the uuid crate's v5 feature isn't enabled in
|
||||||
|
/// the workspace and we didn't want to bump it just for this).
|
||||||
|
fn builtin_id(key: &str) -> uuid::Uuid {
|
||||||
|
let mut h = Sha256::new();
|
||||||
|
h.update(b"clawmates.builtin.team_template\x00");
|
||||||
|
h.update(key.as_bytes());
|
||||||
|
let digest = h.finalize();
|
||||||
|
let mut bytes = [0u8; 16];
|
||||||
|
bytes.copy_from_slice(&digest[..16]);
|
||||||
|
// Force the v4 layout so the id passes any downstream v4-shaped
|
||||||
|
// checks (version nibble = 4, variant bits = 10xx).
|
||||||
|
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||||
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||||
|
uuid::Uuid::from_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct TemplateFile {
|
||||||
|
key: String,
|
||||||
|
name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
stack: Vec<String>,
|
||||||
|
default_topology: String,
|
||||||
|
risk_profile: String,
|
||||||
|
#[serde(default)]
|
||||||
|
mcp_bundles: Vec<String>,
|
||||||
|
#[serde(default = "default_version")]
|
||||||
|
version: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
config: serde_json::Value,
|
||||||
|
#[serde(default)]
|
||||||
|
roles: Vec<TemplateRoleFile>,
|
||||||
|
}
|
||||||
|
fn default_version() -> i32 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct TemplateRoleFile {
|
||||||
|
slot: String,
|
||||||
|
order_idx: i32,
|
||||||
|
system_prompt: String,
|
||||||
|
#[serde(default)]
|
||||||
|
skills: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
brain_seed: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn templates_dir() -> PathBuf {
|
||||||
|
if let Ok(d) = std::env::var("CLAWMATES_TEAM_TEMPLATES_DIR") {
|
||||||
|
return PathBuf::from(d);
|
||||||
|
}
|
||||||
|
// Container default — Dockerfile copies templates/ to /etc/clawmates/templates.
|
||||||
|
let container = PathBuf::from("/etc/clawmates/templates/teams");
|
||||||
|
if container.exists() {
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
// Dev fallback — repo-relative.
|
||||||
|
PathBuf::from("templates/teams")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read every `*.toml` under the templates dir and upsert. Skips files
|
||||||
|
/// that fail to parse but logs the reason so a broken template can't
|
||||||
|
/// block server boot entirely.
|
||||||
|
pub async fn load_builtins(pool: &PgPool) -> usize {
|
||||||
|
let dir = templates_dir();
|
||||||
|
let entries = match std::fs::read_dir(&dir) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"team_template_loader: templates dir {} not readable: {e} — skipping builtin seed",
|
||||||
|
dir.display()
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut loaded = 0usize;
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(|s| s.to_str()) != Some("toml") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match load_one(pool, &path).await {
|
||||||
|
Ok(key) => {
|
||||||
|
loaded += 1;
|
||||||
|
eprintln!("team_template_loader: upserted builtin {key}");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"team_template_loader: failed to load {}: {e}",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
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 file: TemplateFile = toml::from_str(&text)
|
||||||
|
.map_err(|e| format!("parse {}: {e}", path.display()))?;
|
||||||
|
let key = file.key.clone();
|
||||||
|
|
||||||
|
let roles: Vec<UpsertBuiltinRole<'_>> = file
|
||||||
|
.roles
|
||||||
|
.iter()
|
||||||
|
.map(|r| UpsertBuiltinRole {
|
||||||
|
slot: &r.slot,
|
||||||
|
order_idx: r.order_idx,
|
||||||
|
system_prompt: &r.system_prompt,
|
||||||
|
skills: r.skills.clone(),
|
||||||
|
brain_seed: r.brain_seed.as_deref(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let builtin = UpsertBuiltin {
|
||||||
|
id: builtin_id(&file.key),
|
||||||
|
key: &file.key,
|
||||||
|
name: &file.name,
|
||||||
|
stack: file.stack.clone(),
|
||||||
|
default_topology: &file.default_topology,
|
||||||
|
risk_profile: &file.risk_profile,
|
||||||
|
mcp_bundles: file.mcp_bundles.clone(),
|
||||||
|
version: file.version,
|
||||||
|
description: file.description.as_deref(),
|
||||||
|
config: file.config.clone(),
|
||||||
|
roles,
|
||||||
|
};
|
||||||
|
upsert_builtin(pool, builtin)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("upsert {key}: {e}"))?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ pub mod sessions;
|
|||||||
pub mod skills;
|
pub mod skills;
|
||||||
pub mod steps;
|
pub mod steps;
|
||||||
pub mod structure_reify;
|
pub mod structure_reify;
|
||||||
|
pub mod team_templates;
|
||||||
pub mod teams;
|
pub mod teams;
|
||||||
pub mod terminal_tabs;
|
pub mod terminal_tabs;
|
||||||
pub mod threads;
|
pub mod threads;
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
//! Team templates — canonical rosters + tool bundles that materialize
|
||||||
|
//! concrete `teams` rows. Slice 3 of the missions consolidation.
|
||||||
|
//!
|
||||||
|
//! Rows with `source = 'builtin'` are re-upserted from disk (TOML
|
||||||
|
//! recipes under `templates/teams/`) at server boot. `source = 'user'`
|
||||||
|
//! rows are workspace-authored and never overwritten by the loader.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::DbError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TeamTemplate {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub key: String,
|
||||||
|
pub name: String,
|
||||||
|
pub stack: Vec<String>,
|
||||||
|
pub default_topology: String,
|
||||||
|
pub risk_profile: String,
|
||||||
|
pub mcp_bundles: Vec<String>,
|
||||||
|
pub version: i32,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub config: Value,
|
||||||
|
pub source: String,
|
||||||
|
pub workspace_id: Option<Uuid>,
|
||||||
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
|
pub created_at: OffsetDateTime,
|
||||||
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
|
pub updated_at: OffsetDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TemplateRole {
|
||||||
|
pub template_id: Uuid,
|
||||||
|
pub slot: String,
|
||||||
|
pub order_idx: i32,
|
||||||
|
pub system_prompt: String,
|
||||||
|
pub skills: Vec<String>,
|
||||||
|
pub brain_seed: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct TeamTemplateDetail {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub template: TeamTemplate,
|
||||||
|
pub roles: Vec<TemplateRole>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UpsertBuiltinRole<'a> {
|
||||||
|
pub slot: &'a str,
|
||||||
|
pub order_idx: i32,
|
||||||
|
pub system_prompt: &'a str,
|
||||||
|
pub skills: Vec<String>,
|
||||||
|
pub brain_seed: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UpsertBuiltin<'a> {
|
||||||
|
/// Caller-provided deterministic id (typically a hash of `key`
|
||||||
|
/// computed in cm-api's loader — cm-db doesn't need the hashing
|
||||||
|
/// dep just for this one thing).
|
||||||
|
pub id: Uuid,
|
||||||
|
pub key: &'a str,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub stack: Vec<String>,
|
||||||
|
pub default_topology: &'a str,
|
||||||
|
pub risk_profile: &'a str,
|
||||||
|
pub mcp_bundles: Vec<String>,
|
||||||
|
pub version: i32,
|
||||||
|
pub description: Option<&'a str>,
|
||||||
|
pub config: Value,
|
||||||
|
pub roles: Vec<UpsertBuiltinRole<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upsert a builtin template + its roles in one txn. Idempotent.
|
||||||
|
pub async fn upsert_builtin(pool: &PgPool, b: UpsertBuiltin<'_>) -> Result<Uuid, DbError> {
|
||||||
|
let id = b.id;
|
||||||
|
let mut tx = pool.begin().await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO team_templates
|
||||||
|
(id, key, name, stack, default_topology, risk_profile,
|
||||||
|
mcp_bundles, version, description, config, source, workspace_id)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'builtin',NULL)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
key = EXCLUDED.key,
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
stack = EXCLUDED.stack,
|
||||||
|
default_topology = EXCLUDED.default_topology,
|
||||||
|
risk_profile = EXCLUDED.risk_profile,
|
||||||
|
mcp_bundles = EXCLUDED.mcp_bundles,
|
||||||
|
version = EXCLUDED.version,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
config = EXCLUDED.config,
|
||||||
|
updated_at = now()",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.bind(b.key)
|
||||||
|
.bind(b.name)
|
||||||
|
.bind(&b.stack)
|
||||||
|
.bind(b.default_topology)
|
||||||
|
.bind(b.risk_profile)
|
||||||
|
.bind(&b.mcp_bundles)
|
||||||
|
.bind(b.version)
|
||||||
|
.bind(b.description)
|
||||||
|
.bind(&b.config)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Replace-in-place role set. Roles that get removed from the TOML
|
||||||
|
// disappear from the DB; keeps the on-disk source authoritative.
|
||||||
|
sqlx::query("DELETE FROM template_roles WHERE template_id = $1")
|
||||||
|
.bind(id)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
for r in &b.roles {
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO template_roles
|
||||||
|
(template_id, slot, order_idx, system_prompt, skills, brain_seed)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6)",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.bind(r.slot)
|
||||||
|
.bind(r.order_idx)
|
||||||
|
.bind(r.system_prompt)
|
||||||
|
.bind(&r.skills)
|
||||||
|
.bind(r.brain_seed)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_all(pool: &PgPool) -> Result<Vec<TeamTemplate>, DbError> {
|
||||||
|
use sqlx::Row;
|
||||||
|
let rows = sqlx::query(
|
||||||
|
"SELECT id, key, name, stack, default_topology, risk_profile,
|
||||||
|
mcp_bundles, version, description, config, source,
|
||||||
|
workspace_id, created_at, updated_at
|
||||||
|
FROM team_templates
|
||||||
|
WHERE source = 'builtin' OR workspace_id IS NOT NULL
|
||||||
|
ORDER BY source DESC, name ASC",
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(rows.into_iter().map(row_to_template).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get(pool: &PgPool, id: Uuid) -> Result<Option<TeamTemplateDetail>, DbError> {
|
||||||
|
use sqlx::Row;
|
||||||
|
let Some(t) = sqlx::query(
|
||||||
|
"SELECT id, key, name, stack, default_topology, risk_profile,
|
||||||
|
mcp_bundles, version, description, config, source,
|
||||||
|
workspace_id, created_at, updated_at
|
||||||
|
FROM team_templates WHERE id = $1",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?
|
||||||
|
.map(row_to_template) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let role_rows = sqlx::query(
|
||||||
|
"SELECT template_id, slot, order_idx, system_prompt, skills, brain_seed
|
||||||
|
FROM template_roles WHERE template_id = $1
|
||||||
|
ORDER BY order_idx ASC",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
let roles = role_rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| TemplateRole {
|
||||||
|
template_id: r.get("template_id"),
|
||||||
|
slot: r.get("slot"),
|
||||||
|
order_idx: r.get("order_idx"),
|
||||||
|
system_prompt: r.get("system_prompt"),
|
||||||
|
skills: r.get("skills"),
|
||||||
|
brain_seed: r.get("brain_seed"),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(Some(TeamTemplateDetail { template: t, roles }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_key(pool: &PgPool, key: &str) -> Result<Option<TeamTemplate>, DbError> {
|
||||||
|
use sqlx::Row;
|
||||||
|
let row = sqlx::query(
|
||||||
|
"SELECT id, key, name, stack, default_topology, risk_profile,
|
||||||
|
mcp_bundles, version, description, config, source,
|
||||||
|
workspace_id, created_at, updated_at
|
||||||
|
FROM team_templates WHERE key = $1",
|
||||||
|
)
|
||||||
|
.bind(key)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(row.map(row_to_template))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_to_template(r: sqlx::postgres::PgRow) -> TeamTemplate {
|
||||||
|
use sqlx::Row;
|
||||||
|
TeamTemplate {
|
||||||
|
id: r.get("id"),
|
||||||
|
key: r.get("key"),
|
||||||
|
name: r.get("name"),
|
||||||
|
stack: r.get("stack"),
|
||||||
|
default_topology: r.get("default_topology"),
|
||||||
|
risk_profile: r.get("risk_profile"),
|
||||||
|
mcp_bundles: r.get("mcp_bundles"),
|
||||||
|
version: r.get("version"),
|
||||||
|
description: r.get("description"),
|
||||||
|
config: r.get("config"),
|
||||||
|
source: r.get("source"),
|
||||||
|
workspace_id: r.get("workspace_id"),
|
||||||
|
created_at: r.get("created_at"),
|
||||||
|
updated_at: r.get("updated_at"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
// the template's canned phase composition; Slice 4 threads the TOML
|
// the template's canned phase composition; Slice 4 threads the TOML
|
||||||
// recipe engine through here for real template dispatch.
|
// recipe engine through here for real template dispatch.
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +23,10 @@ import {
|
|||||||
type TemplateKind,
|
type TemplateKind,
|
||||||
type TemplatePreset,
|
type TemplatePreset,
|
||||||
} from "@/lib/api/missions";
|
} from "@/lib/api/missions";
|
||||||
|
import {
|
||||||
|
listTeamTemplates,
|
||||||
|
type TeamTemplate,
|
||||||
|
} from "@/lib/api/team-templates";
|
||||||
import { RepoPicker, type PickedRepo } from "./RepoPicker";
|
import { RepoPicker, type PickedRepo } from "./RepoPicker";
|
||||||
|
|
||||||
const mono =
|
const mono =
|
||||||
@@ -42,7 +46,17 @@ export function MissionWizard({
|
|||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [repo, setRepo] = useState<PickedRepo | null>(null);
|
const [repo, setRepo] = useState<PickedRepo | null>(null);
|
||||||
const [teamId, setTeamId] = useState<string>("");
|
const [teamTemplateId, setTeamTemplateId] = useState<string>("");
|
||||||
|
const [teamTemplates, setTeamTemplates] = useState<TeamTemplate[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
setTeamTemplates(await listTeamTemplates());
|
||||||
|
} catch {
|
||||||
|
// Non-fatal: user can still create a mission without a team template.
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
const [scheduleKind, setScheduleKind] = useState<"one_shot" | "cron">("one_shot");
|
const [scheduleKind, setScheduleKind] = useState<"one_shot" | "cron">("one_shot");
|
||||||
const [cron, setCron] = useState("0 */6 * * *");
|
const [cron, setCron] = useState("0 */6 * * *");
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@@ -71,7 +85,7 @@ export function MissionWizard({
|
|||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
template_kind: templateKind,
|
template_kind: templateKind,
|
||||||
repo_id: repo?.repo_id,
|
repo_id: repo?.repo_id,
|
||||||
team_id: teamId || undefined,
|
team_template_id: teamTemplateId || undefined,
|
||||||
schedule,
|
schedule,
|
||||||
description: description.trim() || undefined,
|
description: description.trim() || undefined,
|
||||||
phases: preset.phases,
|
phases: preset.phases,
|
||||||
@@ -268,18 +282,91 @@ export function MissionWizard({
|
|||||||
|
|
||||||
{step === 3 && (
|
{step === 3 && (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||||
<span style={labelStyle}>Team</span>
|
<span style={labelStyle}>Team template</span>
|
||||||
<p style={hintStyle}>
|
<p style={hintStyle}>
|
||||||
Slice 3 ships team templates + auto-provision. For now, paste an
|
Pick a canonical roster. On mission launch the team is
|
||||||
existing team id or leave blank to let phase execution
|
materialized from the template — roles, prompts, MCP bundles,
|
||||||
auto-provision one from the description.
|
and (later) skills + brain seeds. Leave unset to let the
|
||||||
|
mission auto-provision an LLM-derived team from the prompt.
|
||||||
</p>
|
</p>
|
||||||
<input
|
<div style={{ display: "grid", gap: 8 }}>
|
||||||
value={teamId}
|
<button
|
||||||
onChange={(e) => setTeamId(e.target.value)}
|
type="button"
|
||||||
placeholder="optional team UUID"
|
onClick={() => setTeamTemplateId("")}
|
||||||
style={fieldStyle}
|
style={{
|
||||||
/>
|
...templateCardStyle(teamTemplateId === ""),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontWeight: 700, color: "#f3f3f5" }}>
|
||||||
|
LLM auto-provision
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 12, color: "#a0a0a8" }}>
|
||||||
|
Let Claude Sonnet 5 derive 3–5 roles from the description.
|
||||||
|
Best for one-off or exploratory missions.
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{teamTemplates.map((t) => {
|
||||||
|
const active = teamTemplateId === t.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTeamTemplateId(t.id)}
|
||||||
|
style={templateCardStyle(active)}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
|
<span style={{ fontWeight: 700, color: "#f3f3f5", fontSize: 13.5 }}>
|
||||||
|
{t.name}
|
||||||
|
</span>
|
||||||
|
{t.source === "builtin" && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 9.5,
|
||||||
|
color: "#7cd6e0",
|
||||||
|
letterSpacing: ".1em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
BUILTIN
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
marginLeft: "auto",
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 10,
|
||||||
|
color: "#8a8a92",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.default_topology} · {t.risk_profile}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{t.description && (
|
||||||
|
<span style={{ fontSize: 12, color: "#a0a0a8", lineHeight: 1.5 }}>
|
||||||
|
{t.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
|
||||||
|
{t.stack.map((s) => (
|
||||||
|
<span
|
||||||
|
key={s}
|
||||||
|
style={{
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 10,
|
||||||
|
padding: "2px 7px",
|
||||||
|
borderRadius: 999,
|
||||||
|
border: "1px solid rgba(255,255,255,.1)",
|
||||||
|
color: "#cfcfd5",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -330,7 +417,15 @@ export function MissionWizard({
|
|||||||
<ReviewRow k="Title" v={title} />
|
<ReviewRow k="Title" v={title} />
|
||||||
{description && <ReviewRow k="Description" v={description} />}
|
{description && <ReviewRow k="Description" v={description} />}
|
||||||
{repo && <ReviewRow k="Repo" v={`${repo.owner}/${repo.name}`} />}
|
{repo && <ReviewRow k="Repo" v={`${repo.owner}/${repo.name}`} />}
|
||||||
{teamId && <ReviewRow k="Team" v={teamId} />}
|
<ReviewRow
|
||||||
|
k="Team"
|
||||||
|
v={
|
||||||
|
teamTemplateId
|
||||||
|
? (teamTemplates.find((t) => t.id === teamTemplateId)?.name ??
|
||||||
|
teamTemplateId)
|
||||||
|
: "auto-provision from prompt"
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ReviewRow
|
<ReviewRow
|
||||||
k="Schedule"
|
k="Schedule"
|
||||||
v={scheduleKind === "cron" ? `cron: ${cron}` : "one-shot"}
|
v={scheduleKind === "cron" ? `cron: ${cron}` : "one-shot"}
|
||||||
@@ -458,6 +553,18 @@ const secondaryBtn: React.CSSProperties = {
|
|||||||
fontSize: 12.5,
|
fontSize: 12.5,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
};
|
};
|
||||||
|
const templateCardStyle = (active: boolean): React.CSSProperties => ({
|
||||||
|
textAlign: "left",
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 10,
|
||||||
|
border: `1px solid ${active ? "rgba(255,138,122,.6)" : "rgba(255,255,255,.1)"}`,
|
||||||
|
background: active ? "rgba(255,138,122,.08)" : "#101014",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "#eaeaee",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 6,
|
||||||
|
});
|
||||||
const radioRowStyle = (active: boolean): React.CSSProperties => ({
|
const radioRowStyle = (active: boolean): React.CSSProperties => ({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Team templates API client — builtins loaded from disk at server
|
||||||
|
// boot land here. Slice 3 exposes list + get; workspace-authored
|
||||||
|
// templates come later.
|
||||||
|
|
||||||
|
export interface TeamTemplate {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
stack: string[];
|
||||||
|
default_topology: string;
|
||||||
|
risk_profile: string;
|
||||||
|
mcp_bundles: string[];
|
||||||
|
version: number;
|
||||||
|
description: string | null;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
source: "builtin" | "user";
|
||||||
|
workspace_id: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateRole {
|
||||||
|
template_id: string;
|
||||||
|
slot: string;
|
||||||
|
order_idx: number;
|
||||||
|
system_prompt: string;
|
||||||
|
skills: string[];
|
||||||
|
brain_seed: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamTemplateDetail extends TeamTemplate {
|
||||||
|
roles: TemplateRole[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api<T>(path: string): Promise<T> {
|
||||||
|
const r = await fetch(path);
|
||||||
|
if (!r.ok) {
|
||||||
|
throw new Error(`GET ${path} → ${r.status}`);
|
||||||
|
}
|
||||||
|
return (await r.json()) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listTeamTemplates = () =>
|
||||||
|
api<TeamTemplate[]>("/api/team-templates");
|
||||||
|
|
||||||
|
export const getTeamTemplate = (id: string) =>
|
||||||
|
api<TeamTemplateDetail>(`/api/team-templates/${id}`);
|
||||||
@@ -44,5 +44,7 @@ RUN apt-get update \
|
|||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& useradd -u 65532 -M -s /usr/sbin/nologin nonroot
|
&& useradd -u 65532 -M -s /usr/sbin/nologin nonroot
|
||||||
COPY --from=builder /clawmates-server /usr/local/bin/clawmates-server
|
COPY --from=builder /clawmates-server /usr/local/bin/clawmates-server
|
||||||
|
# Builtin templates (team + workflow). Loader upserts them on boot.
|
||||||
|
COPY templates /etc/clawmates/templates
|
||||||
USER 65532
|
USER 65532
|
||||||
ENTRYPOINT ["/usr/local/bin/clawmates-server"]
|
ENTRYPOINT ["/usr/local/bin/clawmates-server"]
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
-- Slice 3 — team templates.
|
||||||
|
--
|
||||||
|
-- Team templates are the canonical rosters + tool bundles used to
|
||||||
|
-- materialize concrete `teams` rows. They ship as TOML recipes on
|
||||||
|
-- disk (see templates/teams/*.toml) and are upserted into these
|
||||||
|
-- tables at server boot.
|
||||||
|
--
|
||||||
|
-- Later slices layer more on:
|
||||||
|
-- - Slice 3.5a: `skills` + `template_role_skills` (m2m join)
|
||||||
|
-- - Slice 3.5d: `agent_template_link` for level-up lineage
|
||||||
|
--
|
||||||
|
-- Discriminators kept TEXT so new templates ship as PRs without
|
||||||
|
-- schema migrations.
|
||||||
|
|
||||||
|
CREATE TABLE team_templates (
|
||||||
|
-- Deterministic id per template kind so seeded rows are stable
|
||||||
|
-- across boots. We use uuid5 of a namespace + the template `key`
|
||||||
|
-- computed in the loader; recording the key here lets us look
|
||||||
|
-- them up by name from the API.
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
-- Slug used in URLs + TOML filenames (e.g. "rust_sdlc").
|
||||||
|
key TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
stack TEXT[] NOT NULL DEFAULT '{}',
|
||||||
|
default_topology TEXT NOT NULL,
|
||||||
|
risk_profile TEXT NOT NULL,
|
||||||
|
mcp_bundles TEXT[] NOT NULL DEFAULT '{}',
|
||||||
|
version INT NOT NULL DEFAULT 1,
|
||||||
|
description TEXT,
|
||||||
|
-- Free-form for template-specific knobs (default cargo features,
|
||||||
|
-- lint policy, etc.). Merged into missions.config when a mission
|
||||||
|
-- is materialized from this template.
|
||||||
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
-- 'builtin' rows are re-upserted from disk at every boot. Rows
|
||||||
|
-- with source='user' are workspace-authored and never overwritten.
|
||||||
|
source TEXT NOT NULL DEFAULT 'builtin',
|
||||||
|
workspace_id UUID, -- NULL for builtins; set for user rows
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX team_templates_workspace_idx
|
||||||
|
ON team_templates (workspace_id) WHERE workspace_id IS NOT NULL;
|
||||||
|
CREATE INDEX team_templates_source_idx
|
||||||
|
ON team_templates (source);
|
||||||
|
|
||||||
|
CREATE TABLE template_roles (
|
||||||
|
template_id UUID NOT NULL REFERENCES team_templates(id) ON DELETE CASCADE,
|
||||||
|
-- role slot within the template (e.g. "planner", "coder"). Unique
|
||||||
|
-- per template — a template can't have two "coder" slots.
|
||||||
|
slot TEXT NOT NULL,
|
||||||
|
order_idx INT NOT NULL,
|
||||||
|
-- The system prompt for this role. Loaded from disk for builtins;
|
||||||
|
-- editable for user templates.
|
||||||
|
system_prompt TEXT NOT NULL,
|
||||||
|
-- Skills attached by default when the template mints an agent for
|
||||||
|
-- this slot. Stored as a name array; Slice 3.5a introduces the
|
||||||
|
-- typed join table + real skills catalog.
|
||||||
|
skills TEXT[] NOT NULL DEFAULT '{}',
|
||||||
|
-- Brain seed content ingested into a fresh agent's .brain on
|
||||||
|
-- creation (Slice 3.5d actually wires the ingestion; this column
|
||||||
|
-- stores the seed markdown from the template).
|
||||||
|
brain_seed TEXT,
|
||||||
|
PRIMARY KEY (template_id, slot)
|
||||||
|
);
|
||||||
|
CREATE INDEX template_roles_order_idx
|
||||||
|
ON template_roles (template_id, order_idx);
|
||||||
|
|
||||||
|
-- Track which template a team was minted from, so level-up (Slice 8.5)
|
||||||
|
-- can diff learned knowledge back into template PR proposals.
|
||||||
|
ALTER TABLE teams
|
||||||
|
ADD COLUMN template_id UUID REFERENCES team_templates(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN template_version INT;
|
||||||
|
CREATE INDEX teams_template_idx
|
||||||
|
ON teams (template_id, template_version) WHERE template_id IS NOT NULL;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
key = "backend"
|
||||||
|
name = "Backend"
|
||||||
|
description = "Rust backend teams — Postgres, DuckDB, graph databases, APIs, wire protocols, middleware."
|
||||||
|
stack = ["rust", "postgres", "duckdb", "graph", "api", "middleware"]
|
||||||
|
default_topology = "pipeline"
|
||||||
|
risk_profile = "coding_readwrite"
|
||||||
|
mcp_bundles = ["clawmates_door", "gitea_forge"]
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "api_designer"
|
||||||
|
order_idx = 0
|
||||||
|
skills = ["decompose_int_items", "openapi_schema", "small_focused_commits"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the API DESIGNER of a Backend team.
|
||||||
|
|
||||||
|
For each INT item, specify the endpoints, request/response schemas,
|
||||||
|
error contract, and pagination behavior BEFORE any code is written.
|
||||||
|
Prefer additive changes; call out breaking ones explicitly.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# API design memory seed
|
||||||
|
|
||||||
|
## Wire discipline
|
||||||
|
- Every list endpoint paginates from day 1.
|
||||||
|
- Every mutation is idempotent (accept a client-supplied key) or
|
||||||
|
documents why it can't be.
|
||||||
|
- Errors use RFC 7807 problem details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "db_engineer"
|
||||||
|
order_idx = 1
|
||||||
|
skills = ["postgres_migrations", "index_selection", "explain_analyze", "write_rust", "workspace_repo_edit"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the DB ENGINEER of a Backend team.
|
||||||
|
|
||||||
|
Own migrations, indexes, transaction boundaries, and query plans.
|
||||||
|
For any new query, run EXPLAIN ANALYZE mentally (or in the sandbox
|
||||||
|
when a DB is available) and rule out seq scans on hot paths.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# DB memory seed
|
||||||
|
|
||||||
|
## Migration protocol
|
||||||
|
- Every migration is forward-only + reversible-by-new-migration.
|
||||||
|
- Never DROP without a two-release deprecation window.
|
||||||
|
- CREATE INDEX CONCURRENTLY on tables > 1M rows.
|
||||||
|
|
||||||
|
## Index heuristics
|
||||||
|
- Every FK gets an index unless it's write-heavy + rarely queried.
|
||||||
|
- Partial indexes for high-cardinality boolean filters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "coder"
|
||||||
|
order_idx = 2
|
||||||
|
skills = ["write_rust", "cargo_build", "cargo_test", "workspace_repo_edit", "git_commit_protocol"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the CODER of a Backend team.
|
||||||
|
|
||||||
|
Implement per the API DESIGNER + DB ENGINEER handoffs. Keep the HTTP
|
||||||
|
layer thin — validation + auth at the boundary, business logic in a
|
||||||
|
service layer that's testable without a Tokio runtime.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "tester"
|
||||||
|
order_idx = 3
|
||||||
|
skills = ["cargo_test", "integration_tests_pg", "coverage_report"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the TESTER of a Backend team.
|
||||||
|
|
||||||
|
Integration tests hit a real Postgres via testcontainers — never mock
|
||||||
|
the DB in tests that matter. Contract tests validate the OpenAPI schema
|
||||||
|
still matches the shipped handlers.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "committer"
|
||||||
|
order_idx = 4
|
||||||
|
skills = ["git_commit_protocol", "small_focused_commits"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the COMMITTER. Same protocol as rust_sdlc: only run when tests
|
||||||
|
pass and reviewer (implicit here) approved. Emit COMPLETED: INT-<NN>.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
key = "frontend"
|
||||||
|
name = "Frontend"
|
||||||
|
description = "Latest React + TailwindCSS + ShadCN — component authoring, accessibility, responsive design."
|
||||||
|
stack = ["typescript", "react", "tailwindcss", "shadcn", "next"]
|
||||||
|
default_topology = "pipeline"
|
||||||
|
risk_profile = "coding_readwrite"
|
||||||
|
mcp_bundles = ["clawmates_door", "gitea_forge"]
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "designer"
|
||||||
|
order_idx = 0
|
||||||
|
skills = ["decompose_int_items", "design_system_check", "a11y_checklist"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the DESIGNER of a Frontend team.
|
||||||
|
|
||||||
|
For each INT item: sketch the component's states, define props, list
|
||||||
|
edge cases (empty / loading / error / RTL / dark mode), and pick the
|
||||||
|
ShadCN primitives to compose. Hand off to the coder with the exact
|
||||||
|
Tailwind class names and any a11y notes.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# Frontend design seed
|
||||||
|
|
||||||
|
## Every component gets four states
|
||||||
|
- empty (no data yet)
|
||||||
|
- loading (skeleton, never a spinner alone)
|
||||||
|
- error (with recovery affordance)
|
||||||
|
- ready
|
||||||
|
|
||||||
|
## Never author raw HTML for a primitive ShadCN ships.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "coder"
|
||||||
|
order_idx = 1
|
||||||
|
skills = ["write_typescript_react", "tailwind_idioms", "workspace_repo_edit", "git_commit_protocol"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the CODER of a Frontend team.
|
||||||
|
|
||||||
|
Working directory /workspace/repo. Implement per the DESIGNER's spec.
|
||||||
|
Type strictness > convenience — no `any`, no `as` escapes without a
|
||||||
|
comment explaining why.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# React/TS memory seed
|
||||||
|
- Server components by default; 'use client' only where interactivity
|
||||||
|
or hooks demand it.
|
||||||
|
- Tailwind class order: layout → box → typography → color → state.
|
||||||
|
- No inline styles on components that render more than once.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "tester"
|
||||||
|
order_idx = 2
|
||||||
|
skills = ["playwright_e2e", "vitest_unit", "a11y_axe"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the TESTER of a Frontend team.
|
||||||
|
|
||||||
|
Unit tests with Vitest for pure components. Playwright for flows.
|
||||||
|
Axe scan every page under test — a11y regressions fail the build.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "committer"
|
||||||
|
order_idx = 3
|
||||||
|
skills = ["git_commit_protocol", "small_focused_commits"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the COMMITTER. Only run on green tests + a11y pass. Emit
|
||||||
|
COMPLETED: INT-<NN>.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
key = "gpu"
|
||||||
|
name = "GPU Programming"
|
||||||
|
description = "CUDA, Metal, ROCm from Rust — low-level GPU application development, kernel authoring, memory hierarchy tuning."
|
||||||
|
stack = ["rust", "cuda", "metal", "rocm", "gpu"]
|
||||||
|
default_topology = "pipeline"
|
||||||
|
risk_profile = "coding_readwrite"
|
||||||
|
mcp_bundles = ["clawmates_door", "gitea_forge"]
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "arch_analyst"
|
||||||
|
order_idx = 0
|
||||||
|
skills = ["decompose_int_items", "gpu_arch_reference", "roofline_model"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the ARCHITECTURE ANALYST of a GPU team.
|
||||||
|
|
||||||
|
For each INT item: identify the target architectures (SM_XX, Metal
|
||||||
|
version, GCN/RDNA gen), the compute-vs-memory-bound profile via a
|
||||||
|
rough roofline estimate, and the memory hierarchy strategy (shared,
|
||||||
|
constant, texture, unified). Hand off with target occupancy + tile
|
||||||
|
shape recommendations.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# GPU arch seed
|
||||||
|
- CUDA: prefer warp-level primitives (shfl_sync) over shared mem when
|
||||||
|
data fits.
|
||||||
|
- Metal: threadgroup memory is 32KB on Apple7+; plan tiles around it.
|
||||||
|
- ROCm: LDS is 64KB; wavefront is 64 threads (vs CUDA's 32).
|
||||||
|
- Always check bandwidth-bound vs compute-bound BEFORE optimizing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "kernel_author"
|
||||||
|
order_idx = 1
|
||||||
|
skills = ["write_cuda", "write_metal", "write_rocm", "write_rust_ffi", "workspace_repo_edit", "git_commit_protocol"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the KERNEL AUTHOR of a GPU team.
|
||||||
|
|
||||||
|
Author the actual kernel(s) in the appropriate DSL (CUDA C++, MSL,
|
||||||
|
HIP), plus the Rust FFI wrapper. Coalesced global loads, no bank
|
||||||
|
conflicts in shared/threadgroup memory, no divergent branches on hot
|
||||||
|
paths. Prove each of those in a comment.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# Kernel seed
|
||||||
|
- Coalescing rule: consecutive threads read consecutive 32/64/128-bit
|
||||||
|
words. Violating it = 10× slowdown.
|
||||||
|
- Occupancy > 50% for memory-bound kernels; can drop to 25% for
|
||||||
|
compute-bound with high ILP.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "bench_engineer"
|
||||||
|
order_idx = 2
|
||||||
|
skills = ["nsight_profile", "metal_frame_capture", "rocprof", "criterion_bench"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the BENCH ENGINEER of a GPU team.
|
||||||
|
|
||||||
|
Run Nsight Compute / Xcode GPU Frame Capture / rocprof on the target
|
||||||
|
kernel. Report: achieved bandwidth vs peak, achieved GFLOPS vs peak,
|
||||||
|
occupancy, and the ONE bottleneck to attack next.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "coder"
|
||||||
|
order_idx = 3
|
||||||
|
skills = ["write_rust", "cargo_build", "cargo_test", "workspace_repo_edit"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the RUST-SIDE CODER. Integrate the kernel + FFI into the Rust
|
||||||
|
library, add safe wrappers, and expose ergonomic APIs. Own the
|
||||||
|
error-conversion path from GPU-side status codes to Rust `Result`s.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "committer"
|
||||||
|
order_idx = 4
|
||||||
|
skills = ["git_commit_protocol", "small_focused_commits"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the COMMITTER. Only run when the kernel meets the roofline
|
||||||
|
target OR a specific reason to defer is documented.
|
||||||
|
Emit COMPLETED: INT-<NN>.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
key = "mobile"
|
||||||
|
name = "Mobile"
|
||||||
|
description = "Expo + React Native for iOS/Android — camera, comms, networking, native modules."
|
||||||
|
stack = ["typescript", "react-native", "expo", "ios", "android"]
|
||||||
|
default_topology = "pipeline"
|
||||||
|
risk_profile = "coding_readwrite"
|
||||||
|
mcp_bundles = ["clawmates_door", "gitea_forge"]
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "designer"
|
||||||
|
order_idx = 0
|
||||||
|
skills = ["decompose_int_items", "ios_hig_check", "material_you_check"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the DESIGNER of a Mobile team.
|
||||||
|
|
||||||
|
For each INT item: define the screen's state model, navigation
|
||||||
|
integration, platform-specific behavior (safe-area on iOS, back-button
|
||||||
|
on Android), and any native-module permissions required.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# Mobile design seed
|
||||||
|
- Safe-area padding is mandatory on every screen.
|
||||||
|
- Deep links go through the app's central router — never hand-crafted.
|
||||||
|
- Every permission prompt has a pre-prompt explaining WHY.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "coder"
|
||||||
|
order_idx = 1
|
||||||
|
skills = ["write_typescript_react_native", "expo_managed_workflow", "workspace_repo_edit", "git_commit_protocol"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the CODER of a Mobile team.
|
||||||
|
|
||||||
|
Working directory /workspace/repo. Prefer Expo's managed workflow;
|
||||||
|
justify any drop to bare workflow. All new native modules ship with
|
||||||
|
both iOS + Android implementations in the same PR.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# RN memory seed
|
||||||
|
- Prefer FlashList over FlatList for anything > 20 items.
|
||||||
|
- Reanimated for anything animating > 3× per frame.
|
||||||
|
- No inline require() — dynamic imports break Metro's tree-shake.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "tester"
|
||||||
|
order_idx = 2
|
||||||
|
skills = ["detox_e2e", "jest_unit", "ios_simulator_check", "android_emulator_check"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the TESTER of a Mobile team.
|
||||||
|
|
||||||
|
Detox e2e on both platforms. Snapshot tests for critical layouts.
|
||||||
|
Run on both iPhone 15 Pro simulator + Pixel 8 emulator before signing
|
||||||
|
off.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "committer"
|
||||||
|
order_idx = 3
|
||||||
|
skills = ["git_commit_protocol", "small_focused_commits"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the COMMITTER. Only run on green tests both platforms.
|
||||||
|
Emit COMPLETED: INT-<NN>.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
key = "rust_sdlc"
|
||||||
|
name = "Rust SDLC"
|
||||||
|
description = "Full software lifecycle for Rust projects — planning, implementation, testing, review, commit. Deep systems + distributed + enterprise backend expertise."
|
||||||
|
stack = ["rust", "systems", "distributed", "backend"]
|
||||||
|
default_topology = "pipeline"
|
||||||
|
risk_profile = "coding_readwrite"
|
||||||
|
mcp_bundles = ["clawmates_door", "gitea_forge"]
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "planner"
|
||||||
|
order_idx = 0
|
||||||
|
skills = ["read_roadmap", "decompose_int_items", "estimate_effort", "small_focused_commits"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the PLANNER of a Rust SDLC team.
|
||||||
|
|
||||||
|
Read the mission's roadmap or research artifact. Decompose the next
|
||||||
|
unconsumed unit of work into concrete INT-XX items with clear acceptance
|
||||||
|
criteria. Estimate effort. Hand off to the coder with:
|
||||||
|
- a specific INT id and title
|
||||||
|
- the files most likely to change
|
||||||
|
- explicit test coverage requirements (≥90%)
|
||||||
|
- any invariants that must not break
|
||||||
|
|
||||||
|
Emit `TASK: INT-<NN> — <title>` on its own line when you begin a new item,
|
||||||
|
and `PLAN_COMPLETE: INT-<NN>` when the plan is fully specified.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# Planner memory seed — Rust SDLC
|
||||||
|
|
||||||
|
## Decomposition heuristics
|
||||||
|
- Every INT item should be small enough that one coder can finish in
|
||||||
|
under 2 hours of focused work.
|
||||||
|
- If an INT touches more than 5 files, split it.
|
||||||
|
- Prefer refactor-then-feature over feature-with-refactor.
|
||||||
|
|
||||||
|
## Acceptance criteria checklist
|
||||||
|
- Behavior described in observable terms (input → output)
|
||||||
|
- Coverage bar named (usually ≥90% for changed lines)
|
||||||
|
- Backwards-compat expectations stated
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "coder"
|
||||||
|
order_idx = 1
|
||||||
|
skills = ["write_rust", "cargo_build", "cargo_test", "workspace_repo_edit", "small_focused_commits", "git_commit_protocol"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the CODER of a Rust SDLC team.
|
||||||
|
|
||||||
|
Your working directory is /workspace/repo. All edits happen there.
|
||||||
|
Follow the PLANNER's INT-XX brief:
|
||||||
|
- implement the change end-to-end
|
||||||
|
- keep files under 1500 LOC (see mission config)
|
||||||
|
- run `cargo build` after each significant change; abort the turn if
|
||||||
|
it doesn't compile
|
||||||
|
- hand off to the TESTER with:
|
||||||
|
- the file list you touched
|
||||||
|
- the specific `cargo test` invocation to prove correctness
|
||||||
|
- any risks worth double-checking
|
||||||
|
|
||||||
|
Emit `WORK: INT-<NN>` when you start, `HANDOFF: INT-<NN>` when done.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# Coder memory seed — Rust SDLC
|
||||||
|
|
||||||
|
## House Rust style
|
||||||
|
- 2024 edition, MSRV 1.98.0+
|
||||||
|
- prefer `let-else` over deep nesting
|
||||||
|
- `?`-based error propagation with `anyhow::Context` at boundaries
|
||||||
|
- struct-of-args when a fn exceeds 5 params
|
||||||
|
|
||||||
|
## Anti-patterns to avoid
|
||||||
|
- unwrap() in library code
|
||||||
|
- clone() as a "make the borrow checker shut up" shortcut
|
||||||
|
- Arc<Mutex<T>> when a channel would do
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "tester"
|
||||||
|
order_idx = 2
|
||||||
|
skills = ["cargo_test", "cargo_nextest", "coverage_report", "criterion_bench"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the TESTER of a Rust SDLC team.
|
||||||
|
|
||||||
|
Given the CODER's handoff, run the tests the planner specified. Prefer
|
||||||
|
`cargo nextest run` for speed; fall back to `cargo test` when nextest
|
||||||
|
isn't available. Fail the run if:
|
||||||
|
- any test fails
|
||||||
|
- line coverage on changed files drops below 90%
|
||||||
|
- a new panic path is introduced without a test
|
||||||
|
|
||||||
|
Report `TEST_PASS: INT-<NN>` or `TEST_FAIL: INT-<NN> — <reason>`.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "reviewer"
|
||||||
|
order_idx = 3
|
||||||
|
skills = ["code_review_checklist", "read_diff", "small_focused_commits"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the REVIEWER of a Rust SDLC team.
|
||||||
|
|
||||||
|
Read the CODER's diff. Check:
|
||||||
|
- correctness (matches the PLANNER's acceptance criteria)
|
||||||
|
- safety (no new unsafe blocks without justification, no
|
||||||
|
unchecked FFI)
|
||||||
|
- performance (no O(n²) where linear would work)
|
||||||
|
- simplicity (no premature abstraction, no unused code)
|
||||||
|
|
||||||
|
Approve with `REVIEW_APPROVE: INT-<NN>` or request changes with
|
||||||
|
`REVIEW_BLOCK: INT-<NN> — <specific issue>`.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "committer"
|
||||||
|
order_idx = 4
|
||||||
|
skills = ["git_commit_protocol", "workspace_repo_edit", "small_focused_commits"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the COMMITTER of a Rust SDLC team.
|
||||||
|
|
||||||
|
Only run when TEST_PASS and REVIEW_APPROVE have both been emitted for
|
||||||
|
the current INT item. Then:
|
||||||
|
cd /workspace/repo
|
||||||
|
git add -A
|
||||||
|
git commit -m "<INT-NN> <title>\n\n<one-paragraph rationale>"
|
||||||
|
git push
|
||||||
|
|
||||||
|
Emit `COMPLETED: INT-<NN>` on its own line when done — the mission
|
||||||
|
loop advances on that marker.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
key = "threejs"
|
||||||
|
name = "three.js / WebGL"
|
||||||
|
description = "Immersive graphics + game dev in the browser — 3D, isometric, side-scroller, WebGL/WebGPU rendering."
|
||||||
|
stack = ["typescript", "threejs", "webgl", "webgpu", "gsap"]
|
||||||
|
default_topology = "pipeline"
|
||||||
|
risk_profile = "coding_readwrite"
|
||||||
|
mcp_bundles = ["clawmates_door", "gitea_forge"]
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "scene_designer"
|
||||||
|
order_idx = 0
|
||||||
|
skills = ["decompose_int_items", "scene_graph_planning"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the SCENE DESIGNER of a three.js team.
|
||||||
|
|
||||||
|
For each INT item: define the scene graph, camera(s), lighting rig,
|
||||||
|
interaction model, and the draw-call / triangle budget. Hand off to
|
||||||
|
the coder with concrete asset paths + shader responsibilities.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# Scene design seed
|
||||||
|
- Target: 60fps at 1440p on mid-range hardware.
|
||||||
|
- Draw calls < 200 per frame; instance ruthlessly.
|
||||||
|
- One directional shadow-caster max; the rest are baked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "coder"
|
||||||
|
order_idx = 1
|
||||||
|
skills = ["write_typescript", "threejs_idioms", "workspace_repo_edit", "git_commit_protocol"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the CODER of a three.js team.
|
||||||
|
|
||||||
|
Working directory /workspace/repo. Prefer InstancedMesh over per-node
|
||||||
|
Meshes. Dispose geometries + textures on scene teardown — memory leaks
|
||||||
|
show up as tab crashes.
|
||||||
|
"""
|
||||||
|
brain_seed = """
|
||||||
|
# three.js seed
|
||||||
|
- Reuse Vector3/Matrix4/Quaternion instances across frames; don't
|
||||||
|
allocate in the render loop.
|
||||||
|
- Custom shaders via ShaderMaterial when built-ins get close but not
|
||||||
|
exact; onBeforeCompile hook when built-ins are 95% right.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "shader_author"
|
||||||
|
order_idx = 2
|
||||||
|
skills = ["write_glsl", "write_wgsl", "write_typescript"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the SHADER AUTHOR. Author vertex/fragment shaders (GLSL for
|
||||||
|
WebGL, WGSL for WebGPU). Comment mathematical steps. Provide a
|
||||||
|
reference image in the PR description for anything visual.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "perf_engineer"
|
||||||
|
order_idx = 3
|
||||||
|
skills = ["chrome_devtools_perf", "spector_js_capture", "webgl_frame_capture"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the PERF ENGINEER. Profile with SpectorJS or Chrome DevTools
|
||||||
|
Performance panel. Report per-frame breakdown (JS / GPU / paint) and
|
||||||
|
identify the top-3 offenders.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
|
|
||||||
|
[[roles]]
|
||||||
|
slot = "committer"
|
||||||
|
order_idx = 4
|
||||||
|
skills = ["git_commit_protocol", "small_focused_commits"]
|
||||||
|
system_prompt = """
|
||||||
|
You are the COMMITTER. Only run when perf targets met. Emit
|
||||||
|
COMPLETED: INT-<NN>.
|
||||||
|
"""
|
||||||
|
brain_seed = ""
|
||||||
Reference in New Issue
Block a user