Watches mission_artifacts for MD entries with render_pdf_status='pending'
and turns them into styled PDFs via:
1. Read source MD from <mission_root>/<path>
2. Call configured LLM (default gemini-2.5-flash) with a document-
typesetter system prompt that constrains style to a self-contained
HTML doc with inline CSS + our color palette
3. Print to PDF via `chromium --headless=new --print-to-pdf`
4. Save alongside source MD (foo.md → foo.pdf) + update
mission_artifacts.rendered_pdf_path + render_pdf_status='done'
Graceful degradation: GEMINI_API_KEY unset OR chromium missing =
row marked failed with a descriptive error, worker keeps ticking.
The frontend's "Open PDF" affordance (Slice 2) light up automatically
when render succeeds.
Boot ordering: PDF worker spawns after task_card_worker. Poll every
30s over up to MAX_PARALLEL=2 rows at a time — respects LLM rate
limits and keeps chromium's peak RAM under control.
Env knobs:
GEMINI_API_KEY — required for LLM step
CLAWMATES_PDF_RENDERER_MODEL — model id, default gemini-2.5-flash
CHROMIUM_BIN — chromium binary, default `chromium`
CLAWMATES_MISSIONS_ROOT — artifact dir root, default /var/lib/clawmates-missions
Dockerfile now installs chromium + fonts-liberation and sets
CHROMIUM_BIN=/usr/bin/chromium so the container image has everything
the renderer needs.
Also bumps workspace tokio deps to include the `process` feature
(required for tokio::process::Command).
Follow-ups:
- Anthropic + OpenAI provider variants (only Gemini in this slice)
- SSE stream on /api/missions/{id}/artifacts for the "PDF ready"
notification instead of poll-via-mission-GET
- Per-template PDF style overrides (currently one house style
for all missions)
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Watches topology_runs' event stream for the INT-XX marker protocol
(see skills/foundation/int-xx-marker-protocol.md) and materializes
mission_tasks rows with typed status so the canvas Tasks tab renders
a live timeline instead of raw agent chatter.
Migration 0051 adds mission_id + mission_phase_id columns to
topology_runs (nullable) so runs enqueued by a mission phase can be
attributed. Populated by future phase executors; NULL for legacy
research/loops runs (parser skips them cleanly).
New Rust surface:
- task_card_parser::parse(text) — line-scanner over TASK/WORK/
HANDOFF/TEST_PASS/TEST_FAIL/REVIEW_APPROVE/REVIEW_BLOCK/COMPLETED
markers. Strict: exact kind + colon + INT- prefix, no in-prose
matches, no bold/code-fence wrappers.
- task_card_parser::apply_for_run(pool, run_id) — reads the run's
mission binding, walks its event payloads, extracts text/output/
content/message string fields (matching every ZeroClaw event
shape we see), parses markers, UPSERTs mission_tasks via the
(phase_id, external_id) unique key from Slice 1.
- task_card_worker::spawn — 15s poller over runs updated in the
last 5 minutes. Idempotent + generous window survives server
restarts + task-scheduling jitter.
Boot wires the worker after the content loaders. Silent no-op when
mission wiring isn't populated yet.
MarkerKind → status mapping (monotonic-forward):
TASK → created
WORK → working
HANDOFF → validating
TEST_PASS → validating
TEST_FAIL → failed
REVIEW_APPROVE → validating
REVIEW_BLOCK → failed
COMPLETED → complete
Follow-ups:
- Wire phase executor to populate topology_runs.mission_id +
mission_phase_id (Slice 6/7/8 work)
- Assign assigned_agent_id via the event's producing agent alias
(currently always None)
- SSE stream on /api/missions/{id}/tasks for live canvas updates
(currently the canvas polls via mission GET)
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Two things land together:
1. Ships the 5 workflow recipes as TOML files under
templates/workflows/*.toml:
- research_only (hub_spoke research → MD + PDF)
- research_and_code (research → coding loop until INT-XX drained)
- security_hardening (scan → research patches → coding with
reviewer approval + full MCP bundle)
- refactor (single-pass coding with dep audit
preamble + before/after benchmarks)
- benchmark (author + baseline benchmarks per stack)
Each declares phases[], per-phase config, default_team_template.
Loaded read-only into an in-memory registry (workflow_registry)
via OnceLock — no DB row per recipe.
2. Ships the mission-launch orchestrator that closes the loop from
Slice 3.5d's mechanics. When a mission transitions draft→running,
`mission_orchestrator::on_launch`:
- Reads mission.team_template_id (skips if unset)
- Loads the team template detail (roles + skills bindings)
- Builds a topology graph from role slots via cm_topology::build
- Inserts the teams row + stamps template_id/version/risk_profile/mcp_bundles
- For each role: agent insert, model binding, runtime provision
(opt-in via RuntimeProvisioner::from_env), brain_seed::ingest
(Slice 3.5d), agent_template_link::upsert (Slice 3.5d),
team_members bind, audit trail
- UPDATE missions SET team_id = ...
Wired into routes::missions::set_status when prior.status='draft'
and new='running'. Failures log + are non-fatal (mission still
flips to running so the user can inspect + retry).
With this, Slice 3.5d's brain-seed + link machinery actually gets
populated, and the MCP skills server's template-defaults-merge path
(Slice 3.5b/d) starts serving real bindings to real agents.
Follow-ups (Slice 5-8):
- Task-card parser watches run events for TASK/COMPLETED markers
→ mission_tasks rows
- PDF renderer worker turns MD artifacts into PDFs
- Before/after benchmark runner honors phases[].config.benchmark
- Security scan MCP bundle exposes cargo-audit/gitleaks/trivy/semgrep
- Level-up endpoints diff learned-vs-seeded via agent_template_link
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Ships the lineage layer that ties agents back to their team template
and wires the MCP skills server to actually merge template default
skills with per-agent overrides.
Migration 0050 adds `agent_template_link` (agent_id PK, template_id,
template_version, role_slot, seeded_at, created_at + indexes for
template/role lookups). Populated at agent-materialization time by
Slice 4's mission-launch orchestrator; read here by the skills MCP
server and by future level-up (Slice 8.5).
New Rust surface:
- cm_db::repo::agent_template_link (upsert / get / mark_seeded /
agents_for_template — the last is what level-up's "prompt upgrade
on template N+1" query needs)
- cm_api::brain_seed::ingest(claw_id, seed_md, identity_prompt)
opens cm_brain::ClawBrain on spawn_blocking, sets system_prompt
on first touch, writes seed as agent_md, commits. Idempotent —
skips when agent_md already populated.
- cm_api::mcp_skills::mcp_skills tools/call now resolves the caller
agent's template + role via agent_template_link and merges
template default skills with per-agent overrides (was overrides-
only in Slice 3.5b).
- cm_api::team_template_loader now binds template_role_skills after
upserting each template — looks up each declared skill by name,
attaches with pin_in_context=true for foundation skills and the
first two role skills. Missing skills log + skip.
- Boot ordering: skills load BEFORE team templates so the binding
lookup resolves.
Follow-up (Slice 4): mission-launch orchestrator calls brain_seed::ingest
+ agent_template_link::upsert when minting a team from a template.
Until that lands, the link is populated only by manual writes; the
MCP merge is silent-no-op for agents without a link (falls through
to overrides-only), which matches the pre-3.5d behavior.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Exposes the skills catalog as MCP resources so agents can discover
+ read skill markdown on demand — the delivery half of the "skills
teach how to think, MCP gives ability to act" model.
New endpoint: POST /mcp/skills (parallel to the existing /mcp door).
Protocols supported:
- initialize — handshake, advertises resources + tools
- resources/list — one entry per skill visible to the caller's
workspace. URI scheme:
skill:global/<name>
skill:workspace/<ws>/<name>
description carries the skill's description
+ when_to_use so the LLM can decide when
to reach for it.
- resources/read {uri} — returns body as text/markdown. Enforces
workspace scope on workspace-authored
skills.
- tools/list — one entry: skills__pinned_bundle
- tools/call — skills__pinned_bundle concatenates every
skill pinned for the calling agent (via
X-ZeroClaw-Agent header) with H1 headers.
Cheaper than N resources/read at turn start.
Adds the `clawmates_skills` bundle to the runtime template config
alongside `clawmates_door`, and teaches the MCP bearer rewriter
(prewrite_daemon_config_with_risk) to inject the workspace-owner
service session bearer on both `clawmates` and `clawmates_skills`
servers.
Follow-ups:
- Slice 3.5c: seed ~40-60 builtin skills across the 6 stacks
- Slice 3.5d: agent_template_link lineage — until it lands, the
pinned_bundle tool can only surface per-agent overrides, not
template defaults (documented inline)
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Introduce the skills catalog: the second half of the two-layer agent
model (skills teach agents HOW to think about a problem; MCP servers
give them the ABILITY to act). Delivery via MCP resources lands in
Slice 3.5b; this slice ships the data model + API surface.
Migration 0049 extends the legacy `skills` table (from 0001_init.sql,
originally a workspace catalog of markdown snippets) with the richer
typing we need — name, when_to_use, tags, source_kind, current_version
— rather than duplicating tables. Also adds:
- skill_versions (version history for level-up promotions +
rollback; back-pointer via promoted_from
JSONB records agent_id / research artifact /
brain memory that produced it)
- template_role_skills (m2m binding skills to team-template roles
with pin_in_context + order_idx)
- agent_skills_ext (per-agent overlay: include=true adds a skill
to the bundle; include=false prunes a
template default for this specific agent)
Rust surface:
- cm_db::repo::skills_catalog with typed Skill/SkillVersion/
AgentSkillBinding structs + upsert_builtin (idempotent — bumps
version + appends to skill_versions ONLY when body changes) +
list_visible/get/get_by_name reads + template + agent binding
helpers + effective_for_agent (merges template defaults with
agent overrides, applies exclude precedence, batch-fetches skill
bodies)
- cm_api::routes::skills_catalog with:
GET /api/skills — list visible
GET /api/skills/{id} — detail
GET /api/claws/{id}/skills — effective binding (accepts
template_id + role_slot as query args to merge in template
defaults)
Follow-ups:
- Slice 3.5b: clawmates_skills MCP server exposes catalog as MCP
resources, honoring pin_in_context for auto-injection
- Slice 3.5c: seed ~40-60 builtin skills across the 6 stacks
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Team templates are the canonical rosters + tool bundles that mint
concrete teams for a mission. Every builtin ships as a TOML recipe
under templates/teams/*.toml, loaded into the DB at server boot.
Migration 0048 adds:
- team_templates (id, key, name, stack, default_topology,
risk_profile, mcp_bundles, version, source,
workspace_id)
- template_roles (m2m: template_id + slot; system_prompt,
skills[], brain_seed)
- teams gets template_id + template_version for level-up lineage
Ships 6 builtins:
- rust_sdlc — planner/coder/tester/reviewer/committer for Rust
- backend — api_designer/db_engineer/coder/tester/committer
(Postgres, DuckDB, graph DBs, wire protocols)
- frontend — designer/coder/tester/committer (React + Tailwind + ShadCN)
- mobile — designer/coder/tester/committer (Expo, RN, iOS, Android)
- gpu — arch_analyst/kernel_author/bench_engineer/coder/committer
(CUDA, Metal, ROCm from Rust)
- threejs — scene_designer/coder/shader_author/perf_engineer/
committer (three.js, WebGL, WebGPU)
Each role has a versioned system_prompt + skill list + brain_seed
markdown. Skills column is a name array today; Slice 3.5a promotes it
to a typed m2m join with the real skills catalog.
Server boot:
- team_template_loader::load_builtins reads TOML from
/etc/clawmates/templates/teams (container) or templates/teams (dev),
upserts idempotently. Deterministic uuid per template key (sha256
of a fixed namespace + key) so ids are stable across boots.
- Dockerfile copies templates/ to /etc/clawmates/templates.
Read API:
- GET /api/team-templates — list all
- GET /api/team-templates/{id} — detail with roles
Wizard:
- Step 3 rewired from a raw team_id text field to a template picker
with "LLM auto-provision" as the default option + one card per
builtin, showing stack, topology, risk profile, and description.
- Mission create now passes team_template_id (not team_id) so phase
execution knows which template to mint from.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Introduce the unified `missions` tier that will replace the current
research_topics + loops split. This slice ships the data model +
backfill + skeleton REST surface; the old wizards keep working in
parallel until Slice 9's big-bang cutover.
Migration 0047 adds:
- missions (top-level workflow: template_kind + team +
schedule + status + config)
- mission_phases (ordered {research|coding|benchmark|
security_scan} phases per mission)
- mission_tasks (typed units of work, e.g. INT-XX cards,
UPSERT-keyed on (phase_id, external_id))
- mission_artifacts (MD/PDF/benchmark/security/diff files with
a pending queue for the PDF renderer worker)
- benchmark_snapshots (before/after pairs per iteration)
Backfill copies existing research_topics + loops rows into the new
tables as one-shot missions with the appropriate template_kind, so
Slice 2's UI can render the full history immediately.
New Rust surface:
- cm_domain: MissionId, MissionPhaseId, MissionTaskId, MissionArtifactId
- cm_db::repo::missions: Mission/MissionPhase/MissionTask/
MissionArtifact structs + insert (txn-wrapped)/get/list/set_status/
phases_for/set_phase_status/upsert_task/tasks_for/register_artifact/
artifacts_for/next_pdf_pending/set_pdf_result
- cm_api::routes::missions: skeleton list/create/get/set_status
routes registered at /api/missions/*
Follow-up slices layer richer behavior (template dispatch, phase
execution, task parsing, artifact rendering) on this foundation.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Coding loops with team_id set MUST run inside their team container so
agents have /workspace/repo bind-mounted. When try_team_gateway_url
silently returned None on error (mkdir perm denied, spawn_team failed,
etc.), the executor fell through to the shared runtime — which has no
repo mount — and the coding agents narrated for 6 minutes without
touching a file. Runs completed "green" with zero commits, hiding real
infra breakage.
Change try_team_gateway_url from Option<String> to Result<Option<String>, String>:
- Ok(Some) — team spawned/reattached; drive it.
- Ok(None) — no team binding on this run; existing per-topic/per-loop
fallback resolvers still apply.
- Err — team was expected but spawn failed; caller fails the run
with a descriptive error instead of silently degrading.
Each failure path in try_team_gateway_url now formats a specific
diagnostic string (docker connect, spawn_team, missing repo_workspace_path,
etc.) that surfaces into topology_runs.error.
The MCP-bearer mint failure is deliberately kept as a warning + best-
effort: some tools will 401 but the container still boots. Only failures
that would prevent code from being touched escalate to Err.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The log SSE endpoint was always resolving the container name from a
research topic id (research-<topic_id>-team). For paired coding loops
that own a team_id, the actual container the topology worker spawns is
team-<team_id>-container (spawn_team) — not the research topic's one.
That mismatch produced a stream of 404s from Docker:
Docker responded with status code 404: No such container:
research-<topic_id>-team
Mirror topology_worker::try_team_gateway_url's precedence:
1. run's loop has team_id → team-<team_id>-container
2. run has research_topic_id → research-<topic_id>-team
3. loop.source_research_topic_id (legacy paired flow) → same as 2
4. else → clear error, no more 404 spam
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The wizard was hard-failing (500) when clawstor was unreachable —
env unset, network error, non-JSON HTML response from a fallback
proxy, or non-2xx status all mapped to ApiError::Internal, which
blocked the wizard from advancing.
Clawstor fan-out is a warmup optimization, not a prerequisite.
Real fleet materialization happens later at spawn time. When the
aggregator is absent, return a skipped FanoutReply (all_ok=true,
empty peers) so the wizard proceeds, and log the reason server-side.
Unblocks: picking clawhdf5 in the ResearchWizard when the tank/
architect clawstor daemons are running but the HTTP aggregator
(`claw-store serve`) isn't deployed yet.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Per-team runtimes had their MCP bearer swap wired in the prior slice,
but per-topic (research_container::spawn) and per-loop (spawn_loop)
containers still baked the stale template bearer and 401'd every
tools/list. Same fix, extended: mint a workspace-owner service session
via runtime_provision::mint_workspace_service_token and inject via
prewrite_daemon_config_with_risk.
Move mint_workspace_service_token from topology_worker into
runtime_provision so all three spawn call sites share the helper.
Callers updated: routes/research.rs (start_topic), routes/research_setup.rs
(prepare_topic_runtime), routes/loops.rs (ensure_loop_container).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
CI's fmt check runs on stable toolchain that rejects let chains in
if conditions. Nest the `if let` inside the block instead.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The runtime template's static clawmates_door bearer is rejected by
cm_auth::authenticate() (needs an auth_sessions row). Every per-team
agent was getting `unauthorized: missing or invalid bearer token` and
`0 tool(s) registered from 0 server(s)`.
Add AuthService::mint_service_session + users::owner_of_workspace and
mint a 30d service session in try_team_gateway_url; inject it into the
freshly-spawned team container's config.toml [[mcp.servers]] clawmates
Authorization header via prewrite_daemon_config_with_risk (bearer arg).
Follow-up: apply the same pattern to research::spawn (per-topic) and
per-loop spawn paths.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Auto-provision (and every other build_team caller) hit
'dangling_reference: claude_cli is not a known provider family'
because provider_alias_for still returned v0.7.x aliases —
claude_cli.default / claude_cli.glm / claude_cli.glm5 /
kimi_cli.default — all deleted upstream when providers.models
schema was restructured.
Now the mapping resolves to real configured aliases:
- 'claude'* prefix → anthropic.default
- 'gemini'* prefix → gemini.default
- 'llama'* / 'groq' → groq.default
- glm* + kimi* → anthropic.default (fallback until glm.default /
moonshot.default provider tables land in the runtime template)
- unknown → anthropic.default
Covers the model picker's full roster (claude-sonnet-5 / opus-4-8 /
haiku-4-5 / sonnet-4-6 / glm-4.6 / glm-5.2 / kimi-k2 /
gemini-2.0-flash / llama-3.3-70b-versatile) plus every legacy
shorthand.
Tests updated to assert the new mappings including the model
picker's ids.
Auto-provisioned agents (via the research + loops team wizard) show
up on the Agents page (same agents::insert → agents::roster path),
and PATCH /api/claws/{id} already handled name/job_title/system_prompt/
avatar/accent/wallpaper. The one thing that was NOT reachable from the
Agents page: swapping the model.
Add a dedicated endpoint:
- PATCH /api/claws/{id}/model { model: string }
- Persists via agents::set_model_binding (DB)
- Best-effort runtime rebind via RuntimeProvisioner::provision_claw
(idempotent — overwrites agents.<alias>.model_provider on the
shared ZeroClaw config)
- Audit-logged as 'agent.model_changed' with the new model in payload
Now an operator can open the Agents page, click a claw that was
auto-provisioned by the team wizard, and swap its model
(claude-sonnet-5 → glm-5.2 → whatever) without recreating the team.
Same DB row, same claw_id, same brain — just a new provider on the
next turn.
Frontend affordance not shipped in this commit — the endpoint is
usable via curl/psql/scripts today; a UI 'Model' picker on the claw
detail card can land in the next Agents-page pass.
Two bundled changes:
── LiveRunLogs prettification ──────────────────────────────────
The Steps + Container tabs were plain mono lines with a single
color per event. Now they get structured layout:
Steps:
- Color-hashed actor pill (stable palette so [Distiller] and
[Novelty Analyst] each get their own hue across the session).
- Phase pill (plan=cyan, work=green, synth=amber, aggregate=purple).
- Token count pill formatted 1.2k / 14.3k / etc.
- Gated-action warning pill in amber when > 0.
- Left-border color strip keyed to the actor for at-a-glance
visual grouping.
- Long outputs collapse to their first 300 chars with a '+ N more'
toggle to expand the full text.
- 'done' events get a green (or red for error) border strip +
pill instead of blending into the stream.
Container:
- Splits '[actor] action (outcome) · msg' into colored spans —
actor pill (deterministic color), action in dim, outcome pill
green/red/dim by state.
- Non-line events (info/error/done) get their own left-border
strip so bash echoes and stack traces don't drown in the daemon
chatter.
- Timestamps switch to HH:MM:SS.mmm — dense but scannable.
Small palette (LOG constants) keeps the color budget bounded — no
new UI vocabulary, just cleaner reads of what was already there.
── Team-wizard governance parity ───────────────────────────────
build_team_with_lifecycle now matches POST /api/claws' governance:
- enforce_new_agent quota check per member (previously bypassed
workspace agent quotas entirely for team/auto-provision paths).
- audit::append('agent.created', ..., {source: 'team_wizard'}) per
member so team-created claws appear in the same audit trail as
individually-created ones. Adding a 'source' key distinguishes
provenance without changing consumers.
.brain (h5) handling was already consistent between the two paths —
both use the lazy on-first-access load_brain hook seeded from
agents.system_prompt. No change there.
Step 5 of ResearchWizard was 'assign agents from workspace roster'.
When the roster was empty, the wizard body was hard-swapped for
NoAgentsGate — you couldn't reach step 5 at all.
Now step 5 shows a 'Team' panel:
- Big cyan card: 'Auto-provision team from this topic'. One click
runs an LLM plan pass, gets 3-5 role slots + system prompts back,
materializes claws via the existing build_team pipeline, stamps
runtime posture, returns a shape that drops straight into the
submit body's agents[]. Card flips green with the derived roster.
- Below that: the classic roster picker, but only when the workspace
actually has ≥1 claw AND auto-provision hasn't landed. Otherwise
hidden — no dead empty-state affordance.
Every gate that required agents.length > 0 to render the wizard body
or the footer is gone. canNext gains a step-5 clause: allow Next when
EITHER auto-team is ready OR the user handpicked from a non-empty
roster.
Backend
- POST /api/teams/auto-provision — accepts {title, description,
outcome_kind, topology_kind?, model?, risk_profile?, mcp_bundles?}.
Derives topology from outcome_kind (integrations → pipeline; else
hub_spoke). LLM plan pass yields a JSON roster of 3-5 roles
(role_slot, name, system_prompt). Materializes team + claws via
build_team, stamps risk_profile (default research_web_readonly) +
mcp_bundles (default [clawmates_door, gitea_forge]). Response
carries team_id + agents[] in the shape /api/research already
expects.
- Every provisioned claw runs on claude-sonnet-5 by default;
overridable via the model field.
Follow-ups (not in this slice):
- Same picker in LoopsWizard (slice C — parallel change, same API).
- Post-create 'Team' section on ResearchCanvas / LoopsCanvas so
users can rebind after the fact (slice D).
- Full Teams tier UI + Agents-page deprecation (slice E).
Prep for the SDLC coding team. Adds two upstream Gitea binaries to
the clawmates-runtime image so team-scoped agents can drive
git.redclaw.dev without custom MCP code:
- tea v0.14.2 (shell CLI: clone/push/pr checkout, git remote helper)
- gitea-mcp v1.3.0 (native MCP server: list_repo_pull_requests,
create_pull_request, create_file, update_file, create_branch,
create_issue, get_file_content, etc.)
Both installed by arch (amd64 / arm64) at image build time; also
adds git to the runtime layer since it was missing (needed by tea's
shell delegates).
research_container::inherited_env now propagates GITEA_ prefixed
vars so a team container inherits GITEA_TOKEN (+ optional GITEA_HOST)
from the server env. Team-scoped daemons can then start gitea-mcp
via stdio with --token-env GITEA_TOKEN.
Followup on gw-04:
1. rebuild clawmates-runtime image against the new Dockerfile
2. add [mcp_bundles.gitea_forge] to the runtime template
3. set GITEA_TOKEN in the server compose env
Wires the per-loop-team arc end-to-end. When a loop with team_id
fires an iteration, the worker now spawns/reattaches a dedicated
team container, mounts the paired research topic's repo at
/workspace/repo (rw), and stamps the team's risk_profile into every
[agents.*] binding on the freshly-written config.toml. Legacy loops
with team_id = NULL keep taking the per-topic / per-loop path
unchanged.
research_container.rs
- team_container_name_for(team_id) = 'team-<uuid>-container'
- team_state_root(team_id) — /var/lib/clawmates-team-state/<uuid>/state
(overridable via CLAWMATES_TEAM_STATE_ROOT)
- prewrite_daemon_config_with_risk: line-based sed that swaps only
the risk_profile line inside each [agents.<name>] block. Avoids
the regex-eats-array-literal trap that bricked the shared runtime
config on the earlier out-of-band edit.
- spawn_team: full-shape idempotent spawner. Same mount + env + label
pattern as spawn/spawn_loop; additionally supports Claude settings
bind-mount + external-bridge attach.
topology_worker.rs
- try_team_gateway_url resolver runs BEFORE the existing per-topic
and per-loop lookups. Cold path: reads team runtime config + paired
research topic repo path, spawns the container, persists coords
back to teams.zeroclaw_container/zeroclaw_gateway_url. Any failure
logs + returns None so the caller falls through to the legacy
shared-container path — team spawn can never brick a run that
could otherwise complete.
Not shipped in this slice:
- Wizard 'existing team' picker (currently just fresh vs reuse)
- Teams tier UI to list/edit/delete teams
- Auto-teardown for stale team containers (piggyback on existing
reaper is a follow-up)
Slice 3a of the per-loop-team arc — prerequisites for the runtime
spawn hookup that lands in 3b:
0046 migration
- ALTER TABLE teams ADD zeroclaw_container TEXT
- ALTER TABLE teams ADD zeroclaw_gateway_url TEXT
Both NULL until the runtime's spawn_team fn (3b) provisions the
container and persists its coordinates. Mirrors the shape already on
research_topics (0038) so the resolver code path can generalize.
cm-db
- team_container_coords / set_team_container_coords: dynamic
sqlx::query() readers/writers for the new columns.
Runtime template (gw-04, out-of-band edit on
/var/lib/clawmates-runtime-template/config.toml + shared runtime
/root/clawmates-runtime/data/.zeroclaw/config.toml)
- New [risk_profiles.coding_readwrite]: adds file_write + shell on
top of the research_readonly baseline. Still excludes http_request /
browser / composio (egress stays behind the MCP door).
Slice 3b will add spawn_team (bind-mounts paired-topic repo, uses
team-scoped state dir, injects team.risk_profile into the config
template) and rewire topology_worker to resolve gateway URL through
team_id when the loop has one.
Second slice of the per-loop-team arc. The paired-coding-loop checkbox
in ResearchWizard step 6 now exposes a two-option picker:
⦿ Provision a dedicated coding team (default when the loop is on)
— fresh 'Coding · <topic>' team row, risk_profile =
coding_readwrite, clawmates_door in mcp_bundles. Loop's
team_id is bound at wizard-submit time.
○ Reuse the research team (legacy) — no team_id bound; coding
iterations spawn against the research topic's container.
Frontend
- New codingTeamMode state, radio picker rendered under the checkbox.
- research.ts createTopic body gains paired_coding_team_mode?: 'fresh'|
'reuse'.
Backend
- CreateTopicRequest gains paired_coding_team_mode: Option<String>.
- materialize_topic_loops takes it through and, when 'fresh', calls
the new provision_fresh_coding_team helper — inserts a teams row
via the existing insert_team_with_lifecycle (pipeline kind, same
graph as the loop), sets its runtime-config via
set_team_runtime_config, then binds loop.team_id.
- All operations best-effort with stderr logging — a team-provision
failure leaves the loop functional under the legacy fallback.
Not shipped in this slice (deferred to runtime hookup slice):
- research_container::spawn keyed on team_id → per-team container
- Config template rewrite injecting the team's risk_profile
- Migration of existing paired loops onto their own teams
The plumbing lands now so the wizard's intent is recorded; the
runtime honors it in the next PR.
Foundation slice for letting a coding loop bring its own team instead
of reusing the paired research topic's team. Turns out the teams
table already exists (0010_teams.sql) with full CRUD — this scales
back to the minimal missing bits:
Schema (0045_teams.sql)
- ALTER TABLE teams ADD risk_profile TEXT (NULL = template default)
- ALTER TABLE teams ADD mcp_bundles JSONB DEFAULT '[]'
- ALTER TABLE loops ADD team_id UUID REFERENCES teams ON DELETE SET NULL
- ALTER TABLE research_topics ADD team_id UUID REFERENCES teams
- Two partial indexes (team_id NOT NULL) for the future cascade queries
cm-db (dynamic sqlx::query so the existing get_team's compile-time
cache doesn't need regenerating):
- TeamRuntimeConfig struct
- get_team_runtime_config / set_team_runtime_config
- team_for_loop / team_for_research_topic (resolvers)
- set_team_for_loop / set_team_for_research_topic (binders)
cm-api
- GET /api/teams/{id} now surfaces risk_profile + mcp_bundles
- PATCH /api/teams/{id}/runtime-config sets them
Not touched (comes in follow-up slices):
- Wizard picker exposing 'reuse research team' vs 'fresh coding team'
- Runtime container spawn keyed on team_id
- Migration of existing paired coding loops onto their own team
The Loops canvas' IterationsTimeline was a flat status-pill list —
no way to see WHAT an iteration was actually doing. Meanwhile the
Research canvas had full LiveRunLogs with Steps + Container tabs
against the same underlying topology_run SSE endpoints. This
factors LiveRunLogs so both canvases share it.
Frontend
- LiveRunLogs gains a directRunId?: string prop. In this mode it
skips the topic-scoped active-runs + pipeline-state polls and
pins activeRun to the given id. topicId stays optional (topic
mode unchanged from ResearchCanvas' perspective).
- LoopsCanvas IterationsTimeline: each row is now a click-to-
expand card. On expand, renders <LiveRunLogs directRunId={...}/>
right below the header — Steps + Container tabs, full SSE tail,
same 320px terminal.
- Auto-opens the newest running/queued iteration so a click on
'Run now' immediately exposes the live pane.
- New CollapsibleSection helper wraps the graph JSON block so it
starts closed. Reference material stays one click away without
cluttering the canvas.
Backend
- run_container_log_sse now resolves the tail target via
loop.source_research_topic_id when the run itself has no
research_topic_id. Paired coding loops (kind=exec with a
source_research_topic_id) reuse the paired topic's team
container, so we tail its docker logs. Pure loop runs still
error with a clearer message.
Two cosmetic bugs surfaced by the successful v0.8.3 pipeline run:
1. **'produced Invalid Date'** — research_outcomes.created_at was
an OffsetDateTime serialized by time's default array format
(`[y, ordinal, hh, mm, ss, ns, tz]`), which browser's
`new Date(...)` can't parse. Add `#[serde(with =
"time::serde::rfc3339")]` matching the pattern already in
threads.rs / routine_runs.rs.
2. **Stale error text on pipeline card** — the runs stage's
`latest_error` walked every run by `created_at DESC` and
returned the first non-empty error, so a topic with an earlier
failed run + a later completed run kept displaying the old
error next to '1 completed'. Now the error only surfaces when
the MOST RECENT run itself failed. Historical failures stay in
the run count but don't leak their message.
Steps summaries only fire AFTER each topology step completes — so a
stalled first turn was completely dark. Add a second tab that
streams the team runtime container's daemon log live via a new SSE
endpoint.
Backend
- GET /api/topology-runs/:id/container-log — workspace-scoped SSE
around bollard's docker.logs(follow=true, tail=200). Buffers on
newline so partial mux chunks don't truncate a log line.
- compact_container_log: parse a zeroclaw daemon line
('[actor] ... zc_action=X zc_outcome=Y ... msg') into
'[actor] action (outcome) · msg'. Framing-only continuations are
dropped; non-zc lines (bash echoes, backtraces) pass through as-is
so nothing interesting is lost. ANSI escapes stripped.
- Everything funnels through one async_stream! so early exits
(workspace check / docker connect / no bound topic) yield an
'error' event and return without breaking Sse::new's single stream
type.
Frontend
- LiveRunLogs gets a sub-tabs strip: Steps · Container.
- New useContainerLog(runId, active) hook — gated by tab so we don't
hold two open SSE streams when the operator isn't looking.
- Same terminal widget renders each container line with a level
color (info/done grey, line default, error red). Sub-tab pill
shows count + status live.
D1-fold loop guard 409'd even the rerun path if the topic was
scheduled — a failed iteration couldn't be restarted until the loop's
next scheduled fire. Now the loop guard only fires for the fresh
'standby' start; the rerun path (status=='processing' + orphan
cancel) works whether or not a loop owns the topic. Loop binding is
preserved either way.
The previous rerun guard (standby OR (processing AND in_flight==0))
still 409'd when a lingering queued/running row existed — typically
an orphan from a server restart mid-pipeline, before the reaper or
stale-checkpoint requeuer had marked it failed.
Now: allow rerun on any 'processing' topic; before enqueuing the
fresh run, batch-cancel every in-flight run for the topic so the
new run isn't racing them. Terminal states (reviewing / publishing
/ published) still 409 as before.
When a topic ends up parked in 'processing' with all runs failed and
nothing in flight, the sidebar card was still spinning as if
progress were happening. Now:
Backend
- topology_runs::run_counts_by_research_topic — batch query that
returns (in_flight, failed-since-last-success) per topic. Used by
the list endpoint; dynamic sqlx::query() so no prepare needed.
- TopicListItem DTO gains runs_in_flight + runs_failed.
- start_topic status guard relaxed: allow (standby) OR (processing
AND runs_in_flight == 0). Blocks accidental double-fires on a
live pipeline; permits rerun on a failed one. Same request body,
same behavior once accepted, so the frontend just POSTs
/research/:id/start on the RotateCw click.
Frontend
- ResearchList detects errored: status===processing && !in_flight
&& failed>0. Swaps the MiniSpinner for a red AlertTriangle and
changes the status text to 'error · N failed'.
- New RotateCw icon button next to the delete Trash — same button
cluster, one click, no wizard re-entry required. Disables while
a request is in flight; error surfaces in the sidebar's shared
error banner.
Root cause: claude CLI in the per-topic research container runs as
uid=0(root). ZeroClaw's claude_cli provider passes
--dangerously-skip-permissions which Claude CLI rejects under root
for security — so the CLI hangs waiting for interactive permission
approval that never arrives, hitting the 600s provider timeout with
zero step records journaled.
Three-part fix:
1. Claude settings bind-mount (research_container.rs):
Optional CLAWMATES_CLAUDE_SETTINGS_PATH env — when set, mount the
host file at /root/.claude/settings.json (read-only) in every
spawned team container. deploy/claude-settings.json ships the
canonical config (permissions.defaultMode = bypassPermissions +
hasCompletedOnboarding). CLI accepts requests immediately with no
--dangerously-skip-permissions flag needed.
2. Verbose per-step log line (topology_worker.rs):
Every checkpoint now writes to stderr:
topology_worker::step run_id=X step=N node=Y role=Z phase=W
output_bytes=B tokens=T gated=G
Visible in docker logs clawmates_server_1 — gives us live
'topology is flowing' signal without opening the canvas, and
makes it obvious when a topology_kind is skipping stages it
shouldn't.
3. Stuck-container reaper (topology_worker.rs):
New 60s-tick loop reap_stuck_runs: for any research topology_run
older than 15 min with zero checkpoint.records, docker-stop its
container and mark the run failed with a diagnostic error. Only
reaps research-bound runs (non-research runs don't own a
container). The existing 180s stale-checkpoint requeuer stays
in place for other failure modes.
Deploy: gw-04 needs
ln -sf /path/to/repo/deploy/claude-settings.json /opt/clawmates/claude-settings.json
CLAWMATES_CLAUDE_SETTINGS_PATH=/opt/clawmates/claude-settings.json
in the server env, plus the timeout lowered from 600 -> 120 in
compose. Both handled in the deploy step outside this commit.