Monday production-readiness audit — The Willows go-live
Target: Monday 2026-08-03 · property appfolio-45 (The Willows, org_sandbox) ·
prod reads current as of 2026-07-31 16:30 MT · 134 adversarially-verified findings, deduped to 117 distinct defects.
Five P0 defects stand confirmed (P0-2, P0-3, P0-4, P0-7, P0-8) and none was closed by the four
in-flight fix branches. All four branches merged to main today and fixed what they were written to
fix — but they addressed the symptoms found on the demo floor, not the failures underneath. Two of them
made a P0 worse: the vendor arm gates were deleted (making the unvalidated /review approve
the last line of defence), and the voice tool was published to the live phone agents ~35 minutes ahead
of the app that serves it.
Conditional GO is reachable — the gate list is below. Recommend a go/no-go re-check Sunday evening against that checklist.
Update, late 2026-08-01: all five confirmed P0s are fixed, deployed and prod-verified (P0-2 #5183/#5188 · P0-3 #5186 · P0-4/5 #5187/#5190 · P0-7 #5184 · P0-8 #5185/#5189 — details on each card). The NO-GO stamp reflects the audit as published; the Sunday re-check should now weigh the open P1s and the three deliberate decisions (dispatch posture, workflow arming, held sentinel repairs), not these P0s.
AMENDED — 2026-08-01 evening
This report originally published nine P0s. Re-checking each against the audit's own adversarial verification layer (135 verify verdicts) and fresh production evidence corrected four of them. The verdict remains NO-GO, resting on the five confirmed P0s — every one of which has a fix PR in flight tonight.
- P0-1 (inbound email goes dark) — REFUTED. A production renewer exists and works:
lambda/outlook-subscription-renewer, deployed and Active, EventBridgerate(30 minutes)ENABLED, zero errors in 5 days, both subscriptions reported healthy at 22:33Z with prior successful renewals on record. The "expiry" timestamps in the original finding were the output of renewals that had just succeeded (the renewer refreshes ~90 min before expiry by design). The original grep missedlambda/; the audit journal already contained this refutation and the roll-up published over it. - P0-6 (Clara phones vendors about jobs never sent) — REFUTED as framed. The double-disarm is the ADR-0112 safety architecture working as designed. The confirmed sibling defect is a P1: the turnover UI toast claims "work orders sent to the vendor" on any non-error response even when dispatch is disarmed and nothing was sent — and it did exactly that during today's demo.
- P0-5 (cancelled WO leaves session live / 14-day rebind) — real, downgraded to P2 by its own verify verdicts. Fix is in flight regardless.
- P0-9 (do not flip to purchase_order) — advice stands, severity downgraded to P2. The missing
EventBridge rule and dead watermark are confirmed; the "grounds all calls" chain was overstated. The mode
stays
work_orderthrough Monday either way.
Live mitigations already applied tonight: the turnover-cohort-walker schedule is
paused (kills the P0-2 retry loop; unpauses when the cap fix deploys); the two /review rows
pointing at deleted work orders are rejected (P0-4 data leg); Willows renewalContactPhone
and renewalContactEmail now point at real recipients (P0-3 config leg — propertyEmail
sink retained pending a deliberate call). Fix PRs for P0-2, P0-3, P0-4/5, P0-7 and P0-8 are in flight with
merge-on-green.
What the four in-flight branches actually cover
All four are on origin/main as of 16:23 MT. Verified by reading the merged code, not the PR titles.
| Branch / PR | Known bug it closes | State | P0 coverage |
|---|---|---|---|
fede/postcall-outcome-chainmerged in #5178 4eab6bffc |
10 — no PM notification on any call terminal state | merged + deployed | None — and it inherits P0-3. notify-pm-outcome.ts:677 on main still reads
.then(() => 'email' as const), and its new resolvePmContactEmail falls back to
propertyEmail, which at The Willows is a @test.local sink. The branch's new
"every terminal state notifies" test goes green while nothing is transmitted. |
fede/callback-reschedule-toolmerged in #5176 36d4be590 |
11 — vendor callback reschedule had zero tools | merged + deployed | None. Publishing it created a ~35-minute live split-brain (now closed, see below), and the same commit set removed the two-factor vendor arm gate — which is what promotes P0-4 from "a gate behind a gate" to "the only gate, and it validates nothing". |
fede/router-staff-before-vendoron main, inbound-router.ts:264-288 |
1 — SMS vendor-walk-before-staff-walk hijacked PM texts | merged | None. Narrow by design — pm-only, non-voice. The identical precedence inversion on the voice path is untouched and is P1-14 below. |
fede/turnovers-scoped-fetchmerged in #5170 bd711fef0 |
7 — turnovers page unscoped fetch stripped test properties | merged | None. |
Known bug 4 (dispatch dedupes against CANCELLED work orders) is not fixed anywhere.
Verified on origin/main today: handle-dispatch-work-order.ts:195 still keys the
dedupe on workOrderId presence + label equality with no status term. Listed as P1-27. |
|||
The nine P0s
Plain-English headline first. Every claim below has a code reference or a production record behind it; the live numbers were re-read at 16:30 MT today.
P0-1 Inbound email goes dark tomorrow — at the demo property and at the live customer REFUTED 2026-08-01
lambda/outlook-subscription-renewer/handler.ts:292 (missed by the original src/ scripts/
grep): Lambda propflow-outlook-subscription-renewer-prod Active, EventBridge rate(30 minutes)
ENABLED per config/automations.toml:1015, 0 errors over 5 days, CloudWatch shows successful renewals of
both subscription ids on 07-27/07-29/07-30, and the 22:33Z tick reports both healthy. The quoted expiry timestamps
are the result of the latest successful renewals — the renewer refreshes ~90 min before expiry
(RENEWAL_THRESHOLD_MINUTES=90). ADR-0003 mandates EventBridge, which is why no Vercel cron exists.
No action needed; a secondary observability defect (renewer reports green on failed runs, alerting=false)
is tracked separately for Gera.The Microsoft Graph mail subscriptions expire Saturday 2026-08-01: Camellia at 14:03 MT, The Willows
at 16:38 MT. Nothing in production renews them. renewSubscription() and needsRenewal()
are exported and have zero non-test callers; none of the 27 Vercel crons touches Graph; there is no polling
fallback. From Saturday afternoon, Microsoft stops POSTing and PropFlow receives no inbound mail for either
mailbox — email-borne notices to vacate, tour replies, vendor replies and lease/turnover intake all stop.
The failure mode is total silence, because the only code that would notice is never called.
Evidence — src/lib/integrations/email/graph-subscription.ts:154; DDB
PROP#appfolio-45/META.emailIntegration.subscriptionExpiresAt = 2026-08-01T22:38:01Z,
PROP#1773625953462 → 2026-08-01T20:03:01Z (both re-read 16:30 MT today).
needsRenewal → renewSubscription and alerts
on failure.P0-2 No turnover work order at The Willows can reach AppFolio — and the retry loop is running right now FIXED · DEPLOYED · VERIFIED
pmsId preserved), hard cap of 6
mirroring the charge lane, permanent failures short-circuit on the first attempt, and the throw path now
escalates. Walker unpaused and observed over 5 ticks: exactly one final sync attempt, a
terminal_failure stamp, cancelled WOs frozen, and one honest critical ("sync ABANDONED — manual
filing required") vs the prior 17 criticals per 75 minutes. The stamp covers all 8 children of the bench
turnover — unit "12" does not exist in AppFolio, so none can sync until the unit mapping is corrected and the
stamp cleared.Unit "12" does not exist on AppFolio property 45, so every child work order fails to create. The sync lane
has no attempt cap — the sibling charge lane got one after an incident where the same uncapped pattern
drove a full MFA login per attempt for seven days, rotated the shared AppFolio session and broke PDF fetch
fleet-wide. The candidate finder also never excludes cancelled work orders, so 45 dead rows pin this turnover
as a candidate forever. syncRemediationAttempts was 22 when the audit opened and reads 54 now:
roughly one real AppFolio browser-agent call per work order every three minutes, indefinitely, plus a
critical alert on every tick. Separately, when the sync throws the function returns before the
escalation block, so that entire class alerts nobody at any attempt count. The demo's headline claim — "the
work order lands in AppFolio" — is false at The Willows and has been since 13:49 MT today.
Evidence — src/lib/domain/turnover/turnover-wo-reconciler.ts:392 (escalation) and
:385 (throw path returns first); turnover-cohort-walker.ts:357; DDB
TURNOVER#trn_78537941-45d5-47f2-84e1-f810e3020310 → syncRemediationAttempts=54,
updatedAt 2026-07-31T22:30:15Z. Also armed on Camellia (autonomousTurnoverEnabled=true).
SYNC_ATTEMPTS_HARD_CAP + a terminal sentinel mirroring
CHARGE_ATTEMPTS_HARD_CAP; exclude cancelled work orders from findUnsyncedWoCandidates;
fall through to escalation on throw; and fix the WLH-12 / WLH-17 unit mapping (P1-22).P0-3 Every PM notification at The Willows is thrown away — and the code records it as delivered
propertyEmail is willows-sink@test.local (still, at 16:30 MT) and there is no
renewalContactPhone, so both legs of every PM page die at the test-recipient gate. The deeper defect
is that no sender checks the result: sendEmail is total and non-throwing for eight suppression
reasons plus hard bounce, and pagePropertyManager discards the outcome and returns
emailDelivered: true unconditionally. So the gas-emergency path tells the tenant "your property
manager has been paged", writes a pm_page_email row onto the thread asserting it, and the loud
"page NOT delivered" alarm — the one signal built to catch exactly this — never fires. Prod
EMAIL_LOG#2026-07-31 shows 87 of 100 rows blocked to that sink, including
[PropFlow GAS EMERGENCY] — The Willows Unit TEST-204, and a real conversation already carries a
recorded URGENT flooding page that was never sent. The same discard is in the vendor-outcome and
purchase-order-required notifiers. This is property-independent: it also fires silently on any real property
whenever emails are disabled, shadow mode is on, or the PM's address is bounce-suppressed.
Evidence — src/lib/domain/maintenance/page-property-manager.ts:138,
notify-pm-outcome.ts:677, notify-pm-po-required.ts:155; the correct predicate
emailDeliveredToRecipient() already exists at email/client.ts:382 and four other senders
use it.
emailDeliveredToRecipient(outcome) and move
the audit-row write inside that gate; point propertyEmail at a real inbox and set
renewalContactPhone. Both halves are needed — the code fix alone still delivers nothing.P0-4 One click in /review places a real vendor call about a cancelled job FIXED · DEPLOYED · VERIFIED
51be5319 → WILL-002178, dial target = the PM's cell by design)
passes; five stale cancelled/missing WOs refuse. The two stale queue rows were rejected earlier tonight.
Correction to this card's evidence: WILL-002181/002182 were not deleted — their displayIds were upgraded
to WILL-1982/WILL-1983 (one dispatched, one cancelled); the genuinely missing rows are
WILL-002088/002119/002120. P0-5's fixes (sessions close with their work order; the 14-day rebind rung now
status-filtered) shipped in the same PRs.dialApprovedVendorSession fetches the work order but never checks its status, never refuses a
missing row, and never checks whether the vendor is archived. The decision route checks only that the session
is awaiting_approval. Three rows are sitting in the queue right now: f3ecb71d points at
WILL-002182, status cancelled, and 51be5319 would dial +14042859387 — the PM's own cell,
which is registered as Chapa Cleaners' number and is also the property's published office and emergency line.
Because the vendor arm gates were deleted today ("the review queue is the only gate"), this approval is the
last line of defence, and it validates nothing about the job. The queue rows are visually indistinguishable
from good ones.
Evidence — src/lib/domain/vendors/dial-approved-session.ts:175;
api/vendor-outreach/[id]/decision/route.ts:47; DDB PROP#appfolio-45/VENDORCALLSESSION#
(3 awaiting_approval, re-read 16:30 MT) cross-referenced against WO#WILL-002178/002181/002182.
archivedAt
inside dialApprovedVendorSession; refuse and surface the reason in /review rather than
dialling. Before Monday, cancel the three stale rows by hand.P0-5 Cancelling a work order leaves its vendor call session live, and the vendor is rebound to the dead job for 14 days DOWNGRADED P2 FIXED
closeWorkOrder, the PATCH open→terminal transition, and DELETE; the recent_outbound
rung carries the same status filters as priorities 2/3.cancelOpenVendorSessions is called from two agent tools only — never from
closeWorkOrder (the single closure primitive every cancel path funnels through) and never from
PATCH /api/work-orders/[displayId]. Inbound vendor binding then falls to priority 4,
recent_outbound, which filters only on direction and a 14-day recency window: no session-status
filter and no work-order-status filter, while priorities 2 and 3 correctly filter on open statuses. Twelve
stale Willows sessions sit inside that window pointing at cancelled or hard-deleted work orders. A vendor who
calls the Willows line back gets greeted about a job cancelled days ago, and any tool call keyed on that id
writes to a cancelled work order or 404s mid-call.
Evidence — src/lib/domain/vendors/inbound-binding.ts:199 (vs the correct filter at
:171); src/lib/domain/maintenance/close-work-order.ts (no vendor-session reference).
cancelOpenVendorSessions from closeWorkOrder and
the work-order PATCH route; add open-status and session-status filters to the recent_outbound rung.P0-6 Clara phones vendors about jobs that were never sent to them REFUTED AS FRAMED
TurnoverDetailClient.tsx:613 toasts "Work approved — work orders sent to the vendor" on any
non-error response even when dispatch is disarmed and nothing was sent — which happened during today's demo.
Whether to arm external dispatch for Monday (so voice and email lanes agree) remains a deliberate product
decision, not a defect fix.Vendor voice calling is on at The Willows (vendorVoiceCallingEnabled=true, 64 sessions dialled,
newest today), but external dispatch is disarmed twice over: VENDOR_DISPATCH_AUTONOMOUS_SENDING is
absent from both the Vercel production environment and the ECS task definition, and
autonomousVendorDispatchEnabled=false on the property. The dispatch gate declares no
armField, so unlike turnover sending there is no durable-row escape hatch — flipping the property
flag alone will not fix it. Net effect: dispatchExternalTurnoverWorkOrders returns
not_allowed on its first line and logs at INFO, so the vendor never receives the dispatch email with
scope and photos, while the voice lane dials them anyway. The allowlist that would backstop a wrong send
(VENDOR_DISPATCH_RECIPIENT_ALLOWLIST) is also unset and fails open.
Evidence — src/lib/temporal/vendor-dispatch-gate.ts:31;
external-dispatch.ts:162; vendor-dispatch-allowlist.ts:14; DDB
PROP#appfolio-45/META re-read 16:30 MT.
vendorVoiceCallingEnabled off so the two lanes agree — and do not claim on the demo
that the vendor received scope and photos.P0-7 The durable turnover workflow sleeps until the move-out date, so nothing it owns runs after the PM approves TIMER FIXED · DEPLOYED
019fb9a3 was
deliberately not reset: its turnover is already scoped, so a reset would file a real AppFolio move-out
draft three weeks early and then close the workflow — it is inert and self-drains 08-21. Remaining
substance, tracked separately: with inline-minted WOs (today: every turnover) the workflow wakes and exits
at done before sync and both dispatch lanes — that fix wants its own patch marker and review.
TURNOVER_WORKFLOW_ENABLED remains unset; arming is a deliberate decision.The workflow parks on await condition(() => false, moveOutWaitMs(...)) before it reaches
the inspection hold. Signals still latch — so inspectionCompleted and chargeConfirmed
set their flags while the body is blocked. Verified in production Temporal: run 019fb9a3… for
today's demo turnover started a timer of 1,744,490 seconds (20.2 days, firing 2026-08-21), received both
signals, and emitted zero commands afterwards. Because notice is normally filed 30-60 days ahead, this
reproduces for every ordinary NTV. The conversational approve path — the one the demo uses — mints work orders
inline but does not sync or dispatch; the workflow owns AppFolio sync, in-house dispatch, external
dispatch, the cleaning-last release, ready-for-listing and re-list, and it is asleep for all of them. The
cohort walker only backstops mint, sync and charges. Compounding: TURNOVER_WORKFLOW_ENABLED is
unset in both runtimes, so the durable workflow has never started for a single Willows turnover — easy to
mistake for "on", because the turnover send arm is armed.
Evidence — src/lib/temporal/workflows/turnover-workflow.ts:539 (timer) and
:591 (inspection hold); src/lib/domain/automations/handlers/ntv.ts:24; prod Temporal
run history, namespace propflow-renewal-prod.
TURNOVER_E2E_MOVEOUT_LEAD_MS.P0-8 The demo PM's phone number cannot be claimed in any org — identity is held together by a hand-written database row FIXED · SANDBOX REPAIRED
deprecateClaim silent fallback is gone (cancelled transactions re-read and re-decide, throw rather
than orphan), a second unreported leak (normalization-drift hiding a row's own sentinel) is fixed, and
upsertPrimaryPhoneClaim now mints-before-deprecates and refuses to clear a verified claim with a
plain-English 409. An exhaustive scan found 22 orphaned sentinels, not 14. The 2 org_sandbox partitions
are repaired and verified (addClaim on the demo contact now returns created; inbound
resolution still picks the PM person). 20 orphans are listed and held for a deliberate go: 8 in org_jpco
(Camellia — read-only), 10 in org_unrouted, 2 in org_propflow_staff (including jose@propflowai.co).
Ledger: BACKFILL_AUDIT#sentinel_repair_0bea2649-0b6.deprecateClaim deletes the dedup sentinel inside a transaction, but when that transaction
cancels it silently falls back to writing the claim row alone and leaves the sentinel alive — no log, no throw.
Fourteen production (org, type, value) partitions now hold a sentinel pointing at a deprecated or
missing claim. Three of them are +14042859387: in org_sandbox addClaim returns
reused_deprecated pointing at a parked prospect, and in org_jpco it throws outright. Someone already
worked around it — claim_demo_pm_fede_cell_20260731, hand-written at 10:28 MT today — and because
that id does not match the sentinel, deprecateClaim can never repair the partition. Any legitimate
write (Admin ▸ Users, email resync, AppFolio sync, an identify_caller merge) removes that row and
cannot replace it. Compounding: upsertPrimaryPhoneClaim deprecates the old phone before adding
the new one and discards addClaim's result, so saving a colliding phone in Admin ▸ Users leaves the
person with zero active phone claims and returns HTTP 200 with a success toast.
Evidence — src/lib/data/dynamo/persons.ts:560;
src/lib/domain/identity/upsert-phone-claim.ts:96; DDB
CLAIM#org_sandbox#PHONE#+14042859387/META → deprecated claim_bdc3f54b.
deprecateClaim fallback fail loud; mint-then-deprecate in upsertPrimaryPhoneClaim and
branch on the result. Until then: do not edit that user's phone in Admin ▸ Users.P0-9 Do not flip vendorJobReferenceMode back to purchase_order DOWNGRADED P2 — ADVICE STANDS
lease-states-history-schedule is also missing, corroborating a pattern) but
refuted the "grounds ALL outbound calls" failure chain as overstated. The operational advice is unchanged:
stay on work_order through Monday; fix and prove PO sync before any flip.All nine Willows purchase-order rows are hand-seeded (syncRunId=demo-seed) and cross their
staleAfter at 2026-07-31 22:15 MT — tonight. POSYNC#WATERMARK has no
lastSuccessAt attribute at all, so the second freshness witness fails too and every row reads
stale from tonight onward. It cannot self-heal: the AppFolio purchase-order report genuinely returns zero rows
for property 45, the sync therefore refuses to mass-remove and errors, and its EventBridge rule was never
created in production anyway. In purchase-order mode, every PM-approved vendor call then resolves
po_required, refuses to dial and bounces silently back to awaiting_approval. Secondary:
isExternallyDispatchable inverts under that mode and starts requiring unitNumber — 28 of
29 Willows unit rows carry an empty one.
Evidence — src/lib/data/dynamo/purchase-orders.ts:378;
dial-approved-session.ts:339; DDB PROP#appfolio-45/POSYNC#WATERMARK
(consecutiveFailures=3, no lastSuccessAt). Current mode re-read 16:30 MT:
work_order — correct.
work_order through Monday. Before any future
flip: create the EventBridge rule, re-seed the purchase orders against a property that actually has a PO book,
and require a real lastSuccessAt on the watermark.CLOSED MID-AUDIT Live voice split-brain — reschedule tool bound ahead of the code
At 15:41 MT the ElevenLabs workspace had reschedule_vendor_visit bound to the Triage agent —
which fronts three live numbers including Camellia's — while production served a build with no such
route, returning HTTP 200 carrying {"error":"Unknown tool"}. Production moved to
611f07880 at 16:16 MT, which carries both the route mapping and the handler, and CI is green on
14d56c5d7. The window closed; the cause did not.
sync-specialists.yml is a bare push: branches: [main] trigger with no CI gate, while
Vercel, Lambda and ECS all gate on CI — so the voice config plane publishes ahead of the app on every
tools-touching merge, and unboundedly whenever main is red. Tracked as P1-42.
The gate — nine items between here and a GO
Five are config or ops actions. Tick them here; the state is shared across everyone on docs.propflowai.co.
Re-subscribe both Graph mailboxes before Sat 14:00 MT— VOID: P0-1 refuted The EventBridge-scheduled renewer Lambda already does this every 30 minutes, with a 5-day zero-error record. No action. (P0-1 amendment)- Point
PROP#appfolio-45.propertyEmailat a real inbox and setrenewalContactPhoneHalf done 2026-08-01:renewalContactPhoneandrenewalContactEmailnow real (PM notices deliver via the #5178 resolution chain).propertyEmailsink retained pending Fede's call. (P0-3) - Gate the three PM senders on
emailDeliveredToRecipientLife-safety page, vendor outcome, PO-required. Small diff; without it the fix above is unverifiable. (P0-3) - Cap the turnover sync reconciler and exclude cancelled work orders DONE 2026-08-01: PRs #5183/#5188 deployed, walker unpaused, loop terminated with one honest escalation — verified over 5 live ticks. (P0-2)
- Re-validate the work order inside
dialApprovedVendorSession, and clear the queue Status + existence + vendorarchivedAt— PR in flight. Stale rows rejected 2026-08-01 (51be5319 stays: staged demo session). (P0-4) - Close vendor sessions from
closeWorkOrderand the WO PATCH route Plus a status filter on the 14-dayrecent_outboundrebind. (P0-5) - Make the vendor voice and vendor email lanes agree Either arm dispatch (with the allowlist populated first) or switch vendor voice calling off. (P0-6)
- Choose the demo turnover so the workflow is not parked Past move-out date, or set the lead env. Fix the timer ordering after go-live. (P0-7)
- Leave
vendorJobReferenceModeonwork_order, and freeze the demo PM's identity row No Admin ▸ Users edits to +14042859387 before Monday. (P0-8, P0-9)
P1 — 42 open
Serious, not go-live-blocking on their own. Grouped by area; the reference is the file and line the fix lands in.
Vendor calling and the /review queue
| # | What breaks | Reference | Fix direction |
|---|---|---|---|
| 1 | /review never refreshes. One fetch on mount, no polling. An approval Clara raises mid-demo never appears, and an approved row is frozen at "Approved — calling shortly" for the rest of the session: real outcomes land 40s–3.5min later, the page's last refetch is at +1.2s. Every sibling operations page polls. FIXED 2026-08-01Verified 2026-08-01Fixed in PR #5209, merged e4d4468c8 — review-queue polling refresh. | review/page.tsx:190 | usePolling(load, POLLING_INTERVAL_MS) |
| 2 | A failed /review fetch renders an empty queue with no error, no banner and no retry — "nothing needs me" is byte-identical to "the API broke". The vendor half is dropped silently on any non-2xx. FIXED 2026-08-01Verified 2026-08-01Fixed in PR #5209, merged e4d4468c8 — explicit dead-API error state. | review/page.tsx:182 | Error state + banner; keep last-good rows |
| 3 | Approve toasts success before any gate runs. Five of seven refusal reasons then say nothing and strand the row at approved with no Approve button and no redial cadence — and the refusal text tells the PM to "approve the call again", which the API 409s. | dial-approved-session.ts:406 | Mirror the po_required branch: patch back to awaiting_approval + notify |
| 4 | Approval requests have no nudge, no expiry and no push. The only signal is one sentence inside the conversation. Sessions sat 66 hours before a human bulk-cancelled them. ADR-0111 specifies "approval expires with the business day"; not implemented. | handle-dispatch-work-order.ts:396 | Business-day TTL + PM reminder |
| 5 | A Thursday-evening or Friday voicemail gets zero redials. All three touches fire inside the promised-callback window, the cadence completes silently with redialsPlaced: 0, and nobody is told. Touches 1 and 2 are structurally dead for every voicemail. | coordination-session.ts:44 | Anchor touches after the callback window; notify on exhaustion |
| 6 | The outbound Vendor Calling agent is outside the sync system. No repo config governs it, tool_ids is empty, and it runs claude-sonnet-5 at temperature 0.3 on an 82k-char prompt while every sibling is sonnet-4-6 at 0.0. Any tool a branch adds can never reach it. | specialists/registry.ts:196 | Add to SPECIALIST_AGENTS + unified sync |
| 7 | Approve→dial has no org boundary. getUserPropertyScope returns null (= all properties) for org_admin, and the decision route checks only that — the sibling read routes add the org check. CONFIRMED · HELDVerified 2026-08-01 — confirmed, heldA real authorization gap, with the verbatim guard sequence captured. Sits on the repo's dangerous-diff hard floor (auth/security changes), which no standing grant waives, so it was not shipped overnight. Exposure is bounded: the user table contains no org_admin rows. | vendor-outreach/[id]/decision/route.ts:43 | Use isPropertyScopeDenied |
| 8 | Two work orders were dispatched to a vendor archived and removed from AppFolio three days earlier. Nothing on the dispatch path reads archivedAt or pmsStatus. REFUTED 2026-08-01Verified 2026-08-01 — refutedDispatch has filtered archived vendors since #4889, and the dial lane is covered by #5187. The two cited work orders were test-harness seeds — no product path produced them. | VENDOR#vendor_appfolio_9945 | Filter archived vendors at dispatch |
| 9 | Walk-path vendor matching accepts archived companies — 11 of the demo org's 15 roster entries are archived, and the confident tier auto-assigns with no recap callout. The dispatch path filters them; the scope path does not. CONFIRMED · FIXED Verified 2026-08-01Confirmed and fixed in PR #5216, merged 34775d8f9 — the walk path no longer auto-assigns retired vendors. | resolve-scope-vendors.ts:420 | .filter(v => !v.archivedAt) |
| 10 | Clara cannot resolve the vendor phrases her own prompt tells her to send. "the handyman", "a plumber", "Chapa Cleaning" all return no match; since #5114 a no-match suppresses auto-assign, so the work order is created unassigned and Clara reports the property's own crew is not on the vendor list. | handle-dispatch-work-order.ts:100 | In-house synonyms + trade stemming |
| 11 | The walk path and the dispatch path give opposite answers to the same spoken company name, with inverted safety postures — one auto-assigns "Chapa Cleaning" at 1.00 confidence while the other says it does not exist. PARTIAL · FIXED Verified 2026-08-01 — partially refutedThe audit's example was fabricated. The underlying walk/dispatch name divergence was real, and is fixed in PR #5216. | resolve-scope-vendors.ts:220 | One shared matcher |
| 12 | Five of nine Willows purchase orders have no resolvedVendorCompanyId despite the vendor rows existing, so a vendor citing PO 90591 gets a po_required refusal against a PO sitting in the table, approved, for exactly them. REFUTED 2026-08-01Verified 2026-08-01 — refutedThe po_required lane is dormant: 0 of 17 properties run in purchase-order mode. Every causal claim in this finding fails. | PO#demo-16 | Backfill the binding + GSI rows |
| 13 | Vendor discovery mints memberships in only one org per AppFolio connection. The Willows can reach 4 of 826 vendors, and because the sync takes the "known" branch it can never repair itself — the only fix path is the admin API. | lambda/appfolio-sync/handler.ts:984 | Mint memberships on the known branch too |
Identity and routing
| # | What breaks | Reference | Fix direction |
|---|---|---|---|
| 14 | Voice recognition has no staff rung — an exact-dial vendor match outranks a verified PM unconditionally. Three sessions dialled numbers carrying an active pm role, including the property's own published office and emergency line. The in-flight staff-before-vendor fix is SMS-only by design. CONFIRMED · UNDERSTATED · FIXEDVerified 2026-08-01 — confirmed, understatedSix sessions, not three. Fixed in PR #5218, merged 77f7bc04b — a PM is no longer greeted as a vendor. | vendor-callback-recognition.ts:50, personalization/route.ts:1108 | Add a pm/staff rung above exact-dial |
| 15 | The Conversation is never anchored to the PM's Person, so capturedBy on the move-out condition report — the deposit audit chain — names the wrong person. Today's Willows walk is stamped with a vendor-membership Person. | personalization/route.ts:658 | Thread callerPmPersonId into the Conversation write |
| 16 | A live resident's spine is shadowed by an empty duplicate. Both her phone and her email resolve to a twin with zero occupancies while the real spine holds two active Willows occupancies. One of six uniqueness-sentinel violations in org_sandbox. | claim-selection.ts:77 | Merge/deprecate the twin |
| 17 | 55% of production Person rows have zero claims. Two Camellia co-tenants minted a ghost Person every 15 minutes for three weeks because the Person row is written before its claims and nothing compensates; both co-signers still have no co-tenant occupancy. | person-stamp.ts:956 | Compensate on mint failure; repair the two dangling sentinels |
| 18 | Saving a phone in Admin ▸ Users can wipe the claim and return 200. It deprecates the old claim before adding the new one and ignores the result — on any of the 15 blocked partitions the person ends with zero active phone claims and inbound calls land as unknown. ALREADY FIXED Verified 2026-08-01 — already fixedClosed by PR #5185 before triage began. | upsert-phone-claim.ts:96 | Mint first, branch on result, 409 on collision |
| 19 | +17206371205 is split-brain — mapped to The Willows, SMS routes to production Clara, voice routes to a bench ElevenLabs agent. One number, two incompatible agent graphs, one property attribution. | phone-lookup.ts:41 | Repoint or retire the number |
| 20 | A warm turnover binding overrides an explicit unit label and skips the intake-stage gate, so "unit 204 — carpet is destroyed, charge the tenant" can be appended to unit 101's condition report. This inverts the precedence the module itself documents. | resolve-turnover-context.ts:140 | Honour the explicit identifier before the stamp |
Turnovers and work orders
| # | What breaks | Reference | Fix direction |
|---|---|---|---|
| 21 | TURNOVER_WORKFLOW_ENABLED is unset in both runtimes — no Willows turnover has ever started the durable workflow; all 20 rows lack a workflowId. Easy to read as "on" because the send arm is armed. | handlers/ntv.ts:24 | Set it, after P0-7's timer fix |
| 22 | Turnover work orders point at units that do not exist (WLH-12, WLH-17 have no UNIT row) and 28 of 29 unit rows have an empty unitNumber. Two rows disagree on how to derive it from the same id shape — the direct upstream of P0-2. | TURNOVER#trn_78537941… | Repair the unit mapping and backfill unitNumber |
| 23 | No Willows unit carries af.unitId, and no sync job stamps it — the only writer is a manual CLI nothing runs. A Willows guest card already sits in production with an empty targetUnitId; a rental application there would be silently skipped. | writers/occupancies.ts:334 | Thread UnitId through rowToRentRollUnit |
| 24 | childWorkOrderIds is dangling and lossy — six ids point at rows that do not exist, and 49 work orders claim a turnover that does not list them (43 on the demo turnover alone). "What is left on unit 12" returns a wrong, short answer. | TURNOVER#trn_78537941… | Reconcile both directions; stop appending without dedupe |
| 25 | Move-out charges on today's demo turnover are stamped terminal_failure — the reconciler has permanently given up and the row is excluded from future sweeps. No automated path will ever file them and no PM surface says so. | turnover-wo-reconciler.ts:68 | Surface terminal charge failures to the PM |
| 26 | The approve toast claims "work orders sent to the vendor" when external dispatch is gated off at all 17 properties, so no email leaves — and the work tracker still labels those work orders "Dispatched". 26(a) ALREADY FIXED 26(b) FIX BUILT · HELD Verified 2026-08-01 — split in two(a) The toast was already fixed
by PR #5193 and hardened by #5200. (b) The tracker label has a fix built and deliberately left unmerged: PR
#5217 is held because status='dispatched' is PMS-owned and a default arm of
mapAfStatus, so the built fix would have printed a false "Not sent yet" over live Camellia row
CAM-1260. An honest fix must scope to PropFlow-owned rows; the prod census is attached to the
PR. | TurnoverDetailClient.tsx:612 | Report the actual dispatch outcome, not the 201 |
| 27 | Known bug 4 is not fixed. The dedupe still ignores work-order status, so redispatching the same task to the same vendor after a cancel returns "That work order already exists" and creates nothing. The cancel path's status flip is inert because nothing reads it. CONFIRMED · FIXED Verified 2026-08-01Confirmed and fixed in PR #5212, merged 9e823c35d — the dedupe no longer counts cancelled work orders, so cancelled work can be re-sent. | handle-dispatch-work-order.ts:195 | Exclude closed/cancelled from the dedupe predicate |
| 28 | The 1-minute AppFolio mirror wipes any locally-booked visit window. scheduledTimeSlot is forced to null on every merge with no local-ownership guard, so a window booked by Clara or the PM is erased within 60 seconds. Masked today only because the Willows creates are failing. CONFIRMED · CAUSE CORRECTED · FIXEDVerified 2026-08-01 — confirmed, cause correctedThe defect is real but the audit's cause is wrong: it is live for every AppFolio-linked work order including Camellia, not masked by the failing Willows creates. Clobber site work-order.ts:963-964. Fixed in PR #5223, merged 3686117ca. Measured blast radius was latent, not live — 2 exposed rows fleet-wide, 0 at Camellia. The ownership-claim guard is deployed to the sync Lambda and prod-verified: 30/30 schedules intact over ~6 post-deploy passes. | writers/work-order.ts:963 | Preserve local schedule while the schedule dirty bit is pending |
| 29 | Two rostered turnovers are permanently un-walkable. The lease-anchor guard refuses start_inspection and fires a founder-paging fatal on each attempt, while the roster keeps offering those units by name. Fired four times in production today. | handle-start-inspection.ts:264 | Gate roster eligibility on inspectability; soft-fail when there is no lease at all |
| 30 | The turnover-intake voice agent is a dead end. Its prompt promises a transfer back to triage and offers to transfer the caller to a human — it has no transfer tool of any kind, and its four business tools all answer NOT_AUTHORIZED to a non-PM. | voice-agents/turnover-intake.ts:29 | Add transfer_to_agent + transfer_to_number and sync |
Messaging, suppression and safety
| # | What breaks | Reference | Fix direction |
|---|---|---|---|
| 31 | Texting START restores SMS but not email. The STOP fans a revocation out to the person's email; the START never reverses it, so a tenant who opts back in is email-suppressed forever while the UI shows consent granted. Six consecutive nights of drift in production. CONFIRMED Verified 2026-08-01 — confirmed, not fixedProd-proven over 6 consecutive nights. Verification also surfaced a fan-out landmine: a phone START would reinstate an email the person had unsubscribed via another channel. Not fixed overnight — suppression-store writes were held for daytime review. | twilio/webhook/route.ts:236 | Pass organizationId (and normalize) exactly as STOP does |
| 32 | There is no STOP button. The only fleet-level SMS lever is global — it mutes Camellia and Yale as collateral, is not on the Arms page, and is bypassed by 16+ direct send sites. Its own settings copy claims coverage it does not have. No per-property SMS brake exists. | twilio/client.ts:265 | Register it, move the check into sendSms, correct the copy |
| 33 | One LLM failure freezes that phone number's whole SMS lane for 6-18 minutes, then the message dies in the DLQ unanswered — no apology ever sent, and Twilio reports delivery throughout. Retries are disabled in production, so a routine 529 is enough. | twilio/webhook/route.ts:480 | Catch around dispatchInbound, canned fallback, ACK |
| 34 | A photo over ~185KB bypasses the queue and runs the whole agent turn inside a 60-second function with no retry, no DLQ and no alarm — and the 120-second agent abort can never fire. The repo's own ADR bans this pattern after an incident that lost 34 of 35 signals. | twilio/webhook/route.ts:486 | Raise maxDuration; S3-offload attachments so photos stay on the queue |
| 35 | Voice promises "the work order will be filed after the call" and the post-call refusal is a logInfo — no PM notice, no tenant follow-up, and the forensics row is written ok: true. Fifteen refusals in two weeks, including a flooding toilet and a worsening kitchen leak. Zero work orders have ever been filed from a voice call at The Willows. CONFIRMED · ROOT CAUSE CORRECTEDVerified 2026-08-01 — confirmed, root cause corrected identityMismatchResponse returns no success field, so result.success is undefined and the sweep takes the log-only branch. The deep fix is the identity spine, which is on Fede's hold list. | voice-postcall-intake.ts:269 | Escalate the refusal, mark the record failed, notify the PM |
| 36 | A multi-issue call files only the first issue. The rest are dropped with no log line of any level and no forensics row — unrecoverable even in an audit — while the mid-call tool tells the model the transcript sweep captures every issue. | voice-postcall-intake.ts:224 | Act on every decision, or record the drop |
| 37 | The anomaly review gate is off for every PM-sender turn — not just the holding message, but the parked draft, the #alerts page and the review record. Monday's demo persona is a PM. | conversation-manager.ts:5862 | Split the condition: suppress only the customer-facing hold copy |
| 38 | A deduped hold sends nothing and alerts nobody. Second failure of the same cause within an hour: the customer gets silence mid-conversation and no new page fires. Voice already handles this correctly; SMS was never back-ported. | anomaly-review-gate.ts:268 | Emit a distinct second-turn line; re-page after N deduped turns |
| 39 | Parked guard drafts render as normal Clara messages carrying internal vocabulary ([HELD-FOR-REVIEW …], [GUARD-BLOCKED …]) — 42 live rows in production, two written today — and they replay into the model's context, so Clara re-asserts the claim the guard blocked. | conversation-manager.ts:5910 | Dedicated kind, internal-notice rendering, exclude from context |
Auth and release engineering
| # | What breaks | Reference | Fix direction |
|---|---|---|---|
| 40 | POST /api/turnovers performs zero authorization — any active session can create a turnover on any property in any org, and one bogus row permanently suppresses the automatic declined-renewal turnover for that unit. The four sibling routes all check. CONFIRMED · HELDVerified 2026-08-01 — confirmed, heldA real authorization gap, with the verbatim guard sequence captured. Sits on the repo's dangerous-diff hard floor (auth/security changes), which no standing grant waives, so it was not shipped overnight. Exposure is bounded: the user table contains no org_admin rows. | api/turnovers/route.ts:51 | Add isAccessDenied + isPropertyInOrgScope |
| 41 | POST /api/simulate/sms is gated by requireUser alone — any active user of any org or role can impersonate any phone number and drive real production side effects: work orders, conversation writes, stopped outreach cadences. ADR-0120 names this route as its proof case. CONFIRMED · HELDVerified 2026-08-01 — confirmed, heldA real authorization gap, with the verbatim guard sequence captured. Sits on the repo's dangerous-diff hard floor (auth/security changes), which no standing grant waives, so it was not shipped overnight. Exposure is bounded: the user table contains no org_admin rows. | api/simulate/sms/route.ts:61 | Platform-admin only, or exclude from the production build |
| 42 | The ElevenLabs sync is push-triggered while every code surface is CI-gated, so the voice config plane publishes ahead of the app on every tools-touching merge — unboundedly whenever main is red. This caused today's split-brain. MITIGATED Verified 2026-08-01 — mitigated, not closedThe sync remains push-triggered by design. PR #5211, merged f8d176c00, added a 6-hour live drift check across all 11 voice agents, so a split-brain now gets noticed. First run measured 11/11 in sync. | .github/workflows/sync-specialists.yml:57 | workflow_run: [CI] with a success guard |
Won't fix by Monday — accept or mitigate
Each of these is real and each has a way to survive the demo without pretending it is fixed.
- External vendor dispatch will not be armed in time. Turn vendor voice calling off for the demo, or keep it on and never claim the vendor received scope and photos — because they did not. Populate the recipient allowlist before ever arming dispatch; it currently fails open. (P0-6)
- Work orders will not land in AppFolio at The Willows. Demo the work order in PropFlow and stop there. The sync cap still has to ship regardless — the loop is consuming a shared AppFolio session every three minutes today, and the precedent for leaving it uncapped is a seven-day fleet-wide outage. (P0-2)
- The durable turnover workflow will still be parked.
Pick a demo turnover whose move-out date has already passed, or set
TURNOVER_E2E_MOVEOUT_LEAD_MSfor the session. Do not narrate the durable workflow as the thing driving post-approval work. (P0-7) /reviewwill not auto-refresh. Reload the page immediately before and ~90 seconds after every approve. Otherwise the row reads "Approved — calling shortly" for the whole demo regardless of what actually happened. (P1-1)- Three stale rows are sitting in the approval queue, one on a cancelled work order and one pointed at the PM's own cell. Cancel all three before Monday and re-seed only what the demo needs. Approving any of them today places a real call. (P0-4)
- Twenty-one phantom vendor visits are live on the demo Outlook calendar, two of them dated 4 and 6 August. Delete those two by hand before Monday. The teardown gap itself is P2 and can wait.
- Duplicate unit labels make voice unit-matching a coin flip.
Cancel the stale test turnovers at
appfolio-45— TEST-103 ×3, TEST-PROOF-1 ×3, TEST-204 ×2, L4TEST-MO-01 ×2. That also revives the staff-implicit SMS binding, which is currently dead because it requires exactly one open turnover. (P2) - The demo PM's identity depends on a hand-written claim row. Do not edit that user in Admin ▸ Users, do not run a merge script against that phone. Repair the sentinels after go-live. (P0-8)
- The vendor calling agent's model and temperature cannot be pinned safely in two days. Keep vendor calls to the scripted scheduling path; it is the one agent sampling at 0.3, on an untracked model, with no tools and no drift check. Onboard it to the sync after go-live. (P1-6)
- There is no per-property kill switch and no real STOP.
Write the incident procedure down before Monday: flip the global
smsEnabled, accept that it mutes Camellia and Yale too, and separately disarm the per-property autonomy flags. Rehearse it once. (P1-32) - Voice post-call work-order creation has never once succeeded at The Willows. Do not demo "call it in and watch the work order appear" until the identity spine resolves a tenant for the calling number. (P1-35)
- Two API routes are unauthenticated in effect.
No new seats — especially no
org_adminorviewerinvitations — until they are gated. Today's exposure is bounded because the user table contains noorg_adminat all. (P1-40, P1-41) - The preview lane silently deletes messages whose processing failed. Do not accept a green preview run as validation for any fix branch — it has already dropped messages twice this week. Verify on the production numbers. (P2)
P2 — 65 open
Real, verified, and none of them blocks Monday. Kept here so nothing has to be rediscovered.
Calendar, UI and turnover mechanics (18)
| Cancelling or closing a work order never removes its Outlook vendor-visit mirror — 21 phantom visits verified live on the demo calendar. | work-orders/[displayId]/route.ts:433 |
| Tour reschedule can silently leave the calendar on the old time — no retry, no DLQ, unlike its vendor sibling on the same helper. | sync-tour.ts:360 |
| Move-out inspection events are create-once and never torn down; one unit already carries triplicate holds. | schedule-inspection-event.ts:79 |
| Reconnecting the leasing calendar strands every stored event id — updates then page on every reschedule and never re-create. | sync-vendor-visit.ts:445 |
| Tenant slot-pick and cancel calendar projection is fire-and-forget inside a handler the repo's own ADR says gets killed. | handle-tenant-confirmation.ts:180 |
The turnovers list "Charges" column is permanently em-dash and its sortable header is a no-op — nothing writes totalEstimatedCost. | scope/route.ts:206 |
| Turnover detail poll-error handling is dead code via a stale closure — a persistently failing 5-second poll is invisible. | TurnoverDetailClient.tsx:481 |
An auto_resolved child work order strands a turnover at Turning forever — three modules disagree on whether it counts as done. | reconcile-turnover-core.ts:64 |
The reconciler resolves children by a fleet-wide scan on turnoverId instead of childWorkOrderIds, picking up orphans the reset tool deliberately detached. | activities/turnover.ts:1028 |
| Every scope/approve door mints work orders before the stale versioned turnover write; a conflict 409s the PM after vendors were contacted, and the retry duplicates. | scope/route.ts:214 |
| Approve is a check-then-act on stage and the SMS and web doors share no claim — two approvals can produce two full sets of work orders. | scope/route.ts:51 |
patchTurnover re-reads inside itself, so its optimistic lock can never fire for the caller and a concurrently-dispatched task is silently dropped. | dynamo/turnover.ts:97 |
POST /api/work-orders links the new work order into any turnover with no scope check, via a stale versioned write. | api/work-orders/route.ts:200 |
start_inspection decides in-flight vs orphaned from turnover.updatedAt — a field every background writer bumps. | handle-start-inspection.ts:184 |
| The exact-match branch of unit matching has no ambiguity guard, and four of nine open Willows labels are duplicated. | match-occupant-unit.ts:47 |
append_notes / revise_finding accept any report id in the caller's org — the conversation's pinned turnover is written but never enforced. | handle-append-notes.ts:151 |
| The voice walk hands the agent every open turnover id, uncapped and with duplicate labels — the mis-pick surface the SMS roster deliberately refuses. | pm-call-context.ts:232 |
| That same list is built with a fleet-wide N+1 read on the ring path of every PM voice call. | pm-call-context.ts:200 |
Vendor, dispatch and purchase orders (11)
| "Held" does not hold for external vendors — suppression is conditioned on the vendor being in-house, so a held task still fires the quote fan-out and pages the in-house tech. | scope-turnover-work-orders.ts:307 |
| External dispatch has no work-order status filter, so a cleaning-last held work order is emailed at scope time; the later release is a silent status flip that emails nobody. | external-dispatch.ts:188 |
| Cross-org contact leak in the scope-path vendor label — another organisation's contact name renders into the turnover timeline. | resolve-scope-vendors.ts:354 |
| Unattended redials have no business-hours or weekend gate, though the quiet-hold activity is registered on the same task queue. | vendor-redial-workflow.ts:16 |
| Outbound vendor voice calls have no suppression gate at all — the one channel where an opt-out is honoured nowhere. | dial-approved-session.ts:426 |
payload_rejected strands the session at approved forever, and the refusal text instructs the PM to do something the API forbids. | dial-approved-session.ts:406 |
| Sessions with no vendor phone are unapprovable by construction, and two vendor test dials reached a Twilio demo greeting — so "successful dial" from that number is not evidence. | PROP#appfolio-45/VENDORCALLSESSION# |
| Seeded demo purchase orders contradict themselves on unit, breaking the vendor+unit single-match fallback the dial depends on. | PO#demo-24 |
| The vendor-email recipient allowlist is unset in production and fails open. | vendor-dispatch-allowlist.ts:14 |
| The purchase-order sync has no EventBridge rule in production despite being declared in the automations registry, and is wedged by nine demo-seeded rows. | lambda/appfolio-sync/deploy.sh:178 |
The lease_states_history job has never run — rule never created, and absent from the registry so the declared-but-not-reporting detector cannot catch it. | lambda/appfolio-sync/deploy.sh:120 |
Post-call, messaging and suppression (14)
| Carrier opt-outs (Twilio 21610) write only the legacy consent row, so the cadence escalates the blocked person to email. | twilio/status/route.ts:91 |
| Backfilled suppression revocations carry no person id, so the fan-out never ran — a scope-all opt-out leaves that person's email fully reachable. | suppression-gate.ts:234 |
The transactional exemption is inert on every proactive dispatch — the pre-compose consent gate is category-blind and blocks first. | messaging/consent.ts:88 |
The Twilio adapter never maps a gate-blocked send to skipped, so a blocked send can be recorded as delivered and can release an at-most-once claim. | adapters/twilio-sms.ts:247 |
| Dispatcher pre-empt lanes (life-safety, rating, state-first) send with a record reason asserting a persistence that never happened — neither the inbound nor the reply is stored. | process-envelope.ts:263 |
Property→org resolution negative-caches null forever per process — one early inbound poisons that instance into silently 200-dropping all its SMS. | twilio/webhook/route.ts:402 |
| The preview inbound event-source mapping reports no batch-item failures, so failed messages are ACKed and deleted — and the deploy script can never repair it. | lambda/inbound-processor/deploy.sh:720 |
| A mid-turn LLM throw persists tool rows but skips the turn-ledger save its sibling performs, leaving orphaned tool rows with no inbound anchor before the retry replays. | conversation-manager.ts:5555 |
| No alarm can fire for a burst of LLM failures — the handler returns batch-item failures rather than throwing, so the metric the alarm watches never increments; the Vercel lane has no coverage at all. | propflow-inbound-processor-prod-errors-burst |
| The review gate's tool set covers none of the maintenance, vendor or turnover surface — and those handlers never emit a typed fatal, so nothing could arm it there anyway. | review-gate-tools.ts:42 |
No human-visible surface exists for any gate verdict — the "PM review item" is a Sentry message, and /review has no anomaly item type. | anomaly-review-gate.ts:198 |
The handyman page dies at logInfo when a property has no tech phone — 15 of 17 properties — while the sibling consent-skip writes a PM-visible timeline entry. | tech-sms-dispatch.ts:800 |
| The emergency ladder logs "SMS sent" and writes an outbound record without checking whether the send returned any message ids. | emergency-no-answer-ladder.ts:91 |
Post-call outcome application decides sync flags and status from a pre-LLM snapshot and writes them over a fresh row — a mid-call cancel can be resurrected to dispatched. | postcall-extraction.ts:1073 |
Data integrity, identity and config drift (22)
"Who took the call" is dead code — the normalizer silently drops spokeWith, and the tests pass because they hand-build the object. | postcall-extraction.ts:924 |
| A second problem a vendor mentions is extracted and then discarded — no work order, no timeline, no PM — after Clara promised to pass it along. | postcall-extraction.ts:944 |
| 211 duplicate display ids at The Willows plus a first-match lookup — a work-order URL can open an archived twin, and edits land on the invisible row. | dynamo/maintenance.ts:195 |
| Three units have two active occupancies each, two of them across different people — "who lives in TEST-312" returns two residents and the rent roll double-counts. | GSI PROPERTY#appfolio-45 |
| Tenant identity resolution is cross-org and ignores the destination property, while the prospect branch hard-gates on it. | identity/resolve.ts:268 |
| Prospect gathering uses the plural person finder for phone and the singular one for email — the email lane drops every prospect not on the trust-cascade winner. | dynamo/leasing.ts:347 |
| A person's active prospect is chosen by random UUID order, so a multi-property prospect is invisible at every property but one. | dynamo/leasing.ts:317 |
| Person merge deprecates the source claim first and silently drops a reused-deprecated result — a merge can delete a caller's phone from the spine. | merge-persons.ts:197 |
| A production claim row carries a verifier outside the union, making the primary-claim comparator return NaN. | claim-selection.ts:78 |
| A vendor contact person with zero identity claims, and a vendor membership pointing at a vendor row that does not exist. | PERSON#pers_demo_chapa_contact… |
| Two work orders hold free-form dates the vendor calendar cannot anchor; both are cancelled, but the write path that produced them is live. | WO#wo_appfolio_604… |
Several turnovers carry conditionReportId as a literal boolean — the same write shape as the known boolean-true bug, on a different field. (No boolean schedule rows survive.) | PROP#appfolio-45/TURNOVER# |
Turnover mutation routes (PATCH, DELETE, cancel, complete, inspect) drop the org-scope check their four siblings run — and /complete fires a PMS write. | api/turnovers/[id]/route.ts:56 |
| No role enforcement on any turnover or work-order API — a read-only seat can approve work orders and dispatch vendors; module flags are UI-only too. | middleware.ts:800 |
| Turnover detail loading uses the fail-open pattern its own helper was written to replace, with no org check, and the server page calls it with no fallback. | load-turnover-detail.ts:38 |
| The reset tool restarts the workflow straight into the move-out park by default and reports "started fresh" even when the terminate failed. | _reset-turnover-shared.ts:197 |
autonomousMaintenanceEnabled=true on the Willows row is a dead flag no code reads — the one switch an operator would reach for during an incident does nothing. | PROP#appfolio-45/META |
| The worker task definition advertises renewals as disarmed while the durable row has them armed — the natural incident check reads the opposite of the truth. | propflow-renewal-worker-prod |
| The Willows carries no capability stage, so every domain is fully live with no shadow valve — and renewals would not honour it even if it were set. | computations.ts:79 |
| A live Outlook OAuth access token is stored in plaintext on the property row, readable by any table read. | PROP#appfolio-45/META.leasingCalendar |
| The staff-implicit SMS binding requires exactly one open turnover, so it is dead at both The Willows (14) and Camellia (7) — a photo-only PM text falls out of turnover intake entirely. | resolve-turnover-context.ts:220 |
Vendor sessions and inspection claims aside, nothing ages out a notice_received turnover — the stale set only grows, which is what killed the binding above. | cancel-turnover.ts |
Method, and what to distrust
134 findings were produced across eleven audit dimensions and each was put through a refutation pass — the reviewer's job was to kill the finding, not confirm it. Severities shown here are the post-verification ratings, not the reporter's claims: roughly 50 of the 134 were downgraded during that pass, one was upgraded, and several had their stated mechanism corrected while the defect survived. Where two findings described the same defect with different severities, the more thoroughly evidenced verdict wins. After de-duplication, 117 distinct defects remain.
Three honest caveats. First, this audit was read-only — nothing here was fixed at publication time
(amended: the walker pause, queue cleanup and recipient config landed the same evening; see the amendment
block up top). Second, the demo property is isTest and sits in org_sandbox, so a
share of the P0 blast radius is configuration that a real property would not have; the code defects underneath
are property-independent and several of them (P0-2, P0-3's discard, P1-17, P1-31) reach Camellia today. Third,
prod moves: the voice split-brain opened and closed inside this audit's window, and the turnover retry counter
climbed from 22 to 54 while it was being written. Re-read the live numbers before acting on them.
Fourth caveat, added by the amendment pass: the "severities shown are post-verification ratings"
claim above did not hold for four of the nine published P0s — P0-1 and P0-6 were published over their own
real:false verify verdicts, and P0-5/P0-9 over P2 downgrades. When reading the P1/P2 tables below,
treat any finding you intend to act on the same way: check its verify verdict first.
Fifth caveat, added by the overnight P1 verification pass: the P1 section reproduced the P0 section's failure mode — 48% of findings did not survive independent verification.
Audit window 2026-07-31, ~13:00–16:30 MT. Repository at origin/main
14d56c5d7; production serving 611f07880.