Commit Graph
312 Commits
Author SHA1 Message Date
Omar Sobh a34be33261 loops: N/M INTs progress pill on source-bound loop cards
ci / rust (push) Failing after 41s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 27s
Users couldn't see how a research-bound loop was progressing through
its integration plan — the consumed_int_ids state existed in the DB
but nothing surfaced it. Now each loop card in the sidebar shows a
cyan pill "3/8 INTs" + source topic title + a thin progress bar, when
the loop is bound to a research topic.

Backend:
- GET /api/loops/progress — bulk read for every loop with a source
  topic. Returns {loop_id, source_topic_id, source_topic_title,
  source_outcome_version, consumed_count, total_int_count,
  current_int_index} per loop. Standalone loops are omitted.
- count_int_ids parses unique INT-<number> ids out of the source
  outcome's markdown — same permissive matcher as the completion
  hook, so what the pill counts matches what the loop can advance.
- Memoized by topic_id inside the endpoint so N loops sharing 1
  source topic only fetch the outcome once.

Frontend:
- listLoopProgress helper + LoopProgress type in the loops API.
- LoopsList fetches loops + progress in parallel on mount.
- Each card looks up progress by loop_id and, when found, renders
  under the schedule line: pill "3/8 INTs · Topic title" with a
  3px cyan progress bar. Title hover shows artifact version.

Follow-ups:
- Refresh button on the card — re-snapshot artifact into
  task_template (cosmetic; the enqueue path already reads latest).
- Reorder rationale extraction — parse "REORDER: <text>" out of run
  output, index as a per-loop event log for a mini-timeline UI.
2026-07-09 18:57:09 -07:00
Omar Sobh ce73abe5ab loops: bridge research artifact into loop iterations (option C + b)
ci / rust (push) Failing after 13m41s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 37s
The bridge lets a coding loop "consume" an integrations research
artifact one INT-XX item per iteration. Options b (order-sequential
iteration) and C (snapshot in task_template + save the pointer for
future refresh) from the design discussion.

Migration 0042 — three new loops columns:
- source_research_topic_id — nullable pointer to research_topics.
- consumed_int_ids TEXT[]   — INT-XX ids the loop has completed.
  Advances when topology_worker parses "COMPLETED: INT-<NN>" markers
  from the run's final output (wired in a follow-up commit).
- current_int_index INT     — monotonic pointer for order-sequential
  iteration. Coordinator addresses INT-<current+1> unless prereqs are
  unmet, in which case it works on the smallest unblocking INT-XX and
  logs the reorder rationale.

Backend:
- cm_db::repo::loops::set_source_research_topic — bind/unbind pointer.
- cm_db::repo::loops::source_research_context   — read pointer + state.
- routes::loops::compose_iteration_task         — new caller-side helper
  that reads the pointer, fetches the topic's latest research_outcome,
  and prepends the artifact + focus instruction to task_template.
- run_now + webhook_receive both pass task_template through
  compose_iteration_task before enqueue. Standalone loops (no pointer)
  behave identically to before.
- CreateLoopRequest accepts `source_research_topic_id`, ownership-
  checked via research_topics::get before persist.

Frontend:
- New ResearchArtifactPicker modal — lists published topics, fetches
  the artifact on pick, returns (topic_id, markdown) to caller.
- LoopsWizard task_template step gains "Import from research artifact"
  button (right-aligned). Click opens the picker. On pick: task
  populates with the artifact markdown, pointer saved, textarea
  expands to 8 rows, small info strip shows "Loop is bound to topic
  <id>. Each iteration will focus on the next unconsumed INT-XX."
- Unlink button reverts to standalone loop mode.

Follow-up (next commit):
- topology_worker completion hook — parse "COMPLETED: INT-<NN>" out
  of the run's final output + update consumed_int_ids +
  current_int_index atomically. Without this, current_int_index stays
  at 0 forever and every iteration works on the same INT.
- Loop card refresh button — re-read source topic's latest outcome
  (useful after a reject-with-revision cycle on the source topic).
2026-07-09 18:47:17 -07:00
Omar Sobh 3ed1d03d2b research: integrations outcome + rich wizard cards + coordinator template
ci / e2e (push) Has been skipped
ci / rust (push) Successful in 22m15s
ci / publish (push) Successful in 2m40s
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 39s
Adds a fifth outcome kind ('integrations') tuned for the "audit repo,
survey papers, propose a menu of concrete integrations" use case.
Every INT-XX item is self-contained (what, how, where, prereqs, effort,
risk, testing, rollback, acceptance) so a downstream loop can execute
one per iteration.

Backend:
- Migration 0041 drops + re-adds the outcome_kind CHECK constraint
  with 'integrations' allowed. Existing rows unaffected.
- VALID_OUTCOMES gains 'integrations'.
- New deliverable_template(kind) returns the canonical section shape
  for each outcome — spec, prod_plan, roadmap, paper, integrations all
  get first-class treatment (prior: all shared a bare label).
- build_coordinator_task injects an ARTIFACT SHAPE block from the
  template into the coordinator prompt, so the final synthesis
  actually matches the promise the wizard made.

Frontend:
- OutcomeKind gains 'integrations'.
- ResearchWizard OUTCOMES list carries a `sections` array per kind.
- Selected card renders an "ARTIFACT WILL CONTAIN" preview so users
  pick by seeing what they'll get, not by reading a one-line hint.
- Integrations card gets the fullest preview (executive summary +
  INT-XX card shape) since it's the most structured deliverable.

Follow-ups queued (next commit): loop wizard "Import from research
artifact" bridge + one-INT-per-iteration mode.
2026-07-09 18:42:46 -07:00
Omar Sobh cca6e2be20 research: pipeline diagnostics + refuse publish without outcome
ci / gates (push) Failing after 8s
ci / rust (push) Has been skipped
ci / frontend (push) Has been skipped
ci / publish (push) Has been skipped
ci / e2e (push) Has been skipped
Two fixes surfaced by the first prod run of the pipeline:

1) Silent skip-to-published bug (R1 gap):
   approve_publish transitioned reviewing → publishing → published
   without checking that an outcome existed. Result: pipeline could
   fail silently (LLM auth error, network, etc), no outcome would be
   written, but state advanced to 'published' and the download endpoint
   returned 404 with no user-visible error. Now refuses with 409
   Conflict when no outcome exists so the frontend can surface WHY.

2) No end-to-end visibility:
   Users had no way to see where a run failed until they clicked
   Download and got nothing. Adds
   GET /api/research/:id/pipeline-state — a read-only per-stage report
   walking:
     - staffing (agents assigned)
     - repo (bound + cloned)
     - container (per-topic team runtime spawned)
     - runs (count + failed count + latest error text)
     - outcomes (count — the artifact rows get_artifact reads)
     - approval (pending flag)
   Each stage returns ok / warn / fail / skip plus optional detail text
   so the failure reason surfaces at the diagnostic level.

Frontend:
   ResearchCanvas shows a compact PIPELINE strip below the topic title,
   green/amber/red dots per stage, click to expand a full checklist
   with per-stage detail (including the LLM error from the last run
   attempt). Polls every 6s while the topic is processing/publishing.

Follow-up:
   - Root cause of the specific failure just observed: Claude CLI in
     the clawmates-runtime container isn't authenticated. Deploy-side
     config sweep (CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY into
     the runtime image env), not a code fix.
   - Structured event stream on top of run_events for real per-step
     replay in the diagnostic panel.
2026-07-09 16:59:59 -07:00
Omar Sobh 1896b78cb7 research: pending-approvals inbox in the sidebar (R3)
ci / rust (push) Failing after 10s
ci / gates (push) Successful in 6s
ci / publish (push) Has been skipped
ci / frontend (push) Successful in 28s
ci / e2e (push) Has been skipped
Before: reviewers only saw a topic was awaiting them if they navigated
directly to it — the approval CTA lived inside ResearchCanvas. If
someone had 5 topics reviewing, they had to click through each to find
which were staffed for their signoff.

Adds a compact inbox strip at the top of the Research sidebar (right
below the header, above the topic list). Only shown when there's at
least one pending publish approval in the workspace:

- Amber pill: "<count> PENDING REVIEW(s)" with a badge, click to
  expand.
- Expanded state: one row per pending approval, showing the topic
  title (looked up from the already-fetched topics list) and a
  "review →" affordance. Row click selects the topic (fires onSelect
  with the topic_id) so the reviewer lands on the ResearchCanvas with
  the approve/reject controls surfaced.
- Load piggybacks on the existing list-fetch effect via
  Promise.all([listTopics, listPendingApprovals]). Refreshes on the
  same bump + polling triggers, so approvals appear/disappear as
  people request/decide them.

Fetch failure for approvals silently degrades to an empty list — the
sidebar still renders the topic list normally.

Follow-up: filter by reviewer role (currently shows all approvals in
the workspace); notification badge on the top nav so the inbox is
discoverable even when someone is elsewhere in the app.
2026-07-09 15:57:45 -07:00
Omar Sobh e3011ed025 research: reject-with-revision loop (R2)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 14s
ci / frontend (push) Successful in 28s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
Before: reviewer rejected a publish → audit log flipped, topic stayed
in reviewing, no way to feed the critique back into the run pipeline.
Reviewers with revision notes had to eat them or hand-message the
coordinator.

Now: reject accepts an optional `notes` field. When present:
- Persisted on the research_publish_approvals row (migration 0039).
- Topic flips `reviewing → standby` so the next `start_topic` is legal.
- `start_topic` reads the most recent rejected-approval notes for the
  topic and prepends "PRIOR REVIEW NOTES (address these in this
  revision):\n<notes>\n---" to the coordinator task.

Loop closes through the same run pipeline — no new spawn code path,
which means the reviewer's guidance flows through the same
topology_worker, run_events, outcome-writer chain and lands as a fresh
research_outcomes row (versioned, prior drafts preserved). No notes on
reject = legacy behavior (topic stays in reviewing, publish requests
still allowed).

Migration 0039 adds nullable `notes TEXT` to
research_publish_approvals. `decide()` gains a `notes: Option<&str>`
parameter (only one caller, updated inline). New
`latest_rejection_notes(pool, topic_id)` helper for start_topic.

Frontend:
- rejectPublish(id, notes?) now sends a JSON body when notes are
  provided.
- ResearchCanvas reject button opens an inline form with a textarea +
  Cancel/"Send back for revision" pair. Empty notes → plain reject.
- Button label switches: "Send back for revision" when notes present,
  "Reject without notes" when empty.

Follow-up:
- Notes shown in the review UI on the resulting draft so the next
  reviewer sees what changed.
- Multiple rejection rounds — currently only the LATEST rejection's
  notes surface. Accumulating history is a schema-only tweak.
2026-07-09 15:56:16 -07:00
Omar Sobh 4831033910 world: fade landmark orbs after 60s idle (V5)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 26s
ci / publish (push) Has been skipped
ci / e2e (push) Has been skipped
Turns out V5 wasn't a no-op — the SSE loop stops emitting a repo:<id>
node once its topic transitions to 'published' (the query filters on
processing/reviewing/publishing), but the client engine's idle-fade
only applied to service/event tiers. Repo and loop landmarks lingered
forever on the client until page reload.

Extends the fade rule to cover repo + loop tiers, with a longer window
(60s vs 22s for service/event) so a brief SSE hiccup doesn't wipe the
whole workspace's landmarks. When a topic reaches 'published', its
`lastSeen` stops advancing on the client; ~60s later the alpha ramps
down and the node is deleted from the map.

Same rule applies to loops that get `enabled=false` after being
turned off — their landmark quietly fades out instead of persisting
until reload.
2026-07-09 15:52:01 -07:00
Omar Sobh 959e766e36 world: per-file heat map — persistent touch-count residual (V4)
ci / rust (push) Failing after 9s
ci / frontend (push) Successful in 25s
ci / gates (push) Successful in 6s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
Files that see repeated touches decay back to visual silence within a
couple of seconds because `n.heat` decays at 0.5/sec. Hard to tell a
hot-spot file (touched 20 times over a run) from a virgin one; both
look the same at rest.

Adds a monotonically-increasing per-node `touchCount` on the engine
and a log-scaled residual on the render that never decays. Files
climbing the touch count grow subtly, tint whiter, and pull more
emissive — even at heat=0.

- GNode gains `touchCount`, initialized 0 on ensureNode and on the
  ROOT sentinel.
- onTouch bumps `touchCount` only for `file:` prefix nodes so
  landmarks + structural orbs don't accumulate a hot halo just because
  agents converge on them.
- WorldCanvas node loop computes `residual = min(0.7, log(1+count)/log(50))`
  and adds it into radius, whiten-lerp, and emissiveIntensity.

Feel: hot files stay softly-warm indefinitely (residual stops climbing
around ~50 touches). Cold files stay cold. Live activity still dominates
via the transient `heat` bump; residual is the ghost trail.
2026-07-09 15:51:21 -07:00
Omar Sobh a49cf86623 world: pre-seed repo tree on clone (V3)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 11s
ci / frontend (push) Successful in 25s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
Repo focus mode used to open into an empty tree — the file/dir nodes
only synthesized as agents touched files via tool calls. Cold-start
users saw a lonely repo:<id> orb with nothing under it. Now the tree
pre-seeds from the actual cloned repo the moment a client subscribes.

Backend:
- active_research_topics also returns repo_workspace_path.
- preseed_repo_paths(clone_path) runs `git ls-files` (bounded to top 200)
  against the on-disk clone. Silently returns empty on any failure so a
  missing clone / git-off-PATH / empty repo just degrades to the pre-V3
  behavior (tree still builds on touch).
- SSE loop, on first sight of a topic per client, emits one
  node.activity per pre-seeded path (label = leaf name, heat 0) so the
  tree is quiet-solid at rest.

Frontend:
- engine.onNodeActivity now synthesizes the dir:<partial> chain for
  file: nodeIds the same way onTouch does — otherwise the pre-seed
  would render as flat leaves under ROOT.
- Same 5-line synthesis extracted from onTouch; both paths now agree on
  the layout.

Cap of 200 keeps the SSE payload bounded on huge repos; the tail fills
in as agents actually touch files. When we later add per-file heat map
(V4), the 200 already-known files get first-class treatment out of the
gate.
2026-07-09 15:49:43 -07:00
Omar Sobh 9e5034cb53 wizards: ensure-chain preflight in AddToTeam + AddToCompany (W1)
ci / gates (push) Successful in 7s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
ci / rust (push) Failing after 13s
ci / frontend (push) Successful in 29s
TeamWizard already ran ensure-chain before /api/teams; the two "AddTo"
modals didn't, so teams/companies created from them landed as
structural orphans (no company/org parent). Same treatment now applied
to both modals + their backend endpoints.

Backend — two symmetric `attach_to_*_id` fields (mirrors what
create_team already exposes):
- ComposeTeamRequest gains `attach_to_company_id`. After team insert,
  create_team_from_claws binds it via companies::add_team with a fresh
  `n{count}` node id.
- CreateCompanyRequest gains `attach_to_org_id`. After company insert,
  create_company binds it via orgs::add_company the same way.

Both bindings are optional — plain POSTs from tools/tests still work.
Ownership is re-checked via `<parent>::get(pool, id, workspace_id)` so
the endpoints can't be tricked into parenting into another workspace.

Frontend — both modals now:
1. POST /api/structure/ensure-chain (empty body → server picks
   "My Workspace" / "General" fallbacks when nothing exists yet).
2. Include the returned parent id in the create request.

AddToOrgModal untouched — orgs are top-level, no parent needed.
MasterPlannerModal untouched — it posts to /webhooks, doesn't create
structural rows.

Follow-up already queued in the original list: same treatment for the
Company/Org "wizard"-flavored surfaces (as opposed to the compose
modals). Currently those don't exist as distinct wizards.
2026-07-09 15:47:46 -07:00
Omar Sobh f9e8d8d779 research: publishing → published + artifact download (R1)
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 26s
ci / gates (push) Successful in 6s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
Before this commit, approve_publish left topics stuck in 'publishing'
forever — the sidebar 'published' bucket was always empty and nothing
surfaced the artifact. Two-part fix:

State machine — approve_publish now transitions reviewing → publishing
→ published in one API call. Real async packaging isn't a thing yet
because the artifact IS the markdown already written to
research_outcomes when the last run completed (topology_worker). The
intermediate 'publishing' state is preserved (schema-level trigger
stamps published_at on landing there) so we keep the option to detour
through it later for pdf render / mirror-to-store / etc.

Download endpoint — GET /api/research/:id/artifact returns the latest
outcome as text/markdown with Content-Disposition: attachment. Filename
sanitizes the topic title to ascii-alnum + dash and appends the outcome
version so accumulated revision drafts (post R2) don't clobber.
Workspace ownership check via research_topics::get; 404 if no outcome
yet (reviewers browsing before a run completes).

Frontend — ResearchList shows a green Download icon-button next to
Delete when status === 'published'. Clicks trigger a plain anchor
download of the .md — no JS blob dance needed since the response is
already an attachment.

Follow-up queued: pdf render (server-side or client-side of the md),
mirror-to-store (S3-ish or Obsidian vault) as an async step during the
publishing→published detour.
2026-07-09 14:05:21 -07:00
Omar Sobh 059873b59f world: always-on topology edges (V2)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 9s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
ci / frontend (push) Successful in 3m25s
The parent → child hierarchy was already drawn between structural nodes,
but agent pawns had no visible tie to their team — you saw a swarm of
free-floating disks with no legible affiliation. Two new persistent
edge kinds:

- Pawn → home team: every agent gets a thin line to their team node,
  so the workspace skeleton reads visually. Line stays whether the
  pawn is idle or working.
- Pawn → active landmark: when a pawn is targeting a repo:<id> or
  loop:<id> orb (the soft-convergence state we shipped in V1 + the
  research pass), an edge appears too. Bridges the gap between
  world.touch bumps so the affinity stays visible during quiet periods.

Focus mode culling honored — pawn/target edges only draw if both ends
are in the visible subtree, so the Gource repo view still reads clean.

All three edge types share the same LineBasicMaterial + draw call, so
this is free at render time.
2026-07-09 13:59:40 -07:00
Omar Sobh 5a2340587e world: loop:<id> landmark orbs (V1)
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 24s
ci / frontend (push) Successful in 4m12s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
Mirror the repo: landmark pattern for scheduled loops. Every enabled
loop in the workspace gets a labeled amber orb in the World, whether
it's currently running or between fires. Assigned agents converge on
it with a soft 0.15 touch — the loop is a persistent landmark, not a
transient run.

Backend:
- active_loops(pool, ws) query joins loops + loop_agents where enabled,
  returning (loop_id, title, agent_id) — one row per (loop, agent).
- SSE loop emits node.activity + world.touch symmetric to the research
  block. Seen-once set dedupes the label emission across agents.

Frontend:
- New "loop" tier in the Tier alias, LEVEL_COLOR (#f0b866 warm amber),
  and ensureNode radius (11 — same landmark size as repo).
- engine.onTouch / onNodeActivity preserve the tier from the loop:
  prefix (previously would have collapsed to service).
- Pawn fireColor tinted amber for loop: touches.
- WorldCanvas: loop tier joins the struct group for solid-at-rest glow
  + always-on labels. Focus mode recognizes loop: prefix (click →
  focused subtree, Esc to exit). Focus pill switches to
  "LOOP FOCUS" in amber when the selected id is a loop.

Contrast with repo: (transient — only appears when a topic is in
processing/reviewing/publishing). Loops are persistent because their
whole point is recurrence.
2026-07-09 13:58:47 -07:00
Omar Sobh 708f45d09b world: solid team + project orbs, hide ROOT, per-topic repo landmarks
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 13s
ci / frontend (push) Successful in 28s
Three fixes to the Live viz per user feedback:

1) Hide the ROOT sentinel — it was rendering at the origin as a red disc
   with no label. It's a physics anchor, not a real orb. Skipping it in
   nodes/edges/labels loops removes the mystery red circle.

2) Emit `repo:<topic_id>` project orbs for active research topics from
   the world SSE loop. Labeled with the topic title, tier "repo" gets its
   own soft sky-blue palette and a landmark radius (r=11 between company
   and team). Assigned agents get a low-weight (0.15) convergence touch
   so pawns cluster around their project's orb even at rest — no wait
   for a file op to see the affiliation.

3) Solid at rest, bloom on interaction. Two engine changes:
   - Structural + repo orbs now have a soft 0.08 glow at heat=0 (down
     from 0.22 ambient) so the disc reads as solid until agents heat it.
   - `world.touch` heat is now weight-scaled (`+w*0.6`) instead of a
     flat `+0.5` regardless of intent. Soft convergence stays soft;
     file ops still explode.

Click a `repo:` orb → the existing Commit F focus mode already treats
that prefix as a subtree root, so users drop straight into the
Gource-style repo detail view with only the files their agents are
touching.

Follow-ups queued: teardown of repo orbs when a topic reaches 'published'
(currently they persist until the SSE loop's status filter drops them,
which is correct); loops equivalent (loop:<id> landmark orbs).
2026-07-09 13:22:27 -07:00
Omar Sobh f88e8642d9 world viz: repo focus mode — click a file/dir/run to enter the tree
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 41s
ci / rust (push) Successful in 4m1s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 12m18s
MVP of the repo-detail sub-view. Click any file/dir/run/repo node
in Live and the viz culls to just that node's subtree — you're
now watching agents crawl the repo instead of the whole workspace.
Esc (or the pill button that appears) exits.

Backend
- routes/world.rs: file-op tool events now emit a `file:<path>`
  world.touch IN ADDITION to the existing `tool:<name>` touch.
  The path is pulled from the tool input's path / target / file
  / filename / url keys (same lookup summarize_input uses, but we
  keep the full string so the client can build a real hierarchy).
  Non-file tools are unchanged — they still hit tool:<name> nodes
  as before.

Engine
- onTouch synthesizes a directory hierarchy when the id starts
  with `file:`. Each intermediate path segment gets a `dir:<acc>`
  node (label = the segment), parented at the previous dir; the
  file itself parents at the innermost dir. Ensures the layout
  spring-simulates as a tree naturally, no separate render mode
  needed.
- New pawn fireColor for file: touches: coral #ff8a7a. Reads as
  "file work" vs #5ec8d8 (tool convergence) vs #5fd08a (run
  activity).

WorldCanvas
- Render pass now takes a `visibleNodes: Set<string> | null`.
  When the selected id starts with file: / dir: / run: / repo:,
  we BFS descendants via parentId and hide every non-descendant
  node. Node meshes, glow sprites, hierarchy edges, and labels
  all gate on the set. Pawns stay visible (agents still dart to
  the focused files).
- ESC handler on window: clears the selection by calling
  onSelect("") when a repo-focus id is set.
- Small "REPO FOCUS · <path>" pill lands at top-center with an
  Esc button so the exit is discoverable at a glance without
  learning the shortcut.
- Dashboard.onWorldSelect now treats empty string as "clear
  focus" (setWorldSel(null)) so the same callback handles ESC.

Not yet: the always-on repo:<topic_id> node emitted at run
start when a research topic has a repo bound. Today the focus
works off run:<id> nodes; a repo:<id> anchor would let users
click without waiting for a first file touch. Also skipped:
per-file heat map / call-count visualization tied to touch
weight over time. Both are natural follow-ups on this bones.
2026-07-09 12:16:05 -07:00
Omar Sobh bce370978d world viz: comm beams + emergent team-shape overlay
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 35s
ci / rust (push) Successful in 2m41s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 3m59s
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.
2026-07-09 11:56:07 -07:00
Omar Sobh aa941b72a1 world viz: agents grow with their brain (log-curve dot size)
ci / frontend (push) Successful in 35s
ci / rust (push) Successful in 2m38s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m40s
ci / gates (push) Successful in 6s
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.
2026-07-09 11:52:45 -07:00
Omar Sobh aed21b654c agents page: 2-col metric grid + stacked full-width rows
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 36s
ci / rust (push) Successful in 3m12s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 34s
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.
2026-07-09 11:44:59 -07:00
Omar Sobh acd2a0f287 structure polish: post-reify nav + ensure-chain + TeamWizard auto-parent
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 25s
ci / rust (push) Successful in 3m54s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 3m4s
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.
2026-07-09 11:41:27 -07:00
Omar Sobh 8b789beec0 structure: reify-orphans endpoint + "give these a home" dialog
ci / gates (push) Successful in 5s
ci / rust (push) Successful in 3m57s
ci / e2e (push) Has been skipped
ci / frontend (push) Successful in 27s
ci / publish (push) Successful in 4m3s
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.
2026-07-09 11:26:07 -07:00
Omar Sobh 99e5207e69 sidebar: click-to-rename org/company/team + strip synthetics from world viz
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 25s
ci / rust (push) Successful in 3m51s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m43s
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.
2026-07-09 11:18:52 -07:00
Omar Sobh 1a4c5eb159 research: inline Approve / Reject on the topic canvas
ci / gates (push) Successful in 15s
ci / frontend (push) Successful in 43s
ci / rust (push) Successful in 2m44s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 47s
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.
2026-07-08 21:24:05 -07:00
Omar Sobh 036d64ee5d research canvas: escape apostrophe in running-hint copy (fixes lint)
ci / gates (push) Successful in 6s
ci / publish (push) Successful in 2m44s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 2m47s
ci / e2e (push) Has been skipped
2026-07-08 18:39:10 -07:00
Omar Sobh 7c1af2e070 research: pipeline-running signal + spinners so users aren't guessing
ci / gates (push) Successful in 7s
ci / frontend (push) Failing after 19s
ci / rust (push) Successful in 4m0s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
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.
2026-07-08 18:36:34 -07:00
Omar Sobh 316cdbf929 research sidebar: delete-with-confirm per topic
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m40s
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 30s
ci / rust (push) Successful in 2m38s
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.
2026-07-08 17:21:51 -07:00
Omar Sobh a2d3d85ebe research pipeline v2: topology-aware start + persisted draft
ci / rust (push) Successful in 2m42s
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 37s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 4m2s
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.
2026-07-08 17:13:46 -07:00
Omar Sobh 81a436d221 research canvas: wider rail + stage explainer + 409 fix
ci / rust (push) Successful in 3m13s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 3m58s
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 38s
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.
2026-07-08 15:59:04 -07:00
Omar Sobh 92e923e585 planner: specialists mode → single agent (backend + UI polish)
ci / gates (push) Successful in 6s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m40s
ci / frontend (push) Successful in 24s
ci / rust (push) Successful in 3m54s
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.
2026-07-08 15:04:00 -07:00
Omar Sobh f0a2bd8a86 master planner: pink brain, agent tab, mode-scoped topologies + svg illustrations
ci / gates (push) Successful in 11s
ci / frontend (push) Successful in 36s
ci / rust (push) Successful in 3m6s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m33s
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/>.
2026-07-08 15:01:40 -07:00
Omar Sobh 0cbde3a3cf agents empty state: kill synthetic fallback + real CTA
ci / publish (push) Successful in 36s
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 35s
ci / rust (push) Successful in 2m38s
ci / e2e (push) Has been skipped
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).
2026-07-08 14:06:24 -07:00
Omar Sobh 58ed8a5948 reap: skip synthetic tree nodes (fixes 422 on batch-delete)
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 37s
ci / rust (push) Successful in 3m11s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m40s
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.
2026-07-08 12:17:58 -07:00
Omar Sobh 984d9a1274 reap: cascade orgs → companies → teams → agents
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 3m56s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 4m4s
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."
2026-07-08 10:36:52 -07:00
Omar Sobh 1a2baee74b loops wizard: stop pulling bearer.ts into the client bundle
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 36s
ci / rust (push) Successful in 2m43s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m31s
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.
2026-07-08 09:37:55 -07:00
Omar Sobh 16fcf8ef96 agents sidebar: fold org/company/team/agent tree into one pane
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 26s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
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.
2026-07-08 05:30:34 -07:00
Omar Sobh a6da19430f loops: repo picker + agent/team/org staffing + sidebar edit/delete
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 12s
ci / frontend (push) Successful in 25s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
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.
2026-07-07 22:09:06 -07:00
Omar Sobh 2562541f5b repos sidebar: fold repos under their org
ci / frontend (push) Successful in 27s
ci / rust (push) Successful in 3m53s
ci / e2e (push) Has been skipped
ci / gates (push) Successful in 7s
ci / publish (push) Successful in 2m40s
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.
2026-07-07 18:37:48 -07:00
Omar Sobh 637e1bdd69 repos: sidebar actions (sync/edit/remove) + edit modal
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 35s
ci / publish (push) Successful in 4m7s
ci / rust (push) Successful in 2m43s
ci / e2e (push) Has been skipped
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
2026-07-07 17:55:20 -07:00
Omar Sobh e858a7f92f repos: Gitea provider (first-class) — sync + wizard default
ci / e2e (push) Has been skipped
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 42s
ci / rust (push) Successful in 2m47s
ci / publish (push) Successful in 2m30s
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.
2026-07-07 15:11:30 -07:00
Omar Sobh 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.
2026-07-07 14:54:39 -07:00
Omar Sobh 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.
2026-07-07 14:44:50 -07:00
Omar Sobh 806ba869e5 teams: ephemeral lifecycle for Scheduled + Triggered planner modes
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 38s
ci / rust (push) Successful in 3m6s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m23s
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.
2026-07-07 04:30:07 -07:00
Omar Sobh 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).
2026-07-07 04:23:00 -07:00
Omar Sobh 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.
2026-07-07 04:21:32 -07:00
Omar Sobh 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).
2026-07-07 04:20:12 -07:00
Omar Sobh 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.
2026-07-07 04:18:29 -07:00
Omar Sobh 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.
2026-07-07 04:16:36 -07:00
Omar Sobh 18a99a970d loops wizard: fix client-bundle break from topology import
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 4m3s
ci / rust (push) Successful in 7m3s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 4m46s
@/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.
2026-07-06 13:03:24 -07:00
Omar Sobh e5820ce927 loops wizard: topology builder in step 2
ci / gates (push) Successful in 13s
ci / frontend (push) Successful in 4m21s
ci / rust (push) Successful in 8m10s
ci / e2e (push) Has been skipped
ci / publish (push) Failing after 6m22s
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.
2026-07-06 12:36:18 -07:00
Omar Sobh 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.
2026-07-06 12:33:42 -07:00
Omar Sobh 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.
2026-07-06 12:28:34 -07:00