Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Changelog

All notable changes to prospero are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. While the project is pre-1.0, the minor version is bumped for new features and the patch version for fixes.

Unreleased

Fixed

  • The published guide no longer ships dead links. An ADR is written to live in docs/adr/, where README.md and ../superpowers/specs/<spec>.md both resolve; copied into the book they did not, because README.md and template.md are deliberately not chapters and nothing ingests docs/superpowers/. mdBook rewrote both to pages it never built, so three links in ADR 0001 and the Source: link of seven ADRs (0002–0007 and 0010) served 404s. sync-adrs.sh now rewrites links that leave the book to absolute repository URLs, resolving each target from the ADR's original location rather than its copied one, and fails the build if a target does not exist — a link rewritten to a missing file is still a dead link. (#268)

Internal

  • The guide's links are checked after every docs build, by docs/guide/check-links.sh. mdBook validates no links whatsoever: it rewrote every one of the dead links above and emitted zero warnings, so "the guide builds clean" was never evidence that it was navigable. The check resolves relative hrefs against the built output and absolute links back into this repository against the working tree — the latter because the rewrite above converts checked relative links into unchecked absolute ones, and a check that skipped them would bless exactly the mistake the rewrite can make. Third-party URLs are not fetched, so the docs build does not depend on anyone else's uptime. (#268)

  • The docs site is rebuilt when CHANGELOG.md changes. sync-changelog.sh ingests it into the guide, but docs.yml was not triggered by it, so a changelog-only commit rebuilt nothing and the published changelog silently fell behind the repository — entries appeared only when some later commit happened to touch a path that did trigger a build. The same guard that covers ci.yml's path filter now also fails the build when a path a sync-*.sh script reads is missing from either of docs.yml's triggers, since the next ingest would otherwise repeat this. (#270)

  • CI no longer skips the cargo gate on a docs change that is compiled into a test. ci.yml skips every cargo step when a diff touches only docs/, *.md or LICENSE, but crates/api/tests/guide.rs include_str!s docs/guide/src/api.md — so an edit to the guide's routes table was the single change most likely to fail the route-coverage test added in #256, and the one change CI would not run it for. The workflow now declares those paths in TEST_INPUT_DOCS and treats them as code; a genuinely docs-only change still skips the gate. The declaration is held to the source by crates/api/tests/ci_filter.rs, which fails the build both when a new doc test input is missing from the list and when a stale entry no longer matches, so the filter cannot quietly go out of date. (#264)

  • The Rust toolchain is pinned in rust-toolchain.toml (1.99.0), so a new stable release can no longer break a build whose code never changed — which is what happened to v0.9.0. CI still installs its toolchain with dtolnay/rust-toolchain@stable; rustup honours this file when cargo runs, so the version lives in exactly one place and upgrading it is a reviewable commit. The file also lists the wasm32-unknown-unknown target, because the targets: input in ci.yml applies to the toolchain the action installs rather than the one this file selects. (#261)

  • caliban-contract pinned to =0.15.0, matching caliban 0.15.0 and moving in lockstep with caliban-operator so the two downstream consumers of the contract do not disagree about its version (caliban-ai/caliban-operator#95). Nothing prospero compiles against changed: launch.rs is the only file that differs between the two contract versions, and prospero imports only from caliban_contract::wire. Note for anyone else downstream — the field CalibandLaunch.router_config kept its type but changed meaning, from inline config JSON to a filesystem path (caliban ADR 0060), so a caller passing JSON still compiles and silently has it read as a path. prospero never sets it.

0.9.0 - 2026-10-03

This release is about letting other programs drive and watch the fleet. One SSE connection now carries every agent's events with a resumable cursor, /mcp serves the fleet to agentic tools, and GET /api/openapi.json describes the whole REST surface so a client can generate its types instead of hand-writing them from prose. Spawns no longer have to be manual: an automation fires one on a cron schedule or a signed webhook. In Kubernetes, a per-session permission posture makes in-cluster agents usable at last, and when the operator refuses a task prospero reports the reason instead of leaving an unexplained dead agent.

**Companion versions.** The permission posture needs caliban v0.14.0 (for
`SpawnSpec.permission_posture`) and caliban-operator v0.6.0 (which admits or
refuses it and supplies the refusal reason). Automations need an event and
config store shared by every replica, so they are enabled for the local backend
and, under Kubernetes, only when `--database-url` is set.

Added

  • Fleet-wide event stream. GET /api/fleet/stream carries every stream's events on one connection, so watching a fleet no longer needs an SSE connection per agent — and a client can finally learn about an agent it has not seen yet. Each event's SSE id: is a fleet cursor: the store's durable insertion order, the only total order a fleet has (per-agent seq cannot order two agents, since both start at 1). ?from=<cursor> resumes exclusively after it, ?from=now skips history, and an absent from replays from the start. Every event is read from the durable store rather than the in-process bus, so delivery is exactly-once across replicas and a slow client is never skipped past. Two new Store methods (replay_fleet, latest_fleet_cursor) are held to identical semantics across JSONL, SQLite and Postgres by a shared conformance battery. Requires read. The dashboard's 5-second poll is gone, replaced by the stream plus a 30-second reconcile so a dropped EventSource cannot freeze the screen. (#219) (#247)

  • The fleet over MCP. /mcp serves the fleet over the Model Context Protocol (streamable HTTP, rmcp — the same crate caliban drives its own adapter with, so both speak an identical protocol version to a client). Nine tools: list workspaces and agents, spawn, status, events, send and end input, kill, respawn. It is a thin adapter over the same FleetProvider seam the REST handlers already use, so both the local and Kubernetes backends work through it and a tool cannot do what the HTTP API cannot. Two choices follow from the consumer being a model rather than a person: history is capped (100 default, 500 max) and returns truncated + next_from, and a bad agent id is a tool error naming the id rather than a protocol error, so the model can correct itself instead of the session failing. Requires operate; workspace administration is deliberately not exposed. (#218) (#249)

  • Automations — scheduled and webhook-triggered spawns. An automation is a stored spawn template plus what fires it: a 5-field UTC cron schedule, or a signed inbound webhook whose payload fills the template's {{ dotted.path }} placeholders. Every firing is recorded, successful or not, in a bounded per-automation history. Exactly-once across replicas is a compare-and-set on the claimed tick rather than the lifecycle lease: a lease makes a double fire unlikely but cannot rule it out, because ownership can change hands between checking and spawning. The webhook signing key is returned once at creation and lives only on core's own stored type, so no response, listing or dashboard view can carry it; signatures are verified in constant time over the raw request bytes, and an absent or malformed header is a rejection. (#220) (#250)

  • An OpenAPI 3.1 document at GET /api/openapi.json. Clients — ariel first — had to hand-write every request and response type from the prose guide. The document is open like the guide it mirrors: it describes the API's shape, carries no fleet data, and a client needs it before it has a credential. Schemas come from the same prospero-types structs the handlers serialize, via schemars, behind an off-by-default openapi feature so the wasm dashboard — which shares those types — does not carry a schema generator it never calls. Requests and responses are generated under their own contracts, because a type is described differently when it is read than when it is written: a serde default makes a field optional to a reader and says nothing to a writer, so one schema per type would misdescribe one direction. Each operation records its required scope in x-prospero-scope. The spec cannot quietly fall behind the router — a test reads lib.rs for the paths and verbs it registers, so a new route, a new verb, or a documented route that is not served all fail the build. (#222) (#253)

  • Per-session permission posture. In-cluster agents were unusable: no human is attached to a pod, so caliban's non-interactive gate denied every Bash/Write/Edit and the refusal reached nobody. permission_posture (supervised | unattended, defaulting to supervised everywhere, fail-closed) is now carried on the spawn body, the CLI (--permission-posture) and the CalibanTask. unattended requires an admin scope — a per-field rule the route scope table cannot express — and each grant is audited with the token name. Under Kubernetes what prospero sends caliband is the posture the operator admitted (status.permissionPosture), not the one requested, so an operator predating the field cannot be used to bypass the Workspace's agentPolicy. Every agent reports the posture it runs under, and unattended agents carry a bypass tag in the dashboard. Also wires spec.isolation.worktrees through to SpawnSpec.isolation_worktree. (#238, #226) (#240)

  • Per-spawn wall-clock limit. timeout_secs on a spawn (prospero spawn --timeout <SECONDS>) caps an agent's wall-clock life. The deadline is a log entry, not a timer — a timer dies with the replica that set it — so prospero writes DeadlineSet to the shared event log, and a replica that never spawned the agent reads it back the first time it polls. Enforcement runs under the lifecycle lease so one replica acts on an expiry rather than all of them racing to kill the same agent, and an AgentTimedOut event is emitted before the kill, so history says why the agent stopped instead of leaving a bare killed that reads like an operator did it. /api/usage counts it as timed_out alongside killed, and the dashboard gains the facet. Deliberately no AgentStatus::TimedOut: that enum is matched exhaustively by clients and mirrors caliban's own lifecycle vocabulary, which has no such state, so the distinct outcome is the event. (#221) (#248)

  • Attribution to a person, not just a token. FleetEvent.actor is the token name, which is right for attributing a request to a credential and useless for a client acting on behalf of other people — ariel holds one operate token and serves a whole Discord, so every agent it spawned read actor: "ariel" whoever asked for it. A request may now carry X-Prospero-On-Behalf-Of, and the events it emits record it as on_behalf_of beside the unchanged actor. A header rather than a spawn-body field, so it covers kill and respawn too and an older daemon ignores it for free. The value is the client's assertion, not a fact prosperod checked: the token remains the authenticated identity and is always recorded, so a false claim stays attributable to the credential that made it, and the audit line names it asserted_on_behalf_of. Validated rather than sanitized, and bounded — it reaches the event log and the structured logs, where a control character could forge what reads as a separate record — and a malformed value is a 400 rather than a silent drop. Both SQL stores gain the column. (#251) (#252)

  • prospero usage. GET /api/usage was rendered only by the dashboard. The CLI now prints the same report as a table — one row per workspace, a total when there is more than one, the window stated up front — or, with --json, the server's own report.

    prospero usage [--since <7d|2w|YYYY-MM-DD|RFC-3339>] [--workspace <name>] [--json]
    

    7d / 2w are resolved on the server's clock, as the dashboard does, so a drifted client clock cannot clip the window; a date is its UTC midnight and a timestamp is converted to UTC. An hour-level window such as 24h is refused with a reason, because usage is bucketed per UTC day and an hour would promise a precision the report lacks. --workspace filters client-side on the raw JSON, so --json keeps fields a newer server adds and both output modes show the same rows. (#223) (#254)

Fixed

  • GET /api/usage validates and normalizes its window. Three bugs, each reproduced by a test that failed first. A large days panicked the handler — ?days=200000000 was enough, chrono overflowing on the subtraction rather than inside Duration::days as the ticket guessed. since and until were never parsed, so ?since=yesterday returned 200 and a report for whatever window that string happens to sort between, because the store compares timestamps lexically on a TEXT column. And for that same reason the spelling of a correct bound mattered: events are stamped …00:00:00.5+00:00, and a Z bound sorts after the . of a fractional second, so an event half a second into the window fell out of it — the handler already normalized its own defaults but never client-supplied bounds. Both bounds are now parsed (400 bad_request on failure) and re-rendered, and days is checked against a shared cap. (#255) (#257)

  • One Drained agent no longer drops every agent on its caliband. wire.rs hand-mirrored caliband's control-plane types, and the mirror had already drifted into a live bug: caliban's AgentStatus gained Drained that the mirror never learned, and with no catch-all a single drained agent failed deserialization of the entire Listed reply, dropping every agent on that caliband from the poll. Those types are now re-exported from caliban's published caliban-contract crate (pinned =0.14.0) — serde-only, so not the wide coupling ADR-0003 rejected, and exactly what its own "revisit if" clause asked for once the mirror became a drift source. Drained maps to Done: terminal, not a failure. (#239) (#245)

  • The operator's failure reason reaches the agent. caliban-operator v0.6.0 refuses an unattended task whose Workspace does not allow it — phase Failed, Ready=False, reason PostureNotPermitted — and creates no pod, so there is no sandbox, no caliband and no agent-side error anywhere: the operator's condition is the only account of what happened. prospero mapped the phase to failed and dropped the rest, leaving an unexplained dead agent and someone waiting for a sandbox that was never coming. Agent gains optional reason and detail, omitted when unset, filled from the operator's Ready condition and only when it is not True. Local agents never carry them, since caliband reports their state directly with no admission step above it. (#241) (#246)

  • Tool results show in the dashboard inspector. caliban has carried each tool call's result on ToolCallEnd since caliban#391, but prospero's normalizer read only tool_use_id and is_error, so the inspector still said the result "needs a caliban-side change". EventKind::ToolFinished gains result and truncated, both defaulted and skipped when empty, so events stored before this change still deserialize. The normalizer takes caliban's bounded display preview and applies its own 8,192-character cap, matching caliban's, so one call cannot put an unbounded body into the event stores or SSE. (#236) (#237)

Documentation

  • ADR-0011 — ACP is a second drive protocol, not a second lifecycle (status: proposed). Records an AgentDriver seam for an agent's data path with caliband keeping every lifecycle, the protocol chosen per spawn and defaulting to NDJSON, and the decision not to make prosperod a process supervisor. Amends ADR-0003. (#217) (#244)

  • Automations documented, across both the API and CLI guides: the create body and template defaults, cron semantics, webhook signing with a worked openssl example, placeholder rules, and run history. The guide's routes table now carries the same build-breaking guard as the OpenAPI document — a test checks it against the paths and verbs lib.rs registers — so a route can no longer ship undocumented. (#256) (#258)

Internal

  • async-trait bumped to 0.1.92. Up to 0.1.89 the macro put #[must_use] on the method it desugars an async fn into, and since that method returns a future — already #[must_use] — clippy 1.99 reports double_must_use on macro-generated code, failing the lint gate for all seven #[async_trait] traits in prospero-core. Nothing in the repo changed to cause it: CI resolves stable at run time, so the build broke when stable moved from 1.97 to 1.99. The upstream bump is the fix rather than an allow.

0.8.1 - 2026-09-16

Fixed

  • The AgentsSettled condition is written in a real cluster. v0.8.0's condition carried lastTransitionTime, which the CalibanTask CRD's condition schema doesn't declare, so server-side apply rejected every write with a 500 ("field not declared in schema"). No condition was ever written, a finished task never reached Completed, and prosperod logged the failure on every poll. prospero now sends only type, status and reason, matching caliban-operator's Condition, and a test pins the apply body to those fields. (#234)

0.8.0 - 2026-09-16

prosperod now authenticates its API, and in Kubernetes it reports agent lifecycle back to the operator. Named API tokens with one of three scopes guard every non-probe route; the dashboard signs in with a token and holds a signed session cookie; and the CLI carries a token. Separately, prosperod writes an AgentsSettled condition to each CalibanTask's status, which caliban-operator v0.5.0 turns into a terminal Completed or Failed phase, so a finished task finally leaves Running.

**Breaking for deployments:** with no tokens configured, prosperod now refuses a
non-loopback `--addr` (such as `0.0.0.0` in a container) unless
`--insecure-no-auth` / `PROSPERO_INSECURE_NO_AUTH=1` is set. Configure
`--api-tokens-file` / `PROSPERO_API_TOKENS_FILE` before upgrading, or opt out
explicitly. See the "Securing the API" guide page.

Added

  • Breaking: API authentication and authorization (ADR-0010). Named tokens (pspo_…) with scope read, operate or admin, stored as SHA-256 hashes in a tokens file; a route-scope table that fails closed to admin; JSON 401/403; and one prospero_audit log line per mutation. With tokens configured, every request needs one, loopback included. /healthz, /readyz, the dashboard shell and /api/session stay open. The dashboard signs in with a token for a 12-hour HMAC-signed HttpOnly; SameSite=Strict cookie bound to the token's hash, and cookie-authenticated mutations must be same-origin. Clustered prosperod with tokens needs --session-key-file so replicas share cookie signing. The token name is recorded as actor on local-fleet spawn and remove events and persisted in every store (nullable column migration). CLI: --token / PROSPERO_TOKEN, --token-file / PROSPERO_TOKEN_FILE, prospero token new <name> --scope <scope> and prospero whoami. (#2) (#227)

  • AgentsSettled condition on CalibanTask status. After each poll in which a pod's caliband answers, prosperod server-side applies one condition under field manager prospero: True/Succeeded when every agent finished, True/Failed if any ended failed or crashed, otherwise False/AgentsActive (an idle interactive task stays unsettled). The apply carries only that condition, never forces, skips tasks whose caliband didn't answer, and runs under the observer lease so clustered replicas don't race. Needs RBAC patch on calibantasks/status (caliban-ai/helm-charts prospero chart 0.2.9); without it prosperod logs a warning per poll and carries on. (#228) (#232)

  • View model thinking in the dashboard. With PROSPERO_INCLUDE_THINKING set, reasoning content is kept through normalization and shown as a collapsible "Thinking" segment on the agent timeline, separate from output. Off by default. (#212) (#216)

Fixed

  • A re-spawned k8s agent that reuses a task name is attached again. A fast delete and recreate of a same-named CalibanTask left the predecessor's terminal mark in place, so prospero never attached to the healthy new agent until prosperod restarted. The poll loop now tracks each CR's uid and clears the mark when a known name returns with a new uid. (#213) (#214)

Documentation

  • User guide pages, and the README and container docs brought current. (#229)
  • Competitor evaluation: re-verified the OpenClaw parity matrix and added OpenHands, Coder, GitHub Agent HQ and Vibe Kanban. (#215)

0.7.0 - 2026-09-13

Follows caliban's removal of its bespoke ollama provider (caliban ADR 0056). prospero fills the caliban worker's SpawnSpec.provider from a repo's stored config, so a repo pinned to ollama now spawns a worker that fails at startup — this release migrates prospero off Ollama and onto the OpenAI-compatible path.

Changed

  • Ollama removed; local inference via openai + base_url (#207): the dropdown, defaults, fixtures, and the dedicated ollama env-projection arm are gone. validate_provider_env now treats a provider with an overridden base_url as keyless — mirroring caliban's keyless local-endpoint change — so a migrated local config (openai + base_url, no key) keeps the keyless UX that ollama had, instead of being rejected for a missing OPENAI_API_KEY. (#208)
  • Reworded the remaining rationale comments off the removed provider's name; the history lives in caliban ADR 0056 (#209). (#210)
A repo or workspace still configured with `provider: "ollama"` will spawn a
caliban worker that fails loudly at startup. Migrate it to `openai` with a
`base_url` pointing at a local `/v1` endpoint.

0.6.0 - 2026-09-09

Finishes what v0.5.0 started. That release made the Rust → WASM dashboard the default and left the old vanilla-JS page at /v1 as a landing spot in case v2 regressed. It didn't, so both the page and the version suffix that existed to distinguish the two are gone: there is one dashboard, served at /, and its files live under /assets/.

The rest is the k8s fleet and its test coverage getting more honest. A pod agent that dies at spawn is a stable state, and prospero now treats it as one instead of re-dialing it every two seconds for as long as the process lives — a loop measured running for twenty-four days against a single stuck task. Two long-open gaps in coverage close alongside it: cross-stream delivery on the clustered bus, which had been #[ignore]d as too flaky to gate on, and streaming under fan-out, which had no coverage at all.

Removed

  • Breaking: the v1 dashboard. The hand-written vanilla-JS page that served / before v0.5.0, deprecated to /v1 in #191, is deleted along with its embedded assets and routes. /v1 and /v1/app.js now 404 — deliberately no redirect and no fallback, so a dead surface does not linger in bookmarks and access logs. Removing it also drops ~200KB of embedded assets from every binary and the known defects v2 was built to fix, most visibly #106's permanently-"running" tool calls. (#197)

  • Breaking: the /v2 alias, and the version suffix generally. With only one dashboard left there is nothing to disambiguate, so the document is served at / and its files move from /v2/... to /assets/.... A prefix rather than the bare root, so the bundle cannot shadow the API namespace. GET /v2 now 404s. Internally the same rename: crates/api/dashboard-v2/ is crates/api/dashboard/, the handler module is dashboard, and the CI job is "dashboard wasm build". (#204)

Added

  • DistributedBus::subscribe_all_ready — an unfiltered subscription whose LISTEN is established before it returns, rather than lazily on the stream's first poll. The lazy form cannot tell a caller when it is safe to publish, and a doorbell rung in that gap is simply not heard. Connection and LISTEN failures now surface as an error instead of silently ending the stream. (#132)

  • Load and soak coverage for streaming under pressure. Forty concurrent attach tasks must each stream to completion carrying their own output and monotonic sequence numbers, so a crossed or silently-unattached stream fails loudly rather than going unnoticed. The in-process bus gains bounded-buffer coverage: a slow subscriber is told exactly how much it lost and keeps working, a subscriber that keeps up never lags, sixty-four subscribers lag independently rather than sharing a cursor, and dropping one mid-pressure leaves the rest untouched. Asserted on observable state rather than elapsed time, so they run in the gate instead of becoming the next ignored test. (#8)

Fixed

  • A terminal pod agent is resolved once, not on every poll. Rejecting a terminal record (#168) removed the reconnect budget that had been incidentally throttling the retry, so the ~2s watch loop re-resolved the same dead agent forever — one warning per poll, indefinitely, for a state that cannot resolve on its own. Observed running for 24 days against a single stuck task. The session plane now remembers terminal agent ids, skips them, and logs the cause once per transition. An explicit stop or restart, or the CR going away, clears the mark so a respawn is re-checked. (#170)

  • Cross-stream subscribe_all delivery is covered in CI again. The test was #[ignore]d because the lazy subscription gave it no way to know when it was safe to ring the doorbell, so it re-rang up to 440 times over 44s and still starved to zero deliveries on a contended runner. It now establishes LISTEN first and asserts on delivery rather than on patience — about a second, including under the coverage instrumentation the flake was worst under. (#132)

Security

  • anyhow bumped to 1.0.104, clearing RUSTSEC-2026-0190 (unsoundness in Error::downcast_mut()). Lockfile only; nothing here calls downcast_mut, but a release should not carry a known-unsound dependency. (#205)

0.5.0 - 2026-08-22

Replaces the dashboard. The hand-written vanilla-JS page is superseded by a Rust → WASM single-page app that shares the server's own types, and it now serves /; the old page remains at /v1, deprecated. Everything an operator previously needed v1 for — launching, killing, replying to an interactive agent, editing a workspace's config, watching a live stream — is in v2, along with things v1 never had: a per-agent timeline with a tool-call inspector, and fleet-wide spend, turns, and outcome charts over a selectable window.

The other half of this release is the k8s fleet telling the truth about what it ran. Terminal outcomes were structurally invisible under PROSPERO_FLEET=k8s: the watch loop derived agent status from the CalibanTask phase, and the operator never advances that past Running, so the new outcome charts read a permanent zero — including for runs that had demonstrably failed. Outcomes are now taken from the pod, recorded once per agent, and survive a restart.

Upgrade note. GET / now serves dashboard v2 rather than v1, and GET /app.js is gone — v1's script moved to /v1/app.js along with the page. /v2 remains a permanent alias for the new dashboard, so existing links to it keep working.

Added

  • Dashboard v2 — a Rust → WASM single-page app, and now the dashboard. A Dioxus SPA in crates/dashboard, compiled to wasm32-unknown-unknown and embedded in prosperod, replacing the hand-written vanilla-JS page. The read model and the control-plane DTOs are the same Rust types the server uses, so the client cannot drift from the API the way a hand-maintained JS copy did. The crate sits outside the cargo workspace on purpose (a wasm-only crate would break the host-target build and sink the coverage floor) and has its own CI job; its built bundle is committed and include_bytes!'d, so an ordinary cargo build still needs no wasm toolchain and one binary still ships the UI (#97).
  • Full operator control from the dashboard. Launch, kill, remove, respawn, interactive input, end-input, and workspace removal — every action gated on /api/capabilities, so the UI never offers an operation the active backend would answer with a 405 (#173).
  • Workspace registration and configuration from the dashboard, with two form shapes chosen at runtime: the local single-provider/env form, and the k8s Workspace-CR editor with sources, named providers, and Secret references, plus the reconciliation-status pill and a provider picker on launch. This also fixed a lossy read: GET /api/workspaces was projecting the CR's source spec down and dropping the git remote and ref, so editing a k8s workspace meant retyping every remote from memory (#175).
  • Live agent stream viewer — replay history from the store, then tail over SSE. A reconnect resumes at last_seq + 1 and anything at or below the high-water mark is dropped, which is the defence against the v1 reconnect storm that duplicated the timeline unboundedly (#105); a close after AgentFinished reads as "finished" rather than an error, so a healthy completed run does not sit there retrying (#178).
  • Per-agent timeline with a tool-call inspector. Tool calls pair start with finish into a collapsible entry, consecutive output coalesces, and the opening context and final accounting become a header and a summary. Pairing is on the tool id and never the name — caliban's ToolCallEnd carries the id but leaves the name empty, which is what left every tool stuck "running" in v1 (#106) (#179).
  • GET /api/usage — cost, turns, and terminal outcomes aggregated per workspace per UTC day over a window (since/until, or days). Computed by the store rather than by replaying the log, with identical semantics across the JSONL, SQLite, and Postgres backends, held there by a shared conformance suite (#180).
  • Fleet overview charts for spend, turns, and outcomes over a selectable 24h / 7d / 30d window. Outcomes are faceted rather than stacked: measured against the real tokens, no ordering of a four-way stack is separable in light mode, so one single-hue chart per outcome is the fix rather than a mitigation. Hand-rolled SVG with native <title> tooltips — the page is served under default-src 'none', so there is no script and nothing for the CSP to refuse (#181).
  • An explicit theme setting (System / Light / Dark), persisted in localStorage, replacing "whatever prefers-color-scheme says". The explicit choice wins in both directions, and the theme is applied before first paint so there is no flash of the wrong one (#183).

Changed

  • The control-plane DTOs moved to prospero-types and now carry both Serialize and Deserialize. They previously lived in prospero-api, which pulls axum and tokio and so compiles for no wasm target, and each carried only the server's half of the contract — so a WASM client would have had to hand-duplicate all eight, reintroducing exactly the drift Rust/WASM was chosen to avoid. They are re-exported from their original paths and the serde output is unchanged, so this is additive for existing consumers (#172).
  • Dashboard v2 is now the default surface. GET / serves the Dioxus/WASM dashboard; /v2 stays mounted as a permanent alias, since the bundle's own asset URLs are absolute /v2/... and existing links point there. The scaffold deliberately parked v2 at /v2 so / stayed untouched while epic #95 landed — that transition is complete (#191).

Deprecated

  • The v1 dashboard has moved to /v1 and is deprecated. It renders a notice pointing at /, and its script now lives at /v1/app.js (GET /app.js is gone). It is kept rather than removed so an operator hitting a v2 regression has somewhere to land, but it receives no further work and carries defects v2 was built to fix — most visibly #106, where a tool call whose finish frame has a blank name stays "running" forever. Removal is a follow-up once v2 has a release of real-world use (#191).

Fixed

  • Restarting prosperod no longer re-counts outcomes it already recorded. A fresh process starts with an empty view of the fleet and re-derives the terminal transition it had already written before the restart, so every outcome facet doubled on each restart — measured on a live cluster as 3 done / 3 failed becoming 6 / 6 with nothing having run. The watch loop now checks the durable log (which outlives the process) before recording an agent's outcome, so an agent that finished once is counted once (#196).

  • Terminal outcomes are now derived from the pod, not the CalibanTask phase. #190 made the watch loop persist the transitions it observed, but it observed status.phase, and the operator never advances that past Running — CRs whose agents finished a day earlier still read Running, so no terminal transition ever occurred and the outcome facets stayed at zero on a real cluster. The loop now applies the same pod-caliband overlay snapshot() has always used, so the component that emits events and the one that renders them finally agree. Pod dials are bounded and concurrent, and an agent is consulted only until it is observed terminal (#194).

  • An unreachable pod can no longer stall fleet observation. The status overlay dialled each pod sequentially with no deadline. A pod that black-holes its SYN (rather than refusing it) blocked until the OS connect timeout, which was survivable when only GET /api/fleet did this and is not now that the watch loop depends on it. Dials are concurrent and bounded; a miss retries on the next poll (#194).

  • Terminal outcomes are now recorded under PROSPERO_FLEET=k8s. The usage aggregate counts done/failed/killed/crashed from persisted status_changed events. The local arm has always emitted them, but the k8s watch loop computed the identical transition diff and only broadcast it in-memory — so on k8s every outcome facet in the dashboard read zero, including for agents that had demonstrably failed. The loop now persists the transition it observes, elected by a single-writer observer lease so replicas don't multiply the counts (#190).

  • The dashboard's usage panel refreshes on its own. It fetched once per window selection and never again, so a finished agent's spend could sit invisible until the operator toggled the window by hand. It now refetches when the fleet poll observes an agent appear, change status, or disappear, and on a one-minute heartbeat — without re-running a 30-day store aggregate on every five-second poll (#190).

  • A k8s workspace's provider base URL survives an edit. The v2 editor had no base-URL input and ProviderInfo did not carry the field, so reopening the editor showed a blank box and saving wrote it back — a routine model edit silently unpicked a self-hosted provider, after which agents died instantly with a ProviderError against localhost (#188).

  • k8s-spawned agents get the resolved provider and model. spawn_spec_from_task sent provider/model as None on the premise that pod env would drive selection; the caliban worker selects from SpawnSpec and nothing else, so every k8s agent fell back to caliban's default and died at preflight with ANTHROPIC_API_KEY is not set. The operator already pins the resolved provider into status.resolvedWorkspace, so its kind and model are projected onto the spawn. ensure_pod_agent also no longer attaches to an agent already in a terminal state — caliband keeps a failed agent in its registry with the endpoint it advertised, but the worker died before binding that port, turning a one-shot failure into a silent reconnect loop (#169).

  • Re-submitting an identical prompt no longer claims a launch that didn't happen. Spawning is idempotent and the k8s CalibanTask name is derived from the spec, so an identical prompt resolves to the run already in flight. POST /api/workspaces/{name}/agents now reports created, and the dashboard says it attached to the existing run instead of "Launched" (#190).

0.4.0 - 2026-07-27

Makes the PROSPERO_FLEET=k8s fleet interactive. Since 0.3.0 a k8s agent could be launched from the dashboard but never talked to: the reply box renders only for an agent that is both interactive and idle, and under the k8s backend neither could ever be true. Closing that took a field on the authoritative CRD (caliban-operator#28) plus both halves of the round-trip here, and turned up a second defect — 0.3.3's agent-id decoupling had quietly broken reply delivery and the 0.3.2 status overlay. Local behavior is unchanged.

Deploy note: requires the caliban-crds chart at >= 0.2.1. Older CRDs have no spec.task.interactive, so the API server prunes the field at admission and the flag never reaches the pod.

Added

  • Interactive agents under PROSPERO_FLEET=k8s. The dashboard's interactive: true now survives the CR round-trip: build_calibantask writes spec.task.interactive (the field caliban-operator#28 added to the authoritative CRD) and spawn_spec_from_task reads it back into the SpawnSpec sent to the pod's caliband. Previously the flag was silently dropped at the CR boundary and the spawn spec hardcoded interactive: false, so a k8s agent could never await input and the dashboard reply box (interactive && idle) could never appear. Requires the caliban-crds chart at >= 0.2.1, or the API server prunes the field at admission (#163).

Fixed

  • Operator replies and the interactive/idle overlay now target caliband's own agent id. #159 decoupled the agent id caliband assigns from prospero's CR name, but send_input still attached by CR name (404, reply lost) and overlay_pod_status still looked records up by id (silent miss, regressing #130's reply box). send_input now resolves the pod's agent via ensure_pod_agent, and the overlay matches each pod's record by endpoint rather than by id. The pre-existing tests used one string for both ids, so they passed while the real path was broken; the new tests use distinct ids (#163).

0.3.3 - 2026-07-19

Completes the PROSPERO_FLEET=k8s control plane so a spawned agent actually runs: the previous two fixes let the caliband pod bind and decoupled spawn from reconcile, but nothing ever started the agent inside the pod. Local behavior is unchanged.

Fixed

  • k8s agents now actually start (and stream). Under the k8s backend, spawning created the CalibanTask CR and a caliband pod but never started the LLM run — caliband is a passive supervisor that begins an agent only on CtlRequest::Spawn, and prospero's session plane only attached, so the attach looped forever on agent not found. prospero now spawns the agent in the pod's caliband (list-or-spawn, one agent per pod) from the CalibanTask prompt, then attaches — idempotent across poll cycles and replicas (ownership-lease-gated). Because caliband assigns the agent id itself, the attach id is decoupled from prospero's stream key (the CR name) so output still streams to the dashboard under the identity /stream expects (#159) (#160).

0.3.2 - 2026-07-18

Two fixes to the PROSPERO_FLEET=k8s control plane found in live-cluster use: spawning an agent no longer blocks the dashboard on the operator's reconcile, and an agent's interactive reply box now appears under the k8s backend. Local behavior is unchanged.

Fixed

  • Spawning an agent no longer hangs the dashboard on reconcile. Under the k8s backend, K8sFleet::ensure_agent applied the CalibanTask CR and then synchronously polled (up to ~30s) for status.phase == "Running" before returning, coupling the HTTP response to the full CR → operator reconcile → pod schedule → Running chain (and blocking the entire budget when the pod never started). It now returns as soon as the CR is admitted; the background watch loop surfaces the agent and attaches its session when it reaches Running — the synchronous poll was redundant with that path (#157).
  • The interactive reply box now appears for k8s agents. The dashboard shows it only when an agent is interactive and idle, and under PROSPERO_FLEET=k8s neither was sourced correctly — the status/interactive fields are now read from the pod's caliband rather than the CalibanTask CR alone (#130) (#156).

0.3.1 - 2026-07-14

Bug-fix follow-up to the 0.3.0 Kubernetes config plane, closing the four issues surfaced in k8s smoke testing on a fresh 0.3.0 deploy. The PROSPERO_FLEET=k8s fleet now stays Ready through a schema-skewed custom resource, surfaces the registered Workspace CRs it manages (instead of a synthetic phantom), and rejects an unregisterable workspace up front. Local behavior is unchanged.

Fixed

  • A single un-deserializable CalibanTask no longer wedges the whole fleet. K8sFleet's watch/readiness path listed CalibanTasks strictly, so one CR that failed to deserialize (e.g. a stale task predating the now-required workspaceRef field) failed the entire poll — the fleet never populated, /readyz stuck at 503, and the pod never became Ready. The list is now decoded per-item, skipping and logging the bad CRs (#148) (#152).
  • The k8s fleet snapshot reconciles with the Workspace registry. GET /api/fleet synthesized a single phantom k8s workspace and never read the registered Workspace CRs, so a registered workspace was invisible in the dashboard while the synthetic k8s entry reported workspace not registered. The snapshot now surfaces the registered Workspace CRs (agents grouped by the workspace they reference), so /api/fleet and /api/workspaces agree and a fresh deploy shows no phantom (#149, #151) (#153).
  • Add-workspace rejects an invalid workspace as 400, not a raw apiserver 422. The dashboard's + add workspace posted a Workspace with empty providers/sources, which the CRD (minItems: 1 on both) rejected — so a workspace could never be registered from the dashboard. The config plane now validates at least one well-formed source and provider before apply (add and edit paths), and the form validates the same client-side (#150) (#154).

0.3.0 - 2026-07-12

The Kubernetes config plane: deploying with PROSPERO_FLEET=k8s is now a real control plane — create and configure workspaces, and launch provider-bound agents, from the dashboard — instead of a read-only viewer that returned 405 Method Not Allowed on Save. Workspaces are first-class Workspace custom resources reconciled by caliban-operator, and the dashboard is backend-aware. Local behavior is unchanged.

Added

  • Kubernetes config plane (core + API). Under PROSPERO_FLEET=k8s, K8sFleet now wires a FleetAdmin over operator-owned Workspace custom resources, so POST / PUT / DELETE on /api/workspaces persist and manage real configuration — multi-source workspaces, a named-provider list, and per-provider credentials referenced by Kubernetes Secret name (prospero never reads the Secret) — instead of returning 405. A backend-neutral WorkspaceConfig DTO lets one API serve both backends (local projects its single-provider subset, unchanged); GET /api/workspaces returns the real Workspace CRs with reconciliation status; async workspace writes answer 202 Accepted; and a spawned agent binds a named provider via providerRef (#142) (#144, #145).
  • Backend-aware dashboard. The dashboard fetches GET /api/capabilities and adapts. On k8s it renders a workspace editor (git sources + a named-provider list with secretName / key Secret references and a default marker), reconciliation status pills (pending / reconciling / ready / failed with the failure message on hover), and a launch-modal provider picker; on local it is byte-for-byte unchanged (#143) (#146).
  • GET /api/capabilities — a backend capability seam the dashboard gates its controls on (#99) (#101).
  • Frontmatter / agent-template support through spawn — a spawn can forward an agent-template markdown file to caliband's SpawnSpec.frontmatter_path (#6) (#102).
  • Guiding Principles & Invariants guide page synthesizing ADRs 0002–0009 (#74) (#104).

Changed

  • The CalibanTask CRD mirror moved from an inline workspace to a workspaceRef (plus an operator-pinned status.resolvedWorkspace), matching caliban-operator's frozen v1alpha1 contract. Pre-v1; existing cluster CRs are recreated under the new schema.

0.2.0 - 2026-07-11

Kubernetes high-availability, a reworked dashboard, and a full QA sweep. A second QA pass over the real prospero/caliband stack filed 23 findings; all are fixed here, alongside first-class leader election for the k8s fleet backend and a new agent-timeline dashboard.

Added

  • Leader election + attach lifecycle for the K8sFleet backend. The session-plane attach — the one path that writes an agent's events to the shared store/bus — is now gated on a per-agent ownership lease, so with 2+ prosperod replicas exactly one replica owns, attaches to, and emits each agent (no more duplicate SSE events or racing per-stream seq allocation). Standalone is unchanged (SelfOwnsAll); a clustered deploy builds a LeasedOwnership lease plus heartbeat. Attach tasks are now promptly torn down on stop/remove/restart, and any agent observed Running — including operator- or peer-created ones — is streamed by the lease owner (#108, #112, #113) (#138).
  • Dashboard agent timeline, tool-call inspector, and run header — a folded event timeline with expandable tool-call segments and a per-run turns/outcome header (#5) (#96).
  • prospero-types crate — the normalized FleetEvent/model DTOs extracted into a small, wasm-compatible serde-only crate the WASM dashboard can share (#98) (#100).

Changed

  • Under PROSPERO_FLEET=k8s, prosperod no longer builds a local FleetManager/poll loop; the k8s backend serves directly over the shared store/bus (#83) (#92).
  • /readyz now reports workspaces_total/workspaces_healthy/ workspaces_unreachable (was repos_*), and user-facing error wording says "workspace" not "repo", matching the vocabulary used everywhere else (#116, #117) (#135).

Fixed

  • Dashboard. Terminal-agent SSE streams no longer reconnect-storm into an unbounded, duplicated timeline with runaway memory (#105) (#128); tool calls resolve ok/fail instead of showing "running" forever (paired by tool_use_id) (#106) (#131); the fleet summary shows the workspace count, the misleading $0.0000 cost is gone, and a favicon is served (#115, #109, #119) (#134).
  • API. Duplicate workspace registration returns 409 Conflict, not a misleading 503 (#111) (#139); an unknown agent's events endpoint returns 404 instead of 200 [] (#118) (#135); api_key_from_env on a keyless provider is rejected at config-set time, and rm no longer races a just-spawned agent or lags the fleet view (#120, #122, #123) (#137).
  • k8s hardening. The session-plane bearer token is never sent over plaintext (#107) (#133); unrecognized CalibanTask phases map to a terminal state, calibandEndpoint is validated, lock poisoning can't wedge the fleet view, the token compare is constant-time, and --fleet-backend k8s on a non-k8s build fails before any side effects (#114, #121, #125, #126, #127) (#136).
  • Tests. De-flaked the distributed_bus PG suite under parallel shared-DB load (#110) (#129) and cli_drives_the_full_stack (#85) (#94).

0.1.1 - 2026-07-05

Fixed

  • The released image now builds prosperod with --features k8s, so the K8sFleet backend is compiled in and PROSPERO_FLEET=k8s works. Previously the image only ran the local backend, so an in-cluster deploy showed an empty fleet (#90). Unblocks the k8s-fleet-backend support in the prospero Helm chart.

0.1.0 - 2026-07-04

Initial containerized and licensed release of the prospero control plane — the agent orchestration layer that sits above many caliband supervisors — as part of the P0 Kubernetes deployment (epic caliban-ai/caliban#274).

Added

  • ghcr.io/caliban-ai/prospero:0.1.0 — multi-arch (linux/amd64 + linux/arm64), non-root container image running prosperod (REST + SSE + dashboard on 7878); also tagged :latest and :sha-<commit>.
  • Helm chart charts/prospero in caliban-ai/helm-charts, rendering standalone (SQLite + PVC) or clustered (external Postgres, N replicas) from one topology value.

Changed

  • Repository relicensed to AGPL-3.0-only, matching its sibling projects.