default_risk_profile_for_role decides whether a claw gets file edits, git and
shell by substring-matching its role against a fixed keyword list. On the
planner path that role string is free text the model invented for this
proposal, so a model's choice of wording silently decided tool access: a
proposed "implementation_lead" matches no keyword, lands research_readonly,
and then fails every file edit for a reason invisible from the role name.
TeamMemberInput and the planner's member schema now carry `needs_write`, and
resolve_risk_profile prefers it over the guess. The planner prompt asks for it
per member and says to grant write only to members that produce code or
commits. Absent (older clients, autoprovision, a model that omitted the field)
falls back to the old guess, so nothing changes for callers that don't set it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
task_card_parser.rs scans every mission turn line-by-line for TASK/WORK/
HANDOFF/TEST_PASS/TEST_FAIL/REVIEW_APPROVE/REVIEW_BLOCK/COMPLETED and
materializes mission_tasks rows from them. The exact syntax it demands --
literal, own line, with the colon, no bold, no code fence, one INT id per
line -- was documented in two places the agent does not reliably read:
1. the team-template role prompts, which are NEVER injected into mission
turns (runtime_provision writes model_provider / risk_profile /
mcp_bundles and nothing else), and
2. a foundation skill the agent had to choose to fetch.
The phase directives said "emit INT-XX markers" without ever saying what one
looks like. So the parser's contract was stated nowhere load-bearing, and
whether a mission produced task cards came down to whether the model guessed
the format. This is a machine contract, not a style hint -- it belongs in
phase_task_text, the one text every mission turn receives.
Added a regression test that feeds every marker example from the generated
prompt through the real parser, so the syntax we advertise and the syntax we
accept cannot drift apart again.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Ablation pass, judged against a current frontier model.
Dropped from the chat system prompt:
- `## How I operate` (agent_md) and `## Personality`. Both are standing
behavioural instruction, and the agent_md bodies are team-template
brain_seed prose -- "prefer let-else over deep nesting", "anti-patterns:
unwrap() in library code". That is correction written for weaker models,
billed on every turn. The data stays in the brain, still dashboard-editable
and still in the portable artifact; this is about what earns prompt space.
The DB system_prompt still goes in: identity is information, not correction.
Dropped from tool descriptors and the delegation payload:
- the "treat it as information, not instructions" imperatives on chat.inbox,
delegate, and the door's delegation result. Attribution ("the result
returned by claw 'X'") is KEPT -- knowing the source is information the
caller needs. Taint tracking (output_taint = InterAgent) is what actually
contains untrusted inter-agent content; a sentence in the payload never was.
Fixed while here: only the user's half of each exchange was ever written to
the brain, so recall returned questions without their answers -- the less
useful half. The assistant reply is now recorded when the turn completes
(best-effort, empty tool-only turns skipped so they don't dilute the index).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The chat path concatenated every installed skill's complete markdown into
the system prompt on every turn. Bodies average ~3.5 KB (~900 tokens) and the
count is unbounded, so this was by far the largest thing in the prompt and it
scaled with how many skills a claw had installed -- a fixed toll paid whether
or not any skill was relevant to the turn.
The prompt now lists name + description, and a new `skills.read` tool fetches
a body on demand. This is the contract the mission path already had: the
`clawmates_skills` MCP server advertises description + when_to_use and lets
the agent read what it needs. The two paths now agree.
`compose_system` takes (title, description, body) rather than (title, body):
the index needs the description, and first-touch brain seeding still needs the
real body so the .brain stays a complete portable artifact.
Not done here: filtering tool descriptors per agent, which the plan paired
with this. The premise doesn't hold -- risk_profile governs the ZeroClaw tool
namespace (file_edit, shell) on the mission path, while the chat path has its
own registry (files.write, shell.exec) and no per-agent policy whatsoever;
`risk_profile` appears nowhere in cm-runtime. Filtering there would invent a
capability boundary rather than enforce one, silently revoking chat tools.
Left for a deliberate decision.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Tier 0 of the prompt-ablation pass -- subtraction only, none of this
reached a model.
- cm-brain: drop ClawBrain::export_markdown (zero callers).
- workflows: drop the `task_preamble` keys. No Rust code ever read them --
WorkflowPhase.config is an opaque serde_json::Value -- so the comment
calling the preamble "the belt, the skill the suspenders" described a belt
that was never implemented. (`commit_policy` is unread for the same reason;
left in place as documentation pending a decision.)
- mcp_door: derive the unknown-tool error from EXPOSED_TOOLS. The literal had
drifted to naming one of the three tools the door exposes.
- Dashboard.tsx: drop TEAM_TEMPLATES/COMPANY_TEMPLATES, defined and never
referenced, and disconnected from the real templates/teams/*.toml.
The substantive one: GET /api/claws/{id}/compartments returned hardcoded
strings for tools/capabilities/safety, identical for every claw. Every card
read "Network: none" and "Shell . blocked" regardless of the claw's real
risk_profile -- which is the actual capability boundary, so the card was
most wrong exactly where it mattered, on a coding_readwrite claw that does
have shell. Now derived from the claw's effective risk_profile (its team's
setting, else the same role-derived default the provisioner applies), with
the allowlists mirroring [risk_profiles.*] in the runtime config.
Note: cm-topology/src/heuristics.rs was slated for deletion here as unused.
It is not -- routes/topology.rs:43 serves it and p0_endpoints.rs:302 asserts
it. Left alone.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Agent containers leaked two independent ways.
1. The four-step teardown (deprovision ZeroClaw -> reap_sandbox -> unlink
.brain/.onion -> hard_purge) was inlined at three call sites and two had
drifted. missions.rs::reap_mission_resources skipped reap_sandbox;
topology_worker::maybe_teardown_ephemeral_team skipped it and the brain
unlink; DELETE /api/claws/{id} (soft delete) released nothing at all, so an
offline claw that can never run again kept its container and bind mount
forever. All four now funnel through claws::purge_agent, with
release_claw_resources for the soft-delete case (containers gone, rows kept).
2. Both orphan reapers listed only the local driver, so a container placed on a
fleet node was invisible to the only backstop that could find it -- this is
what accumulated 144 tc-agent-* orphans on one node. NodeDriverProvider gains
node_ids() (backed by NodeHub::online_ids) and both reapers now sweep every
connected node. The remote sweep is TTL-only on purpose: the boot pass runs
with Duration::ZERO and would otherwise kill a container another instance is
mid-provision on.
Why it was invisible: agent_containers.agent_id is ON DELETE CASCADE, so
hard_purge took the registry row with the agent and left the container
permanently unreferenceable.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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]>
`inject_node_agents` wrote the claw alias as a top-level `"agent"` key on
each graph node, but `cm_topology::Node` only deserializes `{id, role,
level, attrs}` — serde silently dropped it. `TurnRequest::agent` came back
`None` and every mission turn fell back to `ZEROCLAW_DEFAULT_AGENT`
(`scout`), running with scout's workspace and tools instead of the
mission's claws. The runtime trace confirms it: every turn logged
`"agent_alias":"scout"`.
That is why mission agents reported an "empty greenfield" workspace and
emitted artifacts inline instead of writing them: scout is jailed to
`/zeroclaw-data/.zeroclaw/agents/scout/workspace` and cannot see
`/mission/repo`. The per-mission provisioning and `workspace.path` pinning
shipped earlier were correct — they were just applied to agents that
nothing ever drove.
- bind into `node.attrs["agent"]` (top-level key kept for display/debug)
- extract the DB-free `apply_node_agents` and add a regression test that
round-trips through the real `TopologyGraph` deserializer, which is the
guard that was missing
- log loudly in `topology_exec::run_turn` when a node falls back to the
default agent, instead of silently swapping in a different agent
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Mission turns execute against the per-mission runtime container, but claws
were provisioned via RuntimeProvisioner::from_env() — i.e. the GLOBAL gateway.
That daemon loads config once at boot and never re-reads the file, so the
per-mission daemon had no claw_* agents at all: querying it for a mission
claw's risk_profile returned 404 while the global daemon returned 200. With
the alias unresolvable, the daemon silently fell back to the default `scout`
agent, which is jailed to the global workspace — agents reported "the scout
agent workspace" and "/mission/repo isn't accessible", produced no files, and
burned tokens. This is the deeper cause behind the empty-output runs; the
tool-allowlist and workspace-pin fixes were necessary but not sufficient.
- RuntimeProvisioner::for_gateway(url) — aim the provisioner at a specific
gateway (mirrors ZeroClawDriveExecutor::from_env_for_gateway); from_env now
delegates to it.
- mission_orchestrator captures the per-mission endpoint from ensure_container
and provisions every claw there, falling back to the global gateway only
when there is no per-mission runtime (dev/no-docker).
- workspace.path is file-only (the config prop API cannot set a PathBuf), and
the daemon never re-reads the file, so pin_agent_workspaces is now followed
by restart_container(): restart + wait for /health to answer. Agents created
through the daemon's own config API are already persisted to that file, so
they survive; the pairing code is re-minted on every launch.
The readiness probe inspects the /health BODY — exec_capture only fails on
docker errors, so a curl that cannot connect still "succeeds".
Co-Authored-By: Claude Opus 4.8 <[email protected]>
DELETE /api/missions/{id} was a bare `DELETE FROM missions` relying on FK
cascades that only cover mission-owned tables. Everything the mission
provisioned leaked: per-mission runtime container, host workspace dir,
teams (created lifecycle=permanent, so no cascade + skipped by the
ephemeral-teardown path), and every claw's ZeroClaw config, .brain files,
and DB rows. Observed live with 0 missions in the DB: 174 orphaned gateway
claw configs, 7 orphaned teams, 31 agents, 39 .brain files, 6 workspace
dirs, a 4-day-old orphaned container, and 123 detached topology_runs.
delete() now calls reap_mission_resources() before the row delete:
- resolve the mission's teams (mission_teams) → claws (team_members)
- per claw: deprovision_claw (gateway) + rm .brain files + hard_purge (DB),
reusing the manual agent-reap pattern in routes/claws.rs
- delete the permanent-lifecycle teams (team_members cascades)
- delete the mission's topology_runs (else they linger with mission_id
nulled by the cascade and accumulate)
- teardown_container(), now extended to also rm the /mission/repo workspace
dir and tolerate an already-gone container (idempotent for the sweeper +
delete paths)
Runtime-side steps are best-effort (Postgres authoritative; fleet sweeper
reconciles daemon config); DB purges are logged on failure but never block.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Mission agents were burning ~275K tokens producing nothing: the coder had
only file_read and its workspace was the empty ephemeral sandbox, so it
dumped a full spec inline instead of writing files. Two root causes:
1. Risk-profile allowlists used pre-0.8 tool names. `coding_readwrite`
allow-listed `file_write` (renamed to `file_edit` in ZeroClaw 0.8, and
`file_write` now refuses on ephemeral workspaces) and omitted file_edit
/ content_search / glob_search / git_operations — the exact tools the
phase prompt tells agents to use. Since allowed_tools is a strict
allowlist, agents were effectively read-only. Documents the correct
profiles in agent.config.example.toml (they only lived in host config;
the live runtime profiles were corrected via its config API).
2. workspace.path never got set. `agents.<alias>.workspace.path` is an
Option<PathBuf> the ZeroClaw Configurable macro skips from prop
enumeration, so provision_claw's set_prop always 404'd and the whole
call errored into a swallowed eprintln. Removes the dead set_prop and
pins the workspace out-of-band: MissionRuntimeProvisioner::
pin_agent_workspaces patches the shared config file on the per-mission
container (format-preserving via toml_edit, atomic temp+mv); the daemon
applies it on the same reload that surfaces the freshly-provisioned
claws. Covered by unit tests for the TOML stamp.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two stacked issues after risk_profile was fixed:
1. Claws had file_edit + 46 other tools available, but the templates
trained the agents to expect file_read/file_write (older ZeroClaw
tool names). Result: agent output kept saying "I only have file_read"
and dumped implementations into the context window as text.
2. Even with file_edit, the sandbox pointed at
/zeroclaw-data/.zeroclaw/agents/<alias>/workspace/ — NOT
/mission/repo where the checked-out mission repo actually lives.
unrestricted_filesystem=false blocked agents from reaching it.
Fixes:
- provision_claw now takes workspace_path. mission_orchestrator passes
/mission/repo — pins the per-claw workspace via
agents.<alias>.workspace.path to the bind-mount path so file_edit /
content_search / glob_search operate on the mission's git checkout.
- phase_task_text prepends an explicit tool inventory (file_edit,
content_search, glob_search, git_operations, git_forge, ...) plus a
WORKSPACE line pinned at /mission/repo. Each phase directive is
rewritten to reference file_edit / git_operations explicitly and to
call out "do NOT paste code in your reply expecting the platform to
save it."
The provisioner was hardcoding risk_profile=toolfree for every claw,
which the ZeroClaw config explicitly configures to EXCLUDE every
usable tool (shell, file_read, file_write, http_request, browser).
Result: coder/tester/committer claws had zero tools and produced text
in the context window with no ability to actually write files or run
tests — exactly what the last mission summary showed.
Fixes:
- provision_claw now takes risk_profile: &str, passed through from
the team template (development teams already had coding_readwrite,
which now actually gets applied).
- Research team templates updated from toolfree → research_readonly
(file_read) and papers_research → research_web_readonly
(file_read + web_search + web_fetch). Applied to both the on-disk
TOML files and the live DB rows.
- Added RuntimeProvisioner::default_risk_profile_for_role for
auto-provision code paths that lack a template context — picks
coding_readwrite for coder-like roles, research_readonly otherwise.
- Split rebind_model out of provision_claw so the model-change UI
path doesnt inadvertently clobber the existing risk_profile.
Templates DB fixup for missions launched pre-deploy is already
applied via manual UPDATE.
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.
Deprecated ZEROCLAW_WORKSPACE env var (schema.rs:17467) is used by
the daemon as a legacy config-dir pointer that overrides everything
else. Setting it to /mission/repo made the mission daemon compute
its config dir as /mission/repo/.zeroclaw (empty) and fall back to
defaults — zero agents loaded.
This is the actual root cause of Unknown agent errors on WS. The
seed-mount + admin/paircode/new + per-node-agent-injection fixes
we shipped earlier were correct but couldnt take effect because
the daemon wasnt reading our bind-mounted config at all.
Per-agent workspace pinning belongs in config.toml as
agents.<alias>.workspace, not env.
The topology graph shipped from team.graph only carries node.role,
not node.agent. The executor then defaults to alias_for(role) which
falls to ZEROCLAW_DEFAULT_AGENT (scout) — no such agent → 400.
Look up team_members(node_id → claw_id) at enqueue time and stamp
node.agent = claw_<hex> onto every node. Executor now dials the
specific claw provisioned for THIS teams role.
Was masked pre-C3 because the shared runtime hit the same 400 —
never noticed because no one clicked through to a real run there.
Pairing codes are single-use / expiring — a mission that reuses an
existing runtime container on a retry needs a fresh code, not the
stale one from the initial launch. Drop the runtime_endpoint gate
so ensure_container always fires, and its fast path re-mints via
/admin/paircode/new for existing containers.
Fresh runtimes had zero agents in their config so WS handshake with
?agent=scout returned 400. Bind-mount the shared runtimes data dir
so per-mission gateways inherit the seeded claw_* agents.
Per-mission pairing (minted via /admin/paircode/new) still works
against the shared devices.db — each mission gets its own accepted
token. Concurrency caveat on sqlite sessions.db documented in the
const doc comment.
Fresh gateways sometimes boot claim-ing already paired (no
pairing_code in the log banner), which broke the log-scrape approach.
Instead, docker exec into the container and hit the localhost
/admin/paircode/new endpoint that always mints a fresh one-time
code and returns JSON we can parse.
The seed-mount approach didnt work: even with the shared runtimes
data dir bind-mounted, a fresh gateway instance mints a new pairing
key and requires re-pairing. The topology_worker connect returned
401 forever.
New approach — per-mission gateways self-pair:
- Provisioner tails container logs after start, extracts the
X-Pairing-Code from the boot banner
- Persists it on missions.runtime_pairing_code (migration 0059)
- topology_worker constructs ZeroClawDriveExecutor with THAT code
via from_env_for_gateway_with_code, which triggers the lazy
/pair handshake on first turn and caches the returned bearer
Drops the shared-runtime data-dir mount — each per-mission gateway
now owns its own state, restoring the C3 isolation guarantee.
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.
UUIDv7 encodes time in the leading bytes so 12-hex prefixes are
NOT unique across missions minted in the same second. Docker
accepts up to 253 chars; use the full uuid.
- mission_runtime::spawn_sweeper: force-removes runtime containers
for missions terminal for >=30 min, clears runtime_endpoint. Wired
into clawmates-server main().
- docker-compose socket-proxy: NETWORKS=1 so bollard.connect_network
can attach containers to clawmates_edge for provider egress.
- phase_runner ordering: ensure_checkout BEFORE ensure_container so
the mission dir exists before docker mounts it.
- provisioner: mkdir_p the mission dir defensively for research-only
missions that skip checkout entirely.
When a topology_run is bound to a mission whose runtime_endpoint is
set, the worker constructs ZeroClawDriveExecutor against that URL
instead of the env-derived shared gateway. Falls back to shared for
non-mission runs and pre-C3 missions.
With slices 1-3 combined, a mission launched after this deploy will:
1. get its per-mission container spawned during on_launch
2. have its checkout dropped into /var/lib/clawmates-missions/<id>
which is bind-mounted to /mission inside that container
3. run its agents against ZEROCLAW_WORKSPACE=/mission/repo — so
they can see and edit only this missions repo, no bleed-over.
- mission_orchestrator::on_launch now calls ensure_container after
the repo checkout, persists the container_name + endpoint on the
missions row. Non-fatal — logs and continues on docker errors so
dev-mode + tests keep working.
- phase_runner::launch_phase does the same as a fallback for any
mission whose runtime_endpoint is null (pre-C3 or torn down).
Nothing reads the endpoint yet; slice 3 swaps topology_worker over.
- migration 0058: adds missions.runtime_container_name + runtime_endpoint
- new mission_runtime module (bollard): ensure_container /
teardown_container. Container is spawned on clawmates_core +
clawmates_edge networks with just /var/lib/clawmates-missions/{id}
bind-mounted so agents scoped to /mission/repo can only see this
missions repo.
- provider API keys forwarded from the server envs so per-mission
runtimes inherit them.
- Mission struct + repo helpers updated for the two new columns +
set_runtime_binding().
- Unit tests cover container naming determinism + entropy.
Not wired to the orchestrator yet — that lands in slice 2.
Moves ensure_checkout into launch_phase so retries + new phase
launches all trigger the clone/fetch. mission_orchestrator still
does its own checkout at initial launch time, so first-launch
timing is unchanged; this covers the retry + additional-phase
paths.
- 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).
Adds GET /api/topology-runs/{id}/output — trimmed view of the
runs checkpoint (totals + per-turn output previews, capped at
12 turns × 6kB each). The full checkpoint blob can be hundreds
of KB so it was never viable to send through mission polling.
Phase card run rows now expose a "show output" toggle for any
terminal run (completed/failed/cancelled), rendering turns,
tokens, records count, and per-turn agent text. Running rows
still get the live activity stream from the prior slice.
Diagnostic value: on a mission that "completed" without visible
work, this immediately shows whether the agents produced real
output (workspace missing / instructions vague / etc.) or
whether nothing ran at all.
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.
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.
Root-cause fix for "we hit launch, waited overnight, nothing ran."
mission_orchestrator materialized teams + agents fine, but nothing
enqueued the actual work — mission_phases stayed 'pending' forever
and topology_runs count for the mission was 0.
New crates/cm-api/src/phase_runner.rs — background worker on 10s
poll that does three things:
1. start_pending_phases — for every mission_phase with
status='pending' AND parent mission.status='running' AND all
lower-order phases already 'completed', enqueue one
topology_runs row per team whose (mission_id, purpose) matches
the phase kind:
phase=research → teams with purpose='research'
phase=coding → teams with purpose='coding'
phase=benchmark → teams with purpose='coding' (fallback)
phase=security_scan → teams with purpose 'security' | 'coding'
Each run gets a phase-kind-specific task text combining the
mission title/description + a directive for that phase.
Flips phase to 'running' after enqueue.
2. close_finished_phases — SQL sweep that flips phases whose
topology_runs are all terminal to 'completed' (or 'failed' if
any run failed).
3. close_finished_missions — same shape for missions whose phases
are all terminal.
Spawned alongside task_card_worker in clawmates-server main.rs.
Ordering enforced by mission_phases.order_idx — a coding phase
doesn't fire until its research phase completes.
Idempotent: every state transition is guarded so double-firing on a
race is safe. When a mission has no matching teams for a phase (bad
wizard state), the phase stays pending and the runner logs a skip
rather than getting stuck in a fail loop.
Existing topology_worker picks up the queued runs and drives them
through the ZeroClaw executor as usual.
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.
Adds the operator's five categorized research team archetypes:
1. codebase_research — code archeologist, architecture mapper,
flow tracer, vault scribe. Produces Obsidian vault entries
under Codebases/<repo>/ that make future missions faster.
2. papers_research — domain scout, paper reader, library curator.
Pulls arXiv / Semantic Scholar / conference proceedings, keeps
a structured local library under Papers/<topic>/.
3. insight_research — implementation tracker, novelty hunter,
publication drafter. Bidirectional loop that spots
publication-worthy novelty in our own implementations of
external papers.
4. continuous_research — signal harvester, ranker, digest writer.
Standing sweep of RSS + arXiv daily + GitHub trending; produces
a rolling ContinuousResearch/<date>/digest.md.
5. continuous_improvement — brain inspector, improvement proposer,
improvement evaluator. Standing self-audit that files level-up
proposals for the operator to review + measures the outcome.
Each template ships with role system_prompts + brain_seeds authored
in the same voice as the existing backend/frontend/etc templates —
evidence-first, redlines called out, no invention.
Schema + code:
- 0057_team_templates_category.sql — new column with
CHECK (research | development | security | ops). Existing rows
default to 'development'.
- team_templates::UpsertBuiltin + TeamTemplate carry category
(with default_category = 'development' fallback for
Serialize/Deserialize compatibility).
- team_template_loader reads `category = "..."` from the TOML;
absent defaults to 'development' so old templates keep working.
- Wizard step 3 filters:
Research teams panel → templates.filter(t.category==='research')
Development teams panel → templates.filter(t.category==='development')
Operator can no longer accidentally pick backend as their
"research team".
Test fixture updated with category="development".
The templates ship in the server image via the existing
`COPY templates /etc/clawmates/templates` line — no Dockerfile
change needed.
Multi-team refactor changed the error text from
'no team_id and no team_template_id' to
'no team_template_id and no config.phase_teams'. Assertion now
just checks both key phrases.
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.
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.
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.