wizard: 'fresh coding team' picker for paired coding loop
Second slice of the per-loop-team arc. The paired-coding-loop checkbox
in ResearchWizard step 6 now exposes a two-option picker:
⦿ Provision a dedicated coding team (default when the loop is on)
— fresh 'Coding · <topic>' team row, risk_profile =
coding_readwrite, clawmates_door in mcp_bundles. Loop's
team_id is bound at wizard-submit time.
○ Reuse the research team (legacy) — no team_id bound; coding
iterations spawn against the research topic's container.
Frontend
- New codingTeamMode state, radio picker rendered under the checkbox.
- research.ts createTopic body gains paired_coding_team_mode?: 'fresh'|
'reuse'.
Backend
- CreateTopicRequest gains paired_coding_team_mode: Option<String>.
- materialize_topic_loops takes it through and, when 'fresh', calls
the new provision_fresh_coding_team helper — inserts a teams row
via the existing insert_team_with_lifecycle (pipeline kind, same
graph as the loop), sets its runtime-config via
set_team_runtime_config, then binds loop.team_id.
- All operations best-effort with stderr logging — a team-provision
failure leaves the loop functional under the legacy fallback.
Not shipped in this slice (deferred to runtime hookup slice):
- research_container::spawn keyed on team_id → per-team container
- Config template rewrite injecting the team's risk_profile
- Migration of existing paired loops onto their own teams
The plumbing lands now so the wizard's intent is recorded; the
runtime honors it in the next PR.
This commit is contained in:
@@ -147,6 +147,14 @@ pub struct CreateTopicRequest {
|
||||
/// kind='exec' loop bound to this topic with on_artifact_update.
|
||||
#[serde(default)]
|
||||
pub create_paired_coding_loop: bool,
|
||||
/// 0045 fold — team disposition for the paired coding loop:
|
||||
/// Some("fresh") — provision a dedicated coding team with
|
||||
/// coding_readwrite risk profile (recommended)
|
||||
/// Some("reuse") — attach the coding loop to the research team
|
||||
/// (legacy behavior, both share one container)
|
||||
/// None — treated as "reuse" for backwards compat.
|
||||
#[serde(default)]
|
||||
pub paired_coding_team_mode: Option<String>,
|
||||
}
|
||||
|
||||
// The wizard sends a denormalized display object for its own UI. Only
|
||||
@@ -392,6 +400,7 @@ pub async fn create_topic(
|
||||
&body.title,
|
||||
&sched.mode,
|
||||
body.create_paired_coding_loop,
|
||||
body.paired_coding_team_mode.as_deref(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -306,6 +306,7 @@ pub async fn build_topic_graph_json(pool: &PgPool, topic_id: Uuid) -> serde_json
|
||||
/// (D1 fold). Fails soft — logs and returns, letting the topic land
|
||||
/// even if loop creation stumbles. Skips the empty-roster gate
|
||||
/// because `create_topic` already verified the workspace has agents.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn materialize_topic_loops(
|
||||
pool: &PgPool,
|
||||
workspace_id: Uuid,
|
||||
@@ -314,6 +315,7 @@ pub async fn materialize_topic_loops(
|
||||
topic_title: &str,
|
||||
mode: &str,
|
||||
also_coding: bool,
|
||||
coding_team_mode: Option<&str>,
|
||||
) {
|
||||
use serde_json::json;
|
||||
// Build a valid topology graph up front — an empty {nodes: [],
|
||||
@@ -405,6 +407,17 @@ pub async fn materialize_topic_loops(
|
||||
Some(topic_id),
|
||||
)
|
||||
.await;
|
||||
// 0045 fold — when the wizard picked "fresh" for the
|
||||
// coding team, provision a dedicated team row with a
|
||||
// coding_readwrite risk profile and bind it. Runtime
|
||||
// spawn hookup (per-team container + config write)
|
||||
// ships in a follow-up slice; the binding here ensures
|
||||
// the loop already carries its intended team by the
|
||||
// time that lands.
|
||||
if coding_team_mode == Some("fresh") {
|
||||
provision_fresh_coding_team(pool, workspace_id, loop_id, topic_title, &graph)
|
||||
.await;
|
||||
}
|
||||
crate::routes::loops::fire_initial_burst_if_set(
|
||||
pool,
|
||||
workspace_id,
|
||||
@@ -420,3 +433,56 @@ pub async fn materialize_topic_loops(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a placeholder `teams` row + set the loop's `team_id`. The
|
||||
/// team is deliberately member-less at this stage — the graph is
|
||||
/// carried on the loop itself, and the runtime hookup slice will
|
||||
/// either back-fill members lazily on first spawn or wire the loop's
|
||||
/// existing agents against the team via `add_member`.
|
||||
///
|
||||
/// Best-effort throughout: any failure logs to stderr but the loop
|
||||
/// itself stays intact and functional under the legacy shared-team
|
||||
/// fallback.
|
||||
async fn provision_fresh_coding_team(
|
||||
pool: &PgPool,
|
||||
workspace_id: Uuid,
|
||||
loop_id: Uuid,
|
||||
topic_title: &str,
|
||||
graph: &serde_json::Value,
|
||||
) {
|
||||
let team_id = Uuid::now_v7();
|
||||
let team_name = format!("Coding · {topic_title}");
|
||||
// insert_team_with_lifecycle keeps the topology graph so the
|
||||
// runtime can reproduce the roster without a second lookup.
|
||||
let ws = cm_domain::WorkspaceId::from(workspace_id);
|
||||
if let Err(e) = cm_db::repo::teams::insert_team_with_lifecycle(
|
||||
pool,
|
||||
team_id,
|
||||
ws,
|
||||
&team_name,
|
||||
"pipeline",
|
||||
graph,
|
||||
"permanent",
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("provision_fresh_coding_team: insert_team failed for loop {loop_id}: {e:?}");
|
||||
return;
|
||||
}
|
||||
if let Err(e) = cm_db::repo::teams::set_team_runtime_config(
|
||||
pool,
|
||||
team_id,
|
||||
ws,
|
||||
&cm_db::repo::teams::TeamRuntimeConfig {
|
||||
risk_profile: Some("coding_readwrite".to_string()),
|
||||
mcp_bundles: vec!["clawmates_door".to_string()],
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("provision_fresh_coding_team: set_runtime_config failed: {e:?}");
|
||||
}
|
||||
if let Err(e) = cm_db::repo::teams::set_team_for_loop(pool, loop_id, Some(team_id)).await {
|
||||
eprintln!("provision_fresh_coding_team: set_team_for_loop failed: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user