Onboarding plan — Western Slope + Situs

Updated Sep 8, 2026. This is the one page for the initiative: roadmap and progress, the shared-line decision, the audit, and the plan. There is no separate tracker.

Who does what, in what order, what is decided, what still needs a call. Fede: leasing. Gera: maintenance. Rest by who built the module.

Refreshed 4:29 AM MT 2026-09-10 (every pull-request state re-read from GitHub) · Compact top rebuilt 2026-09-08 evening · Status: in active use — this is the page the team works from, not a proposal awaiting review (it was written 2026-09-05 as one, and carried that label until 2026-09-09) · owners Fede + Gera · supersedes nothing; links to the Situs hub and the client intake drafts below

Progress

Live tracker (derived from the pull requests, one group per epic): Western Slope go-live tracker. Status on every row there is re-derived from GitHub, not typed by hand — this page no longer carries its own copy of "% complete."

Western Slope portfolio go-live: engineering areas

AreaDoneLeft
Company and property modelWestern Slope company exists; Fede's login worksImport their 14 properties; merge the two Western Slope companies
Keeping companies' data apartStopgap live, proven with an outside loginStep one: every read names the company, enforced in CI; step two: database keys
Sign-inPreview second-factor skip, typed code and passkey-or-text all mergedNothing outstanding
Customer-facing UIWizard, modules-only view, property page, and the Customers back office all merged (#7313, 8:53 PM MT)Nothing outstanding — the two Settings fixes were settled during the day: company-name prefill merged, billing card hidden rather than trimmed
Shared phone line across 14 propertiesTest line live on one placeholder property; directory transfers built darkDecide where a shared line binds a property (X1); fallback number; missed-call email switch. Merge plan for the prototype agent onto the shared fleet: voice decision page §11 (proposed 2026-09-08)
Listings for a portfolioSingle-property scraper live14-property importer not started; API ingest when the key arrives
Shared mailbox across 14 propertiesPer-property mailbox connect exists; calendar connectedRoute each lead email to the right property by address, not started
Knowledge basePulled; review page built; save-wipe bug fixedRetired-answers fix held; per-property vs portfolio knowledge shape
Guest cards and AppFolioReports-feed ingest built; write-back built, on at Willows onlyFirst-contact guest-card creation not built; write-back not in this launch
ToursBooking works; two-home fixes shipped; cancel-with-two-booked merged 2:40 PM MT (#7369); the stated-name rule is pinned by tests (#7382, merged 3:14 PM MT — tests only, no behavior change); a home is confirmed only when its own booking succeeded (#7381, merged 4:08 PM MT, prompt half live on the line)Two homes on one street still cannot both be booked — the script and the calendar contradict each other, and one has to change; a live two-home call to prove the receipt text end to end — the receipt fix itself merged at 6:56 PM MT and is in production (#7406), but tonight's robot budget is spent, so the proof is owed tomorrow. It was the same shape as today's other two: what Clara says and what reaches the person disagree. Both halves of the confirmed-over-a-clash fix are live as of 5:57 PM MT (#7381, open); Jay's second tour missing
TestingPortfolio bench built, 22 bugs foundOne Twilio test number to unblock six scenarios

Situs: not started. Company record, API access, test property.

Gera: maintenance lane status unknown; 31 other open pull requests (re-counted 1:20 AM MT Sep 9).

Needs you

  1. The stated-name switch on the prototype line. Greeting a repeat caller by an old name is not a bug: the product already replaces a stored prospect name with the one given on the call, behind a per-property setting that is off by default and off here. Turning it on is an activation — one dry-runnable script line. Your call.
  2. The voice merge plan (added 2026-09-09). Six multiple-choice decisions with recommendations on the voice decision page §11.5, plus one go: import the harness number into ElevenLabs. Drawn version: Voice merge, drawn.
  3. This morning's three asks are all answered and closed out.

Everything below is detail. Open a heading only when you want the why.

Focus now — the four things that matter today

What is on the critical path and unblocked right now — for Western Slope's go-live in the week of Sep 21, and for getting Situs started at all. Row-by-row status is on the go-live tracker; the week view further down is the plan.

1. Get Western Slope's leasing mailbox connected (Jason/Kat one-time sign-in) owner: Fede

Done looks like: Jason or Kat has clicked through the one-time Microsoft sign-in on leasing@westernslopepm.com, and mail starts routing through it automatically — no code change needed on our side.

Next action: per the 2026-09-08 audit, this is the single blocking item left for a guest-cards-only go-live — everything else Fede thought was outstanding (the tour calendar) is already connected. Get Jason or Kat to click the sign-in link.

2. Turn on the phone number Western Slope will actually use owner: Fede

Done looks like: their main number forwards to Clara, staff keep their own lines, and the team directory is in her hands.

Next action: send the reply already drafted and waiting in Gmail to Jason.

3. Create the Situs company record owner: Fede

Done looks like: Situs has a company of its own instead of sitting under JP&Co, with the modules they bought and their admin invited — the first thing on the Situs side to move all week.

Next action: add it through the back-office Customers screen, which merged Sep 8 at 8:53 PM MT (#7313) and was the only thing this was waiting on. (This card previously asked you to approve the company-data-isolation plan. That was answered on Sep 8 — approved, Gera owns it, not yet scheduled — so it is no longer an open ask.)

4. Decide the voice merge plan and start the bench owner: Fede added 2026-09-09

Done looks like: the six choices on voice decision page §11.5 are picked (what "one agent" means, the bench, cutover and rollback, how the mode is chosen, the cutover gate, the sequence), and the go is given to import the portfolio harness number into ElevenLabs so the bench line exists.

Next action: read the drawn version (five minutes), pick the six, say go on the harness-number import. Nothing touches Camellia's line or the 970 line until the cutover step, which is a separate go.

Also outstanding whenever you get a minute, not urgent enough to bump the three above: paste the drafted Stripe setup email to Sean; forward Jason's team directory (names, direct numbers, roles) the moment it lands — the in-hours "talk to a person" transfer is built and waiting on the leasing desk's direct line.

What each epic means by "done"

One line per epic. This is the bar each area is measured against; week-by-week progress is in the timeline above, and row-level status on the Western Slope go-live tracker.

EpicOwnerDefinition of done
Company and property modelFedeAll 14 properties under one company; Jay/Jason sign in and see only Western Slope
Data isolationFedeNo company ever sees another company's rows, on any path, enforced in CI
Sign-inFedeTyped code is the default; passkey or text is the second factor; clean on real inboxes
Customer UIFedeA customer admin finishes the wizard and sees only their own modules and properties
Shared phone lineFedeOne number answers all 14 properties; transfers by name; no dead air on outage
Listings for a portfolioFedeUnits and availability refresh automatically for all 14 properties, no manual entry
Shared mailbox routingFedeEvery lead email lands on the right property's guest card automatically
Knowledge baseFedeReviewed by Western Slope; every leasing question answered from it; retired answers never spoken
Guest cards and AppFolioFedeOut of this launch — write-back rollout and first-contact creation are withdrawn
AppFolio writes per companyFedeEvery write Clara can make into AppFolio — guest cards, renewals, work orders, new-lease send, move-outs, month-to-month, units — carries the company and is proven to land only in that company's database; the browser robot has no JP&Co default left; each client has its own Clara login, saved session, and text-code number; a nightly login-and-isolation check is green for JP&Co and Situs; Camellia unchanged throughout. This gate is met BEFORE the first write for Situs Group or Western Slope. First feature on top of it: a phone call from a caller not already in AppFolio creates the guest card; email and web leads are untouched.
ToursFedeAny number of homes booked, rescheduled, or cancelled correctly on a real call
Testing benchFedeAll portfolio scenarios run green on the bench before any property change
Maintenance intakeGeraEvery inbound maintenance scenario handled end to end, two clean bench sweeps
Western Slope: what lands each week

What "done" means: Jay is signed in and sees only Western Slope; their calendar and mailbox are connected; they've reviewed the knowledge base; and Clara is answering the leasing line live on their forwarded number with the missed-call email switched on. The table below is the week view: what each area should land this week, next week, and by go-live week. It is a plan, not a status read — for status, use the go-live tracker, which derives each row from GitHub.

AreaThis week (Sep 8–12)Next week (Sep 15–19)Go-live week (Sep 22)
Company and property modelMerge the two Western Slope companies; Jason's properties imported Tuesday14 properties under one companyDone
Data isolationApproved Sep 8, not scheduled (Gera owns it)Stopgap holdsStopgap holds
Sign-inDone — typed code and passkey-or-text both merged Sep 8, 2:02 and 2:15 PM MTDoneDone
Customer UIDone — back office merged 8:53 PM MT, billing hiddenDoneDone
Shared phone lineFallback number; missed-call email on; directory transfers live once directory arrivesShared-line binding decision (X1)Live on forwarded number
ListingsScraper live14-property importerLive
Shared mailboxMailbox connectedRoute leads to the right property by addressLive
Knowledge baseTheir reviewGaps filledLive
Guest cards and AppFolioReports ingest once key arrivesNot in launchNot in launch
ToursCancel-both, #7381 and its eval #7395 merged and deployed; same-street booking proven on a live call (two visits written back to back); #7406 fixes the receipt naming one home; stated-name switch; Jay's tour fixed by handProven on real callsLive
TestingBench runs without a dedicated numberPortfolio scenarios greenSoak

Situs: not started. Company record, API access, test property.

Gera: maintenance lane status unknown; 31 other open pull requests (re-counted 1:20 AM MT Sep 9).

Decisions for Fede

Only things that genuinely need your call. Scheduling and go-aheads are taken by default and reported under Parked and FYI below. Row-level progress lives on the Western Slope go-live tracker.

DecisionRecommended answerWhy
Turn the stated-name switch on for the Western Slope prototype line?Yes — it is an activation, not a buildThe capability is built and now pinned by tests (#7382, merged 3:14 PM MT, tests only — it changed no behavior). It is gated by a per-property setting that is off here, so today a repeat caller's new name is not written. One dry-runnable script line, recorded in the handoff file. Caveat: even switched on, the greeting in the very first sentence still reads the record before any tool runs, so that one line keeps the old name.session 011
Spend money on the CASA Tier 2 security lab, so PropFlow can read a shared Gmail inbox (needed for a Situs-style leasing@ address)?Proposed, pending Fede's pick: see Google Workspace readiness for Situs GroupReading Gmail needs Google's restricted-scope verification plus a paid CASA lab (roughly $540-$1,000, 4-12 weeks, redone yearly). Everything up to the purchase can start now; the lab itself and the annual reverification are Fede-only.2026-09-15
Otherwise nothing open, as of 4:30 PM MT. Answered Sep 8: the retired-answers-on-calls fix — no, closed unmerged at 2:24 PM MT ("not carrying retired-answer logic on calls right now"; branch kept, reopen if wanted). The billing card — no, #7330 closed at 1:35 PM MT once Fede saw the screenshots, and the card is instead hidden from customers altogether (#7376, merged 2:14 PM MT). Parked as "not now": the Vercel preview test variable and a Twilio number for the test bench.

Leasing identity and household grouping — 2026-09-08/09

Status: Sealed 2026-09-10 — all permutations passing, switch removed

This section is about grouping people into households once a lead has already arrived. For how the lead gets to Clara in the first place, and who Clara replies as, see Integration map: how a lead reaches Clara, and who Clara replies as. For the redesign that puts this grouping logic inside one shared intake pipeline instead of two separate lead writers, see Lead-intake engine: where we are, where we're going.

What broke at Camellia

A prospect Clara already knew applied again through Zillow, which opened a brand-new inquiry in AppFolio. The sync only checks one "primary" email or phone per person, so instead of recognizing the returning prospect it opened a second, disconnected record for them. Their cosigner's application arrived separately, with no AppFolio group id linking it to the first — so nothing tied the two together. A feature added the day before only wrote a hidden "suggest a link" note that no one ever saw. On top of that, AppFolio's own "(Co-signer for [name])" text got saved directly into the cosigner's stored name. Net result: three separate rows for one household — a Camellia cosigner pair plus the returning prospect — and Clara's whole history with this lead was invisible on the row anyone actually looked at.

Owner decisions (Fede, 2026-09-08)

Changes merged 2026-09-09

Change #What it doesLive or dark?Switch
#7417Co-applicants on the same unit link into one household automatically during sync; the old hidden "suggest a link" note is deleted for good.DarkautoLinkCoApplicants, off everywhere
#7420A new application from someone Clara already knows now updates their existing record instead of creating a second one; a first-and-last-name check guards against fusing two different people together; also fixed a bug that could let a household claim two "primary" people at once.Darksame switch
#7424AppFolio's cosigner annotation is stripped down to a role tag and never saved into anyone's name.Livenone
#7418Read-only, scrubbed data-mining tool: pulls real records from JP&Co (live API), Situs Group, and Western Slope (existing read-only dumps).Read-only, no writesn/a
#7419, #7423, #7425, #7426Test harness that streams those real records through the actual grouping engine and grades the result against 8 known failure types; four bugs found and fixed in the harness itself.Test-onlyn/a
#7427A clean, reversible way to test at the Willows without leaving anything behind.In reviewn/a

What the real-data test run found

After the four harness bugs above were fixed, replaying real records from all three companies through the grouping engine found:

Size of the corpus mined, for reference — guest cards: 81 / 883 / 835; applications: 48 / 90 / 111; tenant records: 188 / 1,341 / 260; people: 216 / 1,890 / 941 (JP&Co / Situs Group / Western Slope).

Next

  1. Ship the test-bed's Gmail dot/plus-tag parity fix, and file the guest-card contact-details-change follow-up ticket.
  2. Re-pull the JP&Co corpus with unit ids included.
  3. Prove the fix end to end at the Willows with real guest cards and applications, covering: one applicant alone; a cosigner AppFolio already grouped; a cosigner AppFolio did not group; a known lead reapplying with a dot-variant email address; a family member sharing a phone number; two different people who happen to share a name; someone who withdrew and reapplied; and roommates. Blocked on Fede publicly listing Willows units 101 and 102 with a zero application fee, so the test applications can actually go through.
  4. Once that proof is clean, ask Fede for the go-ahead to turn the switch on at Camellia.

Willows end-to-end proof, first pass (2026-09-09)

The eight permutations were run as real AppFolio applications at the Willows test property and checked in the production database. AppFolio's public application form blocks automated submission at the final step, so applications were created from the staff side; two cases that only the public form can produce (the cosigner name leak and a co-applicant invite) are queued for a human to submit.

PermutationResultNote
Single applicantFailIts record was taken over by the shared-phone case below and pulled into a stranger's household.
Cosigner grouped by AppFolioPassOne card, one household, names clean.
Cosigner not grouped by AppFolioPass for the pairThe pair grouped correctly, but the household also swallowed unrelated applicants on the same unit.
Known lead applies later with a slightly different email and new phonePassSame person, original record advanced to applied, Clara's conversation kept, no second record.
Family member sharing a phoneFailMatched by phone before names were compared; two people fused into one.
Two strangers with the same namePassSeparate people, separate households.
Withdraw then re-applyPartialNew application syncs cleanly; the withdraw step needs a human click in AppFolio.
Roommates grouped by AppFolioPassOne card, one household.

Engine bugs found (none has reached a customer through the new grouping, which is only on at the Willows; the phone match is older code and is live):

  1. A phone or email match adopted an application without checking the name. Fix in progress: names must agree, otherwise a separate person is created.
  2. Grouping without an AppFolio group id treated every applicant on the same unit within three days as one household, fusing competing applicants. Fix in progress: grouping needs a real relationship signal (cosigner-type application, shared contact or address, or AppFolio group).
  3. Leads created from email store the unit as a bare number while applications store the property-scoped id. Fix in progress.
  4. Newly created units were not getting their AppFolio id from the periodic sync; being verified.
  5. Deleting a person left its duplicate-check markers behind; 21 such orphans at the Willows silently stopped every sync there. Repaired at the Willows; fix in progress so deletion removes the markers, the sync heals an orphan on its own, and one bad record never stops a whole property.

Next: fixes deploy dark, the same permutations re-run at the Willows, then the per-property switch is deleted so the behavior is global.

Round 2 (same day, after the first fixes)

Engine at commit 114b08f:

PermutationResultNote
Single applicantPass
Cosigner grouped by AppFolioPass
Cosigner not groupedPass for isolationStrangers no longer swallowed, but the pair itself not linked (the "(Co-signer)" text is not used as a signal).
Known lead applies laterPass
Family member sharing a phonePassTwo people, correct; not placed in one household.
Same-name strangersPass
Withdraw then re-applyPass for isolationCancel click needs a human.
Roommates grouped by AppFolioPass

Side findings: Nine applications, nine households, no fusion anywhere. Email leads stored a blank unit before the application; one old Willows row kept colliding in the guest-card sync.

Round 3 (same day, after the cosigner/shared-phone fix)

Engine at commit a9353b39, after PR #7464:

PermutationResultNote
Cosigner grouped by AppFolioPass
Roommates groupedPass
Same-name strangersPass
Withdraw/re-apply isolationPass
Known lead applies laterPass on identityUnit still blank at intake.
Family member sharing a phoneFAIL, regressionSame surname with a different first name fused back into one person (round 2 had this passing).
Cosigner not groupedFAILThe marker still never links the pair.

Status: Guest-card sync job now runs clean; no household absorbs a stranger. Fix in progress for all three, then round 4; the per-property switch stays until a round passes clean.

Round 4 (2026-09-10, after the round-3 fixes and the shared person-mint name guard)

PermutationResultNote
Single applicantPass
Cosigner grouped by AppFolioPass
Cosigner not groupedPassLinked as cosigner by the "(Co-signer)" marker, nothing else absorbed.
Known lead applies laterPass on identityThe unit is still blank at intake for a free-text "Willows Test - 101" body (fix in progress).
Family member sharing a phonePassSeparate person, same household.
Two strangers, same namePass
Withdraw then re-applyPass for isolationCancel click needs a human.
Roommates grouped by AppFolioPass

Every identity and household case passes on the fixed engine; the guest-card sync runs clean. The per-property switch is being deleted (PR #7483), so the behavior is global.

Leftovers being fixed: the email-lead unit at intake for free-text unit mentions; an empty ghost household left behind after a merge. Then a final round and the Willows cleanup.

Round 5 (2026-09-10)

All eight permutations pass. The lead email's unit — a free-text "Willows Test - 101" — now resolves at intake, before any application arrives. Left: empty household shells after a person is moved into another household, and one live duplicate membership.

Round 6 (2026-09-10)

No person holds two live memberships any more. The emptied shells from round 5 were retired, but not closed — they still showed as open.

Round 7 (2026-09-10)

Found a new, bigger hole: two sync runs for the same property overlapped — a manual trigger landed on top of the scheduled run — and every applicant got a duplicate live prospect row. The shell-close timestamp was also wrong. This is the duplicate-prospect bug reproduced through concurrency, not through a single bad run.

Round 8 (2026-09-10) — sealed

Per-property lease in the sync job: if a second run starts while one is already in progress, it skips and logs it instead of racing. Prospect rows are keyed on the AppFolio application id, so a concurrent duplicate lands on the same record instead of minting a new one. Empty shells close with a real timestamp, plus a sweep that catches any left behind. Result: all eight permutations pass, a deliberate overlap probe (triggering the sync twice on purpose) produced no duplicates, and the corpus replay below shows zero new ghost households against the baseline.

What sealed it

PRWhat it fixed
#7495Resolves the email lead's unit at intake, and the first ghost-fold rule.
#7546Retires the prior membership when someone is re-householded, and closes the emptied shells — gated by role so it never merges into someone else's deal.
#7576Fixes the close step to match the real row shape (it was matching the wrong one).
#7582The concurrency seal: a per-property lease on the sync job, and prospect rows keyed by a natural id so a concurrent duplicate lands on the same row.
#7583Close timestamp is now the real close time, plus a per-property sweep and report categories.
#7483Deletes the per-property switch — the fixed behavior is global, not opt-in.
#7470Regression harness fixes.
#7493, #7565, #7584Follow-ups that kept main green while this landed.

Corpus replay (PR #7470, head 0c64daa36, merged f5df1efd5)

Every real record from all three companies replayed through the sealed engine. Every failure class checked — duplicate person, duplicate row, split household, overmerge, household overmerge, a name leaking from one person's record into another's, a stale primary, a lost Clara conversation stamp, a dropped record — came back zero for every client.

ClientRecordsHouseholdsFailures
JP&Co (Camellia)317770
Situs Group2,3148100
Western Slope1,2067210

Command: npm run household:regress -- --client <name>, one client at a time — concurrent runs on the same machine exhaust memory and take each other down.

What is proven / what is not

Proven: the eight permutations at the Willows on real AppFolio applications and guest cards; the concurrency overlap probe; the full corpus replay above.

Not exercised by a machine — three AppFolio actions only a human can click, so they still need a person to run them: an online cosigner submitting through AppFolio's own invite (this is the pattern that showed the "(Co-signer for X)" name-leak shape); a public-form co-applicant invite; and cancelling an application from AppFolio's own UI, for the withdraw/re-apply case.

Deferred to its own pull request: deriving the household id from AppFolio's application-group reference instead of today's signals. That changes what counts as a merge, so it needs its own baseline argument rather than riding this one.

Cleanup: the PropFlow side is cleaned — test people and their rows are parked, reversibly. Listings 101 and 102 stay up. The eleven test applications and their guest cards at the Willows (JP&Co database, property 45) are left in place, by Fede's decision on 2026-09-10 — not a pending cleanup step. Now that the test property sits in its own portfolio (see the portfolio move, above), client staff can't see them, and they carry no ongoing cost. Delete them only if the Willows is ever re-parented to a different portfolio or property; until then, leaving them is the decision, not a gap.

Merged the same day from this proof: Orphan dedup-sentinel self-heal and per-record isolation (#7439), unit-id consistency and sync stamping (#7441), email-lead unit id and guest-card job isolation (#7455), sync write-conflict retry (#7462), guest-card contact re-stamp (#7463), co-pendency signals (#7464), review-reminder duplicates (#7466), claim-without-sentinel self-heal (#7468), regression command and skill (#7446), prospect-candidate name guard (#7475), name agreement in the shared person mint (#7476), post-merge row consolidation (#7478), round-3 fixes (#7480), bot findings on the name guard (#7479), a Person now holding more than one live value per claim (#7485), (#7486).

2026-09-11 — what the sealed engine missed, and the fixes

Trigger: Fede saw two "Application Review" rows at Camellia for the same unit (unit 120 — AppFolio applications 51 and 52, received 2026-09-04) still sitting separate on 2026-09-11, plus the Miller cosigner pair, and asked how that slipped through after the round-8 seal above. Three different things, found separately, below. Applicants below are identified by their AppFolio application id or role, not by name, except where the customer name (Camellia, Western Slope) is the useful label.

1. The unit-120 pair (applications 51 and 52)

AppFolio did not report a RentalApplicationGroupId (its own field for "these applications belong together") for either application on 2026-09-04 — so each one minted its own row and its own household, correctly, given what AppFolio was saying at the time. AppFolio only started reporting group id 8 on both applications around 2026-09-11, when it approved them. The sync picked the new group id up and stamped it on both rows correctly, but three separate rules then stopped the fold from happening: (a) the card-fold step refuses to fold a row that already owns a live card, (b) rows that already carry a group id are excluded from the co-pendency matching that would otherwise have caught them, and (c) the merge planner treated a bare, newly-arrived group id as uncorroborated and held rather than merging. Net effect: the pair was stuck split with no sync path left to reconcile it.

Why the corpus replay above showed zero: the harness's same-unit rule was retired on 2026-09-10 (#7470), and its ground truth for "should these be one household" is AppFolio's own grouping — so a pair AppFolio had not yet grouped could never register as a failure. Every other Camellia group (1 through 5) had folded correctly by the time of the replay; group 8 was the only one that arrived late.

Fix: PR #7868 (merged 1becfb437) — two different people's own rows independently carrying the same group id now counts as corroboration on its own, so the households merge on the next sync tick; both rows are kept. Residual risk, noted but not a blocker: a reused group id that joins two unrelated applicants at the same property can't be caught by the harness today, because its own ground truth assumes the merge is correct — a live check against AppFolio's own reports is the follow-up.

2. The Miller pair (Camellia, unit 617)

The identity layer had already resolved both rows to one person correctly. The second application, filed 2026-09-08, still minted a second household, because the repair function that's supposed to fix a dead household pointer (ensureHouseholdOfOne) only checked whether the person already had some household — not whether that household was still live. The fix for that exact gap (#7420) landed 2026-09-09, one day after this pair was already broken.

Fixes: PR #7862 (merged 00de2e3ae) — a new inquiry row now joins the person's existing live household at the property where they already hold the primary seat, instead of minting a new one. PR #7864 (merged 5c0905a24) — a one-time repair script, scripts/household-repair/same-person.ts, dry-run by default, one property per run; running it for real needs --execute --property --confirm. Camellia's dry run found nothing left to repair — the Miller pair had already come together on its own once #7862 deployed. The Willows' dry run shows 12 test people and no action needed. Open from review, not started: a harness scenario for this exact "second application arrives a day after the fix" shape, and the edge case where a person co-signs someone else's deal and later applies on their own.

3. Repeat-caller rows (Willows test data)

One person, one household, three inquiry rows, all three labeled primary. This is a design choice from 2026-08-05 — duplicate rows were kept on purpose ("link, don't delete") because they mirror the separate guest cards AppFolio itself creates for each call. Fede, 2026-09-11: that's not a reason to show one person three times — one record per person per property is the target, and phone and web intake need the same "a known person advances their own row" behavior the sync already has, not a fresh row per contact. This is a stated initiative, not started.

Still open

ItemState
3 Camellia people whose stored name still carries AppFolio's literal "(Co-signer for X)" textCleanup identified; not run
5 people with two live primary phone claimsScript built 2026-09-09; execution waits on Fede's go
Western Slope's 33 split householdsNot checked by hand
Reused group id joining unrelated applicantsNo live check yet; harness can't catch it (see above)
Repeat-inquiry and co-sign-then-apply harness scenariosNot started
One record per person per property (repeat-caller fold)Own initiative; not started

QA going forward

A duplicate check now runs on a loop against the live prospects list at every customer property, checking for repeat rows, split households, an unfolded group id, and identity splits by email/phone/name — and reports only new findings, so it doesn't repeat itself every run. Western Slope's first 69 imported rows: zero findings as of 2026-09-11.

Overnight fix loop, Sep 9–10 — before and after

Written 2026-09-13. One night on the Western Slope prototype phone line: 50 robot calls placed against the live line, every verdict read back off the real rows (the call transcript, the tour row, the calendar event, the phone carrier's own sent log) rather than off the test driver's opinion, and ten fixes built and merged against the reproduced cases. Callers are never named here. Sources: the night's test ledger and the defect list in ~/westernslope-data.

The short version. Ten things a caller could hit on this line are fixed and merged. Nine of them are switched off until Fede turns them on — one line each. Two safety guards went live the same night. Nothing a caller hears changed: the line was re-provisioned three times and read back byte-identical every time.

FixBefore, and the call that proves itAfterProofStatusEvidence
1. Message desk After hours, a caller who asked for a person was told their message would be waiting — and nothing was filed anywhere. conv_5901m24m26pqe5zsr4m8megm1psn: "they'll have your message waiting when they're back." The line carried no message-taking tool at all, so there was nothing for the message to land in. Four calls, same outcome, including a genuine first-time caller (conv_7701m24n92nze9arz910jzcrv6a4, conv_4001m24nmb28e6nr4hn7hjjkvs8b, conv_8801m24rjpshfw8ab1a7416t0b0s). One of them closed with "Make sure someone actually calls me." The caller's message is captured and emailed to the leasing inbox. Landed with the switch off, and the off-state pinned by hash to the bytes already on the line. Merge re-provisioned the line; live read-back was byte-identical — same four tools, same three built-ins, same transfer numbers, prompt 59,198 characters, delta 0. Canary call conv_0401m24ny379fs1b9a6cd13kvcbq passed and booked through a real tool call. 119 tests on the provisioning shape, 162 on the wiring fence — which was itself rebuilt after review, because it matched text that survives inside the off-branches and so passed in both states. merged · dark Turn-on: flip the switch, then point the address at the leasing inbox — today it resolves to a PropFlow address, not the customer's. Both are Fede's word. #7497
2. "Three or four bedrooms" offers both A caller asking for three or four bedrooms under $2,600 got two three-bedrooms, then a two-bedroom — she flagged it herself mid-sentence ("aunque esa tiene dos recámaras"). Both four-bedrooms only surfaced after he asked twice more. conv_5901m24m26q3f4arqfmjm7a36h3f. A range is read as a range: homes from both bedroom counts are offered in the first answer. Re-run on a clean number reversed the failure — conv_4401m24pzn9bezpr17s6wqs70x79 named a three-bed and a four-bed in the very first answer and surfaced both $2,350 four-bedrooms. That re-run also established the original failure's real cause: fix 4 below, not the qualifying logic. 13 tests pin the gate shut for this line and for both apartment properties. The bench scenario is written but not yet run — it waits on the bench release phrase. merged · dark No change to this line's script, so no re-provision. #7507
3. Question order, name, counts, and honest notes Three separate faults in the same script. Name never asked on two fully qualified calls that reached named homes and rents — 99 seconds and 157 seconds (conv_4501m24k1t0vfajvfd4h846c29rz, conv_4801m24k1t07ecyrzrw8d4xgwebp). Inventory counts leaked five times, always in the same shape: "Those are the two that fit on bedrooms and budget in Grand Junction", "There are two three-bedrooms on Ember Lane" (conv_9901m24kvcfafe2tgmaydy9sxr10, conv_8601m24mazq8f8erp9bpd2289ag4, conv_3301m24ne5t5fzzttfskgt0st0ze) — telling a caller exactly how many homes the portfolio holds. Claimed a record that was never written: "Thanks — I've got that noted" about a volunteered email address, on a call where no tool fired at all and no guest card exists (conv_3301m24mazwdehyagxkybnjs6gse). One shared question order for portfolio callers, plus three standing rules: always ask the name, never count homes in any phrasing, never say something was recorded unless it was. Three review rounds. Round one caught that the byte-identity guard only checked about 54% of the script; it was rebuilt to cover the whole 758-line fixture with section tripwires. With the switch off the render is byte-for-byte what was already on the line. Merge re-provisioned; read-back identical at 59,198 characters with no new template slots. Canary conv_9801m24qqzhyetttmq6r1rrghmjm passed. The order itself is researched, not invented: five full vendor call transcripts and three published mystery-shop rubrics, with each finding written next to the rule it justifies — one question per turn was unanimous across all five. merged · dark Gera armed the slot on 9/10 — the block is now physically in the live script but renders empty, so what a caller hears is unchanged, and the pre-arming version is pinned as the proven revert. His second pull request removes the last condition and makes the researched order audible on the line. #7500 · #7627 · #7645
4. One Spanish call no longer flips a number to Spanish forever Once a number had spoken Spanish once, every later call from it was answered in Spanish regardless of what the caller spoke. An English speaker was opened with "Hola, soy Clara de Western Slope PM — PROTOTYPE — ¿en qué puedo ayudarle?" and, on one call, answered in Spanish from the first word to the last for 138 seconds (conv_6601m24ke8t2eb4vq42xsdqpdq1g, conv_5901m24m26q3f4arqfmjm7a36h3f, conv_8601m24n0sh3exc8qpby42x9srzv). The bilingual "para español" opening never played, so the caller had no way to signal. It also sent Spanish-speaking maintenance callers to the English call centre — the Spanish leg only fires when detection switches mid-call, which never happens on a call already served in Spanish. The tool's own reason text read "Maintenance call in Spanish, transferring to English maintenance line" (conv_0601m24n2dp5fqnbq314944zc28y, conv_2201m24q5dvsfv69w6erk63zah3w). A stored language preference only decides which language leads the greeting. It never pins the call, and the other language is always offered. Mechanism read off each call's own variables, not inferred: the stored preference is applied whenever live detection is low-confidence, and once set it was never revisited. Reproduced on two different numbers across five calls. 16 tests, including one that pins the old behaviour so the before-and-after is readable. The write side, which was already correct, was left untouched. merged · dark #7510
5. A caller on a shared number can cancel their own tour A prospect booked a tour by phone, got the text saying "Reply here if you need to reschedule" — and then could not. The cancel tool answered {"error":"No prospect found for this phone number"} for the same number the confirmation text had just been delivered to (tour 53bc3ce0, conv_2101m24k1szrf0mryvfw7tkq5cqg). Cause verified against production rows: someone else had used that number first, so the new person was minted correctly but with no phone of their own attached. Everyday shapes — a family phone, a roommate, a number that used to belong to a former tenant — while the tour and its calendar hold stayed standing. The tour is found from two facts that are already proven: the calling number matches the number the booking conversation ran on, and the caller names the tour. Anything less fails closed. No identities are merged. Cause traced to real rows, not code reading alone; the guard that refuses to fuse two people on one number is correct and was kept. 9 tests, 6 of them red before the change, including three that pin what must not happen — a stranger naming the right tour cancels nothing, and a housemate cannot reach the other's booking. The clean counter-case is already proven live: cancel by voice works perfectly for a caller whose number is their own (conv_5401m24rc0pzfefayzfg2vev0776). merged · dark One part — letting the caller name the tour — is outside the switch and applies everywhere, fail-closed. #7511
6. A caller who hangs up mid-message still reaches the team Nothing at all. Row-level forensics on a call cut at 53 seconds while Clara was mid-sentence (conv_8901m24mmaw0epx8k5hzje71qczn): a conversation marked "resolved" with no summary and no topic, two log lines, one guest-card touch — and no email even attempted (zero rows in that day's email ledger for the call, zero dead letters, while the same property delivered six tour emails in the same hour), no text, no task, no escalation. Worse on the message-taking version, cut at "…and I also wanted to ask whether you allow—" (conv_8501m24rpvyjfbmr6z55fww7e2zg): because Clara never reached her commitment sentence, not even a promise row exists. A qualified lead disappears and nothing anywhere records that someone was left hanging. A caller who hangs up mid-message reaches the team by email. Three review rounds, two of them rejecting a version that could not tell a cut-off caller from a satisfied one. The trigger is a positive, fail-closed signal: a real hang-up reason, the caller asked for a person or to leave a message, no tool of any family succeeded, no goodbye, inbound calls only. 45 tests, and each new condition was proven to fail with its own guard deleted. merged · dark Bench end-to-end proof owed once the bench property is released. #7508
7. When the team can't be paged, the promise goes to email, not to silence The system knew a human was owed a callback, tried to page one, failed, and marked the obligation broken inside a second — then did nothing. promise_04bac8e0 on conv_8801m24rjpshfw8ab1a7416t0b0s, off the sentence "Perfect, I'll get that to the team — they'll be in Thursday morning at nine and your message will be waiting for them": broken, reason "cannot reach a person", resolved 618 ms after it was created. Same on the Spanish call, 178 ms (promise_caaef675). No email, text, task or escalation followed either one. A failed page falls through to the team-note email instead of to silence. Root cause traced to the recipient guard refusing both page routes for this property. The fix is additive — a fourth door into the team-note sender that already exists, with two opt-in fields — rather than loosening the guard. 8 tests replaying the real call end to end with only the vendor edge mocked, each guard proven red with itself removed. merged · dark #7535
8. The post-call summary can't invent what a caller "wanted" A caller was cut off at "…and I also wanted to ask whether you allow—". The summary written onto her guest card said she "wanted to ask about pet or other living-situation policies." Nobody said that. And this is not a staff-only field: it is read back into the live script as "previous interactions", so the invented line would have been spoken to her on her next call. A severed sentence is recorded as unfinished. Anything the caller did not say is either dropped or marked as inferred, and the only evidence allowed is the caller's own turns. Cause found in the summary instructions themselves — their own checklist line about "pets, roommates or other living-situation preferences" seeded the guess. 29 tests. Three review rounds, including one that caught the first draft fabricating inside its own truncation detector. Stated honestly in the fix itself: on this exact call the cut-off detector correctly stays silent, because Clara answered the severed turn — what protects that record is the second half, which marks any claim the caller's own words don't support. merged · dark #7532
9. The reminders question is answered with what we actually send A real caller asked "am I gonna get any reminders or text messages about this?" and was told "The leasing team will follow up with you on that." His confirmation text went out 40 seconds later, naming the address, and his reminder timers were already armed — confirmed running in the live workflow with both the day-before and one-hour timers on it. He was told to expect nothing from us. The answer is built from what this property actually sends: the confirmation now, the day-before and one-hour reminders when the timing allows them, and plainly "nothing is sent" when that is the truth. The cadence is fenced against the real timer constants and the confirmation template, so the spoken answer cannot drift from the code. Three review rounds tightened it to conditional — no day-before for a booking under 24 hours out, neither reminder under an hour, receipt only for a visit that is not yet confirmed. 12 tests. merged · dark #7537
10. Never claim a visit with nothing behind it On Sep 7 a caller was told twice that his tour request was in — "I've put in your tour request… the team will confirm shortly" — with zero tool calls behind it, and the post-call step then wrote a tour row with no home on it (conv_0101m1yq1j…, tour b26cc13d). Cause confirmed, not guessed: at that hour the line had no tools bound and no call data arriving at all — the state left by an earlier revert — and the script live that hour told her to say a request was placed. Live, not dark. A call that arrives with no data behind it pages us instead of failing quietly, and the post-call writer refuses to create a tour with no home on it. Ruled side effect: the misleading "request received" text for a vague ask that names no home is gone. Eval pack 7/7 on the production script and 0/1 on the commit before the fix, so the test can actually fail. The evals also caught and banned Clara narrating the empty data out loud ("the available homes block appears to be empty"). Merge re-provisioned the line; the script read back byte-identical with no new template slots. merged · live Two guards live. The call-integrity wording and the post-call contact capture that ship with it stay dark. #7515

What we proved works on the line

Root causes

Still open

Numbers

Calls placed on the lineTours we createdHumans reachedPull requestsLeft open
5018, all 18 cancelled010 merged0

Closed out 2026-09-13: the Sep 7 phantom tour — the row with no home on it, from the fake booking above — was cancelled under Fede's standing grant. It is an internal record on a test property; nothing was sent to anyone, and the close-out was verified against the email ledger, the phone carrier's log and the reminder workflow.

One tour was deliberately left standing — a real prospect reached the line mid-run and booked for the next morning; his call was never touched and his booking is correct. Our own cleanup is now scoped to the exact tours a run created, after an earlier unscoped sweep cancelled the customer's own past-dated test tour (no message reached him; the only notification was an internal email to PropFlow).

How to stand up a simulated new customer (Situs Group / Western Slope shape)

Written 2026-09-10, after the Willows proof above sealed. This is the playbook for building a fake but realistic customer inside a real client's AppFolio database — the same shape as the Willows, but for a client we haven't onboarded yet, or a client where we want to rehearse a change before it touches real prospects. It borrows the Willows method directly: a real staff-side test property, real applications, a reversible reset, a same-day cleanup. Where a step needs Fede's word to actually flip a switch on a customer's data or account, that's called out — this page never does that step for him.

A. What "centralized customer shape" means

Every customer we onboard is one organization — the wall everything else hangs off. Gera's portfolio model (the architecture page) calls this the "nameplate": one row that turns a phone number, a mailbox, or a login into "this is customer X." A simulated customer needs the same shape, or the simulation proves nothing.

  1. One organization, one wall. The org is the unit nothing crosses. A leasing team, a knowledge base, a set of phone lines, a mailbox — all of it hangs off the org, never off a person or a one-off property record.
  2. Leasing team shared across the org's properties — not one leasing person per building. Situs and Western Slope both run this way: a handful of leasing staff work every property in the portfolio, not one per address.
  3. Mail is org-level, not per-property. One admin gives Microsoft (or Google) permission once, for the whole organization, covering a shared leasing mailbox (Western Slope's is leasing@westernslopepm.com) and a shared calendar for tours. Clara replies from that shared mailbox, not from a mailbox we invent per building. See the Western Slope mailbox decision, above, for how this was decided.
  4. A phone line per property only where the client actually wants one. Most customers run one shared line for the portfolio; a property gets its own line only if the client asks. Either way it's forward-only — we never port a client's number, and prospects never see anything but Clara's number when they text back.
  5. One PMS login credential per client company, not per property and not shared across clients. The pattern is clara+<company>@propflowai.co — one AppFolio user for all of that client's properties, so a lockout, a password reset, or a revoked session only ever touches one client, never two. This is why the two AppFolio databases (jpco.appfolio.com and situsgroup.appfolio.com) each need their own login, never a shared one.
  6. One AppFolio database per client, with two pieces of access: an API key for the Reports API (Plus tier for read access; Max tier if a write path is ever wanted — see the caveats below, none of this launch actually writes through the official API) and a browser-usable staff user for anything that has to be clicked, not called — the same staff login the Willows work runs through today.

None of this is new invention — it's the shape Camellia, Situs Group, and Western Slope already answer, laid out as a checklist so a new client gets asked the same six questions in the same order every time.

B. AppFolio side of a simulation

PropFlow Docs