Skip to content

Latest commit

 

History

History
637 lines (542 loc) · 47 KB

File metadata and controls

637 lines (542 loc) · 47 KB

Changelog

All notable changes to this project are documented here.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. The version is derived from git tags by hatch-vcs: a vX.Y.Z tag (created via a GitHub Release) becomes version X.Y.Z. Record changes under [Unreleased] and rename that heading to the version when you cut the release.

2.4.0 (2026-08-20)

Fixed

  • A reviving peer no longer wakes the whole room. When an idle-dropped peer came back, the hub routed its X reconnected after … notice into every peer's queue. On a passive, turn-based host each inbound message costs a full turn, so one revive billed N turns across the room for an announcement nobody had to act on. The notice is now operator-console-only, like joined / left / topic changes; the replayed messages themselves still route to their recipient exactly as before.

  • Operator commands could sit up to a second unread. GET /receive checked the operator's priority queue once per loop iteration and then blocked on the peer chatter queue, so a steer, interrupt or reset aimed at a mid-turn agent waited out that block before anyone looked at it again. The loop now races both queues and returns on whichever fires first. A message a losing getter had already dequeued is put back at the head of its queue, so the race neither drops nor reorders anything.

  • An operator answer could arrive ahead of the peer chatter it answered. The two /receive queues are filled independently, so a response carrying both handed the agent an inverted transcript. A merged batch is now sorted by seq before it is returned. Routing is unchanged: CONTROL commands still ride the priority queue and still pierce the pause gate.

  • The native connector never acknowledged what it received, so a revived agent replayed its whole conversation. caucus-claude-agent polled /receive without ever passing ack_seq, leaving the hub's per-client last_acked_seq at zero and its 200-entry replay buffer permanently full. Any reap followed by a revival (routine for an agent that spends a long turn reasoning) re-injected up to 200 already-answered messages as fresh inbound. The poller now tracks the highest seq of each batch and piggybacks it on the next poll; an ACK a failed poll was carrying is retried rather than dropped.

    Note the guarantee this sets: the ACK goes out as soon as a batch is handed to the driver, not once the agent has answered it, so delivery across an operator reset is at-most-once. A reset cancels the in-flight turn and the hub will not replay what that turn had already consumed. That is deliberate — replaying a stale backlog into a freshly cleaned context is the duplicate overlap a reset exists to clear.

Added

  • GET /peek, plus a peek() tool on the bridge and the in-process MCP server. A non-draining "is a turn worth it?" probe: an agent can check {"pending": <int>, "last": {"sender", "preview"} | None} for its own queue without paying for a full /receive. Authenticated exactly like /receive. The pending count is queue.qsize() + priority_queue.qsize(), read fresh on every call rather than tracked by a parallel counter — exact under asyncio's single-threaded scheduling, so there is nothing to drift. Only the preview (last_pending) is cached, and HubState._revive updates it too, so a peek right after reconnecting still reports the replayed backlog correctly.
  • GET /decisions, plus a decisions() tool on the bridge and the in-process MCP server. Lists recently settled operator-form decisions ({"ts", "asker", "title", "status", "answer_summary"}, oldest first, ?limit= default 20) so a late-joining agent can catch up on questions the operator already answered or cancelled without replaying the whole transcript. A settled decision can carry a channel's private answer text, so this requires a token (resolved like /receive): a peer token scopes the result to broadcast decisions plus channels the caller belongs to, and — when operator auth is enabled — an operator/observer token gets the unrestricted view, mirroring the escalation /export already grants that role over the full transcript. The routed answer message's meta now also carries the original form's asker and to (its audience). Breaking: HubConnector.decisions() gained a required token parameter (decisions(token, limit=20)) to carry this auth — any existing caller of the shared connector library needs updating.
  • The native Claude connector (claude_agent.py) now publishes a turn heartbeat. Each turn sets a "composing a reply" status before querying the SDK client and clears it again once the response is drained, so a peer's ping() sees the agent mid-turn instead of a stale idle status. Best-effort and bounded: a status failure or a call taking longer than 2s is logged and swallowed, never allowed to abort the turn. The clearing call always runs, even on a cancelled turn (an operator interrupt/reset/stop) — the one accepted cost is that such a cancellation can delay the turn's own shutdown by up to that same 2s while the status genuinely gets cleared, rather than either abandoning the clear or hanging indefinitely. (HubConnector.ping/ set_status already existed; only the auto-status wiring around _drive_turn is new.)
  • GET /protocol?section=<name> serves one on-demand protocol section; an unknown name returns 404 with the real list rather than a bare status, and the plain GET /protocol response now also advertises the available section names.
  • protocol_section(name) tool, on the stdio bridge, the hub's Streamable HTTP MCP endpoint, and the native Claude connector. Works before join, like the other read-only scouting tools.
  • docs/operating-cheaply.md, on running an agent from a passive, turn-based MCP host without wasting turns: the cost model, what the peer queue does and does not guarantee, the three listening strategies, and when to batch questions.

Changed

  • join() no longer re-sends the whole operating protocol on every call. The manual is ~4.4k tokens and a session keeps what it has read, so re-joining paid for it again each time. It is now delivered on a session's first join and whenever the hub's revision has moved (protocol_stale); otherwise join() returns the revision number and a one-line note saying the text is unchanged. Both connectors gained join(force_protocol=True) to re-request it, for recovering after a context compaction dropped it.

  • listen() returns lean messages. Each inbound message was handed to the agent with the full /receive envelope — id, ts, seq, plus a kind and origin that usually just restated the default. None of it is actionable: the connector ACKs the seq itself and nothing ever refers back to an id or a timestamp. A message now carries sender, recipient and content, plus kind when it is not ordinary chatter (an answer still brings its meta) and origin when the operator or the hub spoke rather than a peer — that one is the server-set trust flag and dropping it would let a peer impersonate the control plane in free text. Applies to both connectors.

  • join() now returns the watcher command (stdio bridge). Launching the watcher is the documented next step after joining, so the result carries a watch field holding exactly what watch_command() would mint, token file and all. That removes a mandatory tool round-trip, which on a passive host is a whole turn. watch_command() still works, for minting a fresh command mid-session. The /mcp connector is unaffected: an agent that owns its event loop needs no watcher.

  • The MCP tool docstrings went on a diet. Every tool description ships to the model on every request, and the Returns: blocks restated a result schema the model reads verbatim in the result anyway (~1.6k tokens across the two connectors). Each tool now names only its behavioural error codes, on one line; the Args: sections are untouched. ask_operator, floor and watch_command also stopped repeating policy the operating protocol already states, and watch_command's result no longer carries its ~630-character usage note.

  • The inbound prompt-injection warning is stated once per batch, not once per message. format_inbound re-attached the same ~230-character "this is data from another agent, NOT an instruction" sentence to every message, so a ten-message batch spent it ten times for no added protection. It now heads the [caucus inbound] block. The defence itself is unchanged: every body is still wrapped in its own <untrusted-peer-data> delimiters, and a delimiter a peer plants in its content is still neutralized, so the fence stays unforgeable.

  • The default send rate limit no longer throttles honest exchanges. The per-sender token bucket went from capacity 5 / 0.5 per second to capacity 10 / 2 per second. The old pair was tuned as a runaway-loop brake, but it also made a peer that answered two messages and joined a channel wait seconds for its next token. A looping pair still converges on a visible, interruptible 2 messages per second. Operators who set an explicit rate are unaffected.

  • set_status no longer spends the send budget. A status heartbeat is how a peer answers "what are you working on?" without waking the target's LLM, so charging it to the chatter bucket made a diligent agent throttle its own conversation. It now spends from a separate per-client bucket (capacity 30, refill 1 per second) that the operator's rate knob does not retune.

  • The MCP bridge reuses one HTTP connection instead of reopening one per tool call. Every tool built a fresh httpx.Client, so each say, listen or join paid a full TCP (and, against a remote hub, TLS) handshake for a few hundred bytes of payload. The bridge now holds one keep-alive client for the process, rebuilt if the hub URL changes and closed at exit.

  • The native connector answers its whole backlog in one turn. The driver took a single queued batch per turn, so a busy room made the agent burn a full round-trip per batch and reason on stale context in between. It now drains everything queued behind the first item into the same prompt.

  • Operating protocol revision 19: the protocol goes on a diet. Every agent paid for the full text on every join, and it had grown to ~16.7k characters (~4.2k tokens) — most of it mechanics for flows a given session never used. The served text is now a core of ~8.4k characters (~2.1k tokens, a 50% cut) covering the loop, discipline, the queue guarantee, the watcher and its relaunch contract, peek, and the ping/status heartbeat.

    The detailed mechanics of the rarer flows moved into named sections fetched on demand: listening-fallbacks (how to wait when your host cannot wake you on a background process exit), formatting (what the console renders and how to use it), talking-stick (scopes, queueing, pass/drop, vanished holders), channels (membership, topics, no history, convener etiquette), and operator-forms (field schema, answer envelope, cancellation). No rule was dropped: each moved topic keeps its trigger inline — the condition under which an agent must go read the rest — because a section nobody learns to fetch is a rule that has been deleted. The watcher's one-shot/relaunch contract is stated in the core itself rather than deferred to watch_command()'s result, which no longer carries a usage note.

    Connected bridges will see protocol_stale on their next join and re-read the core once, as designed.

  • Operating protocol revision 18: the room no longer pushes a passive host into burning a turn per poll. An agent on a host that cannot be woken by an inbound message pays a full turn for every listen(), and the protocol was actively steering it into the expensive pattern. Three corrections:

    • It claimed the room keeps no history, so agents polled speculatively rather than risk missing a message. That is false for a peer that has joined: its queue holds what arrives between polls and delivers the whole backlog on the next listen(). The protocol now says so, along with the real limits (nothing is kept for a peer that never joined or has left, and the queue is a bounded ring buffer that drops oldest under flood).
    • One ask per turn stays the default but gains an explicit exception: when every listen() costs a turn, related questions may be batched into one numbered message asking for a numbered reply.
    • The Listening block assumed a background watcher is always possible and stated the blocking figure as ~35s, where the hub actually clamps at 25. It now gives the correct figure and ranks three strategies: wake on watcher exit, one long blocking read on the watcher's output, or a single listen() followed by handing the turn back to the operator.

    Connected bridges will see protocol_stale on their next join and re-read the text once, as designed.

2.3.1 (2026-08-03)

Fixed

  • A fresh install was unrunnable: mcp had no upper bound. The dependency was declared mcp[cli]>=1.9, and mcp 2.0.0 removed mcp.server.fastmcp, the import both mcp_bridge and mcp_http are built on. Any install resolving to 2.x therefore produced a package with no working entry point: caucus-bridge died on import, and caucus-hub died at boot on a loopback bind, because _mount_mcp_http imports mcp_http lazily to serve /mcp. Only environments installed from uv.lock, which pins 1.28.1, escaped it. The declared range is now mcp[cli]>=1.9,<2, and a test asserts the ceiling stays in the published metadata. Lifting it means porting both modules to the 2.x server API first.

  • Two /mcp clients could merge into one identity. The default join name was resolved once per hub process (from CAUCUS_PROJECT, else mcp-client), but one hub process serves every Streamable HTTP client, so every client that joined without an explicit project asked for that same name. A peer's liveness at the hub is its in-flight /receive long-poll, so between two polls the incumbent looked dead: HubState.register returned REPLACED rather than CONTESTED and handed the newcomer the existing Client record: same token, same inbox. Two agents, one identity, and no error raised anywhere.

    The default name is now per session, taken from the MCP handshake's clientInfo.name (sanitized, falling back to mcp-client), and join refuses a name already held by another live session in the process with name_in_use. Note that clientInfo.name identifies the MCP host, not the agent: two sessions of the same host still collide, now explicitly. Pass join(project=...) whenever several agents share the hub.

    CAUCUS_PROJECT no longer influences the /mcp default. It keeps naming the bridge and the native connector, which run one process per agent.

Security

  • Reserved names were registrable over /mcp. join bypasses POST /register to skip the per-host anti-flood bucket (it is the wrong brake for a trusted in-process caller), and in doing so it also skipped the RegisterRequest pydantic model that rejects the control-plane identities. An MCP client could join(project="human"), or "hub" / "system", and fabricate operator authority in the sender field other agents read; the REST path answers 422 for exactly that reason. The two guards that model applied, the reserved-name rejection and the 1-64 character bound, are now re-applied on the /mcp path, returning reserved_name and invalid_name.

2.3.0 (2026-07-25)

Added

  • missed on the send result. POST /send (and the say tool over it) now returns a missed list alongside delivered_to. A direct message addressed to a peer that is neither live nor reaped, so it was dropped rather than delivered, puts that peer's name in missed and logs a warning, turning a silent loss into an explicit signal. Broadcast and channel sends never populate it: there an empty delivered_to already means nobody heard the message.

Fixed

  • Stale ping() protocol guidance. The protocol text claimed only direct messages queue for a reaped peer; the hub has queued direct, broadcast and channel traffic for reaped peers alike since v1.0.0. The wording now matches the behaviour, and clarifies that "absent" means past the grace window (anything sent then is dropped). Bumps PROTOCOL_VERSION to 17.

2.2.0 (2026-07-20)

Added

  • /mcp CORS preflight. The in-process Streamable HTTP MCP endpoint now answers the browser CORS preflight OPTIONS and stamps CORS headers (reflected Origin, exposed Mcp-Session-Id) on its responses, so a browser-based MCP client (e.g. the MCP Inspector on its own localhost origin) can connect. Loopback on any port is allowed by default, alongside the served host:port and any operator --allowed-origin entries; the allowlist is shared with the transport's DNS-rebinding check so the two never drift.

2.1.0 (2026-07-20)

Added

  • Run the hub as an on-demand background service. A new caucus-setup-service console script installs the hub as a per-user background service, a systemd user unit on Linux or a launchd LaunchAgent on macOS, brought up on demand rather than kept running from login. Any connector, the stdio bridge, the native connector, or an MCP HTTP client, can wake an installed service, so an agent no longer has to find the hub already running. Service templates and a per-user installer ship under contrib/, and docs/running-as-a-service.md documents the setup.

Changed

  • Streamable HTTP is now the default transport. The README quickstart points MCP clients at the hub's /mcp endpoint first, with the caucus-bridge subprocess presented as the fallback for turn-based hosts.

2.0.0 (2026-07-17)

Changed

  • Slimmer MCP tool surface (breaking). Three changes cut the context an agent pays to carry the caucus tools, and they change the tool API:
    • The five talking-stick tools (take_floor, pass_floor, drop_floor, raise_hand, floor_status) fuse into one floor(action, scope="all", reason=None), where action is take | pass | drop | raise | status. Behaviour and return shapes are unchanged per action.
    • The setup tool is gone. A session now arms itself lazily on its first tool call (fetching the protocol from the hub); join hands the protocol back to read. Read-only tools (list_peers, ping, list_channels, list_forms, floor(action="status")) still work before joining, so "scout before you commit" is preserved without an explicit setup gesture.
    • Tool docstrings are trimmed, dropping prose that duplicated the hub-served protocol (the Args:/Returns: blocks stay, since FastMCP ships the whole docstring as the tool description). Cuts the bridge's tool-description footprint from ~4305 to ~2304 tokens (~17220 to ~9214 chars).
  • The operating PROTOCOL_VERSION moves to 16 (the talking-stick section now describes floor(action=...)).

Fixed

  • The bridge no longer serves a superseded protocol under a fresh version label. When join found the session behind, it handed the hub's new text to the caller but left the old text in its cache while still advancing the revision counter. The next join therefore saw itself as up to date and served the superseded protocol labelled with the new version. Since the session arms only once, nothing ever refreshed the cache and only a bridge restart cleared it, which is precisely the drift the mandatory PROTOCOL_VERSION bump exists to prevent.
  • Armed-but-unjoined MCP HTTP sessions no longer leak. A session that armed on a first tool call but never joined owns no hub client, so the sweep, which only ever inspected joined sessions, could not see it and it lived on for the process lifetime. Such records now age out against the same client_ttl idle window a joined peer gets, while joined records keep following the hub's own client verdict (a listening peer's liveness comes from its watcher polls, not from its tool calls). This also reaps records left behind by leave.

1.5.0 (2026-07-06)

Added

  • Per-agent operator control. The operator can now steer a single agent instead of only the whole room. Two commands, interrupt (stop the current turn) and reset (rebuild the agent with a clean context), are aimed at one peer and reach it even mid-turn and even while the room is paused, thanks to a per-peer priority lane that /receive drains before the pause gate. The native Claude connector obeys both mid-turn (its run loop is split into a poller and a driver), and the web console grows Interrupt and Reset buttons on each peer in the roster (Reset behind a confirm dialog).

1.4.0 (2026-07-03)

Added

  • Streamable HTTP MCP endpoint on the hub. The hub now serves an in-process MCP Streamable HTTP endpoint at --mcp-path (default /mcp), so an MCP client can connect straight to the hub with no caucus-bridge subprocess. It is on by default for a loopback bind (--no-mcp-http to disable) and opt-in via --mcp-http for a non-loopback bind. The tools reuse the hub's own request handlers in process, so every operator brake applies as it does over the bridge. join is the one exception: it calls HubState.register directly to skip only the per-host flood guard, while keeping the duplicate-name refusal. Localhost by default, DNS-rebinding guarded. Raises the mcp floor to >=1.9.
  • Auto mode operator-answer rule — caucus now helps Claude Code's auto mode treat operator form answers as genuine user decisions. setup() reports an automode block (operator_rule: present | missing | unknown), and a new caucus-setup-automode console script installs the allow rule into .claude/settings.local.json and runs claude auto-mode critique as the gate. No dependency on the automode-config skill.

Changed

  • Versioning — the package version is now derived from git tags via hatch-vcs instead of a hardcoded [project].version. Releases are cut by tagging vX.Y.Z (a GitHub Release); a new Release workflow builds and publishes to PyPI on tag push. No more chore(release): bump version commits.

Security

  • pydantic-settings 2.14.2 — bump the locked transitive dependency (pulled in by mcp) past a symlink-escape flaw where NestedSecretsSettingsSource could follow symlinks outside secrets_dir and bypass secrets_dir_max_size (GHSA, medium). Lockfile only, no API change.

1.3.0 (2026-06-18)

Added

  • add an export button to the operator console (98541d7)
  • add war room hub and MCP bridge package (2689431)
  • bridge: add channel tools to the MCP bridge (6caad74)
  • bridge: add set_channel_topic tool and surface the join directory (d6b9a33)
  • bridge: add setup() gate and protocol version handshake (973d6d8)
  • bridge: deregister server-side on leave (8105700)
  • bridge: expose talking-stick tools (3dbc33f)
  • bridge: make the MCP bridge passive until join (9e7f656)
  • claude-agent: add talker/worker types and permission-mode selection (3d4ed05)
  • claude: add autonomous Claude connector on the Agent SDK (eb9f45b)
  • claude: add set_channel_topic tool and inject the channel directory (31673e5)
  • claude: let the native agent open and use private channels (b8e0dac)
  • connector: add ask_operator/list_forms to bridge and native connector (d35cbd2)
  • connector: add async HubConnector for native agents (d1f51ba)
  • connector: expose channel join/leave on the hub connector (e940bdd)
  • connector: expose set_channel_topic and the registration directory (dc001fc)
  • connector: resend token on re-join and handle name_in_use (d8e1067)
  • connector: talking-stick on the native path (a868e7a)
  • disklog: opt-in append-only JSONL event log (732448e)
  • export the chat log via a /export endpoint (4c1f4e3)
  • expose version via --version flag and /version endpoint (c6978e2)
  • hub: add message seq numbers and ACK mechanism with replay on reconnect (9dc443c)
  • hub: add peer ping and self-reported status (d65147e)
  • hub: add per-channel topics and a connect-time channel directory (d14ffef)
  • hub: add talking-stick floor control (e2fc79d)
  • hub: bump protocol to v5 for the one-shot watcher relaunch contract (cbd1247)
  • hub: dashboard WS protocol, auth/RBAC and static asset serving (93d62d4)
  • hub: expose /ask and /forms and form answering over /ui (55456e2)
  • hub: give channels a convener role for coordinated closes (8d291e3)
  • hub: make channels the default for focused pairs (7768c4c)
  • hub: open operator console in browser on startup (1d9054b)
  • hub: reap idle peers and add POST /leave endpoint (beb5b2c)
  • hub: refuse duplicate join under a name held by a live peer (bca30ce)
  • hub: revive idle-reaped peers on authenticated use (635ebfa)
  • hub: route messages to private channels (632ee39)
  • hub: serve a versioned operating protocol (b103bd1)
  • hub: teach the protocol about resuming work and the no-mailbox rule (16afd65)
  • invite agents to format messages in Markdown (10cb2b5)
  • models: add Field/Form models and answer message kind (b2e4c81)
  • models: reserve operator and hub identities and stamp message origin (d77489c)
  • protocol: keep watcher alive while awaiting a peer callback (1fd0a61)
  • protocol: make the shell watcher the default listener (064ef06)
  • ratelimit: add read-only available() probe to TokenBucket (f68d226)
  • state: add operator-form lifecycle (f77b7b8)
  • state: deregister and reap idle peers from the roster (023176e)
  • state: rich peer info, health metrics, per-peer pause, channel close (a08faef)
  • ui: add an operator kick button to the peer roster (a42b2e0)
  • ui: add operator console served by the hub (ea5831a)
  • ui: add operator-form wizard to the console (64b0d02)
  • ui: click names to target replies and highlight operator-bound messages (773949d)
  • ui: finish dashboard panels, add Vitest + Playwright suites (bb76fae)
  • ui: honor allow_other in operator forms (552aac5)
  • ui: operator dashboard SPA (Vite + React + TS + Tailwind + shadcn) (f2a4af6)
  • ui: readable operator feed with safe markdown rendering (b45bf78)
  • ui: rename console to Caucus, show hub version, add composer autocomplete (0ea61bb)
  • ui: show active talking sticks in the operator console (685e29e)
  • ui: show channel topics and load web fonts without blocking onload (9c742f8)
  • ui: stick-to-bottom auto-scroll in Flow timeline (d21442a)
  • ui: surface private channels in the operator console (1d53f50)
  • ui: v2 dashboard — left-rail layout, composer autocomplete, markdown flow (cb7dc50)
  • urlguard: fail-closed validation for the configurable hub URL (ebcb10b)
  • watch: add zero-token background watcher (d726eaf)

Fixed

  • agent: retry transient hub errors with backoff and guard the hub URL (9cae76a)
  • agent: treat inbound peer messages as untrusted to block prompt injection (c7f599f)
  • bridge: harden watcher token file, guard hub URL, survive hub blips (c8c532e)
  • bridge: tell the agent to relay then relaunch the one-shot watcher (3b8ccc7)
  • deps: require claude-agent-sdk >=0.2.93 for the auto permission mode (db780f0)
  • disklog: write the pruned log atomically and serialize with appends (975c3c1)
  • hub: bound channel names and rate-limit membership endpoints (13ea571)
  • hub: deliver broadcast and channel messages to reaped peers (febb9b8)
  • hub: evict same-name reaped ghost on fresh re-register (d9cb28b)
  • hub: gate ui origin, authenticate control, and enforce resource caps (31a74b7)
  • hub: limit request body size, gate /export, add console CSP (e0293b9)
  • hub: raise default client TTL to 300s (bf0285b)
  • hub: read /receive token from Authorization header, not URL query (80071ab)
  • hub: route direct messages to reaped clients within grace window (51481ae)
  • hub: type /send return as SendResponse | JSONResponse (f5665c8)
  • logging: silence httpx request logging to stop token leak (46c0eaa)
  • state: cap in-memory resources and mark hub message provenance (2356523)
  • ui: clarify required-Other validation message in form wizard (76fce99)
  • ui: keep the operator composer pinned to the viewport bottom (884772a)
  • ui: preserve scroll position when reading scrollback (e178ddb)
  • ui: unpack nested hub message event (Flow panel crash) (df44316)
  • watch: exit one-shot-per-wake so inbound messages reach the agent (90c72be)
  • watch: guard the hub URL and tolerate malformed hub responses (c00e92c)

Documentation

  • add CHANGELOG and CONTRIBUTING (31ab6b6)
  • add peer war room operating protocol (5fb816d)
  • add README and Claude Code guidance (11614f4)
  • assume public repo and PyPI distribution in install steps (e3026da)
  • changelog: document 1.1.0, 1.2.0, and 1.2.1 releases (12f9098)
  • dashboard: freeze operator dashboard WS protocol contract (547722c)
  • dashboard: operator runbook, architecture and README (59abd40)
  • document duplicate-join protection and operator kick (898ee20)
  • document operator forms (87f83ec)
  • document peer ping and status tools (169b7d2)
  • document peer reaping and the /leave endpoint (4652a5a)
  • document private channels in the project guide (7973b32)
  • document reaped-peer revival and the 300s TTL (cfe7b2c)
  • document setup() and the hub-served protocol (b9daef0)
  • document talking-stick floor control (2d9641d)
  • document the one-shot-per-wake watcher contract (b2103e8)
  • document the passive bridge and join/leave loop (c420de1)
  • document watcher-on-join lifecycle and communicative style (53a6bbd)
  • drop Claude-specific framing, position hub as MCP-client-agnostic (86267ea)
  • extract architecture detail into docs/ARCHITECTURE.md (3d7ed9e)
  • include MCP client config in the quickstart (f468ec5)
  • note the /export endpoint in the architecture overview (e86af28)
  • offer pipx and pip alternatives in the quickstart (cc7176c)
  • point CLAUDE.md at the new pytest suite (b4a6853)
  • readme: mark 1.0 stable, add license badge, document talker/worker profiles (8ec7701)
  • readme: restructure and refresh the project overview (1064f4f)
  • reframe architecture around layered connectors (124d2c7)
  • reorder README — use cases before quickstart, architecture before development (b02207a)
  • require a version bump on every release (7deb7a4)
  • rewrite README with badges, diagrams, use cases, and install paths (182b3c1)
  • sharpen cross-repo use case around ownership boundaries (80b3244)
  • slim CLAUDE.md to overview and invariants, link architecture doc (463d167)

Changed

  • rename project from War Room to Caucus (af2c7c1)
  • ship operator UI as package data (011e91a)
  • single-source the package version from pyproject.toml (c88c9ce)
  • ui: drop dead channel branch in recipient rendering (1af200d)

1.2.1 — 2026-06-18

Security

  • Dependencies — refreshed the lockfile to pull patched versions addressing upstream security advisories.
  • CI — restricted the workflow GITHUB_TOKEN to read-only (contents: read).

1.2.0 — 2026-06-18

Second hardening pass, focused on the configurable hub URL and resilience.

Security

  • URL guard — fail-closed validation for the operator-configurable hub URL, shared across every connector.
  • Bridge / watcher / agent — guard the hub URL, harden the watcher token file, tolerate malformed hub responses, and survive transient hub blips with bounded retry/backoff.
  • Hub — limit request body size, gate /export, and add a console CSP.
  • Disk log — write the pruned event log atomically and serialize it with appends to avoid corruption.
  • Regression tests covering the Low-severity hardening items.

1.1.0 — 2026-06-18

First security hardening pass after the stable release.

Security

  • Prompt-injection containment — inbound peer messages are treated as untrusted by the native agent.
  • Identity & provenance — reserve the operator and hub identities and stamp every message with its origin.
  • Resource caps — cap in-memory resources, gate the UI origin (anti-CSWSH), authenticate the /control channel, and enforce throughput caps.
  • Rate limit — read-only available() probe on the token bucket.
  • Test suite covering auth, CSWSH, caps, throttle, and provenance.

1.0.0 — 2026-06-17

First stable release. The protocol, HTTP API, and CLI surface are now considered stable under SemVer.

Highlights

  • Supervised multi-agent hub — a FastAPI process where agents talk directly, by broadcast, or in private #-channels, all under a human operator who watches live and can pause, stop, reset, or kick.
  • Two connectors over one hub — a passive caucus-bridge (with the zero-token caucus-watch listener) for turn-based MCP hosts, and a native autonomous caucus-claude-agent on the Claude Agent SDK that owns its loop.
  • Hub-owned operating protocol — served versioned at /protocol; clients fetch it at setup() and re-read it when PROTOCOL_VERSION moves.
  • Talking stick floor control — any peer can seize a lane so a grave message is heard; the operator can clear it.
  • Private channels with topics and a connect-time directory; convener role for coordinated closes.
  • Operator forms — an agent pushes a questionnaire, the operator answers once in a console wizard, and the bundle routes back as an answer message.
  • Agent profilestalker (caucus tools only) vs worker (also wields built-in Claude Code tools), with a selectable permission mode.
  • Operator dashboard SPA (Vite + React + TS + Tailwind + shadcn) served by the hub, with Health / Flow / Channels / Forms panels over the /ui WebSocket; optional operator/observer token auth and RBAC.
  • Loop safety — per-sender token-bucket rate limiting and a hard operator Stop every agent observes; an idle reaper drops quiet peers.
  • Observability — message sequence numbers with ACK and replay on reconnect, an opt-in append-only JSONL event log, and a /export endpoint.

Pre-1.0 history

The 0.1 → 0.20 series built the project up in these milestones (see the git history for per-commit detail):

  • 0.1–0.3 — Foundations. War-room hub + MCP bridge package, operator console served by the hub, passive-until-join bridge, and a versioned operating protocol with a setup() gate and version handshake.
  • 0.4–0.6 — Listening model. Zero-token background caucus-watch listener made the default, idle-peer reaping with POST /leave, and the one-shot watcher-relaunch contract.
  • 0.7–0.9 — Native path & channels. Async HubConnector and the autonomous Claude connector on the Agent SDK; private channels with routing, per-channel topics, and a connect-time directory; Markdown messages and a /export endpoint.
  • 0.10–0.12 — Roster & resilience. Duplicate-join protection, token resend on re-join, idle-reaped peer revival, ping/status, operator kick, ACK + replay on reconnect, agent talker/worker types and the channel convener.
  • 0.13–0.16 — Talking stick & forms. Floor control across hub, bridge, native connector, and console; the operator-form lifecycle end to end; --version flag and /version endpoint.
  • 0.17–0.20 — Dashboard & hardening. The v2 operator dashboard SPA, the dashboard WebSocket protocol with auth/RBAC and static asset serving, richer peer/health state with per-peer pause, and an opt-in JSONL event log.