A record with no building attached must never change what Clara does; who someone is comes from their role at the company. What breaks today, the design, the local proof, the migration.
2026-11-02 · Approved by Fede, Nov 2 (Option B); chain merged Nov 3; live proof pending · local POC and harness built, zero CI, nothing in src · claims audited Nov 2 (20 hard, 2 estimates labeled, 5 unverifiable noted) · rule: Fede, Nov 2 (~/.claude/CLAUDE.md) · related: coworker loop · Gera's portfolio model · testing playbook
Today 107 places in Clara's core logic quietly stop, or skip a safeguard, the moment a record has no building attached. Hugo's email reply was one. Nothing is wrong with the records: company-line calls, call-summary notices and staff threads have no building by design. The code treats "no building" as "no idea who or what this is". Fix: one shared answer to "which company, which building, which roles" that every flow uses, org first, building when known, roles always; a check that blocks new early exits; and the existing guards removed in six small PRs proven by replaying real records.
Recommendation: Option B. Consolidate the five half-resolvers that already exist into one, add the ratchet, bind the matched building to a voice call once the caller confirms it, and migrate the guards flow by flow. All of it is fix work: no switch, live everywhere on merge, proven on the ladder. Five groups need a decision first and are parked below.
What happened to Hugo, before and after
Sunday Nov 1, 4:02 PM MT (record time 2026-11-01T23:02Z; all times Mountain, the company's zone), David called the Situs main line about garbage behind 1683 Alton, unit 110. His number matched no tenant, so the call sat on the company's placeholder "front door" row. At 4:04 PM Clara emailed a call summary to the shared leasing inbox. The notice carried the company, David and the call, and no building. Monday 7:43 AM MT Hugo, the Situs admin, replied from his own address.
Before (production, Nov 2)
The staff check asked "does this notice have a building?" first. It did not, so the answer was "not staff" and Hugo's role was never read. His reply fell to ordinary triage, which guessed a building from the word "Alton" and opened a brand-new thread there.
Clara: "I can't place calls, and I don't see who 'him' is on this thread."
conv_voice_950df6d1 (David) · tn_7e6e9f2b (notice, no property) · msg_cfda4895 (Hugo) · 14fcb780 (the blind thread)
After (rule applied; door fixed in #24489, merged Nov 2)
The check reads Hugo's role at the company first: org admin, active. He is staff. The reply lands on David's call thread, so "him" is David and the number, the address and the complaint are all in view.
Clara: "Got it. I'll text David that the city inspection is today and the area behind 1683 Alton needs to be cleared. I'll let you know when he answers."
The second half, relaying the team's answer to the renter, still needs a building today (PR 3 below). The "after" text is the target behavior, not a production record.
How scope is carried today: one call, three answers
The same call can be "about" three different buildings depending on which code path looks. Hugo's Oct 28 test call showed all three at once: the escalation email used the matched building (correct), the after-call emergency check ran only where the dialled line's Maintenance setting read Off, and on a company line that setting is the placeholder's, not the home's, so the on-call tech heard "Maintenance is off here" about a building where it was Supervised (Gera's words, #23497, merged Oct 29), and the daily hold report showed "Situs Group" with no unit.
How big it is: 107 places
Three numbers, kept apart. A text search finds 1,591 lines that mention a missing property. Five read-only inspectors read about 2,300 lines and sorted each one. 25 of the worst were re-read by hand; all held.
Class
Meaning
Lines
Verdict
(a) bug class
The record has no building by design (company line, call summary, staff thread) and Clara stops or degrades
107
Fede's rule: fix
(b) integrity fence
A building id is present but the row could not be read
260
legitimate, keep
(c) building operation
The step is about one building (its unit list, its calendar)
826
legitimate, keep
(d) unclear
Needs a reader with more context
29
triage in PR 1
Inside the 107, two shapes. About 95 are drops (the split is an estimate: 8 inverse rows are tagged, about 4 more are described in prose): nobody paged, no notice sent, the team's answer not relayed, a tool refused. About 12 are the inverse: a missing building turns a safeguard off, for example the company's "maintenance is off" switch is not applied and property-less notices skip the Supervised hold. Where the 107 live: escalation 18, tools 14, voice integrations 12, agent loop 13, conversations 5, maintenance 5, leasing 5, the rest spread thin.
The ten worst, in plain words
"Get me a person" reaches nobody on a company-line call or thread, although a company-level contact exists.
Forward to the manager marks the thread escalated, so Clara goes quiet, and nobody is emailed.
Hugo's next failure: now that he is recognized, relaying the team's answer to the renter goes through a step that needs a building. The company is computed three lines earlier in the same function and not used. Replay on Nov 2 (#24757) ruled this one out: David's call sits on Situs's front-door row, so the company and the line are the same before and after; the fix still applies to threads with no building at all (Fairhaven: 176 relays that were written and never sent now go out at the company).
A new message on an escalated thread with no building is told to nobody, with no log line.
Verified residents calling the company line are refused their balance, lease and work orders. Violeta (Situs, Oct 31) was greeted by name and still sat on the placeholder. Nov 3 correction (#24772 replay): her tools already worked at her building; she was lost at call start because the phone lookup drops co-tenant rows (162 of 534 Situs residents are co-tenants). Fixed as PR 4c.
No turn-settled event on property-less threads: no classification, grade or maintenance signal.
Clara's spoken promises on company-line calls are never checked.
A tour agreed on a transferred call is never booked or recorded.
Daily team note, staff texts, "message taken" text return "no inbox" silently.
A guard-blocked reply on a property-less thread gets no review card, no page, no row: the hold queue is keyed by building.
A hidden second wall: the step that pages a human about a dead call or a broken promise gets the company only from the building. Fix the checks in front of it alone and the drop moves one step down, so PR 2 fixes both together.
Identity: roles are the answer, phones are the gap
Roles already live as rows scoped to the company, a group or a building, and the lookup by email at the company exists (it is what the Nov 2 fix uses). Two facts from production decide the design:
Company
Buildings
Active staff
Active staff with no phone on file
Situs Group
53 (1 placeholder)
8
8 of 8
Western Slope
90 (1 placeholder)
9
1 of 9
No one at Situs can be recognized by phone today. Hugo's Oct 28 test call created a duplicate "Hugo" from his cell. 0 of 41 callers on the Situs line match a staff phone, which is impossible by construction. Deiby and Lee are email-only; Lee has no calendar.
Roles must beat a stale renter match. Jay, Western Slope's admin, called on Oct 8 with his phone on his staff row, and a prospect record sharing the number won. Clara told him she could only help prospective renters. That call had a building, so the rule is about precedence too: an active staff role decides, with or without a building.
Same bug on the clara@ inbox door (new case, Nov 2, 10:22 AM MT). Lee, Situs staff, replied "This is great, thank you!" to Clara's team intro email from his own address. The copy that reached the clara@ inbox was skipped by the spam classifier as a vendor sales pitch: no property or conversation resolved before the classifier ran, so his staff role was never checked. Fede ruled (Nov 2): a staff member replying "thanks" needs no reply, so the outcome (nothing sent) was right; only the stored label was wrong (spam instead of staff mail). No behavior change needed, label only; no PR. Ingestion cc5beaf3 and the Graph copy 5f71ca23 stay as replay case 11 with expected outcome "stay quiet, labeled staff". Related, not a property case: Eileen asking Deiby on a thread Clara was copied on (ingestion 318bfa8c) is fixed by the stay-quiet rule in Clara's speak decision (Situs Onboarding session, one PR, Hugo's msg_cfda4895 as the must-answer control); the Clara Coworker Vision session ships the threading fix for Clara's staff-facing mail.
First concrete case for the resolver: a staff email Clara sends at the company level gets no thread (Nov 2, from the Coworker Vision threading fix #24534). Today Clara's staff-facing email record builds its thread per building and address, so a send with no building attached (a call summary on the company line, a company-wide notice) gets no staff thread at all. Clara's reply tag then has nowhere to point, and a staff reply to that email still falls to the sender-filed path, which is how Hugo's and the Michael Jordan threads ended up blind. The threading fix merges as cut (byte-identical for building-less sends). Once Option B is chosen, the resolver gives every send an organization-scoped thread when no building is known, and the tag already accepts any conversation id, so the Coworker Vision side is a one-line follow-up. This is PR 1's first consumer. Shipped: #24743 (company thread per address for building-less sends; 195 notices threaded, 94/94 building sends identical) and the Coworker Vision reply-scope fix #24746 (a reply on a company thread resolves at the company, narrows to a building only when the reply names one; 116 at the company, 88 to a named building, old rule planted back: 0 changes).
People answer from their own email. Hugo replied from his own address, not the shared inbox. None of the pulled Situs staff emails (the notice replies and the 8-day team corpus) came from a person writing "via" the shared alias, so the design does not handle that shape.
The design: one resolver, org first
Not a new concept: the pieces exist in five places. The design merges them, which is also the only form the repo's one-source-of-truth rule allows.
Fail closed on the company, never on the building. "No company, no read" stays exactly as Gera's wall says. "No building" is a normal state that narrows nothing.
Bind on confirmation. The mid-call address match stays a pin until the caller says "correct", then it binds the row. Today "correct" changes nothing, which is why three readers disagreed on Hugo's call.
Coverage is not org scope. A building-scoped role or a line that covers a subset of buildings does not become org-wide because the resolver answered "org". Narrowing is the role's scope, not the resolver's.
Options and recommendation
Option
What it is
Pros
Cons
Verdict
A. Resolver + ratchet, migrate as touched
Merge the five half-resolvers, add the check, fix guards only when a flow is otherwise edited
Smallest first PR; no behavior change up front
The 107 stay live for months; Hugo's next failure (PR 3) waits for someone to touch that file
floor, not enough
B. A, plus bind-on-confirm, plus a six-PR sweep by flowrecommended
Everything in A, voice binds the matched building when confirmed, and the guards come out in six flow-sized PRs, each proven by replay
Fixes the ten worst in weeks; each PR is one user-visible effect; ratchet shrinks to zero
About 2,900 lines over six PRs; two groups need Fede's decision first
go
C. Always stamp a placeholder building
Give every record a "front door" row so a building id is never empty
No code reads change
This is what Situs has today: on the Oct 28 test call the placeholder's Maintenance setting answered for the home and the on-call tech was told "Maintenance is off here" (#23497). Hides the problem, breaks safeguards
the foil, no
Local POC and harness
Status: built and green, Nov 2, in a local worktree (branch fede/scope-resolver-poc, last commit f7695c9881), zero CI, nothing under src. One command runs it: npx tsx scripts/scope-resolver-poc/run.ts, exit 0 on the real code, exit 1 on each planted bug. 21 tests in 4 files green; typecheck clean on the directory. Harness is build hygiene; the proof path is the ladder in the next section.
What it does: ten fixtures from real, scrubbed production records (David and Hugo; Hugo's Oct 28 call; Jay; Violeta and Ruth; three more Situs company-line callers; controls with a building attached). It runs the real decision functions as they stand today, then the same decisions through a resolver shim, and prints decision | today | with resolver | rule says. Controls with a building must be byte-identical. Two mutation controls: re-plant the pre-fix guard and Hugo must flip back to "left to triage"; remove the org fail-closed and the harness must fail.
What was replayed
Result
Rows replayed (decision × fixture)
182
Rows with a real building on the record
40, all byte-identical today vs resolver
Outcomes that change on real records
6
notices with no building that now reach the company owner instead of nobody
5
hold-report label on a front-door call where the caller named a home: "Mayor Apartments" instead of "Situs Group"
1 (new decision 7 below)
Outcomes that change on derived or probe rows (labelled, never asserted)
31
Hugo's reply: pre-fix door / today's door / resolver
left to triage / routed to the notice thread / routed to the notice thread
Mutation control A: pre-fix guard planted back
1 violation · HUGO today (HEAD door): "left to triage", expected "routed to the notice thread" · exit 1
The resolver path still routes him, so the control isolates the guard.
Mutation control B: org fail-closed removed from the shim
3 violations · no-company inputs crash downstream (TypeError ×2, "org id is empty") instead of being refused with no_org · exit 1
Green run: 0 violations, three no-company inputs refused, one company-only input resolves.
Ratchet: baseline on main is 122 "if no building, stop" lines in 97 files, matching the inventory. Tree check green; a guard planted in a copied file, a guard in a new file, and a removed guard each turn it red.
Not replayable in the POC: 7 control reply rows had no matching notice in the pull; the "sender is one of our own addresses" check cannot pass on scrubbed senders; reply headers are synthesized; companies carry no time zone, so a property-less record has no hold clock on either side today (PR 1 adds a company zone, see decision 8). Results: scripts/scope-resolver-poc/results/ in the worktree.
Migration: six PRs, each proven by replay
PR1 one scope answer + org-level owner door + ratchet (no behavior change)
├── PR2 a person is reached ─┐ parallel
└── PR3 the team is told, and their answer gets back ─┘
└── PR4 recognized on the company line (residents, vendors, staff by role)
├── PR5 after the call (promises checked, transferred tour booked)
└── PR6 turn settled + company context on text turns
PR
Plain words
Est. lines
Proof before merge
1
One answer to "which company and building"; the org rung reachable without a building; the ratchet
~540
Replay of every live customer's records: zero differences
2
"Get me a person" pages the company owner; forward-to-manager emails someone
~510
Replay: differences only on property-less records, listed in advance; Willows company line: an owner email arrives
3
New message on an escalated thread is announced; the team's reply reaches the renter; daily note and staff texts go out
~460
Same; Willows: a staff reply reaches the test renter
4
Residents, vendors and staff on the company line recognized by role; role beats a stale renter match
~510
Same; Willows: a known resident and a staff member call the company line
5
Promises checked and transferred tours booked on company-line calls
~510
Same; Willows: tour agreed on a transferred call is booked
6
Turn-settled event and company context on property-less text turns
~400
Same; Willows: a text turn logs the event
Estimates scale the Nov 2 fix (57 lines per guard including its test) and are estimates, not measurements. Six PRs instead of one because about 50 sites across six independent flows cannot fit under the 1,000-line gate; each PR is one user-visible effect. All six are fixes: no switch, live everywhere on merge.
Decisions for Fede
TL;DR: one yes unblocks the work: go on Option B, and PR 1 (no behavior change) opens within the hour. Decisions 2 to 8 are needed only before PR 3 or later and can come as the chain moves.
Chain status (Nov 3, 2:45 AM MT): all 16 PRs merged. 1a #24692, 1b #24732, 1c #24695, 1d #24743, 2a #24742, 2b #24782, 3a #24757, 3b #24767, 4a #24764, 4b #24772, 4c #24790, 5a #24765, 5b #24763, 6 #24759, plus the live-run fixes 3c #24842 and 3d #24856 and the replay script #24828. Every change is a fix with no switch, proven by read-only replay of real records (11,6k conversations and 553 notices unchanged where a building exists). Live rung: not yet run. Willows testing is closed; the voice provider ran out of credits on Nov 3 (every call failing since 11:23 PM MT; purchase pending). A text proof at Fairhaven was tried and stopped before sending: every Fairhaven text or email thread gets a building stamped by the number map, so the no-building records there are only voice calls and video sessions. The live proof therefore runs as voice calls once credits are back. The staff-reply leg ran live on Nov 3 (after #24767 and #24746 deployed): a Fairhaven leasing agent who was not on the notice replied by email on the one no-building thread and was admitted by their company-level role; the note did not fit the call, so Clara held a one-line ask back to the agent and sent nothing to the caller, with no "I don't see who"; a second note with no question (a "?" only inside a signature link) got no reply. A third note that fit the call ("can you tell her what the next step is on that faucet?") got Clara's own answer in the same thread within 30 seconds, naming the caller and the faucet with no building on the record: the original Hugo/David failure, now passing live. Three defects found by the run, all filed: the hold was filed under the company's first building (#24835); marking a no-building call handled did not clear Clara's pending question (fixed, #24842); a teammate's follow-up on a handled call was dropped silently (fixed, #24856). Both fixes were proven live the same morning: "handled" closed the notice and withdrew the pending question, and a follow-up on the closed notice got Clara's answer in the same thread within 25 seconds. Voice proof (Nov 3 morning, credits restored, three robocalls to the Fairhaven company line): the owner page at company level, staff recognition with no building, and a tour booked at the home the caller named all passed live. One gap found: the after-call promise check only knows the "pinned home" that the maintenance tool writes, so a tour booked at a named home still left it saying "no home on the call"; fix PR 5c (#24873) merged and deployed Nov 3 17:00Z; its live call could not reach Clara because the new Clara front door (Gera, #24820) timed out waiting for ElevenLabs on every Fairhaven call after 15:07Z and rang the test staff line instead; handed to Gera's side, live proof pending. Not yet exercised live: a tour agreed during a transfer to a person, and a co-tenant calling in (no Fairhaven persona for either without new test data). Open follow-ups: #24697, #24698, #24744, #24760, #24769, #24770, #24773, #24774, #24793 (direct voice path skips co-tenant occupancy), #24822 (company-notice thread copy). Stamp: "Approved by Fede, Nov 2 (Option B); chain merged Nov 3; live proof pending."
New decision from PR 6 (#24759): company-level context for Clara's prompt (settings, knowledge, hours) on front-door threads is NOT in the chain: it changes how the prompt is built and needs evals plus a choice of which company record stands in for a building. Recommendation: separate follow-up with evals, after the chain. Same for tagging front-door threads as 'front-door' in the settled event (today they report 'none' so live behavior is unchanged).
Go on Option B?Fede, Nov 2: yes, "a clean refactor of this", "lots of testing". PR 1 (foundation, no behavior change, proven by a shadow diff of the new resolver against the four old ones over every live customer record) opens now.
Company-level holds.Fede, Nov 2: "not sure if we need that"; no new queue surface is built. A property-less reply follows the company's own hold setting through the existing queue; revisited only if a real property-less held reply shows up. The review queue is keyed by building, so a property-less held reply has nowhere to go today. Options: (i) a company row in the queue, shown on the company page recommended; (ii) file it under the placeholder; (iii) never hold property-less replies, send or drop. Needed before PR 3.
The 12 inverse cases. Closing them makes Clara do less on company-line threads (the company's "off" switches and Supervised holds start applying). Recommendation: yes, they are safeguards and should apply.
Building-scoped staff on the company line. Today only company-scoped roles count as staff (the inbound-resolution code's note, attributed to your Oct 15 ruling). Recognizing a building-scoped leasing agent who calls the main line widens who counts. Recommendation: recognize them as staff of their building only, never company-wide.
Admin visibility of property-less records is sign-in and permissions code, Gera's territory. Recommendation: hand to Gera via the bridge with this page.
Company-level on-call. The emergency chain is per building on purpose. Recommendation: leave as is; PR 2 pages the company owner, not the emergency chain.
Reports use the matched building. The daily hold report labels a front-door call by the row ("Situs Group"); the resolver labels it by the home the caller confirmed ("Mayor Apartments"). Recommendation: yes, reports read the same resolver as everything else.
A time zone on the company. Companies carry none, so a property-less hold has no clock today. Recommendation: add an organization time zone in PR 1, defaulted from the company's buildings when they agree, set on the company page otherwise.
What this changes in existing rules
Repo manual, multi-tenant section: "API routes take propertyId and return 400 when absent; domain functions take it as a required parameter." Amend to: the company is required (the wall); the building is optional wherever the flow can run at company level. The customer-identifier fence is untouched.
Repo manual, org resolution: anchors on the building ("tenant.propertyId → property → org"). Add anchors for property-less entities: the notice's company, the membership's company, the parent's.
Coworker loop page, gate 84 "no property: stop", and the precedent of "hold, don't drop": superseded by "proceed at company level".
Coworker PR 2 (#24494): falls back to the company's first building when nothing names one (after the conversation's property, the notice's, and a building named in the reply). That last fallback is the seam the resolver replaces; nothing else in it depends on the choice. Its cousin (#24496, failed read falls back to first building) is the same shape.
Gera's portfolio model: no conflict. "The org is the wall", "a building without its own answer inherits the org's", roles as company rows, a resolver that "walks building → org, nearest wins" are what this rule leans on.
Records: read-only production pulls, scrubbed (phones to +1000, emails to example.test, first names). Record ids on this page are real. Hugo's reply (7:43 AM MT) came 46 minutes before the fix merged (8:29 AM MT, per GitHub); whether an earlier deploy was live was not checked.
Overlap checked (cross-session messages, Nov 2, in session 009's transcript): Clara Coworker Vision (session 006) owns the notice door and PR 2; this page references and does not touch it. Situs Onboarding (session 004) confirmed no overlap and asked for the three identity cases above. Expected Situs events during the pull (Sunday intro emails, Patricia's tour ask, one open escalation) are not findings.
Not established: why Violeta stayed on the placeholder while Ruth attached to her building (one visible difference is a lease id's shape, a guess until replayed); why Jay's Oct 13 calls took the staff path when Oct 8 did not.
Inferences are labeled inference on this page; everything else traces to a record id, a file and line, or a command.