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
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.
Approve this plan?
Two open questions are in the last fold, each with a recommendation.
10% from Oct 24 · 20% from Oct 31 · 50% from Nov 7 · 100% from Nov 14. Today's % is worked out at the moment a message is decided, from today's date and the steps. There is no cron and no sweeper, so nothing has to wake up to move the ramp.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.
| Covered from part 1 | Not 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.
reviewBy field on its registry entry, set in the PR that adds the gate. Most test gates are still hosting settings that aren't in the registry yet. Until they are, their date goes on their row in the switch inventory, and the command reads it from there. Claude picks the date, and it doesn't have to be exact.npx tsx scripts/gatekeeper/gates-due.ts, prints every gate past its review date, plus every ramp at 100% that is "ready for Live". It shows the company, the building, the gate, the date and who set it. The same list is a filter on /admin/customers ("Review date passed"). The command never writes anything.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.
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 }
foldEffective. A building's own plan beats its company's. The reader uses the same fold as every other setting and never restates "building first, then company".ATTACH#setting#). There is no new field on Organization or Property. The company's Supervised marker stays where it is (settings.moduleModes).ADR-0144 §7 asks for the registry row in the PR. Here it is:
| Gate | scope | kind | owner | source | default | visible | description | decision |
|---|---|---|---|---|---|---|---|---|
gate.plan.<module> | building | feature (under its module) | System admin | settings registry | no 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) |
| Part | What ships, end to end | Dark until |
|---|---|---|
| 1 | The 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. |
| 2 | Multiple 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. |
| 3 | reviewBy 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.
runOutboundSafetyChecks), and the hallucination guard, run inside Clara's loop on the finished draft. That happens before the Supervised hold is asked (both are in conversation-manager.ts; the safety checks come first, then the hold hook). So a released reply has passed every guard that a Live reply passes. The hold was never the fair-housing control. Part 1 adds a test that pins this order.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.