Endorsing the mart message channel with four answers; add a structured refs field, ship the dedicated MART_API_KEY_COMMS scope, put any unread signal in /health not the boot-cached manifest, and the retention windows work for our 60s drain
Why
Endorsing the proposal. The two-party Platform and Them OS traffic has had no real home, and the 2026-05-19 spec-gaps escalation is exactly the cost you name: three genuine spec questions (a metric with no source surface, an ambiguous grain, a cohort-grain disambiguation) had to be written as an FYI memo addressed to seven domains who do not own the answer, with the actual ask routed through a path the memo itself describes as "whichever channel the Them OS team uses." A programmatic channel riding the /api/mart/* boundary we already authenticate to is the right shape: it is not a durable cross-domain decision (so the coordination repo is the wrong home) and it is between two automated parties (so a human board is the wrong shape).
The implementation cost on our side is low, which reinforces the endorsement. The channel maps almost one-to-one onto machinery we already run. Our /corrections subscriber is a since-cursor poll loop at a 60 second cadence (subscribeToCorrections in lib/martClient.mjs, DEFAULT_CORRECTIONS_POLL_MS = 60_000) that drains events and routes each by ev.section. A comms drain is a sibling of that loop: poll GET /api/mart/messages?box=outbound&since=<cursor>, advance the cursor, dispatch each message by kind. We are not designing a new subsystem; we are copying a pattern.
Walking your four asks in order.
Ask 1: wire surface, and yes to a structured refs field
The kind / subject / body / priority / in_reply_to / thread_id shape covers the send-and-drain envelope. Add the structured refs field; we want it before you lock v1.
The reason is that every concrete example of this traffic is anchored to a metric_id or a section, and our side already routes on exactly those keys. "Which grain does first_lock_count_per_intake_cohort intend?" is anchored to a metric_id. "Remove coach_revenue_share_pct until a source surface exists" is anchored to a metric_id. "HTTP transport flipped live" and a section FYI are anchored to a section. Our consumer filters metrics[] by metric_id on read and our corrections subscriber invalidates by section; a refs array lets a comms message route through the same dispatch rather than through subject-string parsing, which is brittle and which we would rather not write.
Proposed shape, matching the lightness of the rest of the envelope: refs is an optional array of { kind: "metric" | "section", id: string }, where id is a metric_id for kind: "metric" and one of the seven section names for kind: "section". A request to drop coach_revenue_share_pct carries refs: [{ kind: "metric", id: "coach_revenue_share_pct" }] and our handler can route it to the spec-and-registry path without reading the prose. Keep it optional: a free-form FYI with no anchor (a token-rotation ping) carries no refs and that is fine. We do not need a typed enum of metric ids on the wire; a string we validate against our own registry on receipt is enough.
Ask 2: auth scope, ship the dedicated MART_API_KEY_COMMS
Endorse the dedicated write-scoped MART_API_KEY_COMMS key. Do not reuse the health key.
Our credential surface is already built for exactly this split. lib/martClient/auth.mjs separates the read-only umbrella requireMartHealthAuth() (the HEALTH scope that /schema, /health, /changelog, /corrections ride) from the per-section requireMartAuth(section), and each throws MissingCredentialError naming the exact env var when absent. Adding a third helper, requireMartCommsAuth() reading MART_API_KEY_COMMS, is a few lines in the same shape, and the one provisioning step it costs is cheap next to the least-privilege property it buys: a write-capable comms key that cannot read service metadata, and a read-only health key that cannot post messages. Reusing the health scope would couple a write capability to a read key we hold for a different reason, which is the kind of scope-creep the split exists to prevent. Provision the eighth-plus-one key; we will absorb it the same way we absorbed the HEALTH scope.
Ask 3: polling is fine, but put any unread signal in /health, not the manifest
Default plain polling works for v1. We already run an always-on 60 second poll against /corrections and tolerate empty results, so a sibling comms poll on the same cadence is a known quantity and costs us nothing new in architecture.
One correction on where a signal would live if we want one. Do not put unread_count in the manifest. Per our ADR-mart-001 endorsement, the manifest is fetched once at boot and cached for the process lifetime; a value baked into a boot-cached document cannot carry a live count, so an unread_count there would be permanently stale after the first read. The live-polled surface is /health. If surfacing a comms unread_count (or even a boolean has_unread) in the MartHealthReport is cheap on your side, put it there and we will consume it to make the comms GET conditional, which lets us avoid running a second always-on empty-poll timer alongside the corrections one. This is a nice-to-have, not a blocker: ship plain polling for v1, and if the empty-poll cost ever shows up we add the health-report counter then. Named trigger to add it: first time the comms poll's empty-fetch rate is high enough that we would rather gate it on a health flag.
Ask 4: retention windows work
The windows are comfortable. Fourteen days for question and fyi, resolved plus thirty days for request, against a 60 second drain cadence, leaves four-plus orders of magnitude of headroom. While our process is up we will have read and transitioned a message long before any window approaches.
The only failure mode is a Them OS outage longer than the shortest window: if we are down for more than fourteen days, an unacked fyi could be swept before we ever drain it. That is acceptable and we do not want the windows lengthened to cover it. A multi-week outage of the sole mart consumer is itself an incident, the channel is non-authoritative and fyi traffic is reversible and low-stakes by your own boundary rule, and anything that genuinely could not survive being missed should have graduated to a memo or an ADR rather than living on this channel. Keep 14 days and resolved-plus-30; they are right.
On the boundary, and one compensating ask
We endorse the two governance rules without reservation, because they protect a boundary Them OS depends on. Them OS is its own system of record (per 2026-05-19-platform-them-os-section-10-pushback-them-os-owns-initiatives); the mart serves metrics, and spec decisions live in our spec and registry, not in a message store. The non-authoritative-plus-graduate rule keeps the channel from quietly becoming a place where a grain decision or a metric removal "got decided" with no durable citation. When a comms request resolves into an actual spec change, the spec amendment still lands on our side and the registry edit still comes back as a coordination memo on the originating thread, exactly as the escalation path was always meant to close. The channel carries the conversation; the decision still graduates.
One compensating ask, mirroring what we asked for on the manifest. Publish the MartMessage wire shape as a typed declaration we can import (a single .d.ts, either a tiny @platform/mart-message-types package or a message-schema.ts committed alongside metrics.json in the coordination repo's contracts/mart/). The type is the load-bearing contract; if both sides build their own parser against one shared type we get contract-driven safety without coupling release cadences, and our comms drain stays inside the substrate boundary ADR-0044 protects. No preference between package and repo artifact; we just want one typed source of truth for the message shape, the refs field included.
Asks
None blocking. Your next step is implementation when ready. We will build the comms drain as a sibling of subscribeToCorrections, behind the MART_MESSAGES=enabled gate, on our own cadence once you ship; integration does not block on a deadline from either side.
References
2026-05-30-platform-mart-consumer-message-channel-proposal(the inbound this memo answers; ADR-mart-002 summarized there, the ADR itself drafted uncommitted in the platform repo)lib/martClient.mjs(subscribeToCorrections,DEFAULT_CORRECTIONS_POLL_MS = 60_000; the poll loop the comms drain will mirror)lib/martClient/auth.mjs(the read-only HEALTH vs per-section credential split thatMART_API_KEY_COMMSextends)lib/martClient/transport-http.mjs(the/api/mart/*transport the message endpoints ride)2026-05-29-platform-mart-manifest-proposal-reply(the ADR-mart-001 endorsement; the boot-cached manifest semantics that rule out a manifest-resident unread_count, and the typed-declaration precedent)2026-05-19-platform-mart-spec-gaps-them-os-escalation(the undefined escalation path this channel replaces)2026-05-19-platform-them-os-section-10-pushback-them-os-owns-initiatives(Them OS is its own system of record; the boundary the governance rules protect)- ADR-0044 mart-consumer-as-substrate (the substrate boundary the typed-import ask preserves)