feat(ui): three-screen mission wizard that asks only what the type needs

Five fixed steps for every mission type, and getting a research document out of
it meant naming a team, choosing a runtime, and writing per-phase completion
conditions under a paragraph explaining what a model checker can and cannot
prove. Two of those steps asked for things the mission does not use, and one of
them blocked outright.

  1  What do you want to do?
  2  Title, a description with a Polish button, repo ONLY if the type needs one
  3  Review -> Launch, plus one collapsed Advanced section

Two hard defects fixed on the way:

- The microVM runtime could not be selected AT ALL. Step 4 gated Next on
  `targetNodeId`, which microVM deliberately never sets because placement picks
  the node per phase. Everything shipped today, the local-GPU backend included,
  was unreachable from the UI.
- Step 3 required a team while every workflow TOML already names one in
  `default_team_template` — which this file ignored. The answer was always
  available and the question was always asked. It is now resolved by key, with a
  category fallback, and shown under Advanced so an operator can see WHICH
  default rather than having to supply one.

A failed `/api/team-templates` request and a genuinely empty list rendered the
identical red banner, which sends the reader looking for missing template files
when the request had 401'd. They now say different things.

`phases[]` is no longer sent unless someone set a completion condition.
`recipeToPreset` strips each phase's `config`, so posting the stripped list
overrode the recipe's real settings — tools, commit policy, loop mode — with
nothing. Omitting it lets `phases_for_create` use the recipe, which is both
simpler and more correct.

Launch keeps its own gate, since Advanced can still produce an unlaunchable
combination — but it names what is missing instead of greying out in silence.

Artifacts get a Download link. Deliberately a plain link to the streaming route
rather than a Blob built from what "Read" already fetched: that content is
capped at 2 MiB and UTF-8-decoded, so reusing it would silently produce a
truncated or undownloadable file for exactly the artifacts worth downloading.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-09 18:50:13 -07:00
co-authored by Claude Opus 5
parent 25f075a8be
commit e4bddeb1ba
3 changed files with 568 additions and 404 deletions
@@ -11,7 +11,7 @@
// research phase can leave dozens of documents. // research phase can leave dozens of documents.
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { FileText, GitMerge } from "lucide-react"; import { Download, FileText, GitMerge } from "lucide-react";
import { import {
getArtifactContent, getArtifactContent,
@@ -172,6 +172,28 @@ export function MissionArtifacts({
> >
{isOpen ? "Hide" : "Read"} {isOpen ? "Hide" : "Read"}
</button> </button>
{/* A plain link, not a fetch-and-Blob.
The read endpoint caps at 2 MiB and decodes as UTF-8, so
building the download from what "Read" already fetched would
inherit both limits and silently produce a truncated or
undownloadable file for exactly the artifacts worth
downloading. This hits the streaming route instead. */}
<a
href={`/api/missions/${missionId}/artifacts/${a.id}/download`}
download
style={{
...secondaryBtn,
padding: "5px 10px",
fontSize: 11,
textDecoration: "none",
display: "inline-flex",
alignItems: "center",
gap: 5,
}}
>
<Download aria-hidden size={12} />
Download
</a>
</div> </div>
{isOpen && ( {isOpen && (
<div <div
@@ -194,7 +216,8 @@ export function MissionArtifacts({
</div> </div>
) : text?.truncated ? ( ) : text?.truncated ? (
<div style={{ fontFamily: mono, fontSize: 11.5, color: "#8a8a92" }}> <div style={{ fontFamily: mono, fontSize: 11.5, color: "#8a8a92" }}>
too large to display inline ({text.bytes.toLocaleString()} bytes) too large to display inline ({text.bytes.toLocaleString()}{" "}
bytes) — use Download
</div> </div>
) : text ? ( ) : text ? (
<MarkdownBlock source={text.content} /> <MarkdownBlock source={text.content} />
File diff suppressed because it is too large Load Diff
+10
View File
@@ -465,11 +465,16 @@ export interface TemplatePreset {
blurb: string; blurb: string;
requiresRepo: boolean; requiresRepo: boolean;
phases: PhaseSpec[]; phases: PhaseSpec[];
/** The recipe's own team choice. Every workflow TOML declares one and the
* wizard used to ignore it, which is why step 3 asked the user to pick a
* team the mission had already chosen. */
defaultTeamTemplate?: string | null;
} }
export const TEMPLATE_PRESETS: TemplatePreset[] = [ export const TEMPLATE_PRESETS: TemplatePreset[] = [
{ {
kind: "research_only", kind: "research_only",
defaultTeamTemplate: "rust_sdlc",
title: "Research only", title: "Research only",
blurb: blurb:
"Produce a styled MD + PDF artifact in the workspace. One-shot or scheduled.", "Produce a styled MD + PDF artifact in the workspace. One-shot or scheduled.",
@@ -478,6 +483,7 @@ export const TEMPLATE_PRESETS: TemplatePreset[] = [
}, },
{ {
kind: "research_and_code", kind: "research_and_code",
defaultTeamTemplate: "rust_sdlc",
title: "Research + Coding Loop", title: "Research + Coding Loop",
blurb: blurb:
"Research a topic against a repo, then loop the coding team through the produced INT-XX items until done.", "Research a topic against a repo, then loop the coding team through the produced INT-XX items until done.",
@@ -489,6 +495,7 @@ export const TEMPLATE_PRESETS: TemplatePreset[] = [
}, },
{ {
kind: "security_hardening", kind: "security_hardening",
defaultTeamTemplate: "rust_sdlc",
title: "Security Hardening", title: "Security Hardening",
blurb: blurb:
"Scan the repo for vulnerabilities, research patches, then apply + verify.", "Scan the repo for vulnerabilities, research patches, then apply + verify.",
@@ -501,6 +508,7 @@ export const TEMPLATE_PRESETS: TemplatePreset[] = [
}, },
{ {
kind: "refactor", kind: "refactor",
defaultTeamTemplate: "rust_sdlc",
title: "Refactor", title: "Refactor",
blurb: blurb:
"Audit dependencies + versions, propose API/SDK adaptations, apply the changes.", "Audit dependencies + versions, propose API/SDK adaptations, apply the changes.",
@@ -509,6 +517,7 @@ export const TEMPLATE_PRESETS: TemplatePreset[] = [
}, },
{ {
kind: "benchmark", kind: "benchmark",
defaultTeamTemplate: "rust_sdlc",
title: "Benchmark", title: "Benchmark",
blurb: blurb:
"Author + baseline benchmarks so subsequent refactors can be measured before/after.", "Author + baseline benchmarks so subsequent refactors can be measured before/after.",
@@ -557,4 +566,5 @@ export const recipeToPreset = (r: WorkflowRecipe): TemplatePreset => ({
.slice() .slice()
.sort((a, b) => a.order_idx - b.order_idx) .sort((a, b) => a.order_idx - b.order_idx)
.map((p) => ({ kind: p.kind, order_idx: p.order_idx })), .map((p) => ({ kind: p.kind, order_idx: p.order_idx })),
defaultTeamTemplate: r.default_team_template ?? null,
}); });