slice 4: 5 workflow templates as TOML recipes + mission-launch orchestrator
Two things land together:
1. Ships the 5 workflow recipes as TOML files under
templates/workflows/*.toml:
- research_only (hub_spoke research → MD + PDF)
- research_and_code (research → coding loop until INT-XX drained)
- security_hardening (scan → research patches → coding with
reviewer approval + full MCP bundle)
- refactor (single-pass coding with dep audit
preamble + before/after benchmarks)
- benchmark (author + baseline benchmarks per stack)
Each declares phases[], per-phase config, default_team_template.
Loaded read-only into an in-memory registry (workflow_registry)
via OnceLock — no DB row per recipe.
2. Ships the mission-launch orchestrator that closes the loop from
Slice 3.5d's mechanics. When a mission transitions draft→running,
`mission_orchestrator::on_launch`:
- Reads mission.team_template_id (skips if unset)
- Loads the team template detail (roles + skills bindings)
- Builds a topology graph from role slots via cm_topology::build
- Inserts the teams row + stamps template_id/version/risk_profile/mcp_bundles
- For each role: agent insert, model binding, runtime provision
(opt-in via RuntimeProvisioner::from_env), brain_seed::ingest
(Slice 3.5d), agent_template_link::upsert (Slice 3.5d),
team_members bind, audit trail
- UPDATE missions SET team_id = ...
Wired into routes::missions::set_status when prior.status='draft'
and new='running'. Failures log + are non-fatal (mission still
flips to running so the user can inspect + retry).
With this, Slice 3.5d's brain-seed + link machinery actually gets
populated, and the MCP skills server's template-defaults-merge path
(Slice 3.5b/d) starts serving real bindings to real agents.
Follow-ups (Slice 5-8):
- Task-card parser watches run events for TASK/COMPLETED markers
→ mission_tasks rows
- PDF renderer worker turns MD artifacts into PDFs
- Before/after benchmark runner honors phases[].config.benchmark
- Security scan MCP bundle exposes cargo-audit/gitleaks/trivy/semgrep
- Level-up endpoints diff learned-vs-seeded via agent_template_link
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
9d9f24a563
commit
565f6cae65
@@ -8,6 +8,7 @@ mod extract;
|
|||||||
pub mod fleet;
|
pub mod fleet;
|
||||||
mod mcp_door;
|
mod mcp_door;
|
||||||
mod mcp_skills;
|
mod mcp_skills;
|
||||||
|
pub mod mission_orchestrator;
|
||||||
pub mod node_rules;
|
pub mod node_rules;
|
||||||
pub mod quota;
|
pub mod quota;
|
||||||
mod recursive_exec;
|
mod recursive_exec;
|
||||||
@@ -20,6 +21,7 @@ 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;
|
||||||
|
pub mod workflow_registry;
|
||||||
|
|
||||||
use axum::routing::{delete, get, patch, post};
|
use axum::routing::{delete, get, patch, post};
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
//! Mission-launch orchestrator. Slice 4.
|
||||||
|
//!
|
||||||
|
//! When a mission's status flips from `draft` to `running`, this
|
||||||
|
//! module fires:
|
||||||
|
//! 1. Materialize a team from `team_template_id` (if set + no
|
||||||
|
//! `team_id` yet) — inserts agents, ingests brain seeds,
|
||||||
|
//! records agent_template_link lineage, wires team_members.
|
||||||
|
//! 2. Bind the resulting `team_id` back onto the mission.
|
||||||
|
//!
|
||||||
|
//! Phase execution (running research/coding/benchmark/security_scan
|
||||||
|
//! against the world) is layered on top by Slices 5–8.
|
||||||
|
//!
|
||||||
|
//! Design notes:
|
||||||
|
//! - Runtime provisioning is opt-in via `RuntimeProvisioner::from_env`.
|
||||||
|
//! Missing runtime = "insert DB rows only, no live claw" — the
|
||||||
|
//! mission still boots; live claws land the moment the runtime
|
||||||
|
//! env is configured + the mission re-launches.
|
||||||
|
//! - Every step is best-effort logged so a partial materialization
|
||||||
|
//! can be picked up by a subsequent launch rather than blocking.
|
||||||
|
|
||||||
|
use cm_db::repo::team_templates::TeamTemplateDetail;
|
||||||
|
use cm_domain::{AccessPolicy, Agent, AgentStatus, WorkspaceId};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::runtime_provision::RuntimeProvisioner;
|
||||||
|
|
||||||
|
/// Called from `routes::missions::set_status` when the transition is
|
||||||
|
/// draft→running. Materializes the team + returns the new team_id
|
||||||
|
/// (or an existing one — no-op when the mission already has a team).
|
||||||
|
///
|
||||||
|
/// Non-fatal on failure: logs + returns Ok(None) so the mission still
|
||||||
|
/// becomes `running` and the user can inspect the error via server
|
||||||
|
/// logs + re-attempt. Later slices surface this on the canvas.
|
||||||
|
pub async fn on_launch(
|
||||||
|
pool: &PgPool,
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
user_id: cm_domain::UserId,
|
||||||
|
mission_id: Uuid,
|
||||||
|
) -> Result<Option<Uuid>, String> {
|
||||||
|
let Some(mission) = cm_db::repo::missions::get(pool, mission_id, workspace_id.as_uuid())
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("load mission: {e}"))?
|
||||||
|
else {
|
||||||
|
return Err("mission not found".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip if already bound.
|
||||||
|
if mission.team_id.is_some() {
|
||||||
|
return Ok(mission.team_id);
|
||||||
|
}
|
||||||
|
let Some(template_id) = mission.team_template_id else {
|
||||||
|
// No template + no team = phase execution will auto-provision
|
||||||
|
// via the LLM path (Slice 2's fallback), or run against the
|
||||||
|
// shared runtime. Nothing to do here.
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let template = cm_db::repo::team_templates::get(pool, template_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("load template: {e}"))?
|
||||||
|
.ok_or_else(|| format!("template {template_id} not found"))?;
|
||||||
|
|
||||||
|
let provisioner = RuntimeProvisioner::from_env();
|
||||||
|
|
||||||
|
let team_id = mint_team_from_template(
|
||||||
|
pool,
|
||||||
|
workspace_id,
|
||||||
|
user_id,
|
||||||
|
provisioner.as_ref(),
|
||||||
|
&template,
|
||||||
|
&mission.title,
|
||||||
|
"claude-sonnet-5",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Bind the team onto the mission.
|
||||||
|
sqlx::query("UPDATE missions SET team_id = $1, updated_at = now() WHERE id = $2")
|
||||||
|
.bind(team_id)
|
||||||
|
.bind(mission_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("bind team on mission: {e}"))?;
|
||||||
|
|
||||||
|
Ok(Some(team_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn mint_team_from_template(
|
||||||
|
pool: &PgPool,
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
user_id: cm_domain::UserId,
|
||||||
|
provisioner: Option<&RuntimeProvisioner>,
|
||||||
|
template: &TeamTemplateDetail,
|
||||||
|
team_name: &str,
|
||||||
|
default_model: &str,
|
||||||
|
) -> Result<Uuid, String> {
|
||||||
|
// Build the topology graph from role slots so the team's `graph`
|
||||||
|
// NOT NULL column is satisfied + downstream topology executors
|
||||||
|
// have a valid shape to iterate over.
|
||||||
|
let roles: Vec<&str> = template.roles.iter().map(|r| r.slot.as_str()).collect();
|
||||||
|
let topology_kind = parse_topology_kind(&template.template.default_topology);
|
||||||
|
let graph = cm_topology::build(topology_kind, &roles)
|
||||||
|
.map_err(|e| format!("build topology graph: {e}"))?;
|
||||||
|
let graph_json =
|
||||||
|
serde_json::to_value(&graph).map_err(|e| format!("serialize topology graph: {e}"))?;
|
||||||
|
|
||||||
|
let team_id = Uuid::now_v7();
|
||||||
|
cm_db::repo::teams::insert_team_with_lifecycle(
|
||||||
|
pool,
|
||||||
|
team_id,
|
||||||
|
workspace_id,
|
||||||
|
team_name,
|
||||||
|
&template.template.default_topology,
|
||||||
|
&graph_json,
|
||||||
|
"permanent",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("insert team: {e}"))?;
|
||||||
|
|
||||||
|
// Stamp template lineage on the team row (Slice 3 columns).
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE teams SET template_id = $1, template_version = $2,
|
||||||
|
risk_profile = $3, mcp_bundles = $4
|
||||||
|
WHERE id = $5",
|
||||||
|
)
|
||||||
|
.bind(template.template.id)
|
||||||
|
.bind(template.template.version)
|
||||||
|
.bind(&template.template.risk_profile)
|
||||||
|
.bind(serde_json::json!(template.template.mcp_bundles))
|
||||||
|
.bind(team_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("stamp template lineage: {e}"))?;
|
||||||
|
|
||||||
|
// For each role: create agent, provision runtime, ingest brain
|
||||||
|
// seed, record link, bind to topology node.
|
||||||
|
for (idx, role) in template.roles.iter().enumerate() {
|
||||||
|
// Node id in the graph follows cm_topology's `n0..` convention
|
||||||
|
// — read the actual node id from the graph so team_members
|
||||||
|
// stays consistent with the topology.
|
||||||
|
let Some(node) = graph.nodes.get(idx) else {
|
||||||
|
return Err(format!(
|
||||||
|
"topology graph produced {} nodes but template has {} roles",
|
||||||
|
graph.nodes.len(),
|
||||||
|
template.roles.len(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let agent = Agent {
|
||||||
|
id: cm_domain::AgentId::new(),
|
||||||
|
workspace_id,
|
||||||
|
name: format!("{} · {}", team_name, role.slot),
|
||||||
|
job_title: role.slot.clone(),
|
||||||
|
system_prompt: role.system_prompt.clone(),
|
||||||
|
avatar: String::new(),
|
||||||
|
accent: default_accent_for(&role.slot).to_string(),
|
||||||
|
wallpaper: String::new(),
|
||||||
|
managed_by: user_id,
|
||||||
|
status: AgentStatus::Online,
|
||||||
|
};
|
||||||
|
cm_db::repo::agents::insert(pool, &agent, &AccessPolicy::default())
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("insert agent {}: {e}", role.slot))?;
|
||||||
|
let claw_id = agent.id.as_uuid();
|
||||||
|
|
||||||
|
cm_db::repo::agents::set_model_binding(pool, agent.id, default_model)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("set_model_binding {claw_id}: {e}"))?;
|
||||||
|
|
||||||
|
// Runtime provisioning is opt-in — no-op if unconfigured.
|
||||||
|
if let Some(p) = provisioner {
|
||||||
|
if let Err(e) = p.provision_claw(claw_id, default_model).await {
|
||||||
|
eprintln!(
|
||||||
|
"mission_orchestrator: provision claw {claw_id} failed (continuing): {e}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ingest brain seed (Slice 3.5d). Non-fatal on failure —
|
||||||
|
// agent still works from system_prompt alone.
|
||||||
|
if let Some(seed) = role.brain_seed.as_deref().filter(|s| !s.trim().is_empty()) {
|
||||||
|
if let Err(e) =
|
||||||
|
crate::brain_seed::ingest(claw_id, seed.to_string(), role.system_prompt.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"mission_orchestrator: brain_seed ingest for {claw_id} failed (continuing): {e}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record lineage (Slice 3.5d) so the MCP skills server can
|
||||||
|
// merge template default skills with per-agent overrides.
|
||||||
|
cm_db::repo::agent_template_link::upsert(
|
||||||
|
pool,
|
||||||
|
claw_id,
|
||||||
|
template.template.id,
|
||||||
|
template.template.version,
|
||||||
|
&role.slot,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("agent_template_link upsert {claw_id}: {e}"))?;
|
||||||
|
cm_db::repo::agent_template_link::mark_seeded(pool, claw_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("mark_seeded {claw_id}: {e}"))?;
|
||||||
|
|
||||||
|
// Wire team_members using the topology node id.
|
||||||
|
cm_db::repo::teams::add_member(pool, team_id, &node.id, claw_id, &role.slot)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("team_members add {claw_id}: {e}"))?;
|
||||||
|
|
||||||
|
// Audit for parity with individual claw creation path.
|
||||||
|
let _ = cm_db::repo::audit::append(
|
||||||
|
pool,
|
||||||
|
workspace_id,
|
||||||
|
cm_db::repo::audit::Actor::User(user_id),
|
||||||
|
"agent.created",
|
||||||
|
"agent",
|
||||||
|
&agent.id.to_string(),
|
||||||
|
serde_json::json!({
|
||||||
|
"name": agent.name,
|
||||||
|
"job_title": agent.job_title,
|
||||||
|
"source": "mission_orchestrator",
|
||||||
|
"template_id": template.template.id.to_string(),
|
||||||
|
"template_version": template.template.version,
|
||||||
|
"role_slot": role.slot,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(team_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_topology_kind(s: &str) -> cm_topology::TopologyKind {
|
||||||
|
use cm_topology::TopologyKind;
|
||||||
|
match s {
|
||||||
|
"pipeline" => TopologyKind::Pipeline,
|
||||||
|
"hierarchical" => TopologyKind::Hierarchical,
|
||||||
|
"star_moe" => TopologyKind::StarMoe,
|
||||||
|
_ => TopologyKind::HubSpoke,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_accent_for(slot: &str) -> &'static str {
|
||||||
|
match slot {
|
||||||
|
"planner" | "arch_analyst" | "designer" | "scene_designer" | "api_designer" => "#7cd6e0",
|
||||||
|
"coder" | "kernel_author" | "shader_author" => "#5fd08a",
|
||||||
|
"tester" | "bench_engineer" | "perf_engineer" => "#ffb44a",
|
||||||
|
"reviewer" | "db_engineer" => "#c98af0",
|
||||||
|
"committer" | "spec_integrator" => "#ff8a7a",
|
||||||
|
_ => "#8a8a92",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -153,8 +153,25 @@ pub async fn set_status(
|
|||||||
if !allowed.contains(&body.status.as_str()) {
|
if !allowed.contains(&body.status.as_str()) {
|
||||||
return Err(ApiError::BadRequest);
|
return Err(ApiError::BadRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snapshot prior state so we can detect the draft→running edge
|
||||||
|
// and fire the launch orchestrator (Slice 4).
|
||||||
|
let prior = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||||
|
.await?
|
||||||
|
.ok_or(ApiError::NotFound)?;
|
||||||
|
|
||||||
cm_db::repo::missions::set_status(&state.pool, id, user.workspace_id.as_uuid(), &body.status)
|
cm_db::repo::missions::set_status(&state.pool, id, user.workspace_id.as_uuid(), &body.status)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if prior.status == "draft" && body.status == "running" {
|
||||||
|
if let Err(e) =
|
||||||
|
crate::mission_orchestrator::on_launch(&state.pool, user.workspace_id, user.user_id, id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("mission {id}: on_launch failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mission = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
|
let mission = cm_db::repo::missions::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||||
.await?
|
.await?
|
||||||
.ok_or(ApiError::NotFound)?;
|
.ok_or(ApiError::NotFound)?;
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
//! Read-only registry of workflow template recipes loaded from
|
||||||
|
//! `templates/workflows/*.toml` at server boot. Slice 4.
|
||||||
|
//!
|
||||||
|
//! Recipes are immutable reference data — no DB row per recipe.
|
||||||
|
//! Slice 2's client-side `TEMPLATE_PRESETS` is a mirror of what
|
||||||
|
//! ends up here; a follow-up serves this registry over an API so
|
||||||
|
//! the client can drop its inline mirror.
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct WorkflowRecipe {
|
||||||
|
pub key: String,
|
||||||
|
pub title: String,
|
||||||
|
pub blurb: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub requires_repo: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub phases: Vec<WorkflowPhase>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub default_team_template: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct WorkflowPhase {
|
||||||
|
pub kind: String,
|
||||||
|
pub order_idx: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub config: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
static REGISTRY: OnceLock<Vec<WorkflowRecipe>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn workflows_dir() -> PathBuf {
|
||||||
|
if let Ok(d) = std::env::var("CLAWMATES_WORKFLOWS_DIR") {
|
||||||
|
return PathBuf::from(d);
|
||||||
|
}
|
||||||
|
let container = PathBuf::from("/etc/clawmates/templates/workflows");
|
||||||
|
if container.exists() {
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
PathBuf::from("templates/workflows")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load recipes from disk. Called once at boot; subsequent calls
|
||||||
|
/// return the cached set. Missing/broken files log + are skipped.
|
||||||
|
pub fn load() -> &'static [WorkflowRecipe] {
|
||||||
|
REGISTRY.get_or_init(|| {
|
||||||
|
let dir = workflows_dir();
|
||||||
|
let entries = match std::fs::read_dir(&dir) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"workflow_registry: dir {} not readable: {e} — no recipes",
|
||||||
|
dir.display()
|
||||||
|
);
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(|s| s.to_str()) != Some("toml") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match load_one(&path) {
|
||||||
|
Ok(r) => {
|
||||||
|
eprintln!("workflow_registry: loaded {}", r.key);
|
||||||
|
out.push(r);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("workflow_registry: failed to load {}: {e}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.sort_by(|a, b| a.key.cmp(&b.key));
|
||||||
|
out
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_one(path: &std::path::Path) -> Result<WorkflowRecipe, String> {
|
||||||
|
let text =
|
||||||
|
std::fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
|
||||||
|
toml::from_str::<WorkflowRecipe>(&text).map_err(|e| format!("parse {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(key: &str) -> Option<&'static WorkflowRecipe> {
|
||||||
|
load().iter().find(|r| r.key == key)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
key = "benchmark"
|
||||||
|
title = "Benchmark"
|
||||||
|
blurb = "Author + baseline benchmarks so subsequent refactors can be measured before/after."
|
||||||
|
requires_repo = true
|
||||||
|
|
||||||
|
[[phases]]
|
||||||
|
kind = "benchmark"
|
||||||
|
order_idx = 0
|
||||||
|
[phases.config]
|
||||||
|
# Slice 7 runs the benchmark_snapshots baseline pass here. This
|
||||||
|
# workflow's job is to AUTHOR the benchmarks + establish the
|
||||||
|
# baseline; subsequent refactor missions consume them.
|
||||||
|
mode = "author_and_baseline"
|
||||||
|
# Which harness to use; per-stack defaults if unset:
|
||||||
|
# rust → criterion / cargo bench
|
||||||
|
# ts/js → vitest --bench / mitata
|
||||||
|
# py → pytest-benchmark
|
||||||
|
harness = "auto"
|
||||||
|
|
||||||
|
default_team_template = "rust_sdlc"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
key = "refactor"
|
||||||
|
title = "Refactor"
|
||||||
|
blurb = "Audit dependencies + versions, propose API/SDK adaptations, apply the changes."
|
||||||
|
requires_repo = true
|
||||||
|
|
||||||
|
[[phases]]
|
||||||
|
kind = "coding"
|
||||||
|
order_idx = 0
|
||||||
|
[phases.config]
|
||||||
|
loop = "single_pass"
|
||||||
|
# Preamble asks the planner to run `cargo tree`, `cargo outdated`,
|
||||||
|
# `npm outdated`, etc. and produce INT-XX items per stale dep.
|
||||||
|
task_preamble = "dependency_audit_v1"
|
||||||
|
commit_policy = "on_green_tests"
|
||||||
|
# Bench before + after the pass so we can measure impact.
|
||||||
|
benchmark = { mode = "before_after" }
|
||||||
|
|
||||||
|
default_team_template = "rust_sdlc"
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
key = "research_and_code"
|
||||||
|
title = "Research + Coding Loop"
|
||||||
|
blurb = "Research a topic against a repo, then loop the coding team through the produced INT-XX items until done."
|
||||||
|
requires_repo = true
|
||||||
|
|
||||||
|
[[phases]]
|
||||||
|
kind = "research"
|
||||||
|
order_idx = 0
|
||||||
|
[phases.config]
|
||||||
|
produces = ["md", "pdf"]
|
||||||
|
default_topology = "hub_spoke"
|
||||||
|
|
||||||
|
[[phases]]
|
||||||
|
kind = "coding"
|
||||||
|
order_idx = 1
|
||||||
|
[phases.config]
|
||||||
|
# Loop policy: keep iterating until the artifact yields no more
|
||||||
|
# unconsumed INT-XX items. Scheduler stops when `consumed_int_ids`
|
||||||
|
# equals the artifact's declared set.
|
||||||
|
loop = "until_no_more_int_items"
|
||||||
|
# Preamble injected at the head of each iteration's task text so
|
||||||
|
# the agents know where the repo lives + how to commit. Slice 3.5c's
|
||||||
|
# `workspace-repo-commit-protocol` skill also covers this — the
|
||||||
|
# preamble is the belt, the skill the suspenders.
|
||||||
|
task_preamble = "workspace_repo_v1"
|
||||||
|
# Only commit when tests pass. Enforced by the team's TEST_PASS
|
||||||
|
# marker before the committer runs. If a coding role wants to bypass
|
||||||
|
# (rare — pure docs commit), it emits COMMIT_POLICY_OVERRIDE: <reason>.
|
||||||
|
commit_policy = "on_green_tests"
|
||||||
|
|
||||||
|
default_team_template = "rust_sdlc"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
key = "research_only"
|
||||||
|
title = "Research only"
|
||||||
|
blurb = "Produce a styled MD + PDF artifact in the workspace. One-shot or scheduled."
|
||||||
|
requires_repo = false
|
||||||
|
|
||||||
|
# Phases run in order. Each entry gets a `mission_phases` row on
|
||||||
|
# mission create; the orchestrator dispatches per-kind executors.
|
||||||
|
[[phases]]
|
||||||
|
kind = "research"
|
||||||
|
order_idx = 0
|
||||||
|
# Phase-scoped config, merged into mission_phases.config on insert.
|
||||||
|
[phases.config]
|
||||||
|
produces = ["md", "pdf"]
|
||||||
|
default_topology = "hub_spoke"
|
||||||
|
|
||||||
|
# Which team template is the "sensible default" for the picker when
|
||||||
|
# the user hasn't explicitly picked one. UI honors this.
|
||||||
|
default_team_template = "rust_sdlc"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
key = "security_hardening"
|
||||||
|
title = "Security Hardening"
|
||||||
|
blurb = "Scan the repo for vulnerabilities, research patches, then apply + verify."
|
||||||
|
requires_repo = true
|
||||||
|
|
||||||
|
[[phases]]
|
||||||
|
kind = "security_scan"
|
||||||
|
order_idx = 0
|
||||||
|
[phases.config]
|
||||||
|
# Slice 8 wires these tools as an MCP bundle. Each finding becomes
|
||||||
|
# a mission_task with external_id = CVE/RUSTSEC/gitleaks fingerprint.
|
||||||
|
tools = ["cargo_audit", "gitleaks", "trivy_fs", "semgrep"]
|
||||||
|
|
||||||
|
[[phases]]
|
||||||
|
kind = "research"
|
||||||
|
order_idx = 1
|
||||||
|
[phases.config]
|
||||||
|
produces = ["md", "pdf"]
|
||||||
|
default_topology = "hub_spoke"
|
||||||
|
# The research phase reads the security_scan phase's findings from
|
||||||
|
# mission_tasks and produces a patch strategy per finding.
|
||||||
|
input_from_phase = "security_scan"
|
||||||
|
|
||||||
|
[[phases]]
|
||||||
|
kind = "coding"
|
||||||
|
order_idx = 2
|
||||||
|
[phases.config]
|
||||||
|
loop = "until_all_findings_closed"
|
||||||
|
task_preamble = "workspace_repo_v1"
|
||||||
|
# Security requires reviewer approval on top of green tests.
|
||||||
|
commit_policy = "on_reviewer_approval"
|
||||||
|
mcp_bundles = ["clawmates_door", "clawmates_skills", "gitea_forge", "security_scan"]
|
||||||
|
|
||||||
|
default_team_template = "rust_sdlc"
|
||||||
Reference in New Issue
Block a user