Commit Graph
122 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 4.7 82b5cf4385 research canvas: refresh claws + make Description collapsible
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 39s
ci / rust (push) Successful in 3m16s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m35s
Two UX cleanups on the research canvas:

1. Agent cards were showing "(missing agent)" for every slot after an
   in-wizard auto-provision because the Dashboard's `claws` prop is SSR-
   rendered and doesn't include agents created during the client session.
   Refetch /api/team/claws on canvas mount + refreshKey change, merge
   with the prop (fresh wins) so newly-provisioned agents resolve
   correctly without a page reload.

2. The Description block was always fully expanded. Wrap it in <details
   open> so the user can collapse it once they've read it, matching the
   existing "Original prompt" pattern.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-18 18:21:37 -07:00
Omar Sobh efc13f2904 trim: drop stale roster-empty comment (LoopsWizard)
ci / frontend (push) Successful in 36s
ci / gates (push) Successful in 4s
ci / rust (push) Successful in 2m57s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m43s
2026-07-18 16:26:15 -07:00
Omar Sobh c9dc96c44b fix: use TopologyKind alias to keep LoopsWizard under 1250 lines
ci / gates (push) Successful in 5s
ci / frontend (push) Failing after 1m6s
ci / rust (push) Successful in 3m33s
ci / e2e (push) Skipped
ci / publish (push) Skipped
2026-07-18 16:26:02 -07:00
Omar SobhandClaude Opus 4.7 c3e8f3ee01 fix: match AutoProvisionedAgent shape + valid OutcomeKind for loops
ci / gates (push) Successful in 15s
ci / rust (push) Successful in 4m29s
ci / frontend (push) Failing after 20s
ci / e2e (push) Skipped
ci / publish (push) Skipped
Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-18 16:24:40 -07:00
Omar SobhandClaude Opus 4.7 279d785f5b loops wizard: extract AutoProvisionCard to stay under 1250 lines
ci / gates (push) Successful in 6s
ci / frontend (push) Failing after 33s
ci / rust (push) Successful in 2m52s
ci / e2e (push) Skipped
ci / publish (push) Skipped
Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-18 16:23:13 -07:00
Omar SobhandClaude Opus 4.7 6f1515b1b0 team runs modal: expose runtime posture (risk_profile + mcp_bundles)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 9s
ci / frontend (push) Failing after 42s
ci / e2e (push) Skipped
ci / publish (push) Skipped
PATCH /api/teams/{id}/runtime-config existed since the team-wizard work
but had no UI — you needed curl to swap a team's risk_profile or add
gitea_forge to its bundles. Add compact pickers to TeamRunsModal above
"Run now": a Risk profile <select> and a Bundle checkbox list.

Fetches current settings from GET /api/teams/{id} on open and PATCHes
inline on every change. No save button — the debounce is the user's
next click. Optimistic state; next-open resync corrects any drift.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-18 16:19:12 -07:00
Omar SobhandClaude Opus 4.7 9cab32c354 loops wizard: auto-provision team in-wizard (parity with research)
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 9s
ci / frontend (push) Failing after 17s
ci / e2e (push) Skipped
ci / publish (push) Skipped
The LoopsWizard hard-blocked on an empty roster via NoAgentsGate — new
users had to bounce to the Agents page first. Match ResearchWizard's
flow: remove the top-level gate, add an auto-provision panel to the
staffing step (6) that derives 3-5 roles from the loop's task via
Claude Sonnet 5 and preselects the returned agents into
selectedAgents so the existing submit path publishes them onto the
loop with zero extra clicks.

Existing rosters still work — the hand-pick panel now sits below the
auto-provision card and only renders when the workspace has agents and
no team was auto-provisioned this session.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-18 16:17:57 -07:00
Omar Sobh e95e251c9c empty state: primary CTA button instead of hunt-for-plus text
ci / gates (push) Successful in 6s
ci / rust (push) Successful in 4m4s
ci / e2e (push) Skipped
ci / frontend (push) Successful in 28s
ci / publish (push) Successful in 35s
The '+ new topic' button (commit 47a4242 unlocked it when the
workspace roster was empty) is a tiny 34x34 corner icon that
disappears against the header on a truly empty page. Users kept
asking why they can't start a research topic when in fact they
could — the target was just invisible.

Replace the 'Hit the + button to launch the wizard' hint with a
proper primary CTA: coral pill 'Start a research topic' with the
Plus icon, centered in the empty sidebar. Same treatment for
LoopsList → 'Start a loop'.

The wizard flow's auto-provisioning behavior means the empty-
workspace path is now the happy path — one click and you get
everything (topic, team, agents, container). The empty state
should invite that click, not require a hunt.
2026-07-17 21:33:30 -07:00
Omar Sobh f8589471bc AgentComputer: clickable model picker (dropdown → PATCH /api/claws/:id/model)
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 27s
ci / rust (push) Successful in 4m20s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m45s
The '● <model>' text on the per-agent Computer slide-out was
read-only — swapping a claw's model required curl. Now it's a
button that opens a small dropdown of the curated fleet models:

- Sonnet 5 / Opus 4.8 / Haiku 4.5 / Sonnet 4.6 (Anthropic)
- GLM 4.6 / 5.2 (Z.AI)
- Kimi K2 (Moonshot)
- Gemini 2.0 Flash (Google)
- Llama 3.3 70B (Groq)

Click a model → PATCH /api/claws/{id}/model → picker closes, the
button relabels to the new selection. The endpoint persists the DB
binding + best-effort re-provisions the runtime; the swap applies
on the claw's next turn. Long-tail models still work via curl with
any string.

Adds an optional onModelChanged callback so callers (Dashboard's
runtime-config enrich cache) can refresh their state without a
full re-render.
2026-07-17 21:18:29 -07:00
Omar Sobh ac8c689f50 logs + team parity: pretty step/container renderers; quota + audit on team creation
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 37s
ci / rust (push) Successful in 2m58s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m53s
Two bundled changes:

── LiveRunLogs prettification ──────────────────────────────────
The Steps + Container tabs were plain mono lines with a single
color per event. Now they get structured layout:

Steps:
- Color-hashed actor pill (stable palette so [Distiller] and
  [Novelty Analyst] each get their own hue across the session).
- Phase pill (plan=cyan, work=green, synth=amber, aggregate=purple).
- Token count pill formatted 1.2k / 14.3k / etc.
- Gated-action warning pill in amber when > 0.
- Left-border color strip keyed to the actor for at-a-glance
  visual grouping.
- Long outputs collapse to their first 300 chars with a '+ N more'
  toggle to expand the full text.
- 'done' events get a green (or red for error) border strip +
  pill instead of blending into the stream.

Container:
- Splits '[actor] action (outcome) · msg' into colored spans —
  actor pill (deterministic color), action in dim, outcome pill
  green/red/dim by state.
- Non-line events (info/error/done) get their own left-border
  strip so bash echoes and stack traces don't drown in the daemon
  chatter.
- Timestamps switch to HH:MM:SS.mmm — dense but scannable.

Small palette (LOG constants) keeps the color budget bounded — no
new UI vocabulary, just cleaner reads of what was already there.

── Team-wizard governance parity ───────────────────────────────
build_team_with_lifecycle now matches POST /api/claws' governance:
- enforce_new_agent quota check per member (previously bypassed
  workspace agent quotas entirely for team/auto-provision paths).
- audit::append('agent.created', ..., {source: 'team_wizard'}) per
  member so team-created claws appear in the same audit trail as
  individually-created ones. Adding a 'source' key distinguishes
  provenance without changing consumers.

.brain (h5) handling was already consistent between the two paths —
both use the lazy on-first-access load_brain hook seeded from
agents.system_prompt. No change there.
2026-07-17 18:20:56 -07:00
Omar Sobh 956be2cf4f wizard: in-place auto-provision team from topic (LLM-derived, sonnet-5)
ci / gates (push) Successful in 17s
ci / frontend (push) Successful in 36s
ci / rust (push) Successful in 4m17s
ci / e2e (push) Skipped
ci / publish (push) Successful in 4m9s
Step 5 of ResearchWizard was 'assign agents from workspace roster'.
When the roster was empty, the wizard body was hard-swapped for
NoAgentsGate — you couldn't reach step 5 at all.

Now step 5 shows a 'Team' panel:
- Big cyan card: 'Auto-provision team from this topic'. One click
  runs an LLM plan pass, gets 3-5 role slots + system prompts back,
  materializes claws via the existing build_team pipeline, stamps
  runtime posture, returns a shape that drops straight into the
  submit body's agents[]. Card flips green with the derived roster.
- Below that: the classic roster picker, but only when the workspace
  actually has ≥1 claw AND auto-provision hasn't landed. Otherwise
  hidden — no dead empty-state affordance.

Every gate that required agents.length > 0 to render the wizard body
or the footer is gone. canNext gains a step-5 clause: allow Next when
EITHER auto-team is ready OR the user handpicked from a non-empty
roster.

Backend
- POST /api/teams/auto-provision — accepts {title, description,
  outcome_kind, topology_kind?, model?, risk_profile?, mcp_bundles?}.
  Derives topology from outcome_kind (integrations → pipeline; else
  hub_spoke). LLM plan pass yields a JSON roster of 3-5 roles
  (role_slot, name, system_prompt). Materializes team + claws via
  build_team, stamps risk_profile (default research_web_readonly) +
  mcp_bundles (default [clawmates_door, gitea_forge]). Response
  carries team_id + agents[] in the shape /api/research already
  expects.
- Every provisioned claw runs on claude-sonnet-5 by default;
  overridable via the model field.

Follow-ups (not in this slice):
- Same picker in LoopsWizard (slice C — parallel change, same API).
- Post-create 'Team' section on ResearchCanvas / LoopsCanvas so
  users can rebind after the fact (slice D).
- Full Teams tier UI + Agents-page deprecation (slice E).
2026-07-17 16:08:07 -07:00
Omar Sobh 47a42423e3 wizards: unlock '+' buttons when roster is empty
ci / frontend (push) Successful in 49s
ci / publish (push) Successful in 2m42s
ci / gates (push) Successful in 7s
ci / rust (push) Successful in 4m6s
ci / e2e (push) Skipped
The 'no agents → button disabled' guard hard-locked the wizard
behind an already-populated roster, forcing users to detour to the
Agents page and come back. NoAgentsGate already handles the empty
state gracefully inside the wizard body; the button just needed to
open it. Applied to both ResearchList and LoopsList.

Follow-up (design in progress): let the wizard auto-provision a
team from the topic/loop itself so users never have to leave to
create agents.
2026-07-17 13:27:08 -07:00
Omar Sobh eb06e91ac3 dashboard(Agents): drop counts caption, align toolbar with title
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 45s
ci / e2e (push) Skipped
ci / rust (push) Successful in 2m51s
ci / publish (push) Successful in 2m39s
Previously the Agents header wrapped a 'N ORGS · N CO · N TEAMS ·
N AGENTS' small-caps caption above the title, with the toolbar
(select/history/brain) hanging off the right. Dense, noisy, and the
World tier already surfaces those counts at higher fidelity.

Now: single-line header — 'Agents' title on the left, toolbar flush
right, both center-aligned. Comment left in place noting where the
caption used to live in case we ever want it back.
2026-07-17 12:51:53 -07:00
Omar Sobh 0b7f247b0e wizard: 'fresh coding team' picker for paired coding loop
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 4m4s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m36s
Second slice of the per-loop-team arc. The paired-coding-loop checkbox
in ResearchWizard step 6 now exposes a two-option picker:

  ⦿ Provision a dedicated coding team (default when the loop is on)
     — fresh 'Coding · <topic>' team row, risk_profile =
       coding_readwrite, clawmates_door in mcp_bundles. Loop's
       team_id is bound at wizard-submit time.
  ○ Reuse the research team (legacy) — no team_id bound; coding
     iterations spawn against the research topic's container.

Frontend
- New codingTeamMode state, radio picker rendered under the checkbox.
- research.ts createTopic body gains paired_coding_team_mode?: 'fresh'|
  'reuse'.

Backend
- CreateTopicRequest gains paired_coding_team_mode: Option<String>.
- materialize_topic_loops takes it through and, when 'fresh', calls
  the new provision_fresh_coding_team helper — inserts a teams row
  via the existing insert_team_with_lifecycle (pipeline kind, same
  graph as the loop), sets its runtime-config via
  set_team_runtime_config, then binds loop.team_id.
- All operations best-effort with stderr logging — a team-provision
  failure leaves the loop functional under the legacy fallback.

Not shipped in this slice (deferred to runtime hookup slice):
- research_container::spawn keyed on team_id → per-team container
- Config template rewrite injecting the team's risk_profile
- Migration of existing paired loops onto their own teams

The plumbing lands now so the wizard's intent is recorded; the
runtime honors it in the next PR.
2026-07-16 20:49:54 -07:00
Omar Sobh b2a38da3f9 loops: 'View full output' modal for a completed iteration
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 4m6s
ci / e2e (push) Skipped
ci / publish (push) Successful in 35s
The expanded iteration panel now exposes the full topology_run.result
in a proper viewer instead of leaving it stranded in Postgres. Reads
/api/topology-runs/:id (existing endpoint, no backend change), prefers
result.final_output (the produced markdown/code), falls back to a
per-step transcript, last-resort a raw JSON dump.

Modal renders as a fixed overlay — Escape or backdrop-click to close.
Header carries kind/status/steps/tokens metadata, Copy button hits
navigator.clipboard, Download button emits a .md file named
run-<id-slice>.md. The button on the row is disabled until the
iteration terminates (completed | failed) — running iterations
already have LiveRunLogs surfacing the tail.
2026-07-16 18:05:57 -07:00
Omar Sobh 2ba40b03b7 loops: per-iteration live logs (Steps + Container tabs), collapse graph JSON
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 38s
ci / rust (push) Successful in 3m15s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m35s
The Loops canvas' IterationsTimeline was a flat status-pill list —
no way to see WHAT an iteration was actually doing. Meanwhile the
Research canvas had full LiveRunLogs with Steps + Container tabs
against the same underlying topology_run SSE endpoints. This
factors LiveRunLogs so both canvases share it.

Frontend
- LiveRunLogs gains a directRunId?: string prop. In this mode it
  skips the topic-scoped active-runs + pipeline-state polls and
  pins activeRun to the given id. topicId stays optional (topic
  mode unchanged from ResearchCanvas' perspective).
- LoopsCanvas IterationsTimeline: each row is now a click-to-
  expand card. On expand, renders <LiveRunLogs directRunId={...}/>
  right below the header — Steps + Container tabs, full SSE tail,
  same 320px terminal.
- Auto-opens the newest running/queued iteration so a click on
  'Run now' immediately exposes the live pane.
- New CollapsibleSection helper wraps the graph JSON block so it
  starts closed. Reference material stays one click away without
  cluttering the canvas.

Backend
- run_container_log_sse now resolves the tail target via
  loop.source_research_topic_id when the run itself has no
  research_topic_id. Paired coding loops (kind=exec with a
  source_research_topic_id) reuse the paired topic's team
  container, so we tail its docker logs. Pure loop runs still
  error with a clearer message.
2026-07-16 17:30:07 -07:00
Omar Sobh ae113dd319 live-run-logs: add Container tab that tails filtered daemon logs
ci / rust (push) Successful in 2m49s
ci / e2e (push) Skipped
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 36s
ci / publish (push) Successful in 3m58s
Steps summaries only fire AFTER each topology step completes — so a
stalled first turn was completely dark. Add a second tab that
streams the team runtime container's daemon log live via a new SSE
endpoint.

Backend
- GET /api/topology-runs/:id/container-log — workspace-scoped SSE
  around bollard's docker.logs(follow=true, tail=200). Buffers on
  newline so partial mux chunks don't truncate a log line.
- compact_container_log: parse a zeroclaw daemon line
  ('[actor] ... zc_action=X zc_outcome=Y ... msg') into
  '[actor] action (outcome) · msg'. Framing-only continuations are
  dropped; non-zc lines (bash echoes, backtraces) pass through as-is
  so nothing interesting is lost. ANSI escapes stripped.
- Everything funnels through one async_stream! so early exits
  (workspace check / docker connect / no bound topic) yield an
  'error' event and return without breaking Sse::new's single stream
  type.

Frontend
- LiveRunLogs gets a sub-tabs strip: Steps · Container.
- New useContainerLog(runId, active) hook — gated by tab so we don't
  hold two open SSE streams when the operator isn't looking.
- Same terminal widget renders each container line with a level
  color (info/done grey, line default, error red). Sub-tab pill
  shows count + status live.
2026-07-15 18:42:03 -07:00
Omar Sobh f70f6c679e research: errored-state card + one-click rerun (no wizard re-entry)
ci / publish (push) Skipped
ci / gates (push) Successful in 22s
ci / frontend (push) Successful in 27s
ci / rust (push) Failing after 58s
ci / e2e (push) Skipped
When a topic ends up parked in 'processing' with all runs failed and
nothing in flight, the sidebar card was still spinning as if
progress were happening. Now:

Backend
- topology_runs::run_counts_by_research_topic — batch query that
  returns (in_flight, failed-since-last-success) per topic. Used by
  the list endpoint; dynamic sqlx::query() so no prepare needed.
- TopicListItem DTO gains runs_in_flight + runs_failed.
- start_topic status guard relaxed: allow (standby) OR (processing
  AND runs_in_flight == 0). Blocks accidental double-fires on a
  live pipeline; permits rerun on a failed one. Same request body,
  same behavior once accepted, so the frontend just POSTs
  /research/:id/start on the RotateCw click.

Frontend
- ResearchList detects errored: status===processing && !in_flight
  && failed>0. Swaps the MiniSpinner for a red AlertTriangle and
  changes the status text to 'error · N failed'.
- New RotateCw icon button next to the delete Trash — same button
  cluster, one click, no wizard re-entry required. Disables while
  a request is in flight; error surfaces in the sidebar's shared
  error banner.
2026-07-15 16:27:43 -07:00
Omar Sobh 43f7880327 agent cards: disk-LED glow on live topology step
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 40s
ci / rust (push) Successful in 3m13s
ci / e2e (push) Skipped
ci / publish (push) Successful in 58s
Wire SSE step events from LiveRunLogs up to ResearchCanvas via an
optional onStep callback (StepPulse: {role, phase, node_id, ts}).
ResearchCanvas keeps a role -> {phase, expiresAt} map; a 250ms tick
clears expired entries so the glow fades naturally.

Each agent card:
- 8px LED dot next to the name (green idle-off / colored when live)
- outer glow via two-layer box-shadow when the agent's role_slot
  matches the last step's role, within a 2.5s window
- inline 'reading' / 'writing' hint below the role

Color mapping (disk-LED metaphor):
- Plan phase   -> cyan  #5ec8d8 (reading / decomposing)
- Work/Synth/Aggregate -> green #5fd08a (writing / producing)

Overlapping steps reset the timer so back-to-back activity on the
same role holds the glow. When no run is active nothing pulses —
LiveRunLogs is silent, no callback fires.
2026-07-15 14:42:57 -07:00
Omar Sobh 45292bf5fb sidebar polish: tighter chevron + '+' drops to Research line
ci / gates (push) Successful in 1m17s
ci / rust (push) Successful in 2m48s
ci / frontend (push) Successful in 1m4s
ci / e2e (push) Skipped
ci / publish (push) Successful in 46s
- Dashboard chevron: 24x24 → 22x22, top:10 right:8 → top:4 right:4
  so it sits in the true corner of the panel.
- ResearchList header: alignItems flex-start → flex-end so the '+'
  button lands on the 'Research' title baseline instead of hugging
  the 'N TOPICS' caption line. Reserved 34px right padding on the
  header so the '+' clears the corner chevron.
2026-07-15 12:59:07 -07:00
Omar Sobh 1876641415 dashboard: collapsible context sidebar (research/loops/repos/infra)
ci / publish (push) Successful in 4m55s
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 43s
ci / rust (push) Successful in 3m33s
ci / e2e (push) Skipped
The earlier PR added a chevron to RosterColumn but the research /
loops / repos / infra tiers render a completely different sidebar
(ResearchList/LoopsList/…) inside a fixed 252px div in Dashboard.tsx
— the RosterColumn toggle never appeared on those pages.

Add a proper collapse on the Dashboard's own context-list wrapper:
- Overlaid PanelLeftClose chevron top-right (top: 10, right: 8) so
  it doesn't fight the per-tier header content.
- When collapsed, wrapper shrinks to 32px with a PanelLeftOpen
  button; canvas gets the reclaimed ~220px.
- State via useSyncExternalStore on localStorage
  ('cm.dashboard.sidebarCollapsed'), same pattern I used on
  RosterColumn to satisfy react-hooks/set-state-in-effect.
2026-07-15 12:06:35 -07:00
Omar Sobh 573956e840 live-run-logs: fix step-record parser + surface pre-first-step phases
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 29s
ci / rust (push) Successful in 3m3s
ci / e2e (push) Skipped
ci / publish (push) Successful in 3m1s
Two fixes based on live smoke test:

- StepRecord parser: the SSE step payload is {node_id, role, phase,
  output, gated, tokens} (orchestrator::StepRecord), not the made-up
  shape my first pass looked for. Render as '[role] phase · <first
  line of output> · Nt · N gated'.

- Pre-first-step visibility: the checkpoint only journals AFTER each
  turn completes, so the container-startup + agent-boot window (often
  30-90s for the first step) was completely dark ('waiting for first
  step…'). Poll pipeline-state every 2s and render its stages as
  pseudo-log lines (setup 🟢 staffing · 3 agents assigned / setup 🔵
  container · starting…) until real step events arrive.
2026-07-15 10:37:33 -07:00
Omar Sobh 2ef50bb30b lint(research): satisfy react-hooks/set-state-in-effect
ci / publish (push) Successful in 4m6s
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 2m49s
ci / e2e (push) Skipped
- RosterColumn: useSyncExternalStore on localStorage instead of
  hydrate-in-effect setState. Same-tab writes nudge subscribers via
  a synthetic storage event.
- LiveRunLogs: reset-on-runId-change via the React-sanctioned
  set-state-during-render pattern (prevRunId ref state), so the
  effect body no longer calls setState synchronously.
2026-07-15 10:20:29 -07:00
osobh 1d69866bcf research canvas: collapsible sidebar + live topology-run logs (#6)
ci / frontend (push) Failing after 21s
ci / rust (push) Successful in 4m12s
ci / e2e (push) Skipped
ci / publish (push) Skipped
ci / gates (push) Successful in 6s
2026-07-15 17:01:48 +00:00
osobh 149ad992bb wizard: materialize picked repo across clawstor fleet at step-2-next (#5)
ci / frontend (push) Successful in 29s
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 11s
ci / e2e (push) Skipped
ci / publish (push) Skipped
2026-07-15 11:33:19 +00:00
osobh 1cb643142c research/publish: gate approve+reject on Owner role (#4)
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 24s
ci / rust (push) Successful in 4m5s
ci / e2e (push) Skipped
ci / publish (push) Successful in 4m32s
2026-07-15 04:28:01 +00:00
Omar Sobh ee05037095 research-pipeline-diag: state-aware statuses (waiting != failing)
ci / gates (push) Successful in 7s
ci / rust (push) Successful in 2m53s
ci / publish (push) Successful in 2m48s
ci / frontend (push) Successful in 37s
ci / e2e (push) Has been skipped
The diagnostic was calling any 0-outcome + 0-completed-runs state a
FAIL — red dot + 'No outcome produced yet — check the runs stage for
the failure reason'. That fires the moment a wizard-materialized
topic lands, before its very first turn even completes, and stays
red for the whole 2-3 min a legitimate coordinator turn runs. Result:
users see 'FAIL' on every fresh topic and can't tell a real failure
from a normal in-flight state.

Backend fix — introduce a `waiting` status (blue/pulsing in UI):

Runs stage:
  0 runs                   -> skip     ('No runs yet — pipeline hasn't fired')
  any running/queued       -> waiting  ('N in flight, M completed')
  all failed               -> fail     (with error text)
  some failed              -> warn
  all completed no fails   -> ok

Outcomes stage:
  outcome_count > 0        -> ok
  0 outcomes + 0 runs      -> skip     ('No outcome yet (pipeline hasn't fired)')
  0 outcomes + any running -> waiting  ('Waiting for the current run to finish…')
  0 outcomes + any failed  -> fail     (the actual silent-bug case)
  0 outcomes + all done ok -> warn     (weird — completed but wrote nothing)

Also suppresses the run stage's `latest_error` detail when the run
status is `waiting` or `skip` — reporting a stale error next to an
actively-running job is what made users think the current run had
failed.

Frontend:
- PipelineStage['status'] union grows a 'waiting' arm.
- Pill color: cyan (#5ec8d8) with a pulsing scale/opacity animation
  (new cm-pulse keyframe in motion.css).
- Strip summary line: 'Pipeline in flight — waiting for run to finish…'
  when there's any waiting stage and no failures.
- Border tint: cyan border when waiting, coral when failing, neutral
  otherwise.

Zero backend semantic changes to the outcome-write path — this is
purely UI truth-telling.
2026-07-11 08:38:15 -07:00
Omar Sobh bfcdca0583 research-canvas: managed-by-loop UI + start_topic guard (fold cleanup)
ci / gates (push) Successful in 18s
ci / frontend (push) Successful in 31s
ci / rust (push) Successful in 2m43s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m57s
Closes the UX gap the fold introduced: the topic canvas was still
showing "Start research" for standby-state topics even when a
scheduled loop already owned the runs. Clicking it would 409 (or
worse: race the loop into a duplicate run). Topic status stayed at
standby forever because the loop path bypassed start_topic's
set_status transition.

Four changes:

1. **Backend status transition** — compose_and_enqueue_iteration for
   kind='research' now calls set_status_if(standby, processing) on
   the topic before the run is enqueued. New DB helper set_status_if
   only advances when the current status matches the "from" arg —
   safe against races and re-invocations. Later iterations no-op
   since the topic is already past standby.

2. **has_managed_loop on TopicDetail** — get_topic hydrates a new
   ManagedLoop struct (loop_id, title, enabled, next_fire_at,
   last_run_id, schedule_summary) when a kind='research' loop is
   bound to the topic. summarize_schedule() derives a human string
   from the loop's triggers jsonb (e.g. "cron: 0 3 * * * · on new
   artifact", "one-shot", "manual"). New DB helper
   loops::research_loop_for_topic returns the row.

3. **Canvas branch** — nextAction takes a managedByLoop flag; when
   set + status=standby, returns null (no button). The canvas
   renders a "MANAGED BY LOOP" strip below the topic title showing
   loop name, schedule summary, next fire time, and enabled dot.
   Reviewer buttons (Request publish / Approve / Reject) still show
   normally in later states — reviewers should still promote outcomes
   even when a loop is producing them.

4. **start_topic guard** — refuses with 409 when a research loop
   already owns the topic. Closes the direct-POST hole for anyone
   bypassing the frontend.

TS type + summarize_schedule live in the same commit so an old
client hitting a new backend just ignores the extra field (no
breakage), and a new client hitting an old backend renders the
classic buttons (managed_by_loop is optional).
2026-07-10 17:52:13 -07:00
Omar Sobh 3e42b1ea39 research-wizard: schedule step (once/nightly/manual) + paired coding loop
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
Completes the research/loop fold. The wizard now closes with a
"How should this research run?" step; picking any mode materializes a
kind='research' loop bound to the topic, and an optional checkbox
adds a paired kind='exec' loop that consumes each new artifact.

Frontend:
- ResearchWizard grows from 5 to 6 steps. Step 6 is the schedule
  picker:
    · Just once — initial_burst=1, no other triggers
    · Nightly   — initial_burst=1 + cron "0 3 * * *"
    · Manual    — webhook_enabled=true
  Below the radios, an optional card offers "Also create a coding
  loop that consumes each new artifact" — creates a paired
  kind='exec' loop with on_artifact_update=true + initial_burst=1
  bound to the same topic.
- createTopic API type extended with `schedule` + `create_paired_coding_loop`.
- Both fields ride the existing POST /api/research call; back-compat
  is preserved when the wizard omits them.

Backend:
- CreateTopicRequest gains TopicSchedule + create_paired_coding_loop.
- After topic + agent attach, create_topic calls
  materialize_topic_loops which:
    1. Creates a research-kind loop titled "Research · <topic>" bound
       to the topic. Triggers vary by schedule mode; next_fire_at
       computed from cron for nightly. Falls back silently if
       loop-create errors so the topic still lands.
    2. Flips kind to 'research' via loops::set_kind (NewLoop doesn't
       take kind directly — default is 'exec' for backward compat).
    3. Optionally creates a coding loop titled "Coding · <topic>"
       with on_artifact_update=true + initial_burst=1.
- cm_db::repo::loops::set_kind — trivial UPDATE helper used by the
  materialize path.

D-answer callouts:
- D1 (fold): every runnable thing is now a loop. "Just once" is a
  research loop with initial_burst=1 and no other triggers.
- D2 (inherit): both paired loops carry the same source_research_topic_id
  — repo binding lives on the topic, not duplicated.
- D3 (coordinator resolves): the research iteration prompt (from the
  earlier commit) instructs the team to preserve stable INT ids and
  mark deprecations; coding loops' consumed lists stay valid across
  versions.

Follow-ups queued:
- Kind pill on LoopsList cards (research=purple, exec=cyan) so users
  can tell them apart at a glance.
- Extract start_topic's task-build so kind='research' iterations
  reuse the same coordinator prompt shape as one-shot runs (they
  currently use a simpler refresh-oriented prompt; that's fine for
  MVP but a rich shared build would give better parity).
- Research topic sidebar shows "linked to N loops" badge.
2026-07-09 23:00:04 -07:00
Omar Sobh 5aa2de7f30 loops-wizard: initial_burst + on_artifact_update trigger UI
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 41s
ci / rust (push) Failing after 50s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
Surfaces the two new trigger fields shipped in the previous commit so
users can compose the new loop patterns without editing raw jsonb.

Step 4 (Triggers) now has five toggles instead of three:
1. **Run at create-time (initial burst)** — number input, defaults to 1
   so every new loop runs at least once. Toggle off = classic behavior
   (no auto-fire).
2. Cron schedule (existing)
3. On previous completion (existing) — pairs with initial_burst > 1
   for a back-to-back burst chain, and with cron for continuous
   iteration between schedule ticks.
4. **On new research artifact** — visible always; auto-disabled with
   an explanatory label when no source topic is bound. Coalesced
   server-side against in-flight runs so rapid revisions don't spawn
   duplicates.
5. Webhook (existing)

Body wiring:
- Sends initial_burst > 0 → { initial_burst: N } in triggers jsonb.
- Sends on_artifact_update only when a source topic is bound (safety
  belt matching the backend's fan-out filter).

Step gate:
- Advancing from step 4 now requires ANY one of the five triggers
  (was just the three legacy toggles). The initial_burst default of 1
  means users can happy-path through this step without deliberate
  action, which is the intended nudge — every loop should have at
  least one path to run.

Read/write parity:
- readInitialTriggers grows two fields so edit-mode reflects existing
  loops' triggers correctly.

Next commit lands ResearchWizard's "When should this run?" step +
optional paired coding loop, and after that the kind='research'
dispatch path.
2026-07-09 22:51:43 -07:00
Omar Sobh ce111273bb loops: reorder history collapsed under the progress pill
ci / gates (push) Successful in 7s
ci / frontend (push) Successful in 26s
ci / rust (push) Failing after 1m14s
ci / publish (push) Has been skipped
ci / e2e (push) Has been skipped
Completes the reorder rationale loop — the previous commit captured
REORDER: markers server-side but nothing surfaced them. Now the loops
sidebar renders a small "N reorders ▸" button under the progress pill
whenever the loop has any reorder events. Click expands to a
newest-first list of "iter <n> · <rationale>" lines so a reviewer can
see, at a glance, when the plan was adjusted and why.

Backend:
- LoopProgress DTO gains recent_reorders: Vec<Value> — newest-first,
  capped at 5 so the card stays compact. Full history remains on the
  loop row's reorder_events column.
- cm_db::repo::loops::recent_reorders — reads the jsonb array, returns
  the last N in newest-first order.
- list_progress populates it per loop.

Frontend:
- LoopReorderEvent + recent_reorders on LoopProgress type.
- LoopsList tracks openHistoryId per-loop (one open at a time).
- Card renders history button + expanded panel styled to match the
  progress pill above.

Notes:
- The event object schema is {run_id, iteration, text, ts}. Fields are
  optional in the TS type so future schema tweaks don't break the
  render.
- 5-item cap chosen so the sidebar card doesn't grow unbounded. If a
  loop accumulates a lot of reorders, follow-up UI can render the full
  history on the loop detail page.
2026-07-09 19:00:39 -07:00
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 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 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 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