Simplification Plan

Decisions consolidated into the Clara-as-a-Coworker tracker, Phase 4 (single list) — answers there count.

Fede's read on the codebase: "too complex to understand… bespoke solutions each session comes up with in isolation." This is the honest answer to that — where it's true, how bad, and what to do about it in three shippable waves.

Aug 26, 2026 · synthesized from 11 dimension inspectors, independently re-checked against 60 proposed fixes · nothing has changed yet — this is a plan, not a diff

1. The picture

In plain terms: this isn't sixty different problems. It's one habit, repeated about sixty times: whenever a session needed something that already existed — a way to page a person, a place to write down what happened on a call, a test harness, an on/off switch — it built its own copy instead of finding the one that was already there. The good news buried in that: the team already built the right version of almost everything, at least once. Most of this plan is deleting dead leftovers and pointing people at the good version that already exists — not designing anything new.

The technical version, for the record: today the system tells a person something through roughly 30–35 separate places in the code (27+ files that directly email a PM/owner/office, plus 3 digest cron jobs, plus 2 vendor-paging paths) — and only 9 of those are wired into the one "door" (the matter lane) built to dedupe and track them. Five of the seven facts checked that get written by more than one part of the system — call/transfer outcome, conversation escalation status, who's muted or being handled, person identity, tour linkage, work-order status — have 2 to 13 independent writers each, instead of the one-writer pattern the promise ledger already proves works. Guardrail/judge/classifier code is 169 files, and inside that, six or seven independently decide "did Clara make an unbacked promise" with zero shared vocabulary. Test harnesses and eval scripts sprawl across ~30 harness/eval directories, with 9 different "replay and grade" implementations and 35 eval scripts, 25 of which hand-copy the same credential-reading code instead of importing the shared one. Scheduled jobs run on 5 different clocks (Temporal, EventBridge, Vercel cron, GitHub Actions, a Mac mini) with 142 scheduled things total, and one of those clocks (GitHub Actions, 24 jobs) isn't tracked in the one manifest built to prevent exactly this drift. On/off switches exist in ~28 files using 3 incompatible conventions. The scripts/ folder alone has 657 files, more than 100 of them one-off scratch tools never cleaned up. And 434 published docs exist, ~95 of them clustered into 12 topics where the same problem gets re-diagnosed by a different session with no link back to the last attempt.

2. By the numbers

Eight places this shows up the most, and roughly what "fixed" looks like for each.

30–35~8–10
ways the system pages a person today, collapsing to that many genuinely different message types, all behind one door
27+ files email a PM/owner/office directly, +3 digests, +2 vendor pages; only 9 go through the shared dedupe system today
5 facts1 writer each
key facts — like "did the transfer connect" or "who is this person" — get written by several different pieces of code instead of one trusted source
2–13 independent writers per fact today (escalation-status 4, transfer-outcome 2, person-identity 13, tour-linkage 2, work-order-status 2)
169 / 71
guard/judge files stay mostly as-is (they check different things on purpose) — but 7 separately reinvent the same "did Clara make an unbacked promise" check
169 non-test guard/detector/judge/gate files total; 20 with "judge" in the name; 7 independent unbacked-promise implementations collapse to 1 shared function
~30 / 95–6
test-harness folders, with 9 homemade "replay a conversation and grade it" engines, most of which can share one
~30 harness/eval directories (24 under scripts/ + 6 top-level trees); a documented reason kept for each system that genuinely can't merge
142 / 55, 1 list
scheduled jobs run on 5 separate clocks; one of those clocks isn't even on the master list meant to track all of them
Temporal, EventBridge, Vercel cron, GitHub Actions (24 jobs, untracked), a Mac mini; 57 inline retry blocks, 12 near-duplicate wrapper workflows
~28 / 32 factories, 1 list
on/off switch files, built three incompatible ways, so there's no single place to check "is this thing actually on"
21 arms in the registry today, at least 2 real switches missing from it
657100+ fewer
scripts in the scripts/ folder, more than a hundred of them one-time throwaway tools nobody deleted
58–126 "scratch" scripts checked into git despite the gitignore rule; some are pinned by a hard-failing test and need that fixed first, not a blind delete
434 / 9512 hubs
published docs; about a hundred of them are really 12 conversations that keep getting restarted from scratch with no link back to the last attempt
~95 pages clustered into 12 topics with weak/zero cross-linking; 2 outright duplicate pages

3. The consolidation map

In plain terms: instead of listing sixty separate fixes, here's the shape they collapse into. Four buckets: one place that knows what actually happened (a call, a transfer, a PMS event), one place that tracks what's still owed to someone (a promise, an escalation, a stuck renewal), one door that decides what a person needs to be told and whether they already were, and a shared toolbox so the next one-off script doesn't have to reinvent things again.

#BucketTodayTargetNotes
1Evidence — facts with one writer5 of 7 checked facts have 2–13 writers each (~24 writer call-paths total)7 facts, 7 writers (1 each)Promise-ledger status and Tour.cancelledAt are already at 1 writer — they're the template, not a problem.
2Obligations — one ledgerPromise ledger: 1 writer (good). Escalation "already being handled" only checked for the test property, not Camellia or any real property. ~30 notify paths never consult it.Every open obligation checkable from one place before a second notice firesThe matter lane (open-matter.ts) is the existing candidate — it needs Camellia armed, not a new system.
3Human Lane — one door~27 files directly email a person, +3 digests, +2 vendor-paging paths ≈ 30–35 distinct paths; only 9 of 27 map onto the published "13 doors" inventory~8–10 distinct message bodies, all routed through 1 door for the routing/dedupe decisionSome exclusions are deliberate (an owner-alert path was pulled OFF the matter lane on purpose after a real leak) — target is "route what's already recommended," not force everything through one pipe.
4Toolkit — harness skeleton~30 harness/eval directories; 9 independent replay+grade implementations~5–6 systems, each remaining split documentedscripts/tour-replay looks cleanly supersedable by scripts/tour-decider-replay.
5Toolkit — fixture + PII scrub8 independent PII scrubbers; 3 incident→fixture pipelines, 3 schemas2 scrubbers (log-masking vs. full fixture replacement are legitimately different jobs); schemas stay split only where the consumer differsDon't force all 8 scrubbers onto one function — hot rule 7 requires full replacement in committed fixtures.
6Toolkit — one detector family169 guard/detector/judge/gate files; 7 independent unbacked-promise checksMost of the 169 stay; the 7 collapse to 1 shared function; 20 judge files share 1 runnerThe repo already names the consolidation base: detectUnbackedPromise in the vendored Cerberus package.
7Toolkit — one sender path2 email transports (legit) + 2 SMS entry points (1 legacy/25 callers, 1 new/12 callers); sender choice hand-set at ~50 call sites, already caused 2 real leaks2 transports stay; 1 SMS entry point; sender choice made by a type-checked gate, not a hand-set booleanThe two "policy gate" functions also need renaming — they do unrelated jobs.
8Toolkit — one scheduler pattern5 runtimes, 142 scheduled things, 1 runtime untracked; 57 inline retry blocks (18 files skip the shared constants); 12 near-duplicate wrappers5 runtimes, all in 1 manifest; retries down to named constants; 12 wrappers behind 1 factoryADR-0123 is already the enforced standard in practice — its status field still says "Proposed."
9Toolkit — one gate factory~28 gate/flag files, 3 conventions, 21 registered arms + 2+ missing~2 gate factories, all arms in 1 registryDon't ban "off by default on both ends" outright — 6+ live arms use that shape deliberately for staged rollout.
10Toolkit — credentials + DB client25–39 scripts hand-roll credential reading; ~56 build a raw DynamoDB client; the client is forked into 2 hand-synced copies1 credential loader, 1 tuned DB client imported everywhereThe fork hasn't drifted yet — this is closing a gap before it bites, not a live bug.
11Docs canonicalization434 pages; ~95 clustered into 12 topics, weak/zero cross-linking; 2 duplicatesSame ~95 pages mostly kept, but 1 canonical hub per cluster with full linking; 2 duplicates deletedOnly 3 pages today use the real machine-redirect "Moved" pattern — extend that, not the 4 hand-authored variants that don't point anywhere.

4. Delete / merge / reuse / doc-consolidate

In plain terms: everything below sorts into four buckets — stuff we can just delete (dead code, zero callers), stuff we should merge (the same thing built twice or more), stuff that's already good and other parts of the code should copy it, and docs that keep re-litigating the same question instead of pointing at one page. Click a tab.

Ranked by lines removed × confidence. Every row below has zero live callers found by a repo-wide search.

WhatVerdictRiskSizeProof required
5 docs/renewal-architecture/*.md files citing a route deleted 3+ months agoConfirmedLow1,395 lines (docs)Grep for the route name repo-wide comes back empty
HybridRepository — 2 copies, 0 callersConfirmedLow818 linesBuild stays green after removal; grep stays empty
Dead elevenlabs/ planning subtreeConfirmedLow~900 linesGrep stays empty
Duplicate approved-to-signed-2026-08-21.htmlConfirmedLow600 lines (doc)No filename references found repo-wide
capture-three-dot-v2/v3/v4.mjs (superseded debug iterations)PartlyLow~232 linesUpdate the 1 doc that still names them
4 of 5 "unused barrel" index.ts re-export filesPartlyLow~59 linesGrep stays empty
StructuredEmailIngester interface stubPartlyLow49 linesGrep stays empty for the stub specifically
src/lib/integrations/sqs/simulate-results.tsConfirmedLow42 linesGrep stays empty
Do NOT delete without more work first
  • 58–126 underscore "scratch" scripts checked into git — at least ~20 are deliberately pinned by a hard-failing governance test. Fix the test's pin table first, or the delete breaks CI.
  • 47–62 self-described "one-shot" scripts not yet archived — the team's own prior retirement doc explicitly kept 2 named examples as live, reusable tools, not leftovers. Needs a recount against that doc first.
  • RenewalSnapshot.channelsAttempted — flagged as write-only/dead, actually read by 3 canary/E2E test scripts (35 references) via a live Temporal query. Not dead. The real opportunity is deciding whether to also surface it in the UI, or drop only the redundant DynamoDB write.

5. The nine bespoke-per-session one-offs

This is the pattern you flagged most. A session hits a wall, doesn't check whether the platform already solved it, and builds its own version. Below: the five cases with real dates on them, in order, then four more confirmed cases where the underlying evidence didn't come with a calendar date attached — reported as such rather than guessed.

Jun 3 – Jun 10, 2026

Four different "tell the office a PMS event happened" emails got built back-to-back in the same week — each one a fresh copy of the same send-and-check code instead of one shared version.

4 renewal/turnover PM-notify senders, each duplicating the same send/delivery-check wrapper.

Jul 24, Jul 28, Jul 31, 2026

The same "a vendor call ended, tell someone" feature got built three separate times, three weeks running — and the team's own tracking doc already calls this out by name and it's still not fixed.

The vendor-call-outcome trio, tracked in the published inventory as "door 5 (×3)."

Aug 4 → Aug 13 → Aug 21, 2026

Three escalation-design docs got written back to back, each one starting over instead of picking up where the last one left off — and the newest one never even links back to either of the first two.

escalation-architecture-decision-2026-08-04.htmlescalation-journey-rethink.html (marked as successor) → escalation-architecture-2026-08-21.html (never linked back to either).

Aug 16 & Aug 19, 2026

Two of the three on/off switches for kill-switch–style features were hand-built two to three months after the shared factory for exactly this shape already existed (that shared factory shipped May 26) — each one copying the same logic instead of importing it.

tour-decider-flag.ts (Aug 16) and voice-callback-flag.ts (Aug 19) each hand-copied the killSwitchEngaged() shape instead of importing createDomainGate (shipped 2026-05-26).

Aug 19, 2026

A new test script was built 8 days after the shared "read credentials safely" helper already existed — and still didn't use it. It's not a one-off mistake either: 25 of the 35 similar scripts repeat this exact pattern.

scripts/eval-application-link-subscription.ts (2026-08-19) vs. scripts/lib/subscription-eval-runner.ts (2026-08-11).

Four more confirmed cases, same pattern — no specific date came with the evidence for these, so none is invented here:

6. Sequencing — three phases, each shippable on its own

You don't have to approve all of this to get moving. Each wave stands on its own and has its own proof before the next one starts.

1 Clean the attic — biggest simplification, lowest risk, ships first

Every confirmed-dead delete from the table above (~4,000+ lines of code and docs, zero callers found anywhere); every pure doc-linking fix; the quick reuse wins (register the 2 missing switches, republish the stale audit doc, add GitHub Actions to the schedule tracker, wrap the 1 unmonitored cron job, flip ADR-0123's status, fix the ElevenLabs config gap); merge the 4 phone-lookup copies; delete the duplicate lease doc and the 2 stale email-template docs.

Proof: the build and full test suite stay green (nothing calls the deleted files — already confirmed by repo-wide search); the schedule tracker comes back green with GitHub Actions now included; every new doc link resolves on a manual click-through. Nothing here changes behavior — it's deleting dead paths and linking live docs.

2 Merge what's genuinely the same shape — medium risk, needs real tests

Merge the 4 renewal/turnover notify senders behind 1 composer (keeping the 2 real differences as parameters); share one activity registry between the 2 Temporal renewal workers; build the shared factory for the 12 near-identical scheduled workflows; route the eval scripts and raw-database-client scripts onto their existing shared helpers; extract only the shared plumbing between the 2 voice tool-call routes (not the safety-check logic); fix the 2 places that bypass the safe way of writing conversation-escalation status; group the 23 Camellia-named scripts into one folder.

Proof: a byte-for-byte email-body diff proving the merged sender's output is unchanged; the existing drift/parity tests stay green for both Temporal merges; a smoke run of several migrated scripts against real, non-prod data; confirm the backfill script and the direct-write call site now go through the safe writer.

3 The hard, judgment-gated redesigns — higher risk, needs your calls

The full redesign of what the conversation-escalation field means (gated on Decision 2 below); merging the 6–7 unbacked-promise detectors into 1 shared function; a shared judge-runner for the 20 judge-named files; the full "one door" human-notification consolidation onto the matter lane (gated on the Camellia arming decision, Decision 1); the voice tool-webhook merge scope (Decision 4); folding the person-identity work into the same "one writer per fact" bucket, since it's already tracked as its own project — this plan just says don't solve it twice.

Proof: for the escalation-status and identity work, run the old and new way side by side for a while and diff them before switching over — same as how every other risky Clara change gets tested first. For the detector work, test against real past incidents AND a fresh pull of real production traffic, not synthetic examples. Nothing in this phase starts before you've answered the 4 decisions below.

7. Decisions for Fede

Four calls, all about how far to push the harder work in phase 3. Click an answer — it saves and shows who answered. Reasoning is under "why" if you want it; the recommended option is marked.

1. How far do we push "one door" for paging a person?

why
Option (b) matches what's already been reviewed and avoids re-breaking a fix that was put in place on purpose — an owner's private reply once leaked into a resident's text thread.

2. The conversation-escalation field secretly means three different things (mute Clara, tell the PM, Clara hit her own safety limit). Fix now or later?

why
Option (b) closes the actual incident risk cheaply — the field still means 3 things, but nothing can silently corrupt it anymore.

3. New on/off switches keep getting hand-built instead of through the shared factory. Require a checklist item, or leave it to review?

why
Option (a) is too risky as a hard rule: at least 6 legitimate switches use "off by default on both ends" on purpose during a staged rollout.

4. Merge the two systems that handle a voice-call request like "what's my balance"?

why
Option (b) captures most of the simplification without touching anything that decides whether an unverified caller can see private tenant data.

Anything else / caveats on an answer

PropFlow Docs