3bbbccb019fd453a721528a59a48afcda03df3a1
32
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fca828b5a1 |
feat(self-audit): a self_audit recipe carries the brief continuous_improvement never had
Role system_prompts are inert on mission turns, so a team without a recipe has no default brief and every caller must write one (v2 wrote the wrong one: 0/50 calls read the record). The recipe points the phase at /mission/memory/PROJECT-MEMORY.md, requires verbatim quotes only where a pattern is reported, and names the clean-record and no-record outcomes as passes — the v3 condition failed a correct clean audit for paraphrasing. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
5c2c63f8e8 |
feat(missions): a human can finally reach the plan/roster review gate
Phase 4 of the plan, plus the PLAN_COMPLETE decision and the gitea_forge
cleanup from Phase 5.
THE REVIEW UI
mission_plan and mission_roster have been complete and reachable by curl
since they shipped, with zero frontend. That matters more than a missing
screen usually would: the decide step is not a convenience, it IS the
safety mechanism. Approving a plan replaces the mission's phases; approving
a roster flips it to the composed engine. A gate nobody can reach is a gate
that is always open or always shut.
MissionProposalDrawer, modelled on LevelUpDrawer which already does
load → review → decide. Reached from a mission's SETUP tab. Verified end to
end against the live backend, not just compiled: a model proposed a roster,
approval flipped the mission to `composed`, and approval on a non-draft
mission was refused.
The plan view shows each phase's done_when, and says plainly when one is
absent — a phase without a completion condition is never judged and reports
completed whatever it did, so its absence is the thing worth seeing.
AND THE DEFECT BUILDING IT FOUND
Every refusal path computed a precise reason — "the mission is running, not
a draft", "no node can boot that backend any more" — logged it to stderr,
and returned a bare {"error":"bad request"}. The person who needed the
sentence was the one clicking Approve; they got two words, and the reason
went to a server log they cannot read.
ApiError::Refused(String) carries it now. Same argument ApiError::Unavailable
was added for ("a 500 with 'internal error' sent them looking for a bug that
was not there"), one status code down. Live: the 400 now reads "this mission
is completed — a roster can only be approved while it is a draft, because
approving one rewrites how the mission will run".
PLAN_COMPLETE, decided
The Skill-Use measurement found that int-xx-marker-protocol documents
PLAN_COMPLETE and task_card_parser never implemented it, so an agent
following the skill exactly was silently ignored. Implemented rather than
removed from the skill: the planner needs a way to say it is done
specifying, and agents already emit it.
Marker ids are now strictly INT-<digits>. `starts_with("INT-")` accepted the
range form `INT-01..02` — observed live — which parsed into an id matching
no real item, so a task card appeared for something that did not exist while
the two items it covered stayed open. Rejecting is right: an ignored marker
is visible, a plausible row is not.
GITEA_FORGE, REMOVED
Named in nine places, defined in none. Harmless while provision_claw ignored
the bundle list; once the list was honoured, an undefined name became a
capability an agent is told it has and does not. Removed from seven team
templates, a workflow recipe, the auto-provision path, and a dropdown a user
could pick it from.
A new test asserts every bundle a template names is defined in the runtime
config — and it immediately found `web_fetch` in two templates I had missed
removing by hand. Same shape as the skill-binding test, one layer up.
Agents reach the forge through git over HTTPS with the ambient GITEA_TOKEN,
which is why nothing ever broke.
Full workspace suite green (106 binaries); frontend builds clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
fe45f72f09 |
feat(podcast): a PODCAST tier, a topics field, and a feed a phone can actually reach
Three gaps between "the pipeline works" and "you can use it".
**1. Topics could not be set.** The wizard never sent `config.topics`, so every
mission created through the UI silently fell back to
`library::default_topics()` — a hardcoded list that is somebody else's research
interests. The card now takes one arXiv search per line, and the description
field says plainly that for this template it IS the brief the agents judge
relevance against.
**2. There was nowhere to see or subscribe.** New PODCAST tier in the left rail,
between AGENT and REPOS: the feed URL with a copy button, the episode list, and
an inline player for checking one at a desk. `GET /api/podcast/episodes` and
`/subscription` back it. The panel also reports how many missions produced no
audio, so a missing day reads as a known gap rather than silence.
**3. The feed 404'd for the only client that will ever request it.** Three
layers each assumed a browser:
- `resolveBearer` is server-only (`next/headers`), so a client component that
imported it broke the build outright. The panel now goes through the
same-origin proxy like every other panel, and the backend mints the feed URL
because the session lives in an httpOnly cookie JavaScript cannot read.
- The `/api` proxy demanded a session COOKIE. A podcast app has none and
carries `?token=` instead — the same shape as the existing `hooks/` prefix,
which is already exempt for exactly this reason.
- The local autologin middleware 307'd it to `/auth/autologin`. A podcast app
follows redirects blindly and would have stored an HTML page as the episode.
Neither exemption weakens auth: the backend still validates the token and
answers 401 to a bad one, verified. `episode_audio` accepts the token from
either the query string or an Authorization header, because the app fetches it
one way and the browser player the other, and refusing either breaks one of the
two ways this is listened to.
`CLAWMATES_PUBLIC_URL` matters and was wrong first: the tailnet root proxies to
a different service on :18789, and this frontend is on :8443. A feed advertising
an unreachable origin syncs silently forever, so `/subscription` returns a
`reachable` flag and the panel warns when it is still localhost.
Verified from a phone's point of view: feed 200 application/rss+xml over the
tailnet, enclosure 200 with 6,739,582 bytes of audio at 421s, bad token 401.
367 tests pass.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
5db695460f |
fix(missions-ui): the results area could not scroll at all
The canvas host is a position:relative BLOCK, so flex:1 on MissionCanvas's root was inert and its height collapsed to its content. That starved the scroller beneath it — scrollHeight === clientHeight — so it never scrolled, and the overflow spilled past the page and was clipped by the host's overflow:hidden. Long results were rendered and then thrown away. Every sibling canvas already used position:absolute; inset:0; missions was the only one that did not. Measured after, on a brief 5x the viewport: one scroller, clientH 736 vs scrollH 3244, scrolling 0 -> 2508 (exactly scrollH - clientH, i.e. the true bottom), zero page overflow, tab strip pinned throughout. Also removed five nested scrollers (70vh on live events; maxHeight caps on run streams, phase summaries, artifact bodies and error traces). Those existed only to work around the missing height and would have become portholes onto the very content the operator is trying to read. The xterm pane keeps its bounded box — FitAddon needs one, and a terminal owning its scrollback is correct. Deleting the header's description peek reclaims 104px for results (header 256 -> 152px); the same text renders in full in Setup -> Overview, as the code's own comment noted. Streaming now follows only when already at the bottom, via a shared useStickToBottom hook replacing two byte-identical copies, plus a "jump to latest" pill neither had. Defaults collapse by mission state, and a remount key fixes scrollTop leaking between tabs — a bug that only appears once scrolling works. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
821cbb8622 |
feat(missions): hold every producing phase to delivering, and read markdown instead of PDFs
Two changes the portal review asked for.
1. `benchmark` and `security_hardening` had no delivery guarantee.
`empty_delivery_is_a_failure` tested `kind == "coding"`, on the reasoning that
"research phases legitimately write nothing to the tree" — which the research
directive three modules over contradicts, since it tells the agent to save
findings under /mission/repo/research/. The cost: a `benchmark` mission is ONE
benchmark phase, and with that phase exempt nothing in the platform could fail
it. Same for `security_hardening`, whose first two phases are security_scan and
research.
Now keyed on PRODUCING_KINDS = coding, research, benchmark, security_scan.
`review` stays exempt — a reviewing phase that changes nothing has done its job,
the same distinction `vm_stop_gate::per_node` makes. The test that encoded the
old rule is rewritten rather than deleted, with the reasoning that replaced it.
All 8 harness fixtures are coding phases, so harness behaviour is unchanged.
2. PDFs are dropped; markdown is the deliverable.
Rendering a PDF meant asking an LLM to convert markdown to HTML — a paid API
call per document, on the critical path of "let me read my research", which
failed on depleted Gemini credits and left every artifact unreadable. Styling at
render time is free, offline, instant and cannot 429.
- `mission_outputs` no longer requests a render.
- New `GET /api/missions/{id}/artifacts/{artifact_id}/content`. The frontend had
no way to READ an artifact at all: it listed paths and offered a PDF preview
that never rendered (and whose `rendered_pdf_path` had no route serving it).
Two containment rules, both enforced: the artifact must belong to a mission in
the caller's workspace, and the CANONICALISED path must stay under `_outputs`
— canonicalise first, because checking the string before resolving `..` is the
classic hole.
- `MarkdownBlock` now uses react-markdown + remark-gfm + rehype-slug. It was a
deliberate zero-dep renderer for "the subset the refiner emits", and that
subset stopped matching reality: agent briefs are largely GFM pipe tables,
which it showed as literal pipes. MissionOutputReader and RefineDiffModal use
the same component and gain tables for free.
- Heading ids come from rehype-slug and `outlineOf` slugs with the same
GithubSlugger, so the outline rail's anchors still resolve. A test pins that
invariant, including duplicate headings.
Styles live in globals.css under `.md-view`: the markup is generated so there
are no class hooks, and this project has no styled-jsx registry — the app-router
requirement is documented in next/dist/docs/01-app/02-guides/css-in-js.md, which
frontend/AGENTS.md exists to make me read.
The artifacts tab moved to `MissionArtifacts.tsx`. MissionCanvas was 1341 lines
against a 1250 limit BEFORE this change — already failing lint; it is now 1248.
238 backend lib tests, 20 backend test binaries, 89 frontend tests, clean tsc,
clean eslint on every file touched, production build succeeds.
|
||
|
|
fe57ce4ed1 |
feat(missions): surface goal conditions and per-pass verdicts in the UI
Makes the completion evaluator usable and observable.
- GET /api/missions/{id}/phases/{phase_id}/evaluations returns every verdict
for a phase, newest pass first, scoped like the summary endpoint.
- MissionPhase gains done_when / max_iterations / iteration, so the phase card
can show what the phase is working toward and which pass it is on.
- PhaseStatus gains 'evaluating' (amber) -- the state between "runs finished"
and "phase done" that only conditioned phases enter.
- New PhaseGoalStrip renders on the phase card, and renders NOTHING for phases
without a condition so unconditioned missions look exactly as before. It
polls only while the phase is running or being judged.
- Mission wizard step 2 gains the condition + a max-passes field.
Two deliberate emphases in the UI:
The evaluator's `reason` is the most prominent element, because it is both the
explanation of why a phase iterated and the literal text handed back to the
agents as guidance -- it is what tells an operator whether the condition is
written well.
The hint copy states the constraint that actually governs whether a condition
works: the judge cannot run commands, it only reads what the agents wrote, so
the condition has to be provable from their output. "cargo test reported 0
failures" works; "the code is well factored" does not. Getting this wrong is
the difference between a phase that converges and one that burns every pass.
An evaluator error is rendered distinctly from a negative verdict, so a judge
outage doesn't read as a judgement on the work.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
||
|
|
0785ac9c79 |
feat(missions): document reader + three-tab IA for the mission page
The mission page made its own output unreadable. Reviewing a research
brief meant scrolling a 300px <pre> nested inside a 260px run box nested
inside the page scroller (plus a 4th scroll region for the description) —
and the text was capped at 6,000 chars server-side with no way to fetch
the rest, so a 53kB brief showed ~11% of itself and silently dropped the
remainder. Eight flat tabs (overview/phases/tasks/team/live/artifacts/
benchmarks/pane) mixed lifecycle, work items, people, telemetry, outputs
and infra at one level, so nothing indicated where the deliverable lived.
Reader:
- GET /api/missions/{id}/documents lists every agent output (titles +
sizes, no bodies); GET .../documents/{run_id}/{index} returns one in
full. Scoped to the mission so a run id from elsewhere can't be read.
- MissionOutputReader: rail (documents grouped by phase) · document ·
outline (headings, click to jump). Exactly one scroll container per
column, never nested. Copy + download .md.
- MarkdownBlock gains fenced code blocks (agent output is full of ```rust,
previously mangled into paragraphs), h4-h6, heading anchors, and an
outlineOf() helper.
Information architecture:
- Three primary tabs with shallow sub-views: RUN (phases/tasks/live) ·
OUTPUT (documents/artifacts/benchmarks) · SETUP (overview/team/pane).
- PhaseRunsList shows a short excerpt with no inner scrollbar and points
at the reader for the full text.
- The header description is clipped, not scrollable; its full text now
has a home in Setup → Overview.
Missions list:
- /api/missions returns MissionListItem — Mission flattened plus
phases_total/phases_done/current_phase, so the JSON stays a strict
superset. Cards render a progress bar and "Coding · 1/2" instead of a
bare status dot.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
||
|
|
c4ecb9baa4 |
missions: collapsible header + scrollable tabs + wrap phase controls
- Mission header title/description now collapsible via chevron next to the title. Persisted in localStorage so it stays hidden across mission switches once the operator has read it — clears more room for phases/tasks/team panels below. - Tabs row: overflow-x auto + per-tab flex:none + whiteSpace:nowrap so 8+ tabs scroll horizontally instead of wrapping and cutting off. - Phase card action row (retry/security/benchmark buttons): flexWrap wrap so long button rows stack cleanly instead of overflowing. - Phase card status row wraps too, and the card itself gets overflow:hidden + minWidth:0 so long content stays inside the border and the parent tab-panel scroll handles vertical growth. |
||
|
|
5c63ef0ed3 |
missions: phase-completion summary card (Claude Opus 4.8 synthesized)
New phase_summarizer background worker fires on any mission_phase
transition to a terminal state (completed/failed). Aggregates every
topology_runs.checkpoint.outputs[] + mission_tasks + mission_artifacts
bound to that phase and asks Claude Opus 4.8 to produce a structured
JSON card:
{ narrative, metrics, sources, tooling, next_actions }
Rendered inline on the mission page under each completed phase via
new PhaseSummaryCard component. Metrics grid is kind-specific:
research surfaces insights/sources/int_cards/artifacts, coding
surfaces cards_picked_up/commits/tests/issues, benchmark surfaces
regressions/improvements, security surfaces findings-by-severity.
New table: mission_phase_summaries (migration 0060), unique per
phase_id — regenerates on retry.
New endpoint: GET /api/missions/{id}/phases/{phase_id}/summary.
Model overridable via CLAWMATES_SUMMARIZER_MODEL. Reuses the
ANTHROPIC_API_KEY prod already carries for mission_refiner.
|
||
|
|
0210f5bf51 |
cleanup(missions): strip refresh debug scaffolding
Removes refreshClicks counter + console.log now that the fetch-hang was root-caused (fetch: cache no-store) and fixed. Keeps the updated-at timestamp indicator as ongoing visual feedback. |
||
|
|
c4e7ca8aa4 |
fix(mission_runtime): seed per-mission gateway with shared pairing state
Fresh mission runtime containers had no ZEROCLAW pairing token so the topology_worker got 401 Unauthorized on WS connect. Mount the shared runtimes /root/clawmates-runtime/data as /zeroclaw-data so the gateway boots pre-paired and accepts the servers ZEROCLAW_TOKEN. Seed dir overridable via CLAWMATES_RUNTIME_SEED_DIR. Known caveat: sqlite sessions dir is shared across concurrent mission runtimes. Fine while topology_worker runs sequentially per mission; next iteration should copy-on-write per-mission. |
||
|
|
b7f0b46971 |
debug(missions): loud refresh diagnostic + drop disabled attr
The refresh button was suspected of being inert when loading is
somehow stuck true. Removes disabled and renders a bulletproof
click counter + loading state next to the icon:
clicks:0 · updated 14:05:12 · idle
- clicks bumps SYNCHRONOUSLY in onClick before any await, so a
non-zero counter proves the click event reaches the handler
- console.log fires alongside for devtools verification
- disabled={loading} removed; if load happens to hang, at least
the user can click again to retry
Temporary scaffolding — will collapse once the root cause is clear.
|
||
|
|
d42398d3a8 |
missions: no-store fetch + visible updated-at timestamp on refresh
- api client: cache: no-store so manual Refresh guarantees a fresh server response (was potentially hitting stale HTTP cache). - MissionCanvas: renders "updated HH:MM:SS" next to the refresh button; the timestamp bumps on every successful load so the click is visibly acknowledged even when nothing else on the page changed. |
||
|
|
1e91a19707 |
missions: fix repo checkout for retries + tokenize git.redclaw.dev clones
- mission_orchestrator: run ensure_checkout BEFORE the team_id short-circuit. Previously, a re-launched or retried mission bailed out at the team_id=already-bound guard and skipped repo checkout entirely, so agents ran against an empty workspace. - mission_workspace: inject GITEA_TOKEN into git.redclaw.dev URLs so clone auth works from the server container. Redact any token echoed back on failure. - refresh buttons on MissionCanvas + MissionsList now spin the icon while loading so clicks are visibly acknowledged. - refresh-spinner keyframe added to motion.css. Requires operator on gw-04: sudo chown 65532:65532 /var/lib/clawmates-missions (applied 2026-07-21 pre-commit). |
||
|
|
66e57c5c1c |
missions: live activity stream per running run on phase cards
Adds a "show activity" toggle to any running topology_run row on
the phase card. Expanded rows mount a compact SSE tail from
/api/topology-runs/{id}/events, rendering step/reasoning/tool
events as they arrive — same stream the LIVE tab consumes, just
scoped to one run.
Extracted the phase-runs list into PhaseRunsList to keep
MissionCanvas under the 1250-line budget.
|
||
|
|
94fecb526c |
missions: retry failed phases + auto-purge on re-launch
Every re-attempted phase now starts with a clean slate:
- phase_runner::launch_phase DELETEs prior status IN ('failed',
'cancelled') topology_runs for the phase before enqueuing the
new ones. Completed runs are kept for audit; only the failure
noise from earlier attempts goes.
- POST /api/missions/{id}/phases/{phase_id}/retry — resets a
failed/cancelled phase to 'pending' (auth-scoped to the calling
workspace + guarded on mission.status='running'). phase_runner
picks it up on the next 10s tick.
- MissionCanvas phase card grows a coral 'Retry' button, visible
only when phase.status='failed' and mission.status='running'.
Click → resets + refreshes; the prior failed run rows disappear
from the card as soon as phase_runner enqueues the new attempt.
Design: auto-purge in phase_runner rather than a separate 'clear
failed runs' endpoint. Users don't have to manually clean up before
retrying; the runner does it as part of the natural work of firing
a fresh attempt.
Verified: cargo check + tsc + eslint --quiet all green.
|
||
|
|
f5bba67e38 |
missions: surface per-phase run errors on the phase card
Adds inline failure debugging to the Phases tab. When you see a
phase card marked FAILED, click the collapsed error summary and the
full topology_run.error text expands under it — the exact stack
trace / provider error / whatever the worker recorded.
Backend:
- TopologyRunSummary gains mission_phase_id + team_id + error
fields. list_by_mission SELECT extended; other constructor
(list_recent) explicitly passes None for the new fields.
- GET /api/missions/{id}/runs response now carries all of the
above so the frontend can attribute failures per phase.
Frontend:
- MissionRunSummary type mirrors backend additions.
- MissionCanvas fetches runs alongside mission on load +
auto-refresh; indexes by mission_phase_id in a memoized Map.
- Each phase card renders a per-run row: colored status pill
(running / completed / failed), short run id, finished_at
timestamp. For failed runs, a <details> collapses the error
text — first line as summary, full 4kB in a monospace <pre> on
expand.
Directly unblocks the "phase says Failed but there's no info to
debug" report. Both research and coding phases get this — the code
path is phase-kind-agnostic.
|
||
|
|
eb1df6acde |
launch: accept config.phase_teams as a valid team source
Missions created via the new multi-team wizard have neither team_id
nor team_template_id set — they carry config.phase_teams. Both the
frontend Launch button gate and the backend set_status precondition
were checking only the old two fields, disabling launch for every
new wizard-created mission with a "No team" tooltip.
- MissionCanvas: hasTeam now also returns true when
mission.config.phase_teams has at least one non-empty list.
- routes::missions::set_status: same check on the server so a
direct API caller with only config.phase_teams also gets past
the gate.
Directly unblocks the "we just finished the wizard, Launch is greyed
out" report. Agents materialize AFTER Launch — the button is the
trigger, not a post-condition of creation.
|
||
|
|
b8b8cb452e |
missions: multi-team model — pick research + development teams
Directly addresses "we want to pick one or more teams to assign to a
mission, first screen research teams, next screen dev teams." A
mission now materializes N teams, each tagged with a phase purpose.
Backend:
- 0056_mission_teams.sql — new join table
mission_teams(mission_id, team_id, purpose). team_id PK because a
team belongs to one mission-purpose. missions.team_id kept as
legacy pointer to the first minted team for single-team surfaces.
- mission_orchestrator::on_launch — reads mission.config.phase_teams
(JSONB shape { research: [tid,...], coding: [tid,...] }), mints
one team per (purpose, template) pair, records each in
mission_teams, binds the first to mission.team_id. Legacy fallback:
if config.phase_teams is absent, uses missions.team_template_id.
Hard error if both are absent.
- GET /api/missions/{id}/teams — returns
[{ team_id, purpose, team_name }], sorted by created_at asc.
Frontend wizard (step 3 rewrite):
- researchTeamIds / devTeamIds — Set<string> multi-selects
- Reusable TeamMultiSelect component (checkbox-style cards)
- Panels rendered conditionally by preset:
hasResearchPhase → "Research teams" panel
hasCodingPhase → "Development teams" panel
neither → "Teams" panel (bench/security-only missions)
- canNext enforces at least one pick in every visible panel
- submit builds config.phase_teams and passes it via CreateMissionRequest
- Review step shows both selections by name
MissionTeamTab:
- Fetches /api/missions/{id}/teams and groups by purpose
- Each purpose renders a section with per-team cards
- Falls back to a single "mission" pseudo-row for legacy missions
that only have missions.team_id (no mission_teams rows)
CreateMissionRequest no longer sends team_template_id from the wizard
— the multi-team config.phase_teams path supersedes it. The backend
still accepts team_template_id for API callers.
Verified: cargo check --workspace + tsc + eslint --quiet all green.
|
||
|
|
0ee689f590 |
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.
|
||
|
|
3ba0485e7d |
mission progress UI: auto-refresh + Team tab + Live events tab
Fills the biggest UX gap surfaced during the deploy walk: hosted
missions had no live-progress surface at all. Now they do.
Auto-refresh:
- MissionCanvas grows a second useEffect that polls getMission
every 3s while mission.status === 'running'. Stops immediately
on terminal state (completed / failed / cancelled). Phases,
Tasks, Artifacts, Benchmarks all update without a manual click.
Team tab (new):
- MissionTeamTab.tsx — fetches /api/teams/{id} + /api/team/claws,
shows a card per member with role slot + an "Open" pill that
calls onOpenClaw(clawId) → Dashboard flips to AGENT tier with
that claw selected, dropping the operator into the existing
ClawCommandCenter surface (WorkingOnNow, ReasoningStream, etc).
Live events tab (new):
- MissionLiveEvents.tsx — polls /api/missions/{id}/runs every 5s
for the topology_runs bound to this mission, opens one
EventSource per active run against /api/topology-runs/{id}/events,
renders as a chronological scrolling feed with per-event kind
pills + per-run short-id badges. Auto-scrolls unless the
operator scrolled up. New runs auto-attach; terminal runs
close cleanly.
Backend:
- cm-db::repo::topology_runs::list_by_mission — SELECT ... FROM
topology_runs WHERE mission_id = $1 ORDER BY created_at DESC.
Uses runtime sqlx::query (not the macro) to avoid a sqlx cache
regen just for this route.
- TopologyRunSummary gains #[derive(Serialize)] + rfc3339 codecs.
- GET /api/missions/{id}/runs — workspace-scoped, returns
{ runs: [...] }.
Dashboard wires onOpenClaw on MissionCanvas → setAgentId + setTier("claw").
Verified: cargo check --workspace + tsc --noEmit + eslint --quiet
all green.
|
||
|
|
cf735312f8 |
mission canvas: cap description height with own scroll
Long refined descriptions (Opus tends to emit full section spines) pushed the tabs + toolbar past the viewport with no way to reach them. Cap the description block at 38vh with its own overflow-y so the header stays reachable no matter how long the brief gets. |
||
|
|
d8c8793c4a |
ci fixes: cargo fmt, eslint entities, max-lines split
CI on
|
||
|
|
6ffbe978b2 |
missions: extract MissionLivePane to stay under 1500-LoC CI budget
Previous push hit the gates job's file-size gate — MissionCanvas.tsx was 1517 lines (17 over). The Live Pane xterm subcomponent is cleanly separable (no shared state with the parent, just takes nodeId + visible props), so it lifts into its own file at zero behavior cost. frontend/src/components/dashboard/MissionLivePane.tsx (new, 106 LoC) frontend/src/components/dashboard/MissionCanvas.tsx (1517 → 1424) Also drops the xterm.css + useResilientTerminal imports from MissionCanvas since only MissionLivePane needs them now. |
||
|
|
a5588b0289 |
herdr phase 2: Live Pane tab (xterm.js → node's herdr TUI)
The killer UX feature: click a mission's Live Pane tab and watch the
actual Herdr TUI on the target node in the browser — cursor, colors,
tool output, all live. WebRTC DataChannel direct where the browser
can reach the node peer-to-peer, WS-relayed fallback otherwise
(same auto-negotiation the INFRA node terminal already uses).
Zero new deployment infra — reuses the existing terminal_ticket +
terminal_ws + PTY-over-control-channel machinery. The one primitive
we grew: PtyTarget::Command variant so the node can spawn an
arbitrary program (\`herdr\`) in the PTY instead of the login shell.
Node daemon (clawmates-node):
- PtyTarget grows a Command { argv } variant
- spawn_command_pty resolves bare names against user + system bin
dirs (matches how tool_update finds claude/kimi)
- PtyTarget::from_frame reads the `command` array from the pty_open
frame; precedence Command > Container > Host
cm-api:
- NodeHub::open_pty grows an optional command argv; when set, the
frame carries it and the daemon spawns the program directly.
- routes::nodes::TermCtrl gains a `command: Vec<String>`; the
fallback branch threads it through.
Frontend:
- core.ts::webrtcConnector takes an optional commandOverride
that ships inside the fallback frame
- nodeHerdrConnector(nodeId) — mints the standard ticket + WS URL
but overrides command to ["herdr"]
- MissionCanvas grows a "pane" tab, visible only when
runtime_kind='local_herdr'. LivePane subcomponent uses xterm.js
(already a workspace dep) via useResilientTerminal, shows a
connecting/relayed/direct pill in the corner.
To watch a mission live: pick "On a fleet node (Herdr)" + target
node in the wizard, launch, click Pane tab → node's Herdr TUI
appears. Navigate to the mission workspace in the Herdr sidebar
(mouse or prefix+w) to zoom into the mission's pane.
Focus-a-specific-pane-directly is a later enhancement — Herdr has
no CLI arg for it yet, so operator navigates the sidebar for now.
Verified: cargo check --workspace + tsc --noEmit both green.
|
||
|
|
1f0117e35a |
mission canvas: add / edit / delete toolbar controls
Top-right toolbar grows three CRUD controls per your request:
- Plus (always visible) — opens MissionWizard, selects the new
mission on create
- Pencil (draft-only) — opens EditMissionModal for title +
description; PATCHes /api/missions/{id}
- Trash (always visible) — window.confirm then DELETEs; sidebar
selection clears via new onDeleted callback
Backend:
- cm-db::repo::missions::update_meta(id, ws, title?, description?)
— COALESCE-based partial patch
- cm-db::repo::missions::delete(id, ws) — hard delete, cascades
via FKs on phases/tasks/artifacts/benchmark_snapshots
- PATCH /api/missions/{id} (draft-only) + DELETE /api/missions/{id}
Frontend:
- lib/api/missions — updateMission + deleteMission clients
- MissionCanvas — three toolbar buttons, EditMissionModal
(title + textarea for description), local wizard state
- Dashboard — passes onSelect + onDeleted so sidebar reacts to
create + delete without stale selection
Edit is draft-only (backend enforces + button hidden past draft) so
in-flight missions can't have their brief mutated out from under
running agents. Delete is unconditional — operator responsibility to
Cancel first if a run is live.
|
||
|
|
ad1cee0b08 |
refine polish: before/after diff view + accept/cancel/restore
Refine no longer clobbers the mission description on click. Flow:
1. Click Refine → server generates the rewrite, returns
{ original, refined } WITHOUT persisting
2. RefineDiffModal shows a side-by-side pane (raw before,
Markdown-rendered after)
3. User picks:
- Accept → PATCH /api/missions/{id}/description commits refined
- Cancel → discards the proposal, description unchanged
- Restore original → forces a write of `original` (undo path
for accidentally-accepted refines, since Accept+Cancel is
still a two-step confirmation)
Backend:
- mission_refiner::refine returns a RefineResult { original, refined }
struct instead of persisting + returning the text
- routes::missions::refine now returns { original, refined }
- routes::missions::set_description added on PATCH
/api/missions/{id}/description (draft-only)
Frontend:
- lib/api/missions — refineMission return type is now RefineResult;
added setMissionDescription
- MissionCanvas — RefineDiffModal + DiffPane subcomponents;
accept / cancel / restore handlers wired to state
Closes task #20.
|
||
|
|
278cbf90b7 | artifacts tab: inline PDF preview via iframe (task #19) | ||
|
|
267b28a762 |
mission canvas: security-scan + benchmark trigger buttons
Fills the frontend gap after slices 7 + 8 shipped the backend runners
without triggers. Each phase card in the Phases tab now grows an
action row when the mission is running or completed:
- security_scan phase → "Run scan" button (POSTs /security-scan)
- benchmark phase → "Baseline" + "After" buttons, iteration auto-
derived from the current benchmark_snapshots count
Findings from security_scan already surface in the Tasks tab via
the task-card parser (external_id = cargo_audit:RUSTSEC-... etc);
the tool prefix in the badge is enough to distinguish sources
without a separate grouped view.
Closes tasks #17 + #18.
|
||
|
|
56201a6985 |
mission canvas: add Refine button + markdown-rendered description
Adds a Refine button to the left of Refresh + Launch on the mission
detail toolbar (draft-only). Clicking it POSTs to a new endpoint that
calls Gemini 2.5 Flash to rewrite the user's freeform description into
a coherent, sectioned Markdown brief (Objective / Context / Scope /
Constraints / Acceptance Criteria / Open Questions) ready for the
research + coding agents to ingest cleanly.
Backend:
- crates/cm-api/src/mission_refiner.rs — Gemini call with a
system prompt that preserves user-provided facts, avoids
invention, and emits raw markdown (not JSON).
- POST /api/missions/{id}/refine — draft-only, 400 on empty
description or non-draft state.
- cm-db::repo::missions::set_description helper.
Frontend:
- MarkdownBlock — tiny zero-dep renderer for h1/h2/h3, bullet +
numbered lists, **bold**, `code`, paragraphs. Deliberately
small; the refiner emits a bounded subset.
- MissionCanvas — Refine button (Sparkles icon, secondary style)
to the left of Refresh; description now renders through
MarkdownBlock instead of a single <p>. Disabled while
description is empty or a refine is in flight.
- lib/api/missions — refineMission client.
|
||
|
|
f843c9ddb1 |
slice 7: before/after benchmark runner
Executes a benchmark harness inside the mission's team container
and records the resulting metrics as a benchmark_snapshots row keyed
on (phase_id, iteration). Baseline pass (iteration=0) captures
before_metrics; each post-iteration call captures after_metrics +
computes delta vs baseline.
Rust surface:
- cm_db::repo::missions::upsert_benchmark_snapshot / benchmark_snapshots_for
- cm_api::benchmark_runner::{baseline, after_iteration, run}
- Harness enum: Auto | Criterion | CargoBench | VitestBench |
PytestBench | Shell (each with a command() vector)
- Auto detection peeks at the repo layout inside the container
(Cargo.toml → CargoBench, package.json → VitestBench, pyproject
→ PytestBench). Falls back to a Shell echo when nothing
identifiable.
- Bencher-format line parser extracts (name, ns_per_iter,
plusminus) so criterion + `cargo bench` output become structured
samples the canvas can diff.
- compute_delta pairs samples by name, emits {before_ns, after_ns,
delta_pct, direction: improved|regressed}.
API:
- POST /api/missions/{id}/benchmark { phase_id, slot, iteration? }
triggers baseline or after run and returns the mission's full
snapshot list.
- GET /api/missions/{id} now includes `benchmarks[]` in the detail
payload.
Frontend:
- New Benchmarks tab on MissionCanvas with iteration + driver
header, plus a 4-column grid (bench / before / after / Δ%) when
delta samples are present. Improved deltas render green,
regressions red.
- TS types + triggerBenchmark() helper in lib/api/missions.ts.
Wiring notes:
- team_container_for_mission reads teams.zeroclaw_container — that's
populated by topology_worker::try_team_gateway_url on first run,
so trigger baseline AFTER the mission's first phase spawns the
container.
- Not auto-fired yet by phase execution; that's the "template phase
executor" work that spans Slices 4-8. Manual API trigger works
today; automated hook is a follow-up.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
||
|
|
fc67936e33 |
slice 2: MissionWizard + MissionCanvas + MissionsList frontend
Adds the unified missions tier to the dashboard. Runs in parallel
with Research + Loops tabs until Slice 9's big-bang cutover.
New files:
- lib/api/missions.ts — typed API client + 5 template presets
(research_only, research_and_code,
security_hardening, refactor, benchmark)
- dashboard/MissionsList.tsx — sidebar list with status pills +
template-kind badges + new-mission CTA
- dashboard/MissionWizard.tsx — 5-step adaptive wizard:
1) template picker (5 cards)
2) title/description + repo (when required)
3) team (placeholder — Slice 3 wires templates + auto-provision)
4) schedule (one-shot or cron)
5) review + launch
- dashboard/MissionCanvas.tsx — 4-tab detail view (overview/phases/
tasks/artifacts), draft→running launch
Dashboard.tsx gets a new "missions" tier + crumb + rail icon (flag).
Slice 2 ships a minimal implementation that creates missions with the
template's canned phase composition. Slice 4 replaces the preset table
with real TOML-recipe dispatch on the server; the client's fallback
presets keep offline preview working.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|