d3a398716b7f52c9d27bdd39e16a0cac86e84d23
297
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bce370978d |
world viz: comm beams + emergent team-shape overlay
Two mechanics on top of the same signal (agent-to-agent events
that were already flowing over the SSE feed but had nowhere to
land visually):
1. Transient comm beams (option 1)
- agent.message → magenta #ff5eae ("A said something to B")
- agent.delegate → coral #ff8a7a ("A handed a task to B")
- room.message → purple #c98af0 fan-out from poster to
every other participant
Each event pushes a Beam { x1..y2, life: 1.4 } into the same
pipeline world.touch already uses. Life decays at 2.2/sec so a
burst is ~500ms of bright line + a particle spark at the target.
Colors are distinct from world.touch (pawn's own color) so a
crowded scene reads as tool convergence vs teammate chatter at
a glance.
2. Emergent talk-shape overlay (option 2 without the topology JSON)
- engine.commEdges: Map<"minId|maxId", CommEdge>. Each call to
onAgentComm refreshes life to 1.0 (or creates the edge). step()
decays life at 1/30 per second, so an edge stays visible for
roughly 30s after the last message.
- Renderer draws every commEdges entry as a dim cyan line
between the pawns' current positions BEFORE the transient
beams, so a bright pulse cleanly overrides the dim shape.
- The shape isn't declared anywhere — it emerges from actual
traffic. First few turns fill in a shape (hub_spoke, pipeline,
mesh, whatever the team actually does); quiet periods let it
fade so the viz doesn't get stuck showing stale wiring.
Engine changes
- New interface CommEdge; new commEdges Map on the engine.
- New onAgentComm(from, to, color) does both jobs — pushes the
transient beam AND refreshes the persistent edge — so
WorldCanvas only wires the subscription once per event type.
- step() decays commEdges alongside beams (separate rate).
- Uses min/max id as the edge key so A→B and B→A collapse to
a single line (colors would fight otherwise), with lastSpeaker
retained for future arrow-hint tinting.
WorldCanvas
- Three new live.on() calls (agent.message, agent.delegate,
room.message) route into engine.onAgentComm.
- Beam render loop pre-appends comm edges before transient
beams so the same LineSegments material handles both.
- Dim brightness: 0.18 * edge.life, so a fresh edge starts at
~18% cyan and fades from there.
Not yet: option-2's "always-visible topology edges from the graph
JSON" (i.e. drawing the coordinator↔spoke lines before ANY comm
happens). Emergent-only means the shape appears once the team
actually talks — good enough for a running team, wrong for the
"static preview of an unstaffed team" case. That's a follow-up
once we plumb the topology.update event's edges array into
the seed.
|
||
|
|
aa941b72a1 |
world viz: agents grow with their brain (log-curve dot size)
Fresh agents start at "size of their letters" — a small dot in
the live viz — and visibly bloom out as their .brain file fills.
Turns "which of these agents is heavily loaded" into a glance
instead of a menu dive.
Taxonomy
- New agent.memory event: { agentId, bytes?, count? }. STATEFUL, so
a late subscriber sees the last value replayed and pawns arrive
pre-sized. count is included in the schema for a future combo
metric but not emitted yet — bytes carries the visual today.
Backend (routes/world.rs SSE loop)
- Per-agent, per-tick std::fs::metadata() on
brain_dir()/claw_<uuid>.h5. Just the inode stat — no HDF5 open,
no memory count, sub-ms per agent. Emit agent.memory { bytes }
only when the value has changed (or on first sight).
- Tracks last_bytes: HashMap<String, u64> in the SSE-stream scope
alongside the existing status HashMap.
- Missing file (agent never provisioned a brain) reads as 0 bytes
and yields scale = 1.0 downstream — pawn stays small.
Engine
- GPawn gains memoryScale (visible) + memoryScaleTarget (chased).
Base is 1.0; ensurePawn initializes both.
- memoryScaleFromBytes(bytes): 1 + log10(1 + bytes/1MB) * 0.6, cap
MAX_MEMORY_SCALE = 3.5. So 10MB ~ 1.6x, 100MB ~ 2.2x, 1GB ~ 2.8x.
Log curve keeps a heavy brain readable without a lite one being
invisible.
- onMemory(e) sets the target. stepPawns eases the visible scale
toward it at ~3/sec — a big incoming snapshot doesn't pop the
sphere; it swells in like it's inhaling.
Renderer (WorldCanvas)
- Live subscription registers agent.memory alongside the existing
status/touch/reasoning listeners.
- Pawn sphere scale = 5 * p.memoryScale (was hardcoded 8). Halo
scales in proportion (max(24, 4.25 * s)) so a memory-heavy agent
reads as a bigger presence, not a small dot with a huge halo.
- AABB bounds for the frame-camera math updated to use s instead
of 8 so the camera actually frames a big agent when it's the
outlier.
Not yet wired: comm lines between pawns when agents talk to each
other (Commit E next), and the topology-edge overlay that renders
the graph shape dimly at rest. Both build on top of this — bigger
dots make comm beams more visible.
|
||
|
|
aed21b654c |
agents page: 2-col metric grid + stacked full-width rows
Reshuffles ClawCommandCenter so the layout stays calm whether or
not the computer panel is slid in from the right.
Top row — was 5 tiles in a horizontal strip that got crowded when
the computer opened; now a `grid-template-columns: repeat(2, 1fr)`
grid that wraps naturally to 3 rows for the 5 tiles.
- Doors / Memory (row 1)
- Loops / Spend (row 2)
- Activity · Live (row 3, spans 1 / -1 so it uses the full width)
Activity here is a new compact ActivityTile — same tool-call
bucket signal as the old bar viz, distilled into MetricTile shape
so it fits the grid. Shows total calls in the rolling 22s window
with a mini sparkline of per-second counts.
Main section — was a 2-column split (LIVE column + Anatomy column)
that shifted around when the panel opened. Now a single vertical
stack, every row full-width:
Working on Now
Reasoning Stream
Throughput (new ThroughputCard — wraps the same tok/min data
in an AnatomyCard so the sparkline gets room; the
old top-row ThroughputTile is retired)
Anatomy Grid (the "Dot Brain" and its neighbours)
Old ActivityBars component is retired — its signal lives in
ActivityTile at the top now. Unused colStyle removed.
Net: the top-of-fold reads as a scanner (four tiny numbers + one
live pulse), the stack below reads as narrative (what's happening,
how it thinks, how much it's producing, what it is). Both scroll
independently of the computer panel.
|
||
|
|
acd2a0f287 |
structure polish: post-reify nav + ensure-chain + TeamWizard auto-parent
Two small quality-of-life fixes on top of the reify commit:
Post-reify navigation
OrphanMigrationDialog already returned team_id in its result;
Dashboard now pushes /?team=<team_id> before router.refresh() so
the user lands on the freshly-materialized team and sees exactly
where their agents just moved. Previously they had to hunt for it
in the newly-rebuilt sidebar.
Wizard auto-materialize (POST /api/structure/ensure-chain)
cm-db: ensure_chain(pool, ws, fallback_org, fallback_company) —
fast path returns coordinates of the first org+company already
bound in this workspace (workspace's oldest org, oldest company
under it). Slow path inserts a new org+company with the
fallback names ("My Workspace" / "General") + binds them via
org_companies. Returns { org_id, company_id, created }. Small
txn — leaves the workspace consistent whether it was already
wired or not.
cm-api: POST /api/structure/ensure-chain accepts optional
fallback_org_name and fallback_company_name in the body (trimmed,
else default). Returns the ids.
CreateTeamRequest gains an optional attach_to_company_id. When
set, after build_team() completes, we look up the company
(workspace ownership check enforced by companies::get), count
its existing teams for a stable n_i node id, and insert a
company_teams binding — so the team lands under the parent
atomically instead of a follow-up round-trip.
TeamWizard now calls ensure-chain before POST /api/teams and
passes the returned company_id in attach_to_company_id. Both
calls are best-effort — if ensure-chain fails (network etc.)
we still try to create the team, and the migration dialog stays
available as the fallback UX. Wizard flow now: fresh workspace's
first team is fully wired from the moment it appears in the
tree — no synthetic "My Workspace" scaffolding ever gets
rendered around it.
The Team/Company create paths not touched here (create_team_from_claws,
company create, org create, MasterPlannerModal scaffold) still
work as before — they just won't auto-parent yet. Later commits
can wire them the same way.
|
||
|
|
8b789beec0 |
structure: reify-orphans endpoint + "give these a home" dialog
Turns the four synthetic tree containers into a real migration path.
Clicking any of them ("My Workspace", "Teams", "Direct",
"Ungrouped") opens a dialog that creates a real
org → company → team chain and re-parents every orphan into it, all
in one DB transaction.
cm-db (new module structure_reify)
- orphan_agents / orphan_teams / orphan_companies: workspace-scoped
SELECTs of entities without a parent binding in team_members /
company_teams / org_companies. Used both by the dialog's counter
and internally by the migration.
- count_orphans: cheap combined-count via three subqueries in a
single SELECT so the dialog only round-trips once for the header.
- reify_orphans(pool, ws, org_name, company_name, team_name):
1. begins a tx
2. inserts a new org + company + team (all `flat`, empty graphs
— user can shape them later via the existing PATCH endpoints)
3. binds company under org (org_companies "n0")
4. binds team under company (company_teams "n0")
5. inserts team_members rows for every orphan agent (n1, n2, …)
6. inserts company_teams rows for every orphan team
7. inserts org_companies rows for every orphan company
8. commits, returns the created ids + moved counts
cm-api (routes/structure)
- GET /api/structure/orphan-counts → { agents, teams, companies }
- POST /api/structure/reify-orphans → { org_id, company_id, team_id,
moved_* }. Trims + rejects any empty name; validates before
starting the transaction so a 400 never rolls anything back.
Frontend
- New OrphanMigrationDialog: fetches counts on open, three name
fields (defaults: Organization "My Workspace", Company "General",
Team "Everyone"), POSTs on save. "Nothing to migrate" state
disables the save button when the workspace is already fully
wired. Copy explicitly notes that everything is renameable in the
sidebar afterward.
- Dashboard: onTreeSelect now branches on SYNTHETIC_TREE_IDS —
clicking a synthetic node opens the dialog instead of falling
through to the (nonexistent) selection. On successful reify,
router.refresh() so the sidebar + world viz reflect the new real
chain.
What this doesn't do yet (next commit)
- Wizard auto-materialize: when creating a team/company via wizard,
auto-create parent placeholders if they don't exist. Deferred so
this commit stays focused.
|
||
|
|
99e5207e69 |
sidebar: click-to-rename org/company/team + strip synthetics from world viz
Two related pieces of the "kill My Workspace" cleanup, landed
together because they share the same file:
Backend
- Three tiny inline-rename endpoints:
PATCH /api/orgs/{id}/name
PATCH /api/companies/{id}/name
PATCH /api/teams/{id}/name
Each takes { name: string }, trims + rejects empty, returns 204.
Backed by rename_org / rename_company / rename_team in cm-db —
single-row UPDATEs scoped to the caller's workspace, NotFound if
the id isn't visible.
- Registered next to the existing PATCH /:id (topology) routes so
they don't collide.
Frontend
- StructureTree accepts an optional onRename and canRename.
TreeRow: click on the label text of a renamable node → the span
becomes an <input>, focus + select-all, save on Enter or blur,
cancel on Escape. The rest of the row (row chevron / row body)
still navigates + selects as before, so single-click behaviour
is preserved for everything except the name text itself.
react-hooks/set-state-in-effect avoided by resetting the draft
in the enterEdit() click handler instead of inside a useEffect.
- Dashboard passes canRename={item.level !== "claw" && !synthetic}
(claws don't have a rename endpoint yet; synthetic scaffolding
gets reified into real rows in the next commit — the wizard
auto-materialize + orphan-migration dialog).
onRename fires the corresponding PATCH and calls router.refresh()
so the label lands in every consumer of the tree.
- World viz seed: new stripSynthetics(roots) helper walks the tree
and lifts children of any synthetic container up to their
grandparent's level. worldCanvasRoots feeds through this before
narrowRoots(). Result: the Live viz no longer shows "My Workspace"
or "Teams" nodes — real agents orbit the world root directly
(which is what you were asking for). Sidebar tree still shows
them so orphaned agents remain visible until the migration lands.
|
||
|
|
1a4c5eb159 |
research: inline Approve / Reject on the topic canvas
The prior "Publish approval pending" pill was a dead-end: it told
users an approver had to sign off but gave them nowhere to do it.
No approvals inbox surface existed, so the topic sat in reviewing
indefinitely.
Backend already allows any workspace member to decide
(decide_publish has no role gate). Wire the inline UX:
- ResearchCanvas fetches /api/research/publish-approvals when the
topic's has_pending_publish_request flag is true, filters to the
match for the current topic, and stashes it as pendingApproval.
- The amber pill grows two buttons — green "Approve & publish"
(POST /publish-approvals/{id}/approve → topic goes to
publishing, published_at gets stamped) and a bordered "Reject"
(POST /publish-approvals/{id}/reject → topic stays in reviewing,
requester can request again). Both disable + relabel while the
request is in flight.
- A "Requested at <timestamp>" line under the pill so approvers
can see how long it's been pending.
- boundApproval derives from pendingApproval only when the loaded
approval's topic_id matches the current topic — protects
against a stale approval leaking in during a topic switch.
- setPendingApproval uses a functional updater that returns the
previous reference when the fetch produced an identical row,
satisfying react-hooks/set-state-in-effect.
|
||
|
|
036d64ee5d | research canvas: escape apostrophe in running-hint copy (fixes lint) | ||
|
|
7c1af2e070 |
research: pipeline-running signal + spinners so users aren't guessing
The prior flow was ambiguous: after hitting Start research, status flipped to "processing" and a "Submit for review" button appeared immediately with no indication that anything was actually running. Users had to guess whether the pipeline was working or stalled. Backend surfaces the truth as a signal: - new topology_runs::active_runs_for_research_topic counts queued+running runs whose research_topic_id matches - TopicDetail includes runs_in_flight: i64 alongside the existing status field, so the canvas can distinguish "pipeline still working" from "runner stalled". ResearchCanvas is now honest about state: - while runs_in_flight > 0, the header status pill grows a cyan "N runs in flight" badge with an inline SVG spinner - the stage-explainer card turns cyan-bordered and shows a "pipeline is running" hint, plus copy pointing the user at the Agents tier where each teammate's activity streams live - the "Submit for review (manual)" button is HIDDEN while any run is in flight — it's an escape hatch for stalled runs only, not the happy-path action. It reappears if runs_in_flight drops to zero but the topic is still marked processing, so a stalled runner can still be nudged along. - the canvas polls getTopic every 4s while status is processing/ publishing or runs_in_flight > 0, so the spinner + outcome swap in automatically when the pipeline completes. ResearchList sidebar: - each row's status dot becomes a spinner when the topic's status is processing or publishing, matching the canvas at a glance - the list also polls every 6s while ANY topic is active, so transitions land in the sidebar without waiting on a parent bump. The poll is gated on a derived boolean to avoid effect thrash. Follow-up: same pattern belongs on LoopsList / LoopsCanvas for loop iterations in flight — same signal (queued+running runs per loop) but not wired here. |
||
|
|
316cdbf929 |
research sidebar: delete-with-confirm per topic
Mirror the row-level delete affordance the loops sidebar already
has. Loops was wired earlier; research had a bare title-only card
with no way to remove a stale topic.
- cm-db: research_topics::delete cascades via existing FK rules
(research_topic_agents, research_publish_approvals, and the new
research_outcomes all CASCADE on topic_id; topology_runs's
research_topic_id back-ref is SET NULL so historical runs stay).
- cm-api: DELETE /api/research/{id} → 204. Idempotent.
- Frontend: deleteTopic helper. ResearchList row is now a card
with the existing title/status/outcome header plus a trash icon
that flips the card into an inline "Delete topic + all outcomes?"
confirm strip. Confirm → red Delete / gray Cancel. If the
deleted topic was selected, selection clears; local counter
bumps the list refetch without waiting on a parent.
|
||
|
|
a2d3d85ebe |
research pipeline v2: topology-aware start + persisted draft
Three connected changes that turn "Start research" from a status flip into a real pipeline that produces a reviewable artifact: - Migration 0036: adds research_topics.topology_kind (default 'hub_spoke') and a new research_outcomes table (id, topic_id, version DESC, body_md, produced_by_run_id, created_at) so each run's final synthesis is versioned and persistent. - Wizard now has a topology picker in the Outcome step — hub_spoke / pipeline / hierarchical / star_moe — with copy that steers users to the right shape (Pipeline for research → distill → analyze → implement rosters, hub_spoke for the coordinator- and-specialists default). - start_topic reads the chosen topology_kind, parses it into a cm_topology::TopologyKind, and dispatches a per-shape coordinator prompt via build_coordinator_task. Pipeline explicitly tells stage 1 not to write the final artifact and propagates a "final stage MUST emit a complete markdown document with measurable acceptance criteria" instruction downstream. The graph builder is called with the topology the user actually picked instead of hard-coded HubSpoke. - topology_worker::freeze_research_outcome fires after every successful complete(). It looks up research_topic_id on the run; if set and final_output is non-empty, it inserts a new research_outcomes row (version auto-derived server-side via coalesce(max(version), 0) + 1). Best-effort — a DB hiccup logs but doesn't fail the run. - TopicDetail now includes topology_kind and latest_outcome. ResearchCanvas swaps in the outcome's body_md (rendered as pre-wrap markdown, versioned header, produced-at timestamp) whenever an outcome exists; the original prompt collapses into an "Original prompt" <details> below so it's still one click away. Pre-run topics still show the description as before. Follow-ups still open: reject-with-revision loop feeding the coordinator, publishing → published transition + real artifact export (md / pdf), and an approvals inbox surface for reviewers. |
||
|
|
81a436d221 |
research canvas: wider rail + stage explainer + 409 fix
Three connected fixes from a single session's feedback:
- Structure rail widened from 60→76 px, tabs 42→58 px wide with a
bit of left padding so labels ("Research", "Visualizations") no
longer bump against the active-tab indicator strip.
- Research canvas: each stage now shows a small "STAGE · <status>"
card explaining what state the topic is in and a "Next → …" hint
describing what the primary button will do. No more guessing
which of standby/processing/reviewing/publishing means what.
- Request-publish 409 fix:
- Backend TopicDetail now includes has_pending_publish_request
(SELECTs pending_for_topic when the topic loads). Frontend
TopicDetail interface + ResearchCanvas honor the flag: when
an approval is already pending the "Request publish" button
is replaced with an amber "Awaiting reviewer approval" pill,
so double-clicks can't 409 in the first place.
- runAction() also catches 409 as a signal-of-success (the
user's intent — "queue for approval" — is satisfied by the
first attempt), refetches the topic, and lets the new
awaiting-approval card render instead of surfacing a scary
error to the user.
|
||
|
|
92e923e585 |
planner: specialists mode → single agent (backend + UI polish)
The frontend already renamed the "Specialists" tab to "Agent" and
rewrote the intro to ask for one specialist, but the backend
prompt was still telling Opus to propose "2 or 3 domain
specialists". Two-facing message got confusing outputs.
- SPECIALISTS_NOTE now says "SINGLE agent … `members` MUST contain
EXACTLY ONE entry … topology_kind='flat' … team_name reads like
a personal handle." Removes the ambiguity.
- TEAM_NOTE range aligned to "4 to 6 agents" (matches the UI hint
and intro).
- Frontend right-panel now reads "PROPOSED AGENT" for specialists
mode (was "PROPOSED TEAM") and pluralization matches the count
("1 agent" vs "3 agents"). Build button switches to "Build agent"
in that mode.
|
||
|
|
f0a2bd8a86 |
master planner: pink brain, agent tab, mode-scoped topologies + svg illustrations
Six planner-modal fixes in one pass:
- Header icon swap: Sparkles → Brain, styled pink (rgba(255,105,180,*))
to match the "brain" framing the user wanted.
- Drop the "Claude Opus 4.8 designs & deploys." subtitle — the title
carries enough weight on its own.
- Rename the first mode tab "Specialists" → "Agent". This card is
specifically for building a single specialist. Updated INTRO to
ask for one agent (job title + system prompt), so the planner
chat behaves accordingly.
- Team hint: "4–8 agents, balanced" → "4–6 agents, balanced" so the
UI matches the intro text ("4–6 person growth team").
- Topology strip is now mode-scoped. Team mode surfaces org-shaped
structures (hierarchical, hub_spoke, star_moe, pipeline, ring,
holacratic, debate, flat); Swarm mode surfaces formation-shaped
ones (swarm, mesh, blackboard, market, flat). `flat` bridges
both because it serves either shape. The strip header now
reads "Topology · N available for {mode}".
- Replaced the ASCII `<pre>` diagram with inline SVG illustrations
per kind. Coordinator nodes render in the accent pink (#ff69b4);
worker/peer nodes are neutral gray. Twelve illustrations total,
one per topology kind, in a lookup map (`TOPO_ILLUSTRATION`).
If we later want the exact clawbernetes.work marketing images,
drop them into /public/topologies/<kind>.svg and swap the
renderer to an <img/>.
|
||
|
|
0cbde3a3cf |
agents empty state: kill synthetic fallback + real CTA
After reaping every real agent/team/company/org, dashboard-data.ts
was still pushing a fabricated "My Workspace → Direct → Ungrouped
(0 claws)" org so the sidebar always had *something*. It was
confusing after a full cascade-reap ("didn't we just delete
everything?") and the placeholder nodes couldn't be selected/deleted
either.
- dashboard-data.ts: drop the empty-workspace fallback (myClawsOrg
is no longer referenced; removed). Real emptiness now returns
{ orgs: [] }.
- Dashboard.tsx: when orgs.length === 0, replace the sidebar tree
with a friendly "NOTHING TO SHOW / Your workforce is empty / Hit
the + …" panel. Canvas gets a purposeful EmptyRosterStage with
a big + button that opens the same MasterPlannerModal the rail
already uses — one wizard, two entry points.
- Removed the old EmptyStage helper (no longer used).
|
||
|
|
58ed8a5948 |
reap: skip synthetic tree nodes (fixes 422 on batch-delete)
dashboard-data.ts synthesizes a few org/company/team placeholders
("my-workspace", "ws-teams", "ungrouped-co", "ungrouped-team") so
an empty workspace still has something to render. Their ids
aren't UUIDs and don't exist in the DB.
Before the cascade-reap change these were unreachable because
selectLevel="claw" restricted the checkbox affordance to leaf
agents. With selectLevel="*" they became selectable, and
POST /api/claws/batch-delete returned 422 (serde couldn't parse
"ws-teams" as a UUID).
Fix: skip synthetic ids in onToggleSelect so they can never enter
selectedAgents in the first place, and filter the request body to
strict UUIDs at send-time as a defense. If the filter empties the
list the modal surfaces a friendly message instead of firing.
|
||
|
|
984d9a1274 |
reap: cascade orgs → companies → teams → agents
Selecting a team/company/org in the Agents sidebar used to only
delete the grouping row; the agents inside survived, ungrouped.
Not what the user wanted, and it left a trail of orphaned runtime
state (containers, .brain files, DB rows) behind.
Backend — POST /api/claws/batch-delete is now a universal cascade
reaper. Body accepts { ids?, teams?, companies?, orgs? } in any
combination. The server walks org → companies_of_org →
teams_of_company → agents_of_team, dedupes against explicit ids,
and hard-purges every unique agent (deprovision ZeroClaw runtime,
tear down sandbox container, unlink .brain/.onion files,
transactional agents::hard_purge). Group rows are deleted last; FK
cascades on team_members, company_teams, org_companies, and
loop_agents/teams/orgs clean up the join tables. Every stage
streams SSE.
Three new cm-db helpers wire the walk: agents_of_team,
teams_of_company, companies_of_org — all DISTINCT selects on the
existing join tables.
Frontend — Dashboard's Agents-tier StructureTree now sets
selectLevel="*" (was "claw"), so the Wrench → checkbox affordance
appears on org/company/team/agent nodes alike; the same-level
invariant in onToggleSelect still prevents mixed batches.
ReapProgressModal collapses to a single POST regardless of kind —
body key derived from kind — and its subtitle is honest:
"Cascading through every agent inside — permanent."
|
||
|
|
1a2baee74b |
loops wizard: stop pulling bearer.ts into the client bundle
Same trap the pre-existing topology import comment warned about: importing from @/lib/api/team or @/lib/api/structure drags http.ts → bearer.ts (uses next/headers, server-only) into the client bundle and Turbopack refuses to build. Replace fetchClaws / fetchTeams / fetchOrgs with plain fetch() to /api/team/claws, /api/teams, /api/orgs. Types are inlined where they were only used for the shape of the JSON response. |
||
|
|
16fcf8ef96 |
agents sidebar: fold org/company/team/agent tree into one pane
The Agents tier used to render a flat list of agent cards with an "Add to teams" button pinned to the bottom. Swap that for the same nested tree the Visualizations tier already uses so the whole workforce reads from one collapsible view. - treeRoots is now worldRoots on both tiers (no more clawNode flat fallback); the Agents tier's selectLevel stays "claw" so the Wrench → select → Delete flow keeps its agent-only scope. - Drop the "Add to teams" footer button plus its addTeamOpen state, AddToTeamModal render, and import. - Header on the Agents tier now shows the full breakdown: N ORGS · N CO · N TEAMS · N AGENTS. |
||
|
|
a6da19430f |
loops: repo picker + agent/team/org staffing + sidebar edit/delete
Adds the missing pieces the wizard needed and the sidebar controls around it: - LoopsWizard is now a 6-step flow (identity → repo → task/topology → triggers → repeat → assign agents) plus the existing secrets card. ResearchWizard picks up the same repo step and a hard gate when the workspace has zero agents. - New LoopStaffingStep with three tabs — Individual / Team / Organization — that mix freely per loop; selections persist via new loop_agents / loop_teams / loop_orgs join tables (0035 migration), each cascading on loop_id so hard-delete stays a single-row DELETE. - Backend CreateLoopRequest / UpdateLoopRequest accept the three lists and apply_staffing does a transactional replace-all; list_loops / get_loop hydrate the lists via a flattened LoopWithStaffing response. - LoopsList sidebar gains per-row enable/disable, edit (reopens the wizard prefilled with the current loop, PATCHes on submit), and delete with an inline confirm. - NoAgentsGate blocks launching a loop or research topic from a workspace with no roster; the sidebar `+` buttons also disable with a tooltip pointing at the TEAM tier. Not yet wired: the run driver still fills role slots from the workspace-wide pool; teaching enqueue_iteration to prefer loop_agents/loop_teams/loop_orgs is a follow-up. |
||
|
|
2562541f5b |
repos sidebar: fold repos under their org
Within a provider connection, repos now group under a foldable org card (the repo's owner login). Each org shows chevron + owner + count, with repos indented under a subtle left rail so the tree reads visually. Sorting: orgs alphabetical, repos within an org alphabetical — makes scanning stable when a re-sync reorders provider output. Collapsed state lives per (connection_id, owner) so the same org name appearing under two providers folds independently. Default is expanded so the first pass after connecting shows everything. |
||
|
|
637e1bdd69 |
repos: sidebar actions (sync/edit/remove) + edit modal
Sidebar: - Each connection header now has three inline icon buttons: Sync now (spins while in flight), Edit (opens the modal), Remove (opens an inline confirm strip). Removes cascade repos via ON DELETE CASCADE. - The connection's last_sync_error surfaces as a red inline banner under the header — no more 'error status with nowhere to see why'. - Sync is POST /api/repos/connections/:id/sync (already existed); after either sync or delete the sidebar re-fetches so state stays consistent. Edit modal (RepoConnectionEditModal): - Loads GET /api/repos/connections/:id, pre-fills owner/base_url/label - PATCHes only the fields that actually changed; empty string on a Some(&str) field sends explicit null so the backend clears it - Sync-now + Remove reachable from inside the modal too - Rotating the token is out of scope: the modal says as much and points the user at delete + re-create through the wizard (the broker doesn't expose an update path, and rotating in place would require duplicating the whole broker->store_secret flow here) Backend: - GET /api/repos/connections/:id — same ConnectionSummary shape - PATCH /api/repos/connections/:id — owner/base_url use Option<Option<T>> double-nesting so 'omit = leave alone' and 'null = clear' round-trip distinctly through serde - repo_connections::update with COALESCE-per-field so the SQL matches the double-Option semantics without an OR-chain per field |
||
|
|
e858a7f92f |
repos: Gitea provider (first-class) — sync + wizard default
Fleet's Gitea (git.redclaw.dev) hosts most of this workspace's repos,
so Gitea gets the same inline sync treatment GitHub already had.
Backend sync_gitea:
- base_url is required — Gitea has no shared 'gitea.com'; we accept
either the instance root (auto-appends /api/v1) or the fully-formed
API base if the user already included the suffix
- /orgs/{owner}/repos when owner set, /repos/search when not (with the
{data: [...], ok: bool} envelope Gitea wraps that endpoint in)
- 404 with an owner surfaces as 'org not found or PAT lacks access',
same UX as GitHub
- 50/page, capped at 20 pages (~1000 repos); short page terminates
- upsert_gitea_repo tolerates the small field-name differences
(stars_count vs stargazers_count, owner.login vs owner.username on
older versions)
Frontend wizard:
- Gitea listed first — matches the workspace's actual usage
- Default provider selection is now gitea
- Token-input placeholder tailored per provider (Gitea's is
'Settings → Applications → Generate New Token (repo)')
GitLab still returns 'not yet supported' — that's the next follow-up.
|
||
|
|
9cb14ddd89 |
repos: real provider-connection wizard
Replaces the earlier placeholder inside RepoConnectionWizardStub with the actual flow (kept the filename so the Dashboard import doesn't churn). - Provider picker (github / gitea / gitlab) as three inline cards - PAT input (password field, never rendered back) - Optional owner override (org or user) - Optional label (defaults to <provider>/<owner>) - Optional base URL — shown only for Gitea / GitLab, hidden for GitHub - POST /api/repos/connections + immediate result card: green when the initial sync succeeded (shows # repos synced), red when the connection persisted but the sync failed (shows the message the backend recorded on repo_connections.last_sync_error). The sidebar refresh fires on both paths so the new row appears either way. Sidebar and detail view already fetch the right endpoints from task #11 — end-to-end works locally on this build. |
||
|
|
076f7724ca |
dashboard: REPOS tier tab + Repos page shell
Inserts a 6th tier tab between AGENT and INFRA (Tier type + TIER_TABS + rail icon). Wires two new sidebar/canvas components with the same list+detail pattern as loops/research: - RepoList: header (provider count · repo count), + button opens the connection wizard, groups repos by provider connection (empty state prompts the user to connect the first). Fetches /api/repos/connections and /api/repos — those routes land in tasks #12-14. - RepoCanvas: repo detail (name, owner, private badge, description, stars/forks/branch/updated, clone URL with copy, open-on-provider link, last-synced footer). Empty + loading + error placeholders. - RepoConnectionWizardStub: minimal 'coming next' modal so the + button is wired end-to-end; real wizard replaces it in task #15. Sidebar header branch updated so the tier renders its own header. Build is clean; the sidebar is fully functional once the backend endpoints respond. |
||
|
|
806ba869e5 |
teams: ephemeral lifecycle for Scheduled + Triggered planner modes
Migration 0033: adds teams.lifecycle ('permanent' | 'ephemeral') and a
topology_runs.team_id back-ref with a partial index for the sibling-in-
flight check.
cm-db repo:
- teams::insert_team_with_lifecycle (insert_team keeps the permanent default)
- topology_runs::enqueue_run_for_team (populates team_id)
- topology_runs::check_ephemeral_teardown — atomic SELECT that only
returns Some when the team is ephemeral AND no siblings are still
queued/running; carries the workspace + bound claw ids for cleanup.
cm-api:
- topology_worker post-terminal hook maybe_teardown_ephemeral_team
runs deprovision_claw on each bound claw (best-effort; failures log
but don't block Postgres deletion), then hard_purge each agent row,
then delete_team.
- routes::teams::build_team_with_lifecycle (build_team keeps default);
run_team enqueues with team_id.
- planner ScaffoldRequest gains mode; lifecycle_for(mode) sets the team
to ephemeral for scheduled + triggered, permanent otherwise.
Frontend MasterPlannerModal passes mode in the scaffold payload so the
backend can derive lifecycle without duplicating the mode taxonomy.
Tests: 3 new (returns claws when no siblings, holds when siblings queued,
ignores permanent teams). 10/10 topology_jobs green; workspace clippy
--tests clean.
|
||
|
|
b0acdfd987 |
master planner: wire user-locked topology_kind through chat + scaffold
Frontend: send topologyKind to /api/planner/chat so the planner's user prompt gets a USER-LOCKED TOPOLOGY block telling Opus to use it verbatim. On buildTeam, override proposal.topology_kind with the user's pick (belt-and-braces — if the planner ignored the lock, we still ship the right shape). Proposal chip renders the effective kind in a lavender tint when it was overridden, with a hover title showing what was replaced. Backend PlannerChatRequest gains an optional topology_kind. Empty / absent = planner picks. Not honored for 'swarm' mode (swarm planner doesn't take a topology kind). |
||
|
|
5e49086af7 |
master planner: topology info panel under the gallery
Selecting a card in the strip expands into an info panel below with: - Coordinator badge (green when the topology has a lead, gray when leaderless — mirrors what cm-orchestrator's planner reads off the graph) - Communication pattern one-liner - 'When to use' guidance - Auto-staffed role distribution chips (from the catalog's role_distribution — same numbers TeamWizard apportions) - ASCII sketch of the shape Cheat sheet is client-side (TOPO_DETAIL) so the expansion is instant on selection — mirrors the exec-plan semantics baked into cm-orchestrator. |
||
|
|
ab4a62ef7d |
master planner: topology gallery strip for Team + Swarm modes
New horizontally-scrollable card strip appears between the mode selector and the split pane when the mode is 'team' or 'swarm'. Cards are fetched from GET /api/topologies (the full 12-kind catalog), rendered as pill cards with name + description. Clicking selects; 'planner picks' clears. Selection persists per-mode via the ModeSlice snapshot, so switching modes doesn't forget the choice. Scaffold wire-through comes next (task #9). |
||
|
|
987f4f0e84 |
master planner: add 'Team' mode + size bands per mode
Modes now: specialists (2–3 domain experts, deep prompts) · team (4–8 balanced roles, coordinator + complements) · swarm (10+ workers, self- verifying loop) · scheduled (ephemeral, cron/one-shot) · triggered (ephemeral, webhook). Backend planner_system_for() gains a TEAM_NOTE using PLANNER_SYSTEM; specialists / scheduled / triggered notes are rewritten to bake in the size + ephemeral guidance. Swarm's system prompt now targets task_count>=10 explicitly. Frontend MODES / INTRO copy match. Chat-preserving switchMode from the prior commit handles the new mode transparently — no state-plumbing changes needed. |
||
|
|
9056937434 |
master planner: preserve state across mode switches
switchMode was resetting messages/proposal/swarm/runSteps/etc. every time. Now it snapshots the current mode's slice into a ref before loading the target mode's snapshot (or a fresh slice if the target was never visited). Switching specialists → swarm → specialists keeps the specialists chat intact. Also route async /planner/chat responses to the mode the message was sent in, not the currently-active mode — user can switch modes while the reply is in flight without the assistant response landing in the wrong chat. |
||
|
|
18a99a970d |
loops wizard: fix client-bundle break from topology import
@/lib/api/topology's apiFetch pulls in bearer.ts which imports @clerk/nextjs/server — the whole chain gets tagged as client-side by Next when LoopsWizard imports it, and 'server-only' breaks the production build. Switch to plain fetch(/api/topologies) with type-only imports (mirrors what TeamWizard does). Local pnpm build now compiles clean; typecheck/lint already pass. This unblocks the publish job on the topology-builder push. |
||
|
|
e5820ce927 |
loops wizard: topology builder in step 2
Step 2 now defaults to a Builder pane: kind picker (from /api/topologies) + team size + role distribution preview, with /api/topologies/build rendering the canonical graph and node/edge counts. Advanced JSON stays as a toggle for hand-crafted graphs — same shape lands in the payload either way, so downstream code is unchanged. Debounced build effect wraps the async load in an inner function to avoid the setState-in-effect cascading-renders lint. Uses the same largest-remainder role apportionment as TeamWizard so builder output matches team-wizard output for the same kind + size. |
||
|
|
9079184bb2 |
loops wizard: expose 'until' repeat policy
Third radio option on the repeat step. Two inputs: event name (defaults
to 'ok') and a within_iters cap (defaults to 20). Ships the full
{kind: 'until', event, within_iters} payload the schema already accepts.
LoopsCanvas repeat summary now formats iters/until/infinite via one
helper — the previous inline expression was rendering 'until' as a bare
label with no event context.
|
||
|
|
6d1dda6197 |
loops: iteration timeline — GET /api/topology-runs?loop_id=X
Extend the topology-runs list route with an optional loop_id filter that returns iterations for a single loop, newest-iteration-first. Adds the iteration and finished_at columns to the summary (skip-null on the JSON so compares stay compact). Backed by list_by_loop in the repo, which uses the existing topology_runs_loop_idx partial index. LoopsCanvas fetches the runs in parallel with the loop detail and renders an iteration timeline card (iteration #, status pill, start time, duration, run id prefix) between the graph section and the actions row. |
||
|
|
d0adc78de4 |
loops UI: real list + canvas + 4-step wizard with webhook secrets card
Seventh and final commit of the Research + Loops arc. Replaces the
placeholder Loops surface from commit 5 with the full working UI.
New:
- lib/api/loops.ts — thin TypeScript client for every /api/loops
endpoint (list, get, create, patch, delete, run-now, enable, disable).
- dashboard/LoopsWizard.tsx — 4-step modal:
1. title + description
2. task template + topology graph JSON (defaults to empty graph; a
visual builder is future work)
3. triggers — any combination of cron / on-completion / webhook,
with the cron pattern input revealed inline when cron is on
4. repeat policy — infinite or fixed-iterations
When the webhook trigger is enabled, submit lands on a fifth SECRETS
screen showing webhook_token + signing_key exactly once (never shown
again by the backend), a copy-to-clipboard row for each, and the
X-Loop-Signature: sha256=<HMAC-SHA256(key, body)> usage snippet.
- dashboard/LoopsList.tsx — replaces the stub. Loop cards with an
enabled/disabled dot + a next-fire chip. Re-fetches on refreshKey.
- dashboard/LoopsCanvas.tsx — replaces the stub. Selected loop detail:
header (title, enabled state, next-fire), triggers card, repeat
policy, task template preview, topology graph JSON, and an action row
(Run now / Enable-or-Disable / Delete with confirm).
Wired into Dashboard.tsx via a `loopsSel` / `loopsRefresh` state pair
matching the Research pattern. onDeleted clears the selection and bumps
the refresh key so the list drops the deleted card and the canvas
returns to its placeholder.
Closes the arc: all 5 tiers active, all state machines driveable from
the UI, and the loop scheduler + cron + webhook plumbing wired end-to-
end. Future work per the roadmap: visual topology builder, iteration
timeline in the canvas (via GET /api/topology-runs?loop_id=X), and the
"until" repeat policy UI.
|
||
|
|
568d3c4b78 |
research canvas: move the setTopic(null) reset inside the async load
Second lint pass caught the standalone setTopic(null) in the early-return branch of the effect — still synchronous at the top of the useEffect. Fold that guard into the async load() so every setState landing on this hook runs inside the async wrapper. |
||
|
|
1466f8aadc |
research UI: fix 3 lint errors — async wrapper + escape apostrophe
Push right after
|
||
|
|
ec1ddc634d |
research UI: real list + canvas + 4-step wizard, wired to the backend
Sixth commit of the Research + Loops arc. Replaces the placeholder
Research surface from commit 5 with the full working UI.
New:
- lib/api/research.ts — thin TypeScript client for every /api/research
endpoint (list, get, create, attach/detach agent, start, submit-review,
request-publish, list/approve/reject publish approvals, wizard refine).
- dashboard/ResearchWizard.tsx — 4-step modal (topic prompt → LLM refine
→ outcome kind → agents). The refine step calls
POST /api/research/wizard/refine which streams the workspace's default
LLM and returns {title, description}. Users can accept, edit, or refine
again. Manual fallback if the LLM call errors.
- dashboard/ResearchList.tsx — replaces the stub. Real topic cards with
status pill (standby / processing / reviewing / publishing / published),
outcome-kind chip, and the + button that opens the wizard. Re-fetches
when the parent bumps refreshKey.
- dashboard/ResearchCanvas.tsx — replaces the stub. Selected topic
detail: title, status header, outcome + published_at meta, assigned
agents grid (matched to workspace claws by id), the full description
in a monospace preformatted block, and a state-appropriate primary
action button (Start research → Submit for review → Request publish).
Wired into Dashboard.tsx via a `researchSel` / `researchRefresh` state
pair: selecting a card sets the id, mutations bump the counter so both
list + canvas re-fetch.
All API calls go through the existing /api/[...path] catch-all Next
proxy — no new server routes needed.
|
||
|
|
2ff00934b2 |
dashboard: expand to 5 peer tiers — add Research + Loops with placeholder canvases
Fifth commit of the Research + Loops arc. Lights up the tier rail (both the chip column and the top-bar crumbs) with two new peer surfaces: [VIZ] [RESEARCH] [LOOPS] [AGENT] [INFRA] The stubs (ResearchList / ResearchCanvas / LoopsList / LoopsCanvas) are self-contained placeholder components — each renders a header with a "0 topics" / "0 loops" chip and a "coming soon" body plus a canvas hero with the tier motif. Real functionality (topic cards, wizard, canvas timelines, publish gate UI) arrives in the next two commits, and slots in by replacing the stub files without touching Dashboard.tsx again. Dashboard.tsx changes: - `Tier` type widened to include `"research" | "loops"`. - railIcon record grew two SVG entries — book+lens for Research, orbit arrows for Loops. Matches the placeholder canvas hero. - TIER_TABS reordered as Viz → Research → Loops → Agent → Infra to match the mental grouping (conceptual work first, machinery second). - Top-bar crumbs mirror the rail order. - New isResearch / isLoops selectors used everywhere the existing tiers were tested; the Agents-header conditional now also skips for the new tiers so ResearchList and LoopsList can supply their own headers. - Sidebar swap short-circuits to ResearchList / LoopsList; canvas swap short-circuits to ResearchCanvas / LoopsCanvas. |
||
|
|
2527a888a2 |
e2e: replace stale post-login heading assertion with a URL wait
The shared signIn helper in every spec asserted
`getByRole("heading", { name: "Clawmates" })` after clicking Sign in, but
the dashboard's "Clawmates" is a decorative <span> in the top-bar logo,
not a heading — so all authenticated tests died on the same helper. Swap
that for `page.waitForURL((url) => !url.pathname.endsWith("/login"))`
with a 15s timeout. Robust across UI redesigns and doesn't couple the
signIn helper to a specific product surface. Should convert ~27 of the
remaining 30 failures to passes.
|
||
|
|
c417097db2 |
LoginForm: restore proper <label> elements + update e2e tests to the single-step flow
The local-mode LoginForm had drifted to using styled <div> elements as labels. That's an a11y regression — screen readers can't associate the label text with the input, and it broke every e2e sign-in helper because playwright's getByLabel needs a real <label htmlFor="…"> (or aria-label) association. Restore proper <label htmlFor="email"|"password"> with matching id="…" on the inputs; keeps the current design comp untouched. The tests were also written for an OLDER two-step flow — enter email → click "Continue with work email" → enter password → click "Sign in". The current form is single-step (both fields, one Sign in click). Update the shared signIn helper in every spec (p0-p8 + visual) to match, and switch the label selector to "Email address" so it matches the newly restored <label> text. Drop the stale a11y assertion in p6 that expected the two-step button. Also refresh the marketing landing check in p0-shell.spec.ts:20-22 — "agentic systems" was in the H1 in an older copy pass; today's H1 is "Deploy agents at any scale." Update the selector. Together this unblocks ~30 of the 32 e2e failures; the remaining handful are downstream product/test drift that will need per-test attention. |
||
|
|
7784edb772 |
frontend: bump @playwright/test 1.60.0 → 1.61.1 for Ubuntu 26.04 chromium support
The e2e job was failing at `npx playwright install --with-deps chromium` with "Playwright does not support chromium on ubuntu26.04-x64" — the fleet's Gitea Actions runners (morpheus/tank/architect) run 26.04 which is newer than 1.60's supported distro set. 1.61.1 adds the platform detection for 26.04. |
||
|
|
298eb8e20e |
e2e: move backend/dex/frontend from 8080 to 18080 so runners don't collide
Two of the fleet's Gitea Actions runners (morpheus, architect) already had 8080 permanently bound by unrelated services (nginx on morpheus, envio-hasura on architect) — every e2e run scheduled there died at playwright's webServer preflight with "http://127.0.0.1:8080/healthz is already used". 18080 is unused across morpheus/tank/architect. Swap 8080 → 18080 in the eight e2e-scoped sites: clawmates.e2e.toml (listen_addr + slack base_url + oauth redirect_base), dex.yaml (client redirect URIs must match backend), playwright.config.ts + tests (p4-slack, p6-oauth), the http.ts dev-fallback origin, and the two shell scripts (e2e-backend safety check, rehearse-install healthz probe). Prod compose (/opt/clawmates/docker-compose.yml on gw-04) is untouched; prod continues to expose the server on 8080 internally on the compose network (that's per-network, not host-shared). |
||
|
|
c5400e1007 |
dashboard: rename Large World → Visualizations, narrow canvas on select
Three surfaces show the tier name: the rail chip, the top-bar crumb, and the sidebar header. Rename rail "WORLD" → "VIZ", crumb + sidebar "Large World" → "Visualizations". Canvas narrowing: the left StructureTree keeps the full org forest so you can still browse everything, but the right-side WorldCanvas now prunes to just the branch containing the current selection. Clicking a company shows only that company's teams; clicking an org shows only that org. Nothing selected still shows the full forest. |
||
|
|
1e9e930425 |
frontend: use-chat — auto-reconnect on SSE transport failure
Cloudflare/Traefik culls idle SSE streams periodically and users see a "connection lost" state even though the run is still going. Add a bounded retry loop (5 attempts, 500ms→8s exponential backoff) that reattaches with resumeFrom. The transcript reducer already dedupes by seq, so replaying is safe even if the server re-emits events we already saw. Aborts (component unmount, user navigation) cut through the backoff via sleepUnlessAborted so we don't hold the socket open through a page transition. Terminal outcomes (run_completed/error) skip the retry. |
||
|
|
85b0e1ac33 |
feat(observe): surface delegation + A2A in the live world feed via audit poll
Edge-initiated inter-agent events (gated delegation, A2A ingress) bypass the
run loop, so the world SSE now polls the append-only audit log (cursor on the
BIGINT id, seeded to max on first pass) and emits:
- delegation.invoked -> agent.delegate {fromAgentId,toAgentId,toName,task}
- a2a.invoked -> a2a.invoked {agentId}
TeamObserver renders agent.delegate as an A->B handoff in the team timeline;
adds the agent.delegate taxonomy type. Reliable live (the synthesized-run path
never streamed — active_runs + first-sight cursor jump skip it).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
||
|
|
7352ed47ab |
feat(observe): team observation mode in the agent chat surface
Adds a "This agent | Team" scope toggle to the chat card's Observe view. Team mode (TeamObserver) resolves the open agent's team (members + names via /api/teams + /api/team/claws), lists the team's group rooms, and renders a live timeline of agent.message / room.message / a2a.invoked among team members — read-only, reusing the workspace SSE feed. No backend changes. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
7589f62aca |
feat(a2a): External access panel + GET /api/a2a/settings
Adds GET /api/a2a/settings (enabled + publicBaseUrl) and an A2ASection in the Fleet overview: enable/disable A2A for the workspace, mint/list/revoke external bearer tokens (token shown once), and the discovery URL. Claw/skill publishing is configured per-claw (follow-up). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
cbfa0ff24f |
feat: agent-to-agent platform on ZeroClaw 0.8.2 — rooms, delegation, A2A ingress
Builds on the v0.8.2 runtime. Four workstreams, all behind the §15 MCP door:
- Group rooms (Phase 1): migration 0026; N-way threads repo with a DM/room
count-guard; chat.send {room} + room.create/invite/leave tools; RoomMessage
-> room.message SSE; /api/claw-chat/rooms* APIs; Observer room badge.
- Per-claw door identity: door caller_agent resolves the X-ZeroClaw-Agent
header (set by the fork) to the specific claw, falling back to roster[0].
- Gated delegation bridge (Phase 3): clawmates__delegate door tool drives a
sibling via the existing /ws/chat ZeroClawDriveExecutor (not A2A); self-deny,
per-workspace hourly budget, audit trail, untrusted-banner result. Native
in-daemon delegation stays off (it would bypass the door).
- A2A tenant ingress (Phase 2): migration 0027 (workspace_a2a + a2a_tokens);
runtime_provision enable_a2a_server/publish_claw; routes/a2a.rs tenant-aware
proxy (per-workspace tokens, injected internal bearer, daemon stays internal,
cards URL-rewritten to the cm-api edge); a2a.invoked taxonomy.
Tests: cm-db room repos, cm-runtime chat tools, door units. sqlx cache updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|