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
@@ -12,7 +12,7 @@
|
||||
// the template's canned phase composition; Slice 4 threads the TOML
|
||||
// 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 {
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
type TemplateKind,
|
||||
type TemplatePreset,
|
||||
} from "@/lib/api/missions";
|
||||
import {
|
||||
listTeamTemplates,
|
||||
type TeamTemplate,
|
||||
} from "@/lib/api/team-templates";
|
||||
import { RepoPicker, type PickedRepo } from "./RepoPicker";
|
||||
|
||||
const mono =
|
||||
@@ -42,7 +46,17 @@ export function MissionWizard({
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
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 [cron, setCron] = useState("0 */6 * * *");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@@ -71,7 +85,7 @@ export function MissionWizard({
|
||||
title: title.trim(),
|
||||
template_kind: templateKind,
|
||||
repo_id: repo?.repo_id,
|
||||
team_id: teamId || undefined,
|
||||
team_template_id: teamTemplateId || undefined,
|
||||
schedule,
|
||||
description: description.trim() || undefined,
|
||||
phases: preset.phases,
|
||||
@@ -268,18 +282,91 @@ export function MissionWizard({
|
||||
|
||||
{step === 3 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<span style={labelStyle}>Team</span>
|
||||
<span style={labelStyle}>Team template</span>
|
||||
<p style={hintStyle}>
|
||||
Slice 3 ships team templates + auto-provision. For now, paste an
|
||||
existing team id or leave blank to let phase execution
|
||||
auto-provision one from the description.
|
||||
Pick a canonical roster. On mission launch the team is
|
||||
materialized from the template — roles, prompts, MCP bundles,
|
||||
and (later) skills + brain seeds. Leave unset to let the
|
||||
mission auto-provision an LLM-derived team from the prompt.
|
||||
</p>
|
||||
<input
|
||||
value={teamId}
|
||||
onChange={(e) => setTeamId(e.target.value)}
|
||||
placeholder="optional team UUID"
|
||||
style={fieldStyle}
|
||||
/>
|
||||
<div style={{ display: "grid", gap: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTeamTemplateId("")}
|
||||
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>
|
||||
)}
|
||||
|
||||
@@ -330,7 +417,15 @@ export function MissionWizard({
|
||||
<ReviewRow k="Title" v={title} />
|
||||
{description && <ReviewRow k="Description" v={description} />}
|
||||
{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
|
||||
k="Schedule"
|
||||
v={scheduleKind === "cron" ? `cron: ${cron}` : "one-shot"}
|
||||
@@ -458,6 +553,18 @@ const secondaryBtn: React.CSSProperties = {
|
||||
fontSize: 12.5,
|
||||
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 => ({
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
|
||||
Reference in New Issue
Block a user