herdr phase 1c: wizard runtime picker + on_launch auto-dispatch
Closes the operator loop for the second-runtime path. Missions
created with runtime='local_herdr' now spawn a Herdr pane on their
target_node automatically on draft→running.
Frontend (MissionWizard):
- Step 4 grows a "Runtime" section above Schedule
- Radio: "Hosted (ZeroClaw)" default | "On a fleet node (Herdr)"
- Local-Herdr shows a dropdown of ONLINE nodes only (from
/api/nodes filtered by status='online')
- canNext blocks Next when local_herdr picked without a node
- Review step shows "Runtime: Herdr on <node-name>" or "Hosted"
- Empty-online-nodes state hints "Connect one from INFRA first"
Backend:
- mission_orchestrator::on_launch grows a NodeHub param; when
mission.runtime_kind='local_herdr' + target_node_id set +
hub present → calls fleet_herdr::dispatch(). Non-fatal:
logs and continues so a research_only mission with a Herdr
runtime chosen accidentally still boots the team.
- routes::missions::set_status passes state.node_hub through.
- Test call sites updated to pass None for the new param
(integration tests don't drive real fleet nodes).
CLI stub: on_launch currently hard-codes cli="claude" for the
Herdr pane. Phase 4 will read that from the team template so a
research team → kimi, gpu team → claude, etc.
Verified: cargo check --workspace + cargo test
-p cm-api --test mission_orchestrator + tsc --noEmit all green.
This commit is contained in:
@@ -37,6 +37,7 @@ pub async fn on_launch(
|
||||
workspace_id: WorkspaceId,
|
||||
user_id: cm_domain::UserId,
|
||||
mission_id: Uuid,
|
||||
node_hub: Option<std::sync::Arc<crate::fleet::NodeHub>>,
|
||||
) -> Result<Option<Uuid>, String> {
|
||||
let Some(mission) = cm_db::repo::missions::get(pool, mission_id, workspace_id.as_uuid())
|
||||
.await
|
||||
@@ -100,6 +101,38 @@ pub async fn on_launch(
|
||||
),
|
||||
}
|
||||
|
||||
// Herdr second-runtime: if runtime_kind='local_herdr', spawn a
|
||||
// pane on target_node running the first available local CLI.
|
||||
// Non-fatal on failure — the operator sees the error in server
|
||||
// logs and can manually retry via POST /herdr-dispatch.
|
||||
if mission.runtime_kind == "local_herdr" {
|
||||
if let (Some(hub), Some(node_id)) = (node_hub, mission.target_node_id) {
|
||||
let prompt = mission.description.clone().unwrap_or_default();
|
||||
let cli = "claude"; // TODO(phase-4): pick from team template
|
||||
match crate::fleet_herdr::dispatch(
|
||||
hub,
|
||||
cm_domain::NodeId::from(node_id),
|
||||
mission_id,
|
||||
cli,
|
||||
&prompt,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(handle) => eprintln!(
|
||||
"mission_orchestrator: herdr pane {} spawned on node {}",
|
||||
handle.pane_id, node_id
|
||||
),
|
||||
Err(e) => eprintln!(
|
||||
"mission_orchestrator: herdr dispatch for {mission_id} failed (continuing): {e}"
|
||||
),
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"mission_orchestrator: mission {mission_id} is local_herdr but node_hub or target_node missing"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(team_id))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user