Commit Graph
30 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 43436d7181 feat(telemetry): push bus for live agent frames
deploy / test (push) Successful in 4m25s
deploy / build (push) Successful in 5m20s
/api/world/live is a 2s database poll, which is right for queryable state and
wrong for a token stream: reasoning only became visible after a step finished
and its row was written. This adds a process-wide broadcast bus that
topology_exec publishes to as the runtime's WebSocket delivers frames, and the
SSE handler forwards without waiting for the next tick.

Measured: the pushed frame arrived ~2.2s before the polled copy of the same
text.

Design notes worth keeping:
- A global (OnceLock), not an AppState field. The publisher is reached through
  phase_runner -> topology_worker -> MissionTap, none of which hold AppState;
  threading a handle through all of them would put a UI concern into four
  layers that have no other reason to know about one.
- Lossy by design. A slow subscriber lags and skips rather than applying
  backpressure to the agent producing. mission_events remains the durable
  record; this bus is the fast path, never the source of truth.
- Only `claw_<uuid>` aliases are attributed. The governor, door and evaluator
  drive real turns under other names, and attributing their output to an agent
  would put words in someone's mouth. Asserted in a test.
- The poll no longer emits `reasoning`: with both paths live, every turn
  arrived TWICE — once pushed, once polled ~2s later. The row is still written;
  this feed just is not its second mouth.

CEILING, measured rather than assumed: turns are not token-level because the
runtime is not streaming. zeroclaw's claude_cli provider runs
`claude -p --output-format json`, which returns ONE result object when the turn
completes — there are no incremental tokens to forward. Making this genuinely
token-by-token needs `--output-format stream-json` and incremental parsing in
the zeroclaw fork, not here. The bus is in place and will carry them the day it
does.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-15 16:29:12 -07:00
Omar SobhandClaude Opus 5 ba9d7aa185 feat(telemetry): WORKING ON NOW shows the mission an agent is on
deploy / test (push) Successful in 4m10s
deploy / build (push) Successful in 5m13s
The last of the three declared-but-never-emitted event types.
`agent.task.update` had no producer anywhere in the backend, so the card read
"idle — no active task" for an agent that was mid-turn.

Derived rather than newly instrumented: an agent is working on its crew's
RUNNING mission, and that mission's phases are the steps (completed/skipped →
done, running/evaluating → active, else pending). Nothing is emitted for an
agent with no running mission, so "idle" stays truthful rather than freezing on
a stale last-known task.

Verified on a live mission: 116 agent.task.update events observed on
/api/world/live, carrying the mission title and phase steps, with the state
advancing pending → active as the phase started.

That closes the set. Of the seven cards in the command centre, five were dark:
three had no emitter at all and two read a table the mission path never wrote.
DOORS and LOOPS were correctly wired the whole time and were reporting an honest
zero.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-15 16:03:42 -07:00
Omar SobhandClaude Opus 5 bf40d10064 feat(telemetry): the reasoning stream actually streams
deploy / test (push) Successful in 4m18s
deploy / build (push) Successful in 5m17s
`agent.reasoning.delta` and `agent.tool.call` have been declared in the taxonomy
and listened for by the command centre since it shipped — and NOTHING ever
emitted them. The world feed emitted five types; neither was among them, so
REASONING STREAM could not populate no matter what an agent did.

The feed is a database poll, not a push bus, so a live card can only show what
was persisted. The worker already holds each step's output text and the claw
that produced it, so it records a `reasoning` mission_event (truncated — the
card renders a tail, not a transcript, and mission_events is capped per phase),
and the feed emits it forward from a cursor that starts at the current max so a
page load streams rather than replaying history.

`tool.call` is emitted from the same place. On the container tier it will stay
empty, and that is correct rather than broken: those agents are tool-free behind
the §15 door. Tool lines appear where agents actually hold tools.

Verified on a live mission: agent.reasoning.delta observed on /api/world/live
carrying the agent's own text, keyed by agentId.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-15 15:58:56 -07:00
Omar SobhandClaude Opus 5 ccbc387f4b fix(agents): stop listing soft-deleted agents
deploy / test (push) Successful in 3m54s
deploy / build (push) Successful in 5m20s
Deleting an agent looked like a no-op: it disappeared from the workforce but
stayed on the Team board, and deleting it again did nothing because the row was
already marked. Two queries selected from `agents` without `deleted_at IS NULL`:

  routes/team.rs   the leaderboard — the surface still showing them
  routes/world.rs  the "working" set — a deleted agent holding a stale
                   agent_containers row rendered as live

Observed on this deployment: /api/workforce correctly returned nothing while
/api/team/leaderboard returned two agents soft-deleted back in June.

NOT changed: those rows still exist. Making delete permanent means hard_purge,
which also deletes usage_events — billing history, 6 credits on one of these
two. Discarding that as a side effect of tidying a roster is an explicit
decision, not something a display fix should smuggle in.

.sqlx regenerated: team.rs uses the compile-time-checked query! macro, so the
cached entry no longer matched.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-14 12:20:00 -07:00
Omar SobhandClaude Opus 5 f8438c32ea feat(viz): kind-specific choreography and a finished mission you can read
ci / gates (push) Failing after 6s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped
Security: the pawns already orbited their destination, so homing them at
the security station gave circling for free. This adds the radial
press-and-retreat — an agent closing on the target and backing off reads
as probing it, where a fixed radius reads as waiting — and holds the
stochastic target release while probing, or the circling breaks up into
stray trips that look like distraction rather than a scan.

Findings are `mission_tasks` rows, one orb each, popped once. There is
deliberately no severity anywhere in the path: the scanner keeps
severity, file and line as substrings inside `title`, so a severity
parsed out of prose and rendered as an orb's RADIUS would be the picture
asserting a measurement the data never contained. Count only.

Benchmarks annotate the station, as text. `delta` has no schema —
compute_delta emits `{kind:"opaque"}` whenever the before/after metrics
were not structurally comparable, which is most drivers. The server
formats the shape it can parse and COUNTS the rest; an unparseable driver
reports "3 sample(s)" rather than an invented improvement, and an opaque
delta says nothing at all.

The finished map: the live label rule gates service/event nodes on
`heat > 0.12`, which is exactly backwards once everything has cooled — a
static map would be unlabelled dots. Frozen, the 25 most-touched nodes
label regardless of heat, phase stations carry a second line counting
what they produced, and the camera is released ONCE so it frames the
result even if the user panned during the run.

Every count in a caption is read off the drawn scene rather than a
parallel tally, so the words and the picture cannot disagree.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-11 09:22:16 -07:00
Omar SobhandClaude Opus 5 9e61e3ba35 feat(viz): what the agents actually did, as structured events
The World could draw a mission's shape but nothing about the work. The
detail existed only as prose in checkpoint.log and model output, where a
tool name is indistinguishable from an agent *talking about* a tool — so
it was never parsed, deliberately. `mission_events` is the structured
channel that replaces it.

Three taps, one table:

- Container tier: the `_ => {}` at the end of topology_exec's typed frame
  stream now matches `tool_call` and reads the tool's JSON ARGUMENTS for a
  path. Never the prose summary — a path scraped from a sentence would put
  files on the map that no agent opened, and the test proves a Grep whose
  summary says "src/main.rs" produces no file touch. The frame name itself
  is unverified, so the same commit ships an unmatched-frame-type
  histogram: a tap that matches nothing looks exactly like a mission that
  used no tools, and this is how one gw-04 run names the real frame.

- microVM tier: a `PostToolUse` hook, the seam vm_stop_gate already proved
  fires under `claude -p`. It copies stdin to /root/tap and exits 0
  unconditionally — a non-zero PostToolUse hook talks back to the model,
  which would turn the observer into a participant. Drained before collect,
  since the VM is destroyed moments later.

- Phase transitions: five identical copies of the pending→running UPDATE
  became one `mark_phase_running`, and `close_finished_phases` grew
  RETURNING. Its CASE decides each phase's status inside SQL from rows the
  statement does not change, so it cannot be re-derived afterwards without
  writing that CASE twice — without RETURNING it emits zero phase.completed
  and reports success.

The settings.json hazard the plan called out: the stop gate wrote the
WHOLE document, so a second hook writer would have silently erased it and
a coding phase would then complete having written nothing — the exact
failure the gate exists to catch. There is now one composer,
`vm_tool_tap::guest_settings`, one writer, and a source-walk test that
fails if anything else writes a settings document.

`mission_events.run_id` carries no FK on purpose: phase_runner DELETEs
topology_runs on retry, and a cascade would erase a phase's whole history
the moment it retried — silently, since a cascade is not an error.

world.rs streams it with a cursor that separates backfill from motion.
Everything already in the table when a subscriber arrives is drawn as
settled history; only what lands afterwards animates. Otherwise opening a
finished mission replays an hour of tool calls as a burst storm.

Bounded twice: 400 events per phase (enforced inside the INSERT, since
two concurrent taps would each read a count below the cap) and a 7-day
retention sweep in mission_gc.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-11 09:16:50 -07:00
Omar SobhandClaude Opus 5 c2fa8067e1 feat(viz): the coding station fractures into the files it worked on
The engine already turned `file:src/lib/a.ts` into a real dir chain, but
both copies of that loop rooted it at the origin — so a mission's files
floated beside the map instead of belonging to the work that produced
them. One `fileParent` helper now serves both call sites; splitting them
was how half the files could end up nesting correctly and half not,
decided by whichever code path saw the file first.

Files hang under the coding station when there is exactly one, else the
single running phase, else the origin. `world.touch` carries no phase id,
so with two coding phases any attribution is invented — the fallback is
the honest answer.

Two ordering hazards, both silent:
- the server emitted files BEFORE phases, so on the first pass a file
  arrived with no station to hang under and first-write-wins pinned its
  tree at the origin. Loops reordered, with a source-walk guard.
- `setFileHome` re-parents trees rooted before the plan landed, for the
  reconnect case the ordering alone cannot cover.

`mission.file` with `source: "tool"` is treated as motion (burst, pawn
beams); `"diff"` is end-of-phase truth and only marks the file present
and warm — bursting every file of a captured diff would set the whole
map alight at once on reconnect.

Directories taper in radius and opacity by path depth, so `src` and
`src/lib/live` no longer render as identical dots.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-11 08:29:55 -07:00
Omar SobhandClaude Opus 5 006432c2dc feat(viz): the mission becomes a map — centre, stations, and agents at theirs
The clump was structural, not cosmetic. Four causes, each fixed here.

`homes` (an agent's resting node) was written only by `seed()`, so every agent
homed to the mission centre and orbited the same dot regardless of which phase
it was on. `setHome` points each agent at its CURRENT phase, and the existing
physics does the rest for free: the pawn rests at its station, the pawn→home
line tethers it there, and a touch becomes a visible departure and return. No
new motion code.

The mission node was seeded as `level: "team"` (my own bug from the focus
work). `seed()` casts that straight to a Tier and `ensureNode` is
first-write-wins, so it was created as a small teal team dot that the later
`node.activity` could never upgrade. That dot at the centre of the scene was
one line.

`mission`/`phase` replace the retired `repo`/`loop` tiers rather than adding a
parallel set. The backend stopped emitting repo:/loop: ids, which left their
whole landmark treatment — bigger radius, distinct colour, always-labelled, 60s
fade instead of 22s — orphaned on prefixes nothing sends. Missions and phases
need exactly that treatment. The two separately-written prefix→tier ternaries
in onTouch and onNodeActivity are now one `tierFor`: they agreed only by luck,
and whichever path saw a node first fixed its tier forever.

Phase stations spring out at 190 rather than the shared 64, or they pack into a
rosette around the centre and the point — agents moving BETWEEN stations — is
invisible. Idle roam is off under a mission scope: wandering to a random node
keeps an idle workspace alive, but inside one mission it sends agents to files
nobody opened, which reads as work and isn't.

Palette is injected at construction and keyed on template_kind, so a benchmark
run and a security sweep no longer render identically to a research mission.
It also collapses two uncoordinated kind→colour maps that had drifted:
LEVEL_COLOR by tier, and the fireColor if-chain by id prefix.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-10 18:17:12 -07:00
Omar SobhandClaude Opus 5 5f85dbb718 fix(world): missions were never really on the wire
Three bugs in one query, each hiding the next, plus one that made the whole
rich layer dead code.

`active_missions` joined `team_members` on `missions.team_id` — the LEGACY
pointer at the first minted team, superseded by the `mission_teams` junction in
0056. It was an INNER JOIN, and `mission_orchestrator::on_launch` deliberately
mints no team for a microVM mission, so the platform's primary execution tier
was dropped by a join and the World has been showing nothing at all for it. And
it selected only `status='running'`, while missions finish in minutes, so the
scene was empty almost always.

Now: join `mission_teams`, LEFT so teamless missions survive (their `agent_id`
is NULL and no pawn beams at them, which is the truth — nothing on this
platform ran that phase except a VM), and include missions finished in the last
24h carrying `status`/`template_kind` so the client can draw a finished map
instead of animating a corpse. `?mission=` scopes the feed server-side.

The whole phase plan now ships as `mission.phase`, including phases that have
not started: a phase list that appeared only as phases began made a five-phase
mission look like a one-phase mission until it was nearly over. Attribution
reuses `phase_runner::purposes_for` rather than copying it — two copies would
let the picture disagree with the machine about who is working on what, which
presents as a rendering bug and is really a lie.

Deleted the checkpoint tail. It read `topology_runs` keyed by an `agent_runs`
id; mission phases live in `topology_runs` under independently generated ids,
so it ran every poll and matched nothing, for every mission, forever. That is
why no mission has ever shown tool or file activity. Not repointed at
`topology_runs`: its only per-step content is the agent's own prose, and a tool
name in prose cannot be told from an agent talking about a tool. The a2a
`run_events` tail is kept — it genuinely works for the path that writes it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-10 17:58:38 -07:00
Omar Sobh 0d8db7ff0b fix: close the three remaining gaps, and repair a test I silently disabled
FIRST, the self-inflicted one. My edit in a20702d inserted a test between an
existing `#[test]` and the function it belonged to. The result compiled and
looked fine: `every_anthropic_spelling_is_one_family` lost its attribute and
STOPPED BEING A TEST, its doc comment ended up describing my test instead, and
my test carried two `#[test]`s. It has not run since — in already-deployed code.
Nothing failed, which is the point: a test that does not run is indistinguishable
from one that passes. Found via a compiler warning I had not read.

The commit message on a20702d said "241 lib tests pass". 240 ran.

Then the three gaps.

1. A security scan could not read history. `ensure_checkout` clones with
   `--filter=blob:none` — full commits, blobs on demand — and the agent
   environment has NO network route to the forge. Measured: gitleaks on a
   4-commit repo reported "1 commits scanned" and "could not fetch <sha> from
   promisor remote". A credential committed and later deleted is exactly what a
   scanner looks for and exactly what a lazy blob withholds. Missions with a
   `security_scan` phase now clone fully; everything else keeps the cheap path.

   (I first blamed `--depth 1`, from a stale module doc comment. The code has
   said `--filter=blob:none` since it was written, and the comment at `clone`
   explains why NOT shallow — a shallow clone cannot push a branch back. Both
   the comment and my claim are fixed.)

2. `benchmark_runner` never ran as part of a benchmark phase. It was reachable
   only from an operator button, so the `author_and_baseline` recipe authored
   benchmarks and measured nothing — `benchmark_snapshots` stayed empty. Now
   baselined from the sweep, SPAWNED not awaited: BENCH_TIMEOUT is 30 minutes
   and that loop also starts, closes, evaluates and captures every phase on the
   platform. A `NOT EXISTS` guard on iteration 0 makes per-tick firing safe. A
   repo with no bench harness logs and does NOT fail the phase — but it logs,
   because "no baseline" must not read like "not attempted".

3. The World's rich layer was empty for every mission. `run_events::append` is
   called only from the a2a path, and `world.rs` tailed only that table —
   while mission per-step detail has always lived in
   `topology_runs.checkpoint.records`, which `topology::run_events_sse` streams.
   The data was never missing; the viz read the one source missions never write.
   Now both are tailed, mapped through the existing `step_started` vocabulary so
   no new event types are needed.

242 lib tests, 20 test binaries, zero warnings.
2026-08-07 16:09:49 -07:00
Omar Sobh d8c8793c4a ci fixes: cargo fmt, eslint entities, max-lines split
ci / gates (push) Successful in 8s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 4m25s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m46s
CI on 6ffbe97 failed on two auto-fixable gates. Both fixed:

  * cargo fmt --all — rustfmt applied across the surface touched
    by the last ~20 commits (world.rs, security_scan.rs,
    routes/{missions,nodes,terminal}.rs, fleet_herdr.rs,
    mission_workspace.rs, benchmark_runner.rs, mission_refiner.rs,
    lib.rs, tests/mission_orchestrator.rs, cm-db/repo/{missions,teams}.rs,
    bins/clawmates-node/src/main.rs)
  * eslint apostrophe escapes in HerdrSessions + MissionWizard
  * eslint max-lines: extracted EditMissionModal + RefineDiffModal
    (each ~200 LoC) into their own files. MissionCanvas drops from
    1424 to 1026, comfortably under both the 1250 eslint cap and the
    1500 CI budget.

New files:
  frontend/src/components/dashboard/EditMissionModal.tsx  (211 LoC)
  frontend/src/components/dashboard/RefineDiffModal.tsx   (208 LoC)

Verified locally: cargo fmt --check clean, cargo check clean,
mission_orchestrator test 3/3 pass, tsc + eslint --quiet both silent.
2026-07-20 12:03:43 -07:00
Omar Sobh a2bc06d313 validation sweep: close residual TODO + strip 'future' stubs
- mission_orchestrator: replace hardcoded cli=\"claude\" with real
  precedence chain: mission.config.cli → template.config.default_cli
  → \"claude\". Missions can now A/B by CLI without a schema change.
- fleet.rs: scope clippy::too_many_arguments allow on
  NodeHub::open_pty — 8 params (session address x4 + target address
  x3) is the actual dimensionality; a struct would be ceremony.
- security_scan.rs: delete future_artifact_root — pure hint-stub,
  no callers, nothing depends on it.
- routes/world.rs: delete the empty normalize() seam + its 30-line
  comment block. normalize_run_event higher up does the real work;
  the empty stub was pre-cleanup scaffolding.

Post-sweep validation across the whole workspace:
  * cargo check --workspace           → clean
  * cargo test --workspace --no-run   → all bins build
  * cargo test -p cm-api --test mission_orchestrator → 3/3 pass
  * cargo clippy -p cm-api -p clawmates-node --tests → 0 warnings
  * tsc --noEmit                      → 0 errors

Wiring audit: every recent route handler is registered in lib.rs.
Every recent frontend component has at least one importer.

Remaining #[allow(dead_code)] entries are deprovision_claw +
unpublish_claw on RuntimeProvisioner — real rollback paths waiting
on the team-delete route. Documented future hooks, not stubs.
2026-07-20 11:41:59 -07:00
Omar Sobh 9bac84c645 world view: restore in-flight landmarks as mission orbs
Slice 9 cleanup removed active_research_topics + active_loops and the
repo:{topic_id} / loop:{id} project orbs they emitted. The World SSE
loop was left with only agents + active runs — no persistent pin for
"this is what the team is working on right now."

Replaces those with a mission-era equivalent: one mission:{id} orb per
running mission, plus world.touch beams from every assigned team
member. Missions outlive individual runs, so the orb persists even
when no run is claimed — matches the UX intent of the legacy
landmarks without the retired research/loops plumbing.

Query: missions ⋈ team_members where m.status='running', grouped by
mission_id for the orb + fanned out per member for the touches.

Closes task #21.
2026-07-19 18:47:03 -07:00
Omar Sobh fdb8cfeecc slice 9 cleanup: drop legacy research/loops backend + tables
Retires the legacy research/loops backend after the missions arc
(slices 1-9) fully replaced it. Frontend cutover was 4663348; this
commit finishes the job on the backend + database.

Migration:
  - 0053_drop_legacy_research_loops.sql — drops the 8 legacy tables
    (research_topics, research_topic_agents, research_outcomes,
    research_publish_approvals, loops, loop_agents, loop_orgs,
    loop_teams) and the 3 topology_runs FK columns
    (research_topic_id, loop_id, iteration). parent_run_id stays;
    recursive_exec still uses it.

Files deleted (11):
  - crates/cm-api/src/routes/{research,loops,research_setup,
    research_pipeline,wizard_repo,probe}.rs
  - crates/cm-api/src/research_container.rs
  - crates/cm-db/src/repo/{research_topics,research_outcomes,
    research_publish_approvals,loops}.rs
  - crates/cm-runtime/src/loops.rs
  - crates/cm-api/tests/research_publish_role.rs

Files edited:
  - crates/cm-api/src/lib.rs — dropped 20 legacy route registrations
    (all /api/research/* + /api/loops/* + /webhooks/loops + probe)
    and module decls
  - crates/cm-api/src/topology_worker.rs — deleted legacy dispatch
    (freeze_research_outcome, advance_loop_after_completion,
    continue_initial_burst, maybe_transition_research_topic,
    parse_reorder_rationale, per-topic/loop gateway resolver).
    reap_stuck_runs now keys on mission_id (not topic_id).
    Executor path unconditionally uses ZeroClawDriveExecutor::from_env
    — mission_orchestrator provisions each claw as an agent inside
    the shared runtime via RuntimeProvisioner, so per-team gateway
    resolution is no longer applicable.
  - crates/cm-api/src/routes/topology.rs — deleted container-log SSE
    endpoint (research/loop-specific), dropped loop_id filter and
    iteration field from ListRunsQuery/RunSummary
  - crates/cm-api/src/routes/world.rs — removed
    active_research_topics/active_loops/preseed_repo_paths;
    World SSE no longer emits repo:{topic}/loop:{id} landmark orbs
    (follow-up task #21 tracks adding mission:{id} equivalents)
  - crates/cm-api/src/runtime_provision.rs — removed now-unused
    mint_workspace_service_token
  - crates/cm-db/src/repo/topology_runs.rs — removed 9 legacy
    helpers (research_topic_id lookup, loop_id_for_run,
    iteration_for_run, active_runs_for_research_topic, etc.)
  - crates/cm-db/src/repo/teams.rs — removed 4 dead helpers
    (team_for_loop, team_for_research_topic + setters)
  - crates/cm-api/tests/topology_jobs.rs — removed loop/topic
    tests, dropped enqueue_run_with_topic helper
  - crates/bins/clawmates-server/src/main.rs — removed
    spawn_loop_scheduler call
  - crates/cm-api/src/routes/mod.rs, crates/cm-db/src/repo/mod.rs,
    crates/cm-runtime/src/lib.rs — module decls stripped

sqlx cache: regenerated against post-migration schema
  (71 files changed, ~+70 / -8896 net)

Test/build: SQLX_OFFLINE=true cargo check --workspace clean;
cargo test --workspace --no-run clean.

Follow-up (task #21): World view lost the in-flight-work landmarks
when repo:{topic} / loop:{id} orbs disappeared. Add mission:{id}
orbs as the missions-era replacement.
2026-07-19 18:37:24 -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 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 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 ebb5b5780b chore: cargo fmt --all — clean up a2a merge's fmt violations
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 49s
ci / frontend (push) Successful in 52s
ci / publish (push) Successful in 3m6s
ci / e2e (push) Has been skipped
The a2a merge (d0d8e7f) landed with a handful of pre-existing rustfmt
diffs that were failing `cargo fmt --all --check` in CI. Pure whitespace
reformatting from `cargo fmt --all`; no semantic changes. Files touched:
mcp_door.rs, quota.rs, routes/a2a.rs, routes/world.rs, runtime_provision.rs,
chat_repos.rs test, and tools/chat.rs.
2026-07-05 18:54:15 -07:00
Omar SobhandClaude Opus 4.8 85b0e1ac33 feat(observe): surface delegation + A2A in the live world feed via audit poll
Edge-initiated inter-agent events (gated delegation, A2A ingress) bypass the
run loop, so the world SSE now polls the append-only audit log (cursor on the
BIGINT id, seeded to max on first pass) and emits:
  - delegation.invoked -> agent.delegate {fromAgentId,toAgentId,toName,task}
  - a2a.invoked        -> a2a.invoked   {agentId}
TeamObserver renders agent.delegate as an A->B handoff in the team timeline;
adds the agent.delegate taxonomy type. Reliable live (the synthesized-run path
never streamed — active_runs + first-sight cursor jump skip it).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-29 05:55:49 -07:00
Omar SobhandClaude Opus 4.8 cbfa0ff24f feat: agent-to-agent platform on ZeroClaw 0.8.2 — rooms, delegation, A2A ingress
Builds on the v0.8.2 runtime. Four workstreams, all behind the §15 MCP door:

- Group rooms (Phase 1): migration 0026; N-way threads repo with a DM/room
  count-guard; chat.send {room} + room.create/invite/leave tools; RoomMessage
  -> room.message SSE; /api/claw-chat/rooms* APIs; Observer room badge.
- Per-claw door identity: door caller_agent resolves the X-ZeroClaw-Agent
  header (set by the fork) to the specific claw, falling back to roster[0].
- Gated delegation bridge (Phase 3): clawmates__delegate door tool drives a
  sibling via the existing /ws/chat ZeroClawDriveExecutor (not A2A); self-deny,
  per-workspace hourly budget, audit trail, untrusted-banner result. Native
  in-daemon delegation stays off (it would bypass the door).
- A2A tenant ingress (Phase 2): migration 0027 (workspace_a2a + a2a_tokens);
  runtime_provision enable_a2a_server/publish_claw; routes/a2a.rs tenant-aware
  proxy (per-workspace tokens, injected internal bearer, daemon stays internal,
  cards URL-rewritten to the cm-api edge); a2a.invoked taxonomy.

Tests: cm-db room repos, cm-runtime chat tools, door units. sqlx cache updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-28 16:11:01 -07:00
Omar SobhandClaude Opus 4.8 66c4a80985 Agents: read-only agent-to-agent observer in the chat card + live agent.message
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 12s
ci / frontend (push) Successful in 26s
ci / e2e (push) Has been skipped
A new "Observe" button (Eye) sits next to "+ New" in the agent chat header; clicking
flips the card into a read-only observer of that agent's conversations with other
agents, updating live as messages happen.

- frontend: AgentObserver (history from /api/claw-chat/* + live agent.message overlay
  filtered to the agent, read-only banner, no composer); ClawChatSection toggle + flip.
- backend: emit a live agent.message run-event when chat.send succeeds — events.rs
  AgentMessage variant, chat.send returns to_id, runtime emits in both tool paths,
  world.rs normalizes agent_message → agent.message SSE. No migration, no new table.

Roadmap (not built): group/multi-party rooms; A2A protocol (a2a-rs) adoption.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-27 09:57:29 -07:00
Omar SobhandClaude Opus 4.8 3554a3aaf2 CI: remove k8s stages, fix the Docker-level pipeline green
ci / frontend (push) Successful in 23s
ci / rust (push) Failing after 27s
ci / e2e (push) Has been skipped
ci / gates (push) Successful in 5s
Survey + fixes so the pipeline passes at the Docker level (no k8s).

- Remove k8s: drop the `sandbox-k8s` job (kind/Calico/--features k8s-tests) and the
  "Helm chart lints" gate step. release.yml was already k8s-clean.
- Rust job:
  - `cargo fmt --all` — fix pre-existing formatting drift (fmt --check was failing).
  - clippy -D warnings: fix 3 lib warnings (cm-brain sort_by_key→Reverse, cm-api
    fleet.rs doc list indentation, node_rules map_or→is_none_or).
  - Regenerate the .sqlx offline cache (was missing the cm-runtime run_loop test
    query → offline compile failed). DB-backed tests use testcontainers at runtime.
  - Set SQLX_OFFLINE=true on the rust + e2e jobs so query! macros compile against
    the committed cache deterministically (no DB needed at compile time).
- Frontend job:
  - Fix the 1 ESLint error (useAgentTelemetry: no setState-synchronously-in-effect;
    tag the slice with agentId + derive null on mismatch).
  - Fix 2 stale panel-params tests (`terminal` is a valid app id now; assert the
    current APP_IDS + use a genuinely-unknown id for the reject case).

Verified locally: fmt clean, clippy --all-targets -D warnings clean (offline),
frontend lint 0 errors, tsc clean, 86/86 frontend tests pass, build OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 18:15:31 -07:00
Omar SobhandClaude Opus 4.8 11a1f22daa Agents page: reorganize into the Agent Command Center (per-agent live metrics)
ci / gates (push) Failing after 13s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Replace the single centered "anatomy profile" + resizable chat split with an
operator command center: compact 62px identity strip → 5-tile per-agent metrics
band → three independently-scrolling LIVE · BRAIN · SURFACE columns. Chat moves to
the computer's Chat app; the right computer pullout (DevicePanel) is untouched.

- backend: cm-api/routes/world.rs emits per-agent `telemetry{agentId,tokensPerMin,
  costPerHr,loops,doorsPending}` in the SSE loop, from 4 batched GROUP BY queries
  (usage_events tokens/min + credits/hr, active routines, pending approvals) — all
  real, no migration. taxonomy `telemetry` gains optional agentId; stateKey now
  keys it per-agent so slices don't clobber.
- frontend: new ClawCommandCenter + anatomy-cards (shared cards extracted from
  Dashboard); useAgentTelemetry(agentId) feeds the metric band (Doors amber>0/
  green=0); LIVE column streams the agent's task.update / reasoning.delta /
  tool.call (replaces the mocked VitalsCard heatmap with a live activity chart).
- Dashboard: left region → full-height ClawCommandCenter; chat launcher opens the
  computer Chat app; removed the dead anatomy cluster + unused imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 11:34:12 -07:00
Omar SobhandClaude Opus 4.8 3b012b12bf World: explosive file sparks + 3 real views (Flat 2D / Live Gource / Brain)
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
- Explosive sparks on file/project I/O: /api/world/live tags file-ish tools
  (read/write/drive/vault/obsidian/edit/save) with touch weight 1; the engine
  carries it as a node "burst" and the renderer fires a big fast particle blast.
- The formation tabs are now genuinely different views:
  - Flat — the previous 2D React-Flow forest (WorldFlow), overlaid.
  - Live — the WebGL Gource clone (glow sprites + sparks + trails + beams).
  - Hierarchy — a NEW neural-brain view: agents are neurons in a dormant
    two-hemisphere mesh; active agents fire signal particles along pathway edges
    to their nearest neighbours (chaining), so the brain lights up as agents work.
- Render groups (gource/brain) + 3D particle system so signals travel in depth;
  shared particle update; OrbitControls (rotate + pinch-zoom) across all WebGL views.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 22:49:23 -07:00
Omar SobhandClaude Opus 4.8 380b95da2a World: normalize real run_events into the live taxonomy (the data unlock)
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
/api/world/live now tails each active run's journal (run_events) and normalizes
the runner's existing events into taxonomy events — no runner change needed:
- text_delta       -> agent.reasoning.delta (the live reasoning stream)
- step_started     -> agent.tool.call + node.activity + world.touch on the tool
                      node (agents visibly converge on the tool they're using)
- approval_required -> door.request
Per-run seq cursor streams forward only (skips backlog on first sight). This
lights up REAL data for both the World view and the upcoming Observe surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 21:52:51 -07:00
Omar SobhandClaude Opus 4.8 53279e6339 World: Phase C — replay scrubber (Gource-style playback)
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
- /api/world/replay?hours= — reconstructs a sorted, timestamped taxonomy timeline
  from the workspace's run history (agent_runs ⋈ sessions) for client playback.
- engine.clearWorldNodes() — drop transient world nodes/targets for a loop restart.
- WorldCanvas: a WorldClock control (Live ⇄ Replay) feeding the same engine —
  play/pause, 1x/2x/4x speed, seek bar, loop. Live subscriptions detach in replay.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 21:39:03 -07:00
Omar SobhandClaude Opus 4.8 05c54de47d World: real-data convergence — pawns roam the live tree; runs drive world.touch
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
- engine: when there are no service/event touch-nodes (a quiet feed), pawns roam
  the real org/company/team structure so the view is always alive with zero
  synthetic data; real touch-nodes take priority when present.
- /api/world/live: emit world.touch + node.activity per currently-running run
  (agent_runs joined to sessions, state='running') so each working agent visibly
  converges on its active-run node; fold running agents into working status;
  telemetry.loops = active run count.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 21:29:06 -07:00
Omar SobhandClaude Opus 4.8 39bacbd1d2 World page: live WebGL Gource-style visualization (Phases A+B)
Replaces the static React-Flow world forest with a real-time WebGL view of the
swarm working, plus the live event contract that feeds it.

Phase A — the live contract + feed:
- frontend/src/lib/live/taxonomy.ts — the 13-event Clawmates Event Taxonomy as
  typed TS (the shared World/Observe contract).
- frontend/src/lib/live/useClawmatesLive.ts — native shared-singleton live client
  (EventSource to /api/world/live, typed fan-out, replay-of-last-state to late
  subscribers, refcounted, synthetic fallback so views are always alive).
- crates/cm-api/src/routes/world.rs — GET /api/world/live, authed + workspace-
  scoped SSE emitting the taxonomy (real agent.status from live-container state,
  a topology.update of the workspace's agents, telemetry with real doorsPending),
  mirroring run_events_sse. normalize() seam documented for run_events->world.touch.

Phase B — the WebGL engine:
- frontend/src/components/world/engine.ts — Gource-inspired force-directed model:
  org/company/team tree (sibling repulsion + parent spring + friction), agent
  pawns that converge on the touched node and beam it, world nodes that glow with
  heat and fade when idle. Framework-agnostic (renderer-independent) state+math.
- frontend/src/components/world/WorldCanvas.tsx — three.js scene (ortho cam,
  UnrealBloom), render loop syncing engine state, camera auto-fit, HTML labels,
  raycast click->select, Hierarchy/Flat/Live formation switch.
- Dashboard.tsx: swap <WorldFlow/> -> <WorldCanvas/> at the world-tier seam
  (shared claw-tier pieces untouched; WorldFlow.tsx kept for now).
- Adds three (+ @types/three).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 21:23:34 -07:00