Gatekeeper rollout plan

Let 5–10% of Supervised conversations go out on their own, ramp it up on a schedule, and give temporary gates a review-by date. A plan for Gera to approve. Nothing is built yet.

2026-10-22 · plan only, no code · builds on ADR-0144 and #21757

The idea

A Supervised module gets a ramp: 0% means everything is held, as it is today. 10% means one conversation in ten goes out without waiting, and the same thread always gets the same answer. 100% means everything goes out, and then a system admin switches it to Live with one click.

The ramp moves up by date. Rejecting a held message pauses it. Supervised and test gates can carry a "review by" date. When that date passes, nothing happens and no alert goes out. Claude can list the overdue ones when asked.

Three PRs, each under 300 lines

  1. One fixed %: Clara's text and email replies are released at a stored % for one module at one company. Released replies are labeled on /review.
  2. The schedule: steps by date, a rejection pauses the ramp, and the plan is shown and edited on the customer page.
  3. Review-by dates: a "which gates are overdue?" command and a filter on the admin customer list.

Your call

Approve this plan?

Two open questions are in the last fold, each with a recommendation.

1 · How the ramp works

What a released message looks like

Both send doors, for texts (twilio/client.ts) and for emails (company-live-gate.ts), refuse an unapproved Clara message at a Supervised building, and they page someone when they do. A released reply that reached a door without an approval would be lost, not sent. To avoid that, a released reply is held exactly as it is today. The hold row is then marked at once as released by the rollout (10%), and its id is passed to the door as the approval. The doors don't change, every release leaves a record, and /review can show and filter it ("Released automatically · 10%"). That's what lets reviewers spot-check what went out.

What it covers, and what it doesn't

Covered from part 1Not covered (stays fully held)
Clara's text and email replies, in every module, that go through her conversation loop. Those all pass one rule, decideSupervisedReplyHold, so one check there covers them.Two maintenance replies that skip the loop: the router's quick acknowledgements (the first-rating and first-answer replies, held after the loop) and replies to the in-house handyman. They are held at their own sites, and they join in part 2.
Outreach Clara starts herself (renewal offers, past-due reminders, prospect follow-ups), owner reports and team notices. Each can join later, one lane per PR.
Property-software writes (held turnover and new-lease writes, HeldPmsWrite): always held, whatever the plan says. A wrong charge in AppFolio costs more than a slow reply.

Urgent repairs skip the hold today (skipsSupervisedHold), and that stays as it is.

2 · Review-by dates (expiry)
3 · Who sets it

A system admin, meaning Gera, Fede or Sean. This uses the check #21757 just made standard: requirePlatformAdministrator, which reads the staff platform-admin roster. The plan is written through a route under /api/admin/companies/[id]/…. admin-scope-inventory.drift.test.ts already fails CI on any route there that skips that check, so no new permission code is needed.

The usual flow: you tell Claude "start Maintenance at Situs at 10%, then 20% next week". Claude proposes the steps and a review-by date, and the plan is saved through the admin page under your name. Starting a ramp at a real company turns sending on for part of its conversations, so it needs a system admin's word each time. Once a plan is saved, the steps move on their own, because the plan was the approval.

4 · Data model, the smallest change

Add one new registry key, with one slot per module: gate.plan.maintenance, gate.plan.leasing.prospects and so on. This follows the existing slot pattern (vendor_roster.plumbing). It is a PARAMETER key, writable at the company and building levels. An absent row means "no plan", which is today's behaviour. The value is:

{ steps: [{ percent, from }], cleanDays?, lastRejectAt?, reviewBy?, startedAt }

ADR-0144 §7 asks for the registry row in the PR. Here it is:

Gatescopekindownersourcedefaultvisibledescriptiondecision
gate.plan.<module>buildingfeature (under its module)System adminsettings registryno plan (everything held, as today)yes (see Q2)While the module is Supervised, a share of conversations goes out without waiting for a person.(a)
5 · The three PRs
PartWhat ships, end to endDark until
1The registry key, with one fixed % only (one step). The pure pick and today's-% functions. The check inside decideSupervisedReplyHold. The "released by the rollout" hold state, with its id passed to the send doors. A /review label. The system-admin route, plus gates-due --show to read a plan back.A system admin stores a plan. With no plan anywhere, behaviour is byte-identical to today.
2Multiple steps, with clean days. The reject path records lastRejectAt. A "Rollout" select row under the module on /admin/customers/[id], built on FeatureSelectRow the way "Handyman texts" is. A "Released automatically" filter on /review. The two maintenance reply sites that skip the loop (quick acknowledgements, handyman replies) go through the same check.As in part 1.
3reviewBy on the plan and on registry entries. The drift test that refuses it on plain features. gates-due lists overdue gates and "ready for Live" ones. The filter on the admin customer list.Nothing to turn on; the command only reads.

Each part branches off origin/main, and up to three are open at once. Part 1's PR body carries the ADR-0144 §6 census, read from prod with the real readers. Because no plan row is stored anywhere, the expected result is that no building changes. It also carries the Dark: gate.plan | feature | proof | turn-on: system admin line.

6 · How we prove it
7 · Risks
8 · Two open questions (with a recommendation each)

Q1. When someone rejects a held message, should the ramp pause at its current %, or drop back one step?

Q2. Should the customer's own team see the % on their settings page ("Supervised · 10% go out on their own"), or only our staff?

Already decided on this page, not asked: property-software writes stay fully held, and 100% never switches a module to Live by itself.

PropFlow Docs