One door out

The design for the single gate every outgoing message passes through — what it decides, what beats what, and how we stop anyone building a second way out.

2026-08-29 · Status: Proposed — design only · Nothing here has shipped

The invariant

Every message that leaves the system passes one gate, and the gate's answer is law.

Two halves, and the second is the hard one. One gate means there is no other way out — not a lane with its own rules, not a client someone can call directly. Its answer is law means no caller may soften it, retry it, or decide their message is special.

Why it is not true today

There are three layers of checks, not one, and they cover overlapping but different sets of lanes. Messages Clara writes herself get the full set. Messages sent by workflows get fewer. And five lanes call the texting client directly — turnover nudges, a renewal picture-message, two internal notices, and follow-ups on an open question — skipping the shared gate entirely. The code documents that gap itself. It is why muting a person by hand last night took four records and still left a hole.

The biggest hole, and why it is not simply a bug

The instinct is to call that a bug and remove it. That would be wrong. The exemption exists for a good reason: an operational alert to our own staff must not be blocked because a resident once opted out of marketing. Genuine internal mail has to pass.

The actual defect is that the claim is asserted by the caller and never verified. The code says "this one is internal" and the gate believes it. Nothing confirms the recipient is a member of staff. Among the lanes that use it: bulk sends, forward-to-a-manager, notes about unknown callers, holdover renewal notices, and tour confirmations — that last one worth a second look, since a tour confirmation goes to a prospect.

So the design job is not a ban. It is: prove that all 44 really do target staff, and make the 45th prove it too — a check that runs in the build rather than a rule people remember.

The rule we already wrote and never enforced

We decided this once before. The standing rule is one check, at every door, failing closed. Today 24 different modules call that check — and there is no list of the doors and no test that walks them. A new door added tomorrow turns nothing red; it simply exists, unchecked, until someone finds it during an incident.

That gap is the real reason the test rig below has to exist. The decision was correct and is three months old; what was missing was anything that notices when reality drifts away from it.

Two principles from last night

What the gate decides

One question, asked once, at the last point before a message reaches a carrier: may this specific message go to this specific person right now? Everything else — what the message says, whether it is a good idea, whether it repeats — belongs upstream. The gate is not a critic; it is a door.

Precedence — what beats what

RuleMeaning
Three levels of "stop"Marketing only (a renewal opt-out), all outreach (a general opt-out), and muted
Muted ignores message typeEvery other level grades by what kind of message it is. Muted does not: labelling a send "operational" must not walk through a mute.
Their opt-out beats our muteIf a person has said STOP, that holds no matter what we set. Mute is our decision; opt-out is theirs and is a legal boundary.
Unmuting never clears an opt-outTurning our switch off restores normal conversation only for people who never opted out. It can never revive contact with someone who asked us to stop. This exact bug was caught in review before merge.
A human's deliberate send passes a mute — and only a muteA person pressing send is the one exception, and it is scoped precisely: it clears the muted level, never an opt-out, never a legal hold.
Emergency and safety always passAt every level, logged as the exception each time.

The rule that keeps it one door

Nothing outside the gate may hold a messaging client. Not a workflow, not a script, not a new lane written next quarter. The gate owns the carrier connections; everyone else asks the gate.

Enforced by the build, not by memory: a check that fails the build when any file outside the gate imports a messaging client. Design intent, stated plainly — a rule that depends on reviewers noticing is the rule that produced today's five bypass lanes.

How we would prove it

What already exists, so this does not overclaim novelty: a nightly canary watches suppression on one property — useful, but it is not required and it does not block anything before merge; and about fourteen unit tests pin the decision table itself. What does not exist is any test rig whose subject is the gate. That is the gap.

So the new rig deliberately does not re-argue the decision table — those unit tests own it — and it never re-implements the checking logic; it imports the real functions, because a rig with its own copy of the rules proves only that the copy agrees with itself. What it adds:

One practical constraint on that last point: only five checks are required on the main branch, and our pinning jobs deliberately run after a push rather than on every pull request — a cost decision from 2026-08-21 that we are not reversing. So this rides inside the unit-test check that is already required, rather than arriving as a new gate of its own.

The choices that are genuinely open

1. What should the gate do when it cannot reach its own records?

In plain terms

Today a lookup failure blocks marketing but lets everything else through, so a database blip cannot swallow a lease document or a safety notice. The muted level inherits that.

2. How do the five bypass lanes get fixed?

In plain terms

Five places send texts directly today. Either we rewrite each to go through the gate, or we make the client itself refuse to work outside the gate — which fixes all five at once, plus the sixth nobody has written yet.

The safety checks that rewrite her words — inspection findings

Separate from whether a message may leave: what our safety checks do to it on the way out. One of them rewrote a sentence into "I don't have that answer on file right now" and shipped it twice to a real prospect — the line he called robot-like. That prompted a full read of the pipeline, against the code and against 72,558 real turn records. It found more than one bad sentence.

What else the inspection found

What this adds to the design above

  1. The text that ships is the text that was graded. One cheap mechanical pass runs last, over whatever any check substituted, and every check records the final text. This turns two safe-by-convention holes into safe-by-construction, and kills the false audit trail.
  2. Every outbound lane passes the same pipeline — content checks move to the gate, or the gate refuses a message nothing has graded. This closes the cadence bypass and makes this page's harness the single test surface for suppression and content.
  3. A check never writes customer prose an engineer typed mid-sentence. Its options become: block and escalate using curated copy from one reviewed place, ask Clara for another draft under the constraint, or delete the offending clause with no replacement. Splicing a literal into the middle of her sentence is retired. For the promise check specifically, the cheapest fix is configuration, not code: the robot line is the fallback for "no owner on file", and that field is empty at every property but the bench.
  4. Every altering check is visible — a before-and-after record for each rewrite, a trace entry whether or not it fired, and all sixteen listed in the review screen.
  5. No permanently disarmed checks. Two are switched off by default and have never fired in production. Arm them behind the harness, or delete them.

Appendix — seed corpus for the future harness

An adversarial review of the mute and gate behaviour as it exists today ran independently — its job was to find ways a message could still reach someone who should hear nothing. The findings have landed: 38 cases, each with the exact place in the code and a recipe to reproduce it, sorted into leaks, ways around the gate, weaknesses, and things already covered.

The machine-readable corpus lives at ~/agents/006/mute-red-cases-2026-08-29.md — one entry per case with its anchor and reproduction recipe, ready to be turned into tests rather than re-derived.

The split matters. Anything that is a genuine defect in the mute already merged is being triaged now for immediate failing-test-first fixes — those do not wait for next week. Everything else becomes the seed corpus for this harness.

The classes worth naming, one line each:

PropFlow Docs