missions: hard-require team template — block empty-team launches
Root-cause fix for the "mission runs with zero agents" bug. Three
enforcement layers now guarantee a launched mission has a team:
1. mission_orchestrator::on_launch — the previous
\`return Ok(None)\` when both team_id and team_template_id are
None is now \`return Err(...)\`. That branch was never a real
"auto-provision later" path; it was a silent no-op that let
the mission flip to running with nothing to run.
2. routes::missions::set_status — the draft→running transition
now (a) rejects with 400 when team_id + team_template_id are
both null, and (b) runs on_launch BEFORE flipping status +
returns 500 on failure. No more orphan "running" missions
with no materialization.
3. MissionWizard step 3 — removed the misleading "LLM
auto-provision" tile (fake code path). First real template is
pre-selected on mount; canNext requires teamTemplateId set;
empty state surfaces a red warning if no templates loaded.
4. MissionCanvas Launch button — disabled with a "No team" label
and explanatory tooltip when the mission has neither team_id
nor team_template_id (defense-in-depth for legacy rows or
direct-API missions).
Also flipped the mission_orchestrator test that expected
Ok(None) → now expects a specific error message.
Prod cleanup: reset the stuck mission
019f814c-d36f-7d60-8915-1ce100683133 (running with team_id=NULL) back
to draft so the operator can delete or attach a template.
Verified: cargo check --workspace + tsc + eslint all green;
mission_orchestrator test updated to match new contract.
This commit is contained in:
@@ -410,20 +410,35 @@ export function MissionCanvas({
|
||||
>
|
||||
<RefreshCw size={13} />
|
||||
</button>
|
||||
{mission.status === "draft" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={launch}
|
||||
disabled={launching}
|
||||
style={{
|
||||
...primaryBtn,
|
||||
opacity: launching ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<Play size={13} style={{ marginRight: 4 }} />
|
||||
{launching ? "Launching…" : "Launch"}
|
||||
</button>
|
||||
)}
|
||||
{mission.status === "draft" && (() => {
|
||||
const hasTeam =
|
||||
mission.team_id !== null || mission.team_template_id !== null;
|
||||
const disabled = launching || !hasTeam;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={launch}
|
||||
disabled={disabled}
|
||||
title={
|
||||
!hasTeam
|
||||
? "Cannot launch: mission has no team + no team template. Delete and recreate with a template picked in step 3, or attach a team via the API."
|
||||
: "Launch this mission"
|
||||
}
|
||||
style={{
|
||||
...primaryBtn,
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
cursor: disabled ? "not-allowed" : "pointer",
|
||||
}}
|
||||
>
|
||||
<Play size={13} style={{ marginRight: 4 }} />
|
||||
{launching
|
||||
? "Launching…"
|
||||
: hasTeam
|
||||
? "Launch"
|
||||
: "No team"}
|
||||
</button>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<h1 style={{ margin: 0, fontSize: 20, color: "#f3f3f5" }}>{mission.title}</h1>
|
||||
|
||||
Reference in New Issue
Block a user