Protocol spec
The musterd wire protocol: the Envelope every message travels in, the acts Members exchange, and the rules any conforming server or client MUST follow.
Version: musterd/0.3 (draft)
Status: v0.3 draft — designed in the open, versioned from the first commit. v0.3 adds the terminal resolve act (thread-close — the open-vs-done axis, ADR 025) over v0.2; it is a backward-compatible MINOR (one new act, no change to existing fields). v0.2 added the minimal trust model (single-active Members + reclaim grace) and roster activity over v0.1. The full shared-teams governance model is otherwise designed but not yet specified — it activates when the daemon stops being localhost-only. Its wire-level design lives in Appendix A (Unreleased) below; the rationale is in docs/design/membership-model.md + docs/design/security.md.
License: MIT (same as the implementation).
This is the normative protocol. Implementations (this repo's
@musterd/server,@musterd/protocol, and any third-party server or client) MUST conform to it. The implementation-facing distillation with file/function detail isdocs/architecture/02-protocol.md; where that and this file disagree, this file wins. Changes to this spec are versioned and require an ADR (docs/decisions/).
The keywords MUST, SHOULD, MAY are used per RFC 2119.
1. Model
musterd coordinates Teams of Members with shared messaging.
- A Team is a named, persistent group — a standing roster, not a project. It outlives any task, session, or repository.
- A Member is a durable identity within exactly one Team.
kindisagentorhuman— humans are first-class Members, not approvers. A Member has a name (unique within its Team), a free-text role, a lifecycle (forever | session | until <ts>), an optional availability schedule (stored, not enforced), and an optional recurring working_hours schedule. A Team may set the default schedule; a Member schedule replaces it for that Member (ADR 206). - A Presence is where a Member is currently attached — a Surface such as
cli,claude-code,codex, oropencode. One Member MAY have multiple simultaneous Presences (like a person on desktop + phone). A Member is not a session. - The server routes each message to wherever the recipient is present; an offline recipient's messages remain in the durable log and surface via their Inbox (cursor-based).
These five terms — Team, Member, Presence, Surface, Act — are the glossary; conforming implementations MUST use them with these meanings in any user-facing surface.
2. Envelope
Every message is an Envelope (JSON):
{
"id": "<ULID>", // client-generated, globally unique
"v": "musterd/0.3", // protocol version; MUST match server's supported version
"team": "<team-slug>", // [a-z0-9-], 1..32
"from": "<member-name>", // sender, a Member in `team`
"to": { "kind": "member", "name": "<member-name>" }, // or {"kind":"team"} or {"kind":"broadcast"}
"act": "<act>", // one of the 8 acts (§3)
"body": "<string>", // human/agent-readable content; MAY be empty
"thread": "<ULID|null>", // optional thread root id; null/absent starts a thread
"meta": {}, // optional, act-specific (§3); unknown keys MUST be preserved
"ts": 1733760000000, // sender clock, epoch ms; server records its own receive time too
}
Recipient (to) is one of:
{"kind":"member","name":"<name>"}— delivered to that Member.{"kind":"team"}— delivered to every current Member of the Team except the sender.{"kind":"broadcast"}— in v0.1, delivered asteam. The distinct kind is RESERVED for future cross-Team/announce semantics; implementations MUST keep it distinct on the wire even while delivering it as team.
An eligible set (ADR 254) narrows who OWES an answer without narrowing who can see the act: a {"kind":"team"} Envelope MAY carry meta.eligible, an array of 2–4 distinct Member names. It is delivered to the whole Team as above, but only the named Members owe a reply, and the FIRST accept/decline naming the act (via meta.in_reply_to) discharges it for all of them. Only message, request_help, and challenge may carry one; a server MUST reject an eligible set naming an unknown, departed, observer, or sending Member rather than silently dropping the name.
Validation: an Envelope with an unknown act MUST be rejected. Unknown meta keys MUST be accepted and preserved (forward-compatibility). A server MUST reject an Envelope whose from/team do not match the authenticated Member.
3. Collaboration acts
Acts are the typed intents of coordination, grounded in the Co-Gym collaboration-act taxonomy (Shao et al., Collaborative Gym, arXiv 2412.15701). v0.1 defined seven; v0.3 adds resolve (ADR 025), the steering trio steer/challenge/defer (ADR 103), ask (ADR 147), and insight (ADR 327) for thirteen:
| Act | Meaning | Required meta/fields |
Optional meta |
|---|---|---|---|
message |
plain communication, no protocol semantics | — | — |
status_update |
report what you are doing / have done | — | progress (0..1), state (string) |
request_help |
ask a Member or the Team to assist / unblock you | — | blocking (bool), topic (string) |
handoff |
transfer a unit of work to someone | — | artifact (string), summary (string) |
accept |
accept a prior request_help/handoff/challenge |
meta.in_reply_to (ULID) |
— |
decline |
decline a prior request_help/handoff/challenge |
meta.in_reply_to (ULID) |
reason (string) |
wait |
paused / blocked — bare, deciding (ask_ref), or deferring (defer_ref) |
— | reason; ask_ref+until (duration string); defer_ref+until ({lane}|{reply:true}) |
resolve |
close a thread — mark the work it tracks done | thread (ULID) |
reason (string) |
steer |
change direction — a directive that supersedes prior direction | — | urgent+urgent_reason |
challenge |
ask a Member to justify a task/assumption or reconsider | — | urgent+urgent_reason |
defer |
shelve a Goal on the plan | meta.goal_id (string) |
— |
ask |
direct a question to a human, carrying an answerability contract | meta.species (consult|escalate|approve), meta.tier (advisory|standard|blocking) |
resolution rides status_update (meta.ask_outcome+meta.ask_ref); the deciding reply rides wait (meta.until) |
insight |
save a reusable finding so the whole team can find it | meta.headline (1..120 chars) |
meta.tags (≤8 strings), meta.repo (slug) |
Rules:
acceptanddeclineMUST carrymeta.in_reply_toreferencing the Envelope they answer, and SHOULD setthreadto that Envelope's thread (or itsidif it was a root).resolveis thread-terminal: it MUST carry a non-emptythreadnaming the thread it closes (a no-thread root is closed by passing its ownid). It marks the thread — the proto-work-item — done, supplying the open-vs-done axis the other acts lack (accept≠ finished). It MAY follow anacceptor close a thread directly without one. Authority: any Member of the Team MAYresolvea thread; v0.3 does not enforce a closer (the norm is the opener or the assignee). Conforming UIs SHOULD treat a thread carrying aresolveas closed and stop surfacing its openrequest_help/directed asks as pending.- The steering acts (ADR 103) give a "change of direction" first-class semantics on the interrupt line (ADR 088), and ride it for delivery — no new delivery machinery.
steeris a directive that is interrupt-class by definition (it reaches a busy Member even when not flaggedurgent); the neweststeerdirected at a Member supersedes all prior steers to it (ADR 017 newest-wins applied to direction), so a late-waking Member sees only the current direction, never a contradictory stack.challengeis epistemic (warn-never-block) and tier-configurable — it interrupts only when its sender flags iturgent; it is answered with evidence (anacceptcarrying justification, or a plan change).deferMUST carrymeta.goal_idnaming the Goal it shelves (ADR 257 retired the numeric position, so shelving is its whole meaning); it records/surfaces the plan mutation as a first-class act. - The ask act (ADR 147) is directed-to-human traffic:
meta.speciespicks the kind (consult|escalate|approve) andmeta.tierderives the timeout + no-answer policy the agent itself runs (top tier holds; below-top proceeds with a recorded risk-acceptance). The no-answer resolution ridesstatus_update(meta.ask_outcomenamingmeta.ask_ref); the human "deciding — check back" reply rideswait(meta.ask_ref+meta.until). - The insight act (ADR 327) saves a reusable finding for the whole team. It is written team-visible on intent — there is no private variant, so seat memory's privacy boundary (ADR 093) is untouched. The finding text rides the envelope body (server-capped at 2048 bytes); retrieval is pull-only through a derived index over the log (a rebuildable cache — never a source of truth, ADR 259); no digest or standing-context injection (ADR 212). Durable findings promote into
docs/wiki/as a norm, not machinery. - Acts are the stable contract;
metais the extension point. New acts are a versioned change to this spec.
4. Identity, Presence, Lifecycle
- Authentication: each Member has a secret token, issued once when the Member is added. A request/connection presents the token; the server authorizes it to act as that Member in that Team and no other. Servers MUST store only a hash of the token, never the plaintext.
- Presence lifecycle: a client attaches a Presence by connecting (declaring its Surface), keeps it alive with heartbeats, and detaches on disconnect. A Member is online while it has a fresh Presence, offline otherwise; away is set only explicitly by a client and MUST NOT be inferred by the server. Heartbeat cadence and timeout are implementation parameters (this repo: 15s heartbeat, 45s timeout).
- Single-active is kind-scoped (v0.2 + ADR 042; ADR 017 supersedes ADR 010's refusal): agent Members are single-active, newest-wins — an agent MAY hold at most one live Presence at a time, and on a new attach for an agent that already has a live Presence the server MUST keep only the newest session: it takes over, and the existing one is told it was
superseded(this repo: WSerror) and dropped. (This replaces the earlier "refuse the second withmember_busy" rule — refusing locked a Member out of its own seat after a reload/orphaned session.) Human Members instead fan out: a human MAY hold multiple simultaneous Presences (watch on a phone while acting on a laptop), so a new human attach adds a Presence alongside the existing ones — no displacement. Agent displacement is workspace-scoped (ADR 068): a same-workspace attach does not immediately supersede the incumbent (a transient health-check probe must not flap the seat); instead a same-workspace successor that proves durable (survives a short grace window still attached) reaps its predecessor, which is toldsupersededwithsame_workspace:trueand — being a replaced reload orphan — SHOULD exit rather than linger dormant (ADR 092). Either way, on a clean detach the server SHOULD hold a seat for a short reclaim grace (this repo: 45s, tracked asheld_until) so the same Member can rejoin without losing it; the grace is swept per-Presence by the reaper, and a Member goes offline only when its last Presence drops. (Rationale: single-active exists to stop N concurrent autonomous minds wearing one agent identity — an agent hazard, not a human one; a person across devices is the original "one Member, many Presences" — §1, ADR 042. The v0.3 seat-claim model governs who may take a seat once the daemon leaves localhost; ADR 007/010/017/042.) - Roster activity (v0.2; idle rename ADR 140): a roster/status response carries, per Member, a coarse
activityofoffline | idle | working, derived server-side by a two-clocks rule — liveness (presence) decidesofflinevs present, and the lateststatus_updatedecidesidlevsworking(a self-reported task). The backing task summary rides instatewith alast_status_attimestamp (for staleness display). These fields are optional/additive — a v0.1 client that ignores them still conforms. When offline, optionaloffline_reason(reconnecting | disconnected | signed_off | off_hours | unknown) explains why (ADR 141). - Attach context — provenance & workspace (v0.2, ADR 014): on attach a client MAY declare two facts about why and where the Presence exists: a
provenanceofsession | asked | hook | scheduled | daemon(why this attachment exists — e.g.sessionmeans a human opened a harness session,scheduledmeans a timer started it), and aworkspacestring (a "where" label, e.g.repo@branch). The server records both on the Presence and surfaces them on the roster; it MUST NOT guess them (they are facts known only to the attaching client). Both are optional/additive and carry no routing or authorization meaning — they are read context, rendered as such. (Rationale: presence answers "is anyone there"; provenance answers "why are they there" — don't make one layer carry another's question. Seedocs/design/human-agent-dynamics.md§2.) - Driver co-presence (v0.2, ADR 021): on attach a client MAY also declare a
driverstring (≤80 chars) — the name of the human steering this session, when one is — so the roster can renderdriven by <name>instead of showing the driving human as offline. Like provenance/workspace it is a fact known only to the attaching client (the server MUST NOT guess it), is optional/additive, and carries no routing or authorization meaning — it names a co-present human but does not link to, authenticate, or stand in for that human's Member. (An adapter authenticates only as the agent; it never holds the human's token. Seedocs/design/human-agent-dynamics.md§54.) - Member lifecycle:
forever(default),session(intended to last one working session), oruntil <ts>. The server stores lifecycle, availability, and working hours but does NOT enforce schedules or auto-expiry at runtime (enforcement is on the roadmap). Schema/field support exists from day one so enforcement can be added without a breaking change.
5. Transport
A conforming server MUST expose the message-routing semantics of §2–§4. This repo's server offers two bindings (full detail in docs/architecture/02-protocol.md):
- WebSocket for live, present clients: handshake
hello → welcome → subscribe → subscribed, thensend/deliver/heartbeat/presence/ack/errorframes. - HTTP/JSON for stateless one-shot clients (team/member management, send, inbox fetch, presence ping).
Both bindings MUST funnel sends through one validate→persist→route path so semantics are identical.
Delivery guarantee: at-least-once. The message log is authoritative; each Member has a cursor (high-water mark). A client MAY receive a message both live and again on inbox fetch after reconnect; clients MUST dedupe by Envelope.id.
6. Versioning & compatibility
- The version string is
musterd/MAJOR.MINOR.v0.1was the first;v0.2added single-active newest-wins + reclaim grace, roster activity, attach provenance/workspace, driver co-presence (new error codesmember_busy/superseded);v0.3is current — it adds the terminalresolveact (ADR 025), the steering triosteer/challenge/defer(ADR 103), un-stubs the reservedmemoryseam on theoccupiedframe into a seat-scoped continuity envelope (A.3, ADR 093), and adds the optional harness-attestedmodelfield on theclaimframe + heartbeat (A.3, ADR 101 — per-occupancy,unknownwhen omitted). ADR 275 adds optionalsurfaceon the heartbeat so occupancy can follow a mid-session capture (absent ⇒ no change, same never-clear rule asmodel). ADR 301 adds optionalmodel_source(observed|environment|binding) besidemodelon claim, heartbeat, grant-less requests, and as server-stampedmeta.model_sourceon every act; omitted by older clients and when there is no model (never defaulted tobinding). ADR 147 had added theaskact (the species/tier answerability contract, with no-answer resolution ridingstatus_update) and ADR 327 adds theinsightact (team memory — headline + capped body, retrieved through a derived index). All MINOR additions are additive (new acts and new optional fields, no change to existing required fields): a client that ignoresmemory/model/surface/model_sourceor does not recognize a new act is unaffected. - Within a MAJOR, MINOR additions MUST be backward-compatible (new optional
meta, new optional fields, new endpoints, new error codes). New acts or any change to envelope-required fields are a MINOR-or-greater, spec-versioned change requiring an ADR. - A server MUST reject a client whose declared
vit does not support, with aversion_mismatcherror.
7. Roadmap (informative, not part of v0.1 conformance)
These are designed-around but not specified/required in v0.1; see ROADMAP.md:
- Step-level streaming transport option (StreamMA finding: step-level streaming beats wait-for-complete) as a v2 transport mode; v0.1 sends whole Envelopes.
- Schedule enforcement of
availabilityandlifecycle. - Team-to-team federation (the reserved
broadcastkind anticipates this). - Additional Surfaces (iOS, web, Slack).
- Sandboxed runtime for member execution.
Schema and wire formats in v0.1 already reserve the fields these need, so adding them does not break v0.1 clients.
References
- Co-Gym (collaboration acts): Collaborative Gym: A Framework for Enabling and Evaluating Human-Agent Collaboration, arXiv 2412.15701.
- MAST (coordination-failure analysis motivating the layer): Why Do Multi-Agent LLM Systems Fail?, arXiv 2503.13657.
Appendix A — v0.3 shared-teams governance (SHIPPED — the live auth model)
Status: SHIPPED. v0.3 P0–P3 landed on
main(2026-06-30) — the hard auth cutover (ADR 069 build plan; ADRs 075–078; banned-inert read-gate follow-up PR #37). This shared-teams governance model — seats, agent key + grants, capabilities, approval lane, audit — is now the live identity/auth model: the per-member-token path (§4) and thehelloframe are removed, replaced by theclaimhandshake + team agent key + admin-issued grants + human credentials described below. The collaboration Envelope/Acts (§2–§3) are unchanged — governance layered on without touching them. Rationale + design review:docs/design/membership-model.md,docs/design/security.md; the two universes capability split (what musterd enforces vs. provisions/declares for the harness) is ADR 026.Spec-hygiene follow-up (tracked, not yet done): fold this appendix into the normative body (§1 identity, §4 auth) and retire the now-superseded
hello/per-member-token prose in §1–§6, which describes the pre-cutover (v0.2) model. Until then: read §1–§6's join/auth text as historical, and this appendix as current.
A.0 What the cutover changed (v0.2 → v0.3, shipped in P3)
| Area | v0.2 (former, §1–§6) | v0.3 (shipped) |
|---|---|---|
| Identity | flat Member (name, kind) | Seat in a Role (Member = named Seat) |
| Auth unit | per-member token = one member | agent key (harness) + admin-issued Grant (seat occupancy) |
| Join | hello {team, as, token} → presence |
claim {seat|role} → occupy, or → request to an admin |
| Authorization | token == member | grant required; default live admin approval; team opt-in pre-issued |
| Concurrency | N sessions = N presences of one member | single-active per agent seat (2nd → claim_conflict); human seats fan out (ADR 042) |
| State | presence.status + left_at |
three axes: account · availability · activity (with working staleness) |
| Observers | none | human-only read-only watchers |
| Governance | none | own lane: roles, seats, grants, requests, status — all audited |
A new join/auth handshake is a MAJOR-of-MINOR change; it landed as the isolated P3 cutover (ADR 069 decision 2, 2026-06-30) — one coordinated breaking moment across every surface, with the acts (§2–§3) untouched. (Note: musterd/0.3 also shipped the resolve act, ADR 025.)
A.1 Roles, Seats & Capabilities
- A Role is admin-defined (
backend,frontend,reviewer,lead…). It groups seats (capacity = its seats) and carries default capabilities + an optional charter. - A Seat is the identity record:
{ id, team, role, name?, kind: agent|human, account_status, occupied_by?, availability?, activity?, capabilities, charter? }.nameis optional for agent seats (handle<role>-<n>if absent), conventional for humans. A seat'scapabilitiesstart from its role's defaults and may be narrowed per seat, never widened. - Capabilities (fixed set):
can_message(scope),visibility_level,tool_allowlist,declared_resource_scopes,can_flag_urgent,can_observe,is_admin. Servers MUST enforce them on every in-band operation; external scopes (repo/dir/tool) are declared here and enforced by the harness/sandbox (Principle 4). ADR 026 frames this as the two universes — in-band acts musterd enforces vs harness tools it provisions + declares — and makes the Role a harness-agnostic provisioning template. Custom RBAC is roadmap. - Charter is identity metadata (what the seat is for + instructions); musterd stores and serves it, never enforces behavior. A memory/context blob rides the claim response (A.3) as a seat-scoped continuity envelope (ADR 093):
memorycarries{ headline, saved_at, size_bytes }ornull, with the body fetched on demand — an additive MINOR that un-stubs the seam once reserved here. - An agent seat has at most one live occupant (single-active); a human seat may have multiple concurrent occupant Presences (kind-scoped, ADR 042). Humans claim their own named seat; agent seats may be claimed by name or by an open seat in a role.
A.2 Credentials
- Agent join key — team-scoped secret; authenticates a harness/session; rotatable; hashed. Not an identity and not sufficient to occupy a seat.
- Grant — admin-issued authorization to occupy a seat/role. Fields:
{ id, team, scope: seat|role, target, issued_by, lifetime: "once"|"ttl"|"standing", expires_at?, single_use?, revoked? }. At live approval the admin picks the lifetime (once / N-hours TTL / until-revoke), so reconnects within the window don't re-prompt while keeping "no silent grant." Seat/role-scoped, expiring, revocable. Every issue/use/revoke is audited. - Human credential — per-human-seat secret; acts as that human; observes if role permits.
- Admin — capability on a human seat (creator default).
Servers MUST store only hashes of keys/credentials/grants. A banned seat's credential MUST be rejected.
A.3 Claim handshake (WS) — replaced the hello frame (removed in P3)
Note (local claim-on-first-use is already shipped without this frame — ADRs 032/033). The local claim experience from
provisioning-recipe.md§5–§6 — the overloadedteam_join,musterd claim, theMUSTERD_CLAIMfolder policy, and client-side pending presence — is built on the existinghello/members primitives, not this handshake: locally a seat is a member + its per-member token, auto-mint is the unauthenticatedPOST /members, occupy ishello(newest-wins
- grace, §74), and
claim_conflictis the unique-nameconflicton mint. This appendix'sclaim/grant frame is the governed path (agent key + admin grant + the request lane); it shipped in P3 (2026-06-30) and is now the sole claim path — the localhello-based occupy this note contrasts against is removed. (Server-side pending presence — a seatless session on the roster — is the request lane's live-approval state, A.5.)
State machine: connecting → authenticated(key) → claim → (occupied | refused | pending) → [subscribed] → live.
// client → server
{ "type":"claim", "v":"musterd/0.x", "team":"dawn",
"key":"<agent key | human credential>",
"target": { "seat":"Ada" } | { "role":"backend" } | { "observe": true },
"grant":"<grant token>"?, // omitted → triggers a request (default path)
"surface":"claude-code",
"model":"claude-opus-4-8"?, // harness-attested model id (ADR 101) — attested, never
// verified; per-occupancy (the durable seat stays
// model-agnostic, ADR 087); omitted → "unknown", never blocks
"model_source":"observed"|"environment"|"binding"? } // which tier produced `model` (ADR 301);
// omitted with the id, or by older clients; never `unknown`
// on the wire (absence is not an assertion)
// server → client
{ "type":"occupied", "seat": <Seat>, "presence_id":"01J…", "server_time": <ms>,
"charter": "<role/seat charter + instructions>"?, // identity metadata, served not enforced
"memory": { "headline":"<≤120 chars>", "saved_at": <ms>, "size_bytes": <int> } | null }
// seat-scoped continuity envelope (ADR 093);
// null when nothing saved. The body is NEVER
// here — fetch via GET /teams/:slug/memory.
{ "type":"refused", "code":"claim_conflict"|"forbidden"|"not_found"|"disabled"|"banned"|"expired_grant",
"message":"…", "claimable":["…"], "hint":"musterd team add <name> --kind agent --role backend" }
{ "type":"pending", "request_id":"01J…", "message":"asked admins to authorize this claim" }
Rules:
- Valid grant for the target + seat free →
occupied(accountprovisioned→active). - Valid grant + seat occupied →
refused {claim_conflict, claimable, hint}. - No grant →
pending: the server opens a claim request (A.5) routed to admins. On approval the server emitsoccupied(orrefusedon deny/timeout). If an admin is co-present in the same session, approval MAY be immediate. observe: truerequires a human credential whose seat role permits observing; agents MUST be refused (forbidden).- An agent seat MUST have at most one live occupant; a human seat MAY have multiple concurrent occupant Presences (kind-scoped single-active, ADR 042).
memorycarries the seat's continuity envelope (ADR 093) —{ headline (≤120 chars), saved_at, size_bytes }— ornullwhen the seat has saved nothing. It is the seat-scoped working note an occupant saved before wrapping up; the body is fetched on demand overGET /teams/:slug/memory(seat-authenticated), never delivered on this frame. Additive and back-compat: a client that ignoresmemoryloses nothing.
A.4 Release & grace
- On clean disconnect or
leave, the occupancy is held for a grace window = the presence timeout (45s). A re-claimof the same seat within the window re-occupies without a new grant or request. After it, the seat returns toclaimableand a teampresenceoffline event fires. During grace the seat showsonline(held).
A.5 Governance lane (own surface, audited)
Governance is not carried by the collaboration Envelope/Acts. It is a distinct set of operations and a request object.
Request { id, team, kind: "claim"|"teammate", from_session, target?, status: pending|approved|denied|expired, decided_by?, ts, model?, model_source? }. Created on a no-grant claim (kind claim) or an explicit "I need a teammate" (kind teammate). Optional model (ADR 101) and model_source (ADR 301) carry the claimant's attestation across the approval gap so the approved occupancy is not born unknown / un-tiered. Routed to admins; surfaced via GET /teams/:slug/requests and a notification. An admin approves (issues a grant; for teammate, creates a seat then grants), denies, or it expires.
Governance operations (admin-only; A.7 HTTP): create/rename/disable/ban/archive seats; create/rename roles; issue/revoke grants; rotate the agent key; set team policy (e.g. allow_pre_issued_grants); decide requests.
Every governance operation and every grant issue/use/revoke writes an audit record (security.md): { ts, actor, action, target, result }.
A.6 State model (three axes)
Account (Axis 1): provisioned → active → (disabled ⇄ active) → banned ; any → archived. Non-active seats are not claimable.
Availability (Axis 2): available | away | dnd | away_until(ts) | off_hours. For humans, presence is implicit (connected → online; idle → away) while away/dnd/away_until are explicitly set, never inferred. For agents, availability is mostly available while occupied. Full schedule enforcement is roadmap.
Activity (Axis 3, only while occupied): offline (unoccupied) | online (idle) | working | talking.
workingcarriesmeta.statefrom the seat's lateststatus_update; self-reported, never inferred.workingpersists while occupied + alive; after 5 min without a freshstatus_updateit is rendered stale (working: x · Nm), never reverting toonline; it clears on release/timeout.- Two clocks: heartbeat = alive; last
status_update= fresh.
Display resolution (first match wins): archived/banned/disabled → provisioned(created · waiting to join) → away(off until <ts>) → unoccupied(offline) → occupied(working: x · Nm / talking: y / online).
A.6a Notifications & urgency
Delivery is unchanged (at-least-once, cursor-based); notification tiering is a recipient-side policy the server supports:
- Loud (notify/page): acts directed at the recipient (
request_help/handoff/accept/declineto them, @mention) + governance approval requests. - Quiet (stream only): ambient
status_update, broadcasts, others' threads. - Held: while the recipient is
away/dnd, messages queue → digest on return.
Breakthrough: away holds all except an urgent-flagged ping; dnd holds quiet but passes directed pings and urgent.
urgent is an envelope meta.urgent: true with a required meta.urgent_reason. It MUST be gated by the sender seat's can_flag_urgent capability, is audited, and the recipient MAY mark it wasnt_urgent (recorded against the sender). Servers MUST reject urgent from a seat lacking the capability. (Acts themselves are unchanged; urgent is a meta flag, not a new act.)
Localhost down-payment (ADR 044). v0.2 ships the mechanism of this section without the governance, mirroring ADR 035's notify down-payment. Shipped: the availability axis is self-set + stored + on the roster (
POST /teams/:slug/availability, reusingmembers.availability— no migration);meta.urgent+meta.urgent_reasonis enforced byactMetaRules(additive optional meta → no protocol-version bump); and the Loud/Quiet/Held tiers + away/dnd breakthrough run client-side inmusterd notify. Deferred to the v0.3 governed superset (the named seams):can_flag_urgentenforcement (urgent is ungated on localhost),urgentauditing, the recipientwasnt_urgentfeedback,off_hours+ schedule enforcement, and the A.7 admin endpoints / A.9 seats migration.
A.7 HTTP deltas
| Method | Path | Notes |
|---|---|---|
POST |
/teams |
returns { team, seat(creator+admin human), human_credential, agent_key, policy } |
POST |
/teams/:slug/roles |
admin; create/rename a role; set its default capabilities + charter |
POST |
/teams/:slug/seats |
admin; provision a seat (role, name?, kind) in provisioned; returns the seat (no token) |
POST |
/teams/:slug/seats/:id/capabilities |
admin; per-seat capability narrowing (may not widen past the role) + seat charter |
POST |
/teams/:slug/seats/:id/status |
admin; { to: active|disabled|banned|archived } |
POST |
/teams/:slug/grants |
admin; issue a grant { scope, target, lifetime, expires_at?, single_use? } → grant token |
DELETE |
/teams/:slug/grants/:id |
admin; revoke |
POST |
/teams/:slug/agent-key/rotate |
admin |
POST |
/teams/:slug/policy |
admin; e.g. { allow_pre_issued_grants: bool } |
GET |
/teams/:slug/requests |
admin; pending claim/teammate requests |
POST |
/teams/:slug/requests/:id/decide |
admin; { decision:'approve', lifetime:'once'|'ttl'|'standing', ttl_hours? } | { decision:'deny' } — approve issues a grant of the admin-chosen lifetime |
POST |
/teams/:slug/claim |
stateless claim mirror of WS claim |
GET |
/teams/:slug/members |
roster projected by viewer: seats with role, account/availability/activity + watching list; non-admins never see credentials/grants/audit/policy/other charters |
POST |
/teams/:slug/availability |
set the caller's seat availability (available|away|dnd|away_until) |
GET |
/teams/:slug/audit |
admin; audit records |
Sending an Envelope still requires the sender to hold the occupancy of from (replaces token==member). All read endpoints return a viewer-scoped projection per the recipient's visibility_level.
A.8 Error / refusal codes
Add claim_conflict (seat occupied; 409), expired_grant (410/403). Reuse forbidden (bad key / not allowed to observe / not admin), not_found (no such seat/role), and surface account states via refused.code (disabled/banned). version_mismatch covers an older client hitting this server.
A.9 Migration
members→seats(+role,account_status; droptoken_hash). Addroles,grants,requests,audit, and teampolicy/agent_key_hash, per-humancredential_hash.- Schema migration; since musterd is pre-1.0 and local, a one-shot reset of existing local DBs is acceptable (documented), or a best-effort migration that mints an agent key, creates one role per distinct
members.role, turns members intoactiveseats, and marks the creator admin. - Surface changes:
team addprovisions a seat (no token); MCP envMUSTERD_TOKEN→MUSTERD_AGENT_KEY(+ optional pre-issuedMUSTERD_GRANT) — the seat is resolved from.musterd/binding.json(claim), not baked into the env (MUSTERD_CLAIMsurvives only as a manual override for binding-less/CI folders, PR #58);initand CLIjoinmove to the claim/request flow.
A.10 Portable wake context (unreleased — ADR 209)
This section specifies the additive portable-continuity contract before implementation. It does not change a Team’s wake authorization: enrollment, loop toggles, rate caps, and the host actuator stay the ADR 131/179/191/199 mechanism.
Wake context request and response
// recipient-authenticated client → server
POST /teams/:slug/wake-context
{ "act_id": "01J…" } // exactly one of act_id or lane_id is required
// server → recipient
{
"context": {
"version": 1,
"wake": {
"kind": "reply|handoff|review|work_order",
"act_id": "01J…"?,
"lane_id": "01J…"?
},
"objective": { "action": "reply|review|continue_lane|begin_lane" },
"state": {
"lane": { "id":"01J…", "state":"…", "owner_seat":"Ada"?, "branch":"…"? }?,
"thread": { "id":"01J…", "participant_count":2, "unread_count":1, "latest_act":"handoff"? }?,
"memory": { "headline":"…", "saved_at":<ms>, "size_bytes":<int> }?
},
"fetch": ["inbox_thread", "lane_detail", "seat_memory", "git_artifact"],
"delivery": {
"requirement": "portable|transcript_required",
"intended": "fresh|resume"
}
}
}
The packet is a server-derived orientation index. It MUST NOT contain an Envelope body, a seat-memory body, a lane title, source text, or other agent-authored free text. The full bodies remain explicit, recipient-scoped reads. The packet creates no durable context store.
A server MUST authorize an act_id request only when the Act was delivered to the calling Member. It
MUST authorize a lane_id request only when the caller owns the Lane or is the recipient of the
current review/handoff derivation for it. A disallowed request MUST return forbidden without
revealing whether the Act or Lane exists.
Delivery selection and reporting
Wake Orders MAY add continuity_requirement: "portable"|"transcript_required" and
intended_delivery: "fresh"|"resume". Omission preserves legacy behavior for a mixed-version
daemon/host pair.
portable selects a fresh spawn. transcript_required is a server-controlled classification that
the initial implementation permits only for a recent directed reply; it is never selected by an
Envelope body. The host MAY resume only if local capture and policy checks pass and must retain the
same-lease fresh fallback. Handoff, review, and work-order wakes are portable.
Ordinary inbox reply wakes retain legacy delivery unless the team residency policy's
portable_inbox_replies cohort flag is enabled (its default is false). When enabled, those
orders are also portable/fresh. This is a rollout selector, not a sender-controlled field.
Deferred acts and wake eligibility
An act its recipient deferred (a wait carrying meta.defer_ref) is not a wake reason: the
Member said "not now". Deferred targets are suppressed from the wake candidate set whether or not
their condition has since fired.
When the team residency policy's raised_deferral_wakes flag is enabled (default false), a
deferral whose condition HAS fired becomes a candidate again. Such an order always takes the
batched lane, whatever lane its act would otherwise derive and even when that act is urgent — a
deferral must not jump the interrupt line it removed the act from. A seat pinned to the interrupt
lane therefore never receives one. Rate limiting is unchanged: the act id remains the exhaustion
key.
Wake reports MAY add delivery_outcome: "fresh"|"resumed"|"fresh_fallback", plus non-content
measurements transcript_bytes and transcript_age_ms. No report carries a session ID or transcript
path. A fresh_fallback outcome means a resume attempt failed and the fallback fresh session
occupied.
A.11 Shared Seeds before Lanes (unreleased — ADR 291)
A Seed is a captured Team idea, distinct from a Lane. Shared-Seed ingest accepts only
source: "slack" with a non-empty meta.user; a human Member's optional slack_user_id resolves
that identity (ADR 311). Its relay_id, Slack source, raw body, captured_at, Slack user id, and
resolved submitting Member are immutable. An unknown Slack user creates no Seed and advances the
cursor after a body-free unknown_submitter diagnostic; an unsupported source fails parsing and does
not advance the cursor. Every accepted capture starts open; ingest performs no semantic
classification of its body (ADR 312). Its lifecycle is
open|exploring|needs_clarification|clarified|completed|promoted. A Seed may have one active
agent explorer and a narrow public thread of clarification, answer, brief, or conclusion entries.
An exploration result carries one exhaustive final brief: problem and context, external evidence, viable approaches and trade-offs, constraints, risks, unknowns, recommendation, and proposed Lane title/detail. A non-actionable result also carries a conclusion and completion time. Promotion records automatic/manual provenance, whether research was skipped, and its timestamp.
The additive HTTP surface is GET /teams/:slug/seeds, GET /teams/:slug/seeds/:id, and authenticated
claim, clarification, answer, final-brief, and manual-promotion operations. The server validates every
body with the corresponding Seed schema. Only an agent may claim/explore; only the active explorer asks
or finalizes; posting a question is the only transition into needs_clarification; only the submitting
Member answers. Promotion atomically creates one ordinary, unowned
Lane and links it to the Seed; retries return the existing linked Lane. No raw body, Slack user id, or
thread body appears in logs, telemetry, or audit details.