Clara's leasing prompt contradicts itself

Read-only sweep · origin/main @ cccca7995 · row prompt-self-contradiction-sweep · 25 Oct 2026

No eval row can find any of these, and that is the whole point. A row sees Clara's reply. It never sees that her instructions disagreed with each other. So every contradiction below presents as a Clara defect, gets debugged as one, gets a wording fix, and comes back — because the wording was never the problem. The prompt's own docblock records this happening: clara-leasing.ts:952–956 says the model copied a worked example verbatim over the instruction two lines away, twice in a row, in a run where every other case passed.

Three were handed to this sweep; nine are recorded. The sharpest: the time_conflict fix landed in the text prompt and a live copy survives in the voice one — and the code already knew (tools-leasing.ts:5565).

The nine, one line each.  1 the time_conflict invented cause still live in the voice prompt, contradicting its own tool result — 2 an exemplar offered at :908 and condemned verbatim at :911 — 3 hard-fail item 6 vs decider f4ef955f2 — 4 “shortly” on an ALWAYS SAY list and a banned-clock list — 5 “the leasing team” 19× against a pinned ruling to say “the property team” — 6 RULE 7.0’s required hand-off is a RULE 7.7 failure three ways — 7 every PRESENTING UNITS exemplar ends on the close the next line forbids — 8 notes vs preferences in three places, which Gera decided on 2026-09-22 — 9 the same offer-then-condemn shape again, making it a convention.

Three fixed or in flight (#22318, #22337, #22349); two need a human; the rest are code fixes against rulings already made.

The nine contradictions — file, line, both passages, status

Every quotation read out of git show origin/main:<path>, never the shared checkout. Lines are origin/main @ cccca79951c7bec5fc9a199680a54c11b71abbb8.

#WhereWhat disagreesStatus
1 agents/clara/lib/voice-agents/leasing.ts:397
inside PROMPT (exported at :159) of the live agent Clara — Leasing, agent_8601krxqwzd2fhyrpqcfm71ndzh3
The voice prompt hands Clara the invented cause the tool result forbids, in the same turn.
Prompt: If schedule_tour returns time_conflict, present the suggested alternatives: "That just got booked — I have [alt1] or [alt2] on that day. Which works?"
Tool result, tools-leasing.ts:5570 timeConflictMessage(): You do NOT know WHY it is unavailable, so never give a reason for it: never say it just got taken, just got booked, someone grabbed it, it filled up, it was being held, or that it was available a moment ago.
Distorts: #9185 / the Western Slope live call of 2026-09-17 21:10:11Z, the 2026-09-29 Western Slope 2 PM incident, and eval check-availability-slot-reason (issue #18914).
Already recorded and deferred: tools-leasing.ts:5565–5568 — FOLLOW-UP, NOT DONE HERE: the shared leasing prompt still carries the "That just got booked" example at agents/clara/lib/voice-agents/leasing.ts. Removing it changes Camellia's rendered prompt bytes, so it is its own change with its own parity proof.
OPEN
the text-prompt twin was fixed by #22318 (merged 2026-10-25 04:39Z); this copy was not in that diff
2 clara-leasing.ts:908 vs :911 The exemplar is offered, then condemned verbatim three lines down.
:908 — Decline generically: "I can't share details about other people — but I'm happy to get your visit set up."
:911 — BAD: "I can't share details about other people — but I'm happy to get your visit set up!" with no word about Sam → it drops the note they asked you to make.
A model that applies :908's bullet as written produces :911's BAD. :909 carries the missing precondition (the note and the decline go in the same reply), so this is resolvable by a careful reader — which is exactly what :952–956 measured the model not being.
In the denied-applicant prompt it is worse: the two become the same string. The mutation controls at :1391–1392 rewrite :908's exemplar to Decline generically: "I can't share details about other people — I'll make a note and have the team follow up.", and :1401–1402 rewrite :911's BAD to BAD: "I can't share details about other people — I'll make a note and have the team follow up." with no word about Sam. Byte-identical sentence, offered and condemned.
OPEN
#22337 names it and declares it out of scope: fixing it means rewriting the exemplar with lockstep mutation-regex repair
3 clara-leasing.ts:945, pre-send hard-fail item 6 A rule contradicts a recorded decider ruling.
Prompt: If your reply puts any of those words next to that person or "other people" — "Sam's application, unit, or status", "other people's applications or units" — → HARD FAIL (RULE 13)
Decider f4ef955f2 (resolved, answered by Fable): A GENERIC category list with no name or pronoun tied to her ("I can't share anything about other people's applications or status") does NOT fail: it mirrors the visitor's own question and would read the same if Carly were not on file.
With the nameless list banned too, saying nothing is the only safe move — and the measured failure mode is Clara dropping the principal's name entirely. Distorts eval row neutral-ack-no-status-leak.
FIX IN FLIGHT
#22337, open. Its diff narrows item 6 to a record kind tied to her name or a pronoun and blesses the nameless list, citing f4ef955f2
4 clara-leasing.ts:817 and :821 vs :1698 "shortly" is on an ALWAYS SAY list and on a banned-clock list, in the same rendered prompt.
RULE 8, :817 — ALWAYS SAY: "I'll check with the leasing team and get back to you shortly.", and :821 adjudicates it explicitly: the three phrasings above are complete as written — "shortly" is register, not an interval.
RULE 7.7, :1698 — forbidden: anything with a clock on it: "tomorrow", "shortly", "within 24 hours", "either way", "as soon as I hear back".
Both render in every leasing prompt: claraAutonomousHandoffRules(…) is interpolated unconditionally at :2288, and a test is named for it — src/__tests__/autonomous-deferral-shape.test.ts, renders at every property — the coworker prompt is gone (PR 4a).
The code sides with RULE 7.7: that same test asserts the code's own deferral template (autonomousDeferral, rendered by line()) against a banned regex that includes shortly. That is a test over the template, not a rewrite over Clara's output — so the honest statement is that the sentence the code is built to emit may not contain shortly, while RULE 8 tells the model to say it. RULE 8's ALWAYS SAY list is the outlier of the three.
OPEN — new
5 clara-leasing.ts, 19 places in :671–946 vs :1670 The desk name: a founder ruling is pinned on one block and contradicted 19 times in the block beside it.
RULE 7.7, :1670 — ONE NAME FOR THE TEAM, EVERY TIME (Fede's ruling, 2026-09-07) … The fix is not a smarter split — it is not naming a desk at all. Say "the property team", full stop.
CLARA_CONCERNS_RULES contains the string the leasing team 19 times — but a string count is not a prescription count, so here is the split. Eight are in sentences Clara is told to say: :686, :727, :748, :782, :817, :819, :829, :833. Five are in NEVER-SAY or banned lists (:811, :813, :841, :848, :858) and so agree with :1670 rather than contradicting it. The remaining six are instructions to the model (ALWAYS ROUTE THESE TO THE LEASING TEAM, :671) or narrative about a prod incident (:787), which Clara never speaks. The contradiction is the eight, because Fede's :1670 ruling governs what Clara says.
The guard is exactly as wide as its author imagined: autonomous-deferral-shape.test.ts asserts expect(CLARA_AUTONOMOUS_HANDOFF_RULES).not.toContain('the leasing team') — one const, while the const rendered immediately above it is full of them.
OPEN — new
6 clara-leasing.ts:686 (+ :687, :690) vs :1667, :1673, :1674 RULE 7.0's mandatory hand-off sentence is a failure under RULE 7.7, three ways over.
RULE 7.0 makes it required — :686 2. Hand it off: "that's something our leasing team handles directly as part of the application — I'll have them confirm the details with you.", and :687 Sending part 2 WITHOUT part 1 is a fail; :690's GOOD example is built from it.
RULE 7.7 rejects that sentence for naming a desk (:1670), for being third-person team follow-up (:1673 Never "the team will reach out" / "someone will contact you": the answer comes back through you), and for being composed at all (:1674 USE THESE WORDS. Not words like them — these … A deferral you compose yourself is the failure, however good it sounds) — whose FAIL list names a near-twin: "voucher acceptance is something our property team handles directly".
:1657 claims a blanket override (Wherever the rules above tell you to promise team follow-up, THIS RULE OVERRIDES THEM), so precedence exists on paper. It does not remove a prescribed sentence whose twin is listed as a failure 988 lines later.
needs a human
which sentence wins is a behaviour decision, not a lane's call
7 clara-leasing.ts:2020, :2021, :2026, :2028 vs :2029 Every PRESENTING UNITS exemplar ends on the closing question the next line forbids.
The TEMPLATE (:2020), the WORKED EXAMPLE (:2021) and both GOOD examples (:2026 voice, :2028 text) all end — want to come see them?, with no times in the reply.
:2029 — After presenting units, PUSH TOWARD A TOUR — don't just ask "would you like to schedule a tour?" Call check_availability and present the returned morning/afternoon appointment times.
The file's own docblock is the measurement: :952–956 — PRESENTING UNITS' GOOD examples END on the literal sentence "Want to come see them?", and the model copied the example verbatim. Instructions lost to a demonstration, twice in a row, in a run where every other case passed.
needs a human
whether a units reply carries times is a behaviour decision
8 clara-leasing.ts:1961, :1989 and :2154 vs tools-leasing.ts:410 The prompt contradicts a founder ruling that was made 33 days ago.
Tool description, tools-leasing.ts:410 (update_prospect.notes) — Anything they say they WANT in a unit goes in preferences, never here.
Prompt, in three places — :1961 notes is for unstructured signals only (parking, floor preference); :1989 notes: any other preferences (parking, floor, pets, roommate, etc.), the most explicit of them; and :2154 if the caller asks about pet fees, parking costs, or mentions a roommate, capture these details in notes or ai_summary. A floor preference is a want in a unit.
This was raised and it was decided. Seven decider receipts carry the question (f248be919, f3baa0d90, fe4c7203e, ffe933c9c, fd2e91bbe, fbcc02f46, f8f02f260) and every one is verdict ERROR, answered_by: null — so it went to the human rung. Block b90028734, raised 2026-09-22 16:07Z, is state: resolved fourteen minutes later: by: gera via /agents, choice Put wants in Preferences, and show Preferences on the card.
So the tool description is the ruling and the prompt is the drift. Its own measurement (#10609 / #10613) found update_prospect carried notes 0 times in 36 single-turn cases, while prod row 68bab116 carried lease-term and budget remarks in notes — the behaviour followed the prompt.
FIXED IN FLIGHT
#22349, open — all three sites, with two preference_capture: false regression rows and a pre-change control run. Gera decided it on 2026-09-22 and the prompt contradicted him for 33 days
9 clara-leasing.ts:686 vs :689 The same offer-then-condemn shape as #2, which is why it is listed: the pattern is recurring, not an isolated slip.
:686 offers the hand-off sentence; :689 labels the identical sentence BAD … → WRONG: never mentions the voucher; pure template.
Unlike #2, the disambiguating sentence is immediately adjacent and unambiguous (:687, Sending part 2 WITHOUT part 1 is a fail), so this one is low severity on its own. It earns a row as evidence that the prompt's authoring convention is “quote the clause, then condemn the clause standing alone”.
LOW
How the sweep ran, and the four positive controls that prove it ran

A single-line grep misses a wrapped prompt string, so nothing here used one. sweep.py flattens each file to one whitespace-normalised string before any pattern is applied, maps each match back to a source line, extracts every double-quoted span of 20–400 characters, then reports a span that appears both near a condemnation marker (BAD / NEVER / HARD FAIL / BANNED / WRONG / ❌) and near an offer marker, by exact normalised match and by fuzzy match at ratio ≥ 0.82.

Coverage. 4,045 quoted spans: 825 in clara-leasing.ts, 2,785 across 54 non-test files under agents/clara/lib/voice-agents/, 435 across clara-unified.ts, clara-maintenance.ts, clara-voice-renewal.ts, clara-personality.ts, clara-text.ts, clara-sms.ts, spoken-delivery-rules.ts. The invented-cause sweep additionally walked 1,269 non-test files under agents/clara/, evals/prompts/ and src/lib/domain/leasing/.

Four positive controls, so a zero is evidence about the sweep and not about the prompt.

  • The known 908/911 pair — must be found. Found, as a 0.99 fuzzy pair and an exact pair.
  • time_conflict — 30 hits across 8 files.
  • NEVER INVENT WHY — 3 hits, all in clara-leasing.ts.
  • A deliberately backtick-crossing wrapped string, goes in `preferences`, never here — this one returned 0 until the normaliser stripped backticks and escapes, then returned 1. That is the row's warning reproducing itself on the first try, and it is why the normaliser strips them.

One truncation trap, hit and corrected. The first invented-cause pass printed 18 hits and reported HITS: 27. The surviving voice sibling (finding #1, the headline) was in the 9 the print cap hid. The pass was re-run with per-file counts and no cap. A result you only half-printed is not a result.

Two claims this page does not make. It does not claim a behaviour — every assertion above is about what a prompt file or a tool description says, quoted, with the file and line it says it at; no reply was generated and no eval was run. And the binding of schedule_tour to the live voice agent is read from leasing.config.json's own dated note (it names the 6 inline tools that were not bound as of 2026-10-16, and schedule_tour is not among them) — ElevenLabs was not probed.

What came back clean, and the 25 pairs still unread

Adjudicated by reading the surrounding passage, and clean: all 20 flagged pairs in clara-leasing.ts — the 9 above are what survived. The rest were a prospect's own line appearing in both a BAD and a GOOD transcript (:893/:899, :1980/:1981), an acknowledgement prescribed in two places (:920/:942), a denied-applicant mutation-control regex quoting the line it rewrites (:1747, :1392/:1404), or a correctly-scoped clause: :2228 quotes would you like to schedule a tour? as the trigger for calling check_availability first, not as a reply to send, so it does not contradict :2029. voice-agents/leasing-many-homes.ts (4 pairs, every one a ban on a different sentence that my classifier mis-attributed); voice-agents/vendor-outbound.ts :261/:331 (the same ban restated) and :289/:376 (turn-scoped: the timing ask is banned on the job line and prescribed as its own later turn).

Flagged and NOT yet adjudicated — listed so the next lane does not have to re-derive them, and so this page does not read as a clean bill it has not earned: voice-agents/after-hours-message-desk.ts :272/:1023/:1027 and :1040/:1084; voice-agents/leasing.ts :203/:204/:275/:352 and :344/:383/:551 and :535/:551; voice-agents/triage.ts :143/:406 and :188/:192; voice-agents/renewals-off-directive.ts :54/:256/:258; voice-agents/many-homes-triage-handoff-slot.ts :5/:19/:69; voice-agents/maintenance-off-directive.ts :71/:184; voice-agents/prototypes/western-slope-portfolio-triage.ts (10 pairs — a prototype, liveness unverified); clara-unified.ts :320/:350 and :224/:235 and :807/:1284; clara-maintenance.ts :224/:391 and :383/:469.

What this lane did not do, on purpose

This lane was read-only on prompt files. #22337 was open and being pushed to on clara-leasing.ts while this ran, so nothing here edits a prompt. Two findings are behaviour decisions — #6 (which sentence wins when RULE 7.0 and RULE 7.7 both prescribe) and #7 (whether a units reply carries times) — and a lane does not settle those. #3, #5 and #8 are not decisions at all: each one has a recorded ruling (f4ef955f2; Fede 2026-09-07; Gera 2026-09-22 on block b90028734) that the prompt has not been brought in line with, so each is a code fix with its own citation. #8 is now PR #22349, opened from this lane, because a decision a founder made in fourteen minutes and nobody executed for 33 days is the one finding that must not be filed again. Finding 1 is likewise plain: delete the invented cause from voice-agents/leasing.ts:397 and replace it with the shape clara-leasing.ts now carries. tools-leasing.ts:5567 says what that costs: Removing it changes Camellia's rendered prompt bytes, so it is its own change with its own parity proof — a voice-prompt diff, so the fast lane runs twice before the PR exists.

The mechanism, which outlives any of the nine. Three of these findings were already written down by someone who then handed the fix to nobody: tools-leasing.ts:5565 names the exact surviving That just got booked line and the file it lives in (#1); preference-vs-fact.yaml says Measuring it is this pack's job; changing it is not and Naming that is this pack's job; settling it is not (#8); and block b90028734 was answered by a founder in fourteen minutes and then sat unexecuted for a month. A “follow-up” written into a comment is a prerequisite owned by nobody — it reads as diligence and behaves as a drop, invisible to CI, to the reviewer and to every eval row. That shape is worth a guard; no single one of the nine lines is.

Read from git show origin/main:<path> and git archive origin/main at cccca79951c7bec5fc9a199680a54c11b71abbb8, fetched 2026-10-25; never from the shared checkout. Decider receipts read from ~/.claude/jobs/fable/ (4,549 receipts; 95 matched a Clara-leasing question pattern). PRs read with gh pr view / gh pr diff: #22318 (merged 2026-10-25T04:39:34Z), #22337 (open). Sweep script and raw output in the lane scratch dir scratchpad/lanes/prompt-self-contradiction-sweep/.

PropFlow Docs