herdr phase 1a: missions runtime_kind + target_node schema

First slice of the second-runtime path. Missions now carry
runtime_kind ('zeroclaw' | 'local_herdr') + target_node_id (FK to
nodes) so the mission_orchestrator + phase executors can dispatch
differently depending on where the operator wants execution.

Migration:
  - 0055_missions_runtime_kind.sql — adds runtime_kind (NOT NULL
    DEFAULT 'zeroclaw' + CHECK), target_node_id (nullable FK ON
    DELETE SET NULL). All existing missions backfill to 'zeroclaw'
    so behavior is unchanged.
  - topology_runs also grows herdr_workspace_id / herdr_tab_id /
    herdr_pane_id text columns so a resumed run can reattach to the
    same Herdr pane instead of spawning a duplicate.

Code:
  - cm-db::repo::missions — Mission + NewMission carry the two new
    fields; all SELECTs updated; INSERT COALESCE-defaults
    runtime_kind to 'zeroclaw' when unspecified.
  - routes::missions::create — validates runtime_kind and requires
    target_node_id when kind='local_herdr' (400 otherwise).
  - lib/api/missions.ts — RuntimeKind type; Mission carries both;
    CreateMissionRequest optional fields.

Behavior is opt-in: no path exists yet to actually create a
local_herdr mission — that lands in Phase 1c (wizard picker). This
commit just makes the schema + validation in place so Phase 1b's
fleet_herdr dispatch module can key on it.

Tests: mission_orchestrator integration test still green.
This commit is contained in:
Omar Sobh
2026-07-20 09:45:15 -07:00
parent 1f0117e35a
commit d6dbd044c8
4 changed files with 73 additions and 4 deletions
+21 -4
View File
@@ -30,6 +30,10 @@ pub struct Mission {
pub status: String,
pub description: Option<String>,
pub config: Value,
/// 'zeroclaw' (default, headless) | 'local_herdr' (attended, fleet node)
pub runtime_kind: String,
/// FK → nodes(id); only relevant when runtime_kind = 'local_herdr'
pub target_node_id: Option<Uuid>,
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
@@ -106,6 +110,9 @@ pub struct NewMission<'a> {
pub schedule: Value,
pub description: Option<&'a str>,
pub config: Value,
/// Defaults to 'zeroclaw' when None.
pub runtime_kind: Option<&'a str>,
pub target_node_id: Option<Uuid>,
pub phases: Vec<NewMissionPhase>,
}
@@ -127,8 +134,10 @@ pub async fn insert(pool: &PgPool, m: NewMission<'_>) -> Result<Uuid, DbError> {
sqlx::query(
"INSERT INTO missions
(id, workspace_id, title, template_kind, team_id,
team_template_id, repo_id, schedule, status, description, config)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'draft',$9,$10)",
team_template_id, repo_id, schedule, status, description, config,
runtime_kind, target_node_id)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'draft',$9,$10,
COALESCE($11,'zeroclaw'),$12)",
)
.bind(mission_id)
.bind(m.workspace_id)
@@ -140,6 +149,8 @@ pub async fn insert(pool: &PgPool, m: NewMission<'_>) -> Result<Uuid, DbError> {
.bind(&m.schedule)
.bind(m.description)
.bind(&m.config)
.bind(m.runtime_kind)
.bind(m.target_node_id)
.execute(&mut *tx)
.await?;
@@ -169,7 +180,8 @@ pub async fn get(pool: &PgPool, id: Uuid, workspace_id: Uuid) -> Result<Option<M
let row = sqlx::query(
"SELECT id, workspace_id, title, template_kind, team_id,
team_template_id, repo_id, schedule, status,
description, config, created_at, updated_at, completed_at
description, config, runtime_kind, target_node_id,
created_at, updated_at, completed_at
FROM missions WHERE id = $1 AND workspace_id = $2",
)
.bind(id)
@@ -188,6 +200,8 @@ pub async fn get(pool: &PgPool, id: Uuid, workspace_id: Uuid) -> Result<Option<M
status: r.get("status"),
description: r.get("description"),
config: r.get("config"),
runtime_kind: r.get("runtime_kind"),
target_node_id: r.get("target_node_id"),
created_at: r.get("created_at"),
updated_at: r.get("updated_at"),
completed_at: r.get("completed_at"),
@@ -204,7 +218,8 @@ pub async fn list_by_workspace(
let rows = sqlx::query(
"SELECT id, workspace_id, title, template_kind, team_id,
team_template_id, repo_id, schedule, status,
description, config, created_at, updated_at, completed_at
description, config, runtime_kind, target_node_id,
created_at, updated_at, completed_at
FROM missions WHERE workspace_id = $1
ORDER BY created_at DESC LIMIT $2",
)
@@ -226,6 +241,8 @@ pub async fn list_by_workspace(
status: r.get("status"),
description: r.get("description"),
config: r.get("config"),
runtime_kind: r.get("runtime_kind"),
target_node_id: r.get("target_node_id"),
created_at: r.get("created_at"),
updated_at: r.get("updated_at"),
completed_at: r.get("completed_at"),