Clara's Guard Layer — the 10,000-ft Investigation

2026-08-25 · Commissioned by Fede off the Camellia missing-application-link incident (PR #6278 thread) · Every file:line verified against origin/main at head 6167989bc8 (2026-08-25); the incident trace read directly from propflow-prod (read-only)

The punchline, up front.

Contents

  1. §1 The incident, exactly — provenance
  2. §2 Current state — every guard, verified
  3. §3 Gap analysis — the 3 actions, the trace-judge bar
  4. §4 Golden dataset — exists vs described
  5. §5 The #6278 mechanism — refactor / eliminate / evolve
  6. §6 Recommendation + per-guard fit
  7. §7 Staged migration + proof gates
  8. §8 Decisions — both answered
  9. Appendix — verification notes & corrections

§1 The incident, exactly — what change introduced the mechanism, and why it decided the way it did

Fede's ask, verbatim: "I want to know exactly what change introduced the guard that re-wrote the message, why it decided to re-write the 'right' response and what other gaps it has."

The precise answer: no guard re-wrote anything. The mechanism that discarded the correct reply is not a guard at all — it is the agent loop's final-text-extraction seam, and it has a three-commit history:

DateChangeWhat it did
2026-03-23c199434602Introduced lastIntermediateText: when Clara writes prose in the same response as a tool call and the post-tool response comes back empty, deliver the pre-tool prose instead of nothing. A fallback, nothing more.
2026-05-27aea19ca983
PR #1540
The change Fede is asking about. Stopped persisting pre-tool prose as its own message. From the commit body: "the agent loop was persisting that text as its own assistant message AND then persisting the final post-tool reply — rendering two near-identical customer-facing bubbles… The intermediate text was only ever meant as a fallback… Outbound delivery sends only the final responseText, so persisting the draft was pure duplication. This surfaced after RULE 7.5 (PR #1243) made Clara reliably emit reply-text + an escalation tool_use in one turn, so every leasing escalation produced a double bubble." After this, only the final iteration's text ships; pre-tool text survives solely as the empty-final fallback.
2026-08-19Application-link incident → FINAL_REPLY_STANDS_ALONE_RULEFirst production proof the seam drops content: "your application link has been sent" shipped over a draft carrying the full fee schedule. Fixed at the prompt layer (clara-delivery.ts:147): the final message must stand alone and contain everything.
2026-08-25Daniel's email → PR #6273 (prompt) + PR #6278 (ce121fd357, deterministic salvage)Second production proof — with the 08-19 prompt rule injected on that very turn. The prompt layer alone demonstrably does not hold; #6278 added the code-layer backstop at the seam itself.

1.1 Why the 2026-05-27 decision was right then and wrong for Daniel's turn shape

PR #1540 fixed a real defect: on the escalation shape it targeted, the pre-tool text and the final text were near-identical duplicates, so "deliver only the final" was safe — the content survived in the final bubble. The fix's implicit assumption is final ⊇ draft: whatever mattered in the draft is restated in the final reply. That assumption held for the duplicate-bubble shape and is false in general. On Daniel's turn the draft held the substance — the application link (https://www.camelliadenver.com/#floor-plans, pulled verbatim from PROPERTY INFO), the 12-month special, studio ~$1,000 / 1BR ~$1,200 ranges — and the final was a content-free wrap-up ("All set — just let me know your preferred day and time…"). Same seam, opposite content distribution — and the loop deleted the substance. The model's own stored [[WHY]] on the final turn claims it "Provided application link, surfaced the matching 12-month special, presented unit ranges": it genuinely believed the draft had shipped.

1.2 The trace, step by step — proof no guard altered the turn

Read directly from propflow-prod (TRACE#trace_de3ff842-a479-4425-9795-6040ff7b10b7, conv a05c3c99-…, 2026-08-25T19:18Z). All ten steps:

  1. agent:identity_resolution — verified_prospect
  2. agent:tenant_workorder_context — success
  3. agent:capability_composition — LEASING, ESCALATE_PM · 19 tools
  4. claude-email-initialiteration 0: the complete, correct reply (application link camelliadenver.com/#floor-plans, the 12-month special, studio ~$1,000 / 1BR ~$1,200) + the update_prospect tool call, same response
  5. tool:update_prospect — success
  6. claude-email-loop-1iteration 1: the "All set —" wrap-up. Only this text shipped.
  7. safety:internal-prompt-strip — no leak detected, blocked=false
  8. safety:fair-housingCompliant blocked=false
  9. safety:promise-backing — no unbacked handoff claim, blocked=false
  10. safety:pii-strip — no PII, blocked=false

So on this turn: every guard that ran passed the text through unmodified. The typed-tool-call strip recorded no step (no markup). The hallucination guard never ran — it only arms behind a renewal-offer or active-tour snapshot, and a first-contact Zillow lead has neither. The policy gate (the honesty layer) and the leasing fee-fabrication guard — the two mechanisms whose job description covers this claim family — are both disarmed by default and recorded nothing. The discard happened before any guard saw anything: iteration 0's text was never a candidate for delivery in the first place.

One more honest nuance: even armed, the policy gate as specced would likely not have caught this turn. Its judge verifies claims in the draft against evidence; Daniel's delivered reply made no false claim — it omitted the answer. Omission-of-the-ask is a completeness failure, a different check (it needs the inbound message as an input, which the gate today does not receive — §3.3). The thing that catches Daniel's shape deterministically is exactly what #6278 shipped.

§2 Current state — every guard on Clara's outbound path, verified against code

There are two stages. Pre-generation guards decide whether Clara replies at all; the post-generation pipeline mutates or replaces the drafted reply before send. All line numbers are agents/clara/lib/agent/conversation-manager.ts on origin/main (2026-08-25, post-#6278) unless noted.

2.1 Pre-generation / in-loop guards

GuardWhereLayerOn tripStatus
Runaway auto-reply ceilingCM L3808 · guards/runaway-ceiling.tscode, deterministicSuppresses further auto-replies (two-AI / echo loops); parks for human reviewARMED
Farewell loopCM L3904 · guards/farewell-loop.tscode, deterministicSuppresses reply to trailing closings ("take care!")ARMED
Escalated-thread gateCM L1089 / L4023 · escalated-gate.tscode, deterministicSuppresses auto-replies on threads a human ownsARMED
Emergency guardCM L7794codeIn-loop life-safety routingARMED
Duplicate-WO guardCM L7613codeBlocks duplicate work-order creation in-loopARMED
FirstTurnViolation retryCM L7348code → LLMThe only regenerate loop in the codebase: leasing turn-1 with no tool call → re-calls Claude with forced tool_choice, onceARMED
Prompt-layer rulesclara-leasing.ts:93–95,145,1033 · clara-delivery.ts:147promptNEVER INVENT A FIGURE / NEVER INVENT A WHEN, fair-housing + HUD service-animal rules, RULE 7.0 route-to-team, FINAL_REPLY_STANDS_ALONEALWAYS INJECTED

2.2 The post-generation pipeline — one chokepoint, sixteen mechanisms, fixed order

This is the "centralized layer that runs before Clara responds." It already exists: every text/email reply flows through these in exactly this order.

#MechanismWhereLayerOn tripScopeStatus
1Intermediate-draft-drop salvage (PR #6278)CM L8493 · intermediate-draft-drop-guard.tscode, pure/deterministicAuto-heal: merges dropped URL/$ facts from the pre-tool draft back in, additively; fails closed on any tool failurenot voiceARMED
2Empty-final fallbackCM L8519codeDelivers last pre-tool text when the final is empty (the 2026-03-23 behavior)allARMED
3Decision-why extractionCM L8547 · safety/decision-why.tscodeSilently strips Clara's internal [[WHY]] marker; Sentry-critical on malformed residuetext/emailARMED
4Typed-tool-call stripCM L8573 · safety/tool-call-markup.tscodeStrip; if nothing survives, substitutes an honest "couldn't get that done" + office phone; Sentry-criticalallARMED
5Internal-prompt stripCM L8618 · safety/internal-prompt-strip.tscodeStrip paragraphs quoting system context / naming internal tools; Sentry-criticalallARMED
6aFair housingCM L8663 → compliance/outbound-safety.ts registry → policies/fair-housing-policy.tsprompt + LLM classifierRewrite (classifier-authored suggestedRewrite) or block → canned safe reply. Fails CLOSED on classifier errorleasing, prospects/unknown only (not tenants, not PMs, not voice)ARMED
6bPromise-backingsame registry · policies/promise-backing-policy.tscodeRewrite unbacked "I've passed this to the team" into an honest first-person commitment + deferred real escalation open (fires only after the text is final)same as 6aARMED
7PII stripCM L8687codeStrip, silentall outboundARMED
8Hallucination / made-up-price guardCM L8736 · guards/hallucination-guard.tsprompt + code, deterministicBlock → safe escalation ack, original draft kept as audit row, and auto-fires the real escalation (renewal_escalate / notify_leasing_team) so the promised follow-up is trueonly behind an open renewal offer or active tour snapshotARMED (narrow)
9Output gate (ADR-0101 S2)CM L8997 · turn-integrity/output-gate.tscodeLog only — date/weekday consistency + access-code leak vs committed tour factsper-property flagOBSERVE-ONLY
10Anomaly Review GateCM L9017 · turn-integrity/anomaly-review-gate.tscodeHold: fatal tool failure this turn → park the draft for PM review, send a fact-free holding message, page #alerts (deduped, circuit-breaker)per-property flag, not PM turnsARMED
11Turnover fabrication guardCM L9150code, deterministicBlock → one of three honest replies; draft parked for auditturnover approval turnsARMED
12Turnover async-honesty guardCM L9264codeRewrite completion framing ("created") → in-progress framing while WOs are still mintingturnoverARMED
13Turnover edit-honesty guardCM L9328 · turnover-edit-honesty-guard.tscodeBlock/rewrite on claimed edits that didn't happenturnoverARMED
14Same-turn escalation constraint + team-inbox silenceCM L9447 / L9410code, deterministicTemplate-substitute the ack+handoff copy on escalating turns; on team-monitored email inboxes, suppress entirely (Decision 9)escalating turnsARMED
15Policy Gate — the honesty layerCM L9610 · compliance/policy-gate.ts + guards/trajectory-judge.ts + guards/claim-prefilter.ts + policies/escalation-claims-policy.tscode registry + Haiku judge behind a bilingual regex prefilterpass / repair(fire the tool that makes it true) / hold(park + holding copy) / block(reserved) — full enforce wiring exists incl. audit rows, bilingual correction copy, repair-success = "a human was actually reached"not voice, not PMDISARMED (POLICY_GATE_MODE=off)
16Leasing fee-fabrication guardCM L9996 · leasing/lease-terms/fee-fabrication-guard.tscode, deterministicBlock any $ figure not traceable to this turn's rendered grounding (prompt + tool results + pricingDetails) → parked draft + handoff ack + auto-escalationleasing, not voice, not PMDISARMED (LEASING_FEE_FABRICATION_GUARD_MODE=off)

Reading the table against Fede's vision: the "centralized layer that runs before Clara responds" is not missing — it is rows 1–16, one call site, fixed order. What's fragmented is the decision logic: rows 8, 11–14 are hand-written per-domain checks bolted on over four months of incidents, while row 15 is the general framework they were supposed to converge into — built, spec'd (docs/planning/policy-gate-spec.md, PR #5815), measured offline, and never armed. Row 16 is a second finished-but-dark guard. The architecture source of truth (§4.5, "The honesty layer — history, failure, redesign") already tells this exact story and prescribes the convergence; this doc's job is to confirm the code matches it (it does) and put the arming decision on the table.

§3 Gap analysis — Fede's three actions, then his trace-judge bar

3.1 The three actions: rewrite / flag / auto-heal

Desired actionExists?Evidence
Auto-heal (preferred)EXISTS, armed in 3 forms(a) #6278 salvage merges dropped facts back — deterministic auto-heal; (b) the hallucination guard auto-fires the real escalation its substitute text promises; (c) the policy gate's repair verdict fires the tool that makes an unsupported claim true — implemented, disarmed. The "make it true" design beat blocking decisively on real data: 0/166 false positives vs 0/28 true catches for the blocking twin (source-of-truth §4.5, "the natural experiment").
Flag for engineering / human reviewEXISTS, armed in 2 forms(a) Anomaly Review Gate holds the draft, sends holding copy, pages #alerts; (b) every blocking guard parks the original draft as a msg_blocked audit row visible on the conversation + Decision Line ("a safety check caught it"); (c) the policy gate's hold verdict generalizes this — disarmed.
Push back to Clara to rewriteDOES NOT EXISTNo guard verdict re-invokes the model. Nearest neighbors: FirstTurnViolation re-calls Claude with forced tool_choice (pre-guard, turn-1-leasing only, missing-tool-call only); fair housing ships an LLM-authored suggestedRewrite (authored by the classifier in the same call — not a regenerate-with-feedback loop through the agent).

3.2 Should the rewrite loop be built? The honest trade

The absence is a decision the codebase already made once, implicitly: post-generation guards are deterministic precisely because they backstop a non-deterministic generator. A regenerate loop reintroduces that non-determinism at the safety boundary, plus:

Position taken here: don't build it as part of arming; add a revise verdict later, scoped to completeness findings only, one retry, deterministic re-check, never on voice — and only once the offline replay shows the residual class actually occurring. That decision is raised for Fede in §8.

3.3 The trace-judge bar: "inspect the whole trace… second pass against ground truth, independently, as a judge"

Mapping Fede's four inputs against what the built (disarmed) gate actually receives (EvaluatePolicyGateInput, policy-gate-types.ts:143–149):

Fede's barGate todayGap
"what tools it called"YES — this turn's toolSteps with real result payloads
"what context it had"PARTIAL — last 20 earlier-turn rows + distilled grounding facts (tour snapshot, prepared offer, escalation status)Not the rendered system prompt / property knowledge Clara actually saw; grounding is a curated subset
"what came in"NOcontextMessages is sliced to before this turn (slice(0, turnStartIndex), CM L9628); the current inbound message is not an inputBlocks any completeness check ("did the reply answer the ask?") — the Daniel class is invisible to the judge by construction
"what it reasoned"NO — thinking blocks are not captured into the trace or the judge inputThe [[WHY]] marker (Clara's own one-line rationale) IS captured per turn and would be a cheap proxy input; full reasoning capture is a separate lift
"independently as a judge"YES — separate model (Haiku, temp 0, forced structured output, 2.5s budget, fail-open-and-counted on timeout), behind a bilingual claim prefilter so it never runs on "sounds good!" turns
"block problematic messages"BUILT — hold/repair wired end-to-end; block reserved for high-confidence deterministic hitsDISARMED

So the built gate is a trace-level judge in shape — it grades the draft against tool evidence and history, not just against token patterns — but it is a claim verifier, not yet a completeness checker, and two of Fede's four inputs (the inbound, the reasoning) aren't plumbed. Both are additive input changes to PolicyGateEvalInput, not redesigns; the replay harness already reconstructs turns from AgentTrace rows that carry the inbound, so the offline measurement can validate the enriched judge before it ever runs live.

§4 The golden dataset — what exists vs what Fede described

Fede: "Us as the humans we'll be annotating conversations to build a golden data set of what good and bad is which then should serve as our regression suite to avoid bad changes going prod."

PieceTodayVerdict
Per-domain regression evals57 promptfoo domain configs / 59 datasets (evals/); every classification bug must become a case; CI-gated on Clara-path PRsEXISTS — but single-turn/prompt-level, not whole-conversation good/bad
Guard replay over real trafficscripts/eval/replay-guards.ts — 12 registered guard predicates, deterministic replay over ≥30 days of prod, hand-labeled per row (the RCA merge gate). #6278's 7-row labeled table is the live exampleEXISTS — this IS human annotation, but per-guard, per-PR; labels live in PR bodies, raw reports gitignored — no durable corpus accumulates
Auto-pull + judge disagreement pipelineevals/pull-golden-data.ts (pull transcripts + SMS, LLM-judge, flag disagreements → evals/candidates/)DORMANT — the com.propflow.eval-pipeline launchd job is not loaded on the mini and evals/candidates/ does not exist in the repo
Post-hoc conversation judgingconversation-review-dailyPAUSED since ~Aug 16 (source of truth, open decision Q1) — nothing currently reviews a finished conversation after the fact
Whole-conversation annotation surfaceMISSING — no way for Fede/Gera/PMs to label a real conversation good/bad from the UI, and no store those labels land in

Sketch of the missing half (deliberately small): (1) a label action on the conversation page — good / bad + one-line why + which turn — writing a PII-safe pointer row (conversationId + turn index + label, never copied text); (2) the labeled set becomes a replay manifest: the existing replay harness re-runs the guard chain + (offline) the judge over exactly those conversations on every candidate build, asserting bad ones trip and good ones don't; (3) the same rows calibrate the trajectory judge (its spec already calls for a ≥200-case gold set). This turns the annotation habit Fede described into the regression gate he described, using two harnesses that already exist. It also answers Q1: resuming post-hoc judging feeds the candidate queue.

§5 The #6278 mechanism — refactor, eliminate, or evolve?

Fede's third ask: what other gaps does the mechanism have, and does it get refactored, eliminated, or evolved?

5.1 What the seam + salvage still miss

5.2 Verdict: EVOLVE — and it's already increment 0

Not eliminate: removing it reopens both August incidents (the 30-day replay shows it catching exactly those two real-person turns, 0 unexplained false positives). Not refactor-now: it is pure, measured, registered in the replay harness, and additive. The architecture source of truth's honesty-layer migration plan names increment 0 as "fix the outbound-composition contract that let a correct answer get silently dropped across tool-calling rounds" — #6278 is that increment, for the mechanical-token subset. Evolution path: (a) short term, keep as-is; (b) when the policy gate registry is armed and vocabularies unified (§6), re-home it as a rewrite-class policy so its verdict/trace/measurement ride the same rails; (c) the residual omission classes (dates, unit numbers, answers) belong to the judge's future completeness check + optional bounded regenerate (§3.2), not to more regexes — piling fact-class regexes onto the salvage is how guard sprawl started.

§6 Recommendation — arm and converge, don't rebuild

Is a refactor warranted? Yes — but it is an arming and consolidation refactor, not a build. The settled verdicts in the architecture source of truth (§4.5) already prescribe the single-chokepoint, three-way-verdict design; the code already implements it; what's missing is the operational sequence that turns it on without repeating P8 (the Aug 13–16 guard that told three real customers "nothing is scheduled" — reverted, and the reason the guard-change merge gate exists).

6.1 Can the Policy Gate express each existing guard? The fit table

Checked per guard against the gate's verdict shape (pass/repair/hold/block) — with one honest finding: the two registries speak different verdict vocabularies. outbound-safety.ts says pass/rewrite/block (text substitution); policy-gate.ts says pass/repair/hold/block (no rewrite/substitution verdict). Consolidation needs the union — add a rewrite (substitute/merge text) verdict to the gate contract, or the fair-housing/promise-backing/turnover-rewrite class cannot migrate.

GuardFits pass/rewrite/repair/hold/block?Note
Fair housingrewrite / blockNeeds the rewrite verdict + replacement text; LLM classifier stays its own policy module (fail-closed)
Promise-backingrewrite + deferred repairIts deferred-escalation pattern is a gate-level primitive worth keeping (act only after text is final)
Hallucination guardblock + repairBlock-with-substitute + auto-fire = exactly block + repair composed; its snapshot scoping becomes the policy's appliesTo
Turnover trio (fabrication / async / edit)block / rewriteDeterministic; three appliesTo-scoped policies
Fee-fabrication guardblockAlready registry-shaped; the gate spec explicitly reserves block for exactly this class of high-confidence deterministic hit
#6278 salvagerewrite (additive)Merge = rewrite with deliveredText
Same-turn escalation template + team-inbox silencerewrite / suppress — borderlineSuppression ("send nothing") is not a gate verdict today; either add suppress or leave this at the call site — it is turn-shape routing as much as safety
Anomaly Review GateholdIs the hold mechanism the gate's enforce path already reuses
PII / typed-markup / internal-prompt / decision-why stripsDo not migrateThey are unconditional deterministic transforms, not judgments — no verdict, no measurement question. Keep as the fixed pre/post stages around the registry
Pre-generation guards (runaway, farewell, escalated-gate, FirstTurnViolation)Out of scopeThey decide whether to reply, not whether the reply is true — a different layer, correctly separate

Bottom line: every judgment-type guard fits once rewrite is added to the verdict contract; the strips and the reply-suppression layer correctly stay outside. Nothing found that structurally cannot migrate.

6.2 The recommendation in five lines

  1. Write the missing ADR. The guard framework's change-control is cited as "ADR-0123" in code — that number is actually scheduled-job-placement; the guard-framework ADR was never written. Write it: one chokepoint, the unified verdict vocabulary, the measurement obligation, the arming ladder. (Fix the two stale citations in the same PR.)
  2. Close the judge's two input gaps (inbound message; [[WHY]]/reasoning proxy) and add the completeness check the Daniel class needs — offline first.
  3. Measure, then arm the Policy Gate: run the already-spec'd offline replay (60 days, all channels, subscription runners), hand-label per the RCA gate, then POLICY_GATE_MODE=enforce — repair + hold only, block stays reserved — bench property first, then Camellia. Same ladder for LEASING_FEE_FABRICATION_GUARD_MODE.
  4. Migrate bolted-on guards into the registry one at a time, each with a zero-verdict-drift replay (same parity discipline the workflow-permutation harness uses). No behavior change per migration PR.
  5. Decide the rewrite loop with data (§3.2): only after measurement shows the omission-residual class, as a bounded revise verdict — never as a general regenerate.

§7 Staged migration sketch — each stage with its proof gate

StageWhatProof gate before it ships / arms
0 DONEDeterministic seam fix (#6278) + prompt rules (#6273, FINAL_REPLY_STANDS_ALONE)Shipped: 30-day replay, 7 would-alter, both real-person incidents caught, 0 unexplained FPs
1Guard-framework ADR + verdict-vocabulary union (rewrite added to the gate contract); fix the ADR-0123 miscitationsDocs-only + type-level; drift test pinning the registry contract
2Judge input enrichment: inbound message, [[WHY]] proxy, completeness checkOffline only — replay over 60 days of reconstructed traces (the spec's Track C/D machinery, subscription runners); Daniel + 08-19 turns must flag; labeled FP rate within the target before any live wiring
3Arm POLICY_GATE_MODE=enforce (repair+hold only) — Willows bench → Camellia. Arm the fee-fabrication guard on the same ladderThe RCA 2026-08-15 merge gate, per guard: ≥30-day prod replay, every real-person would-alter hand-labeled, any unexplained real-person FP blocks. Plus rollback = one env unset (fail-closed by construction)
4Migrate existing guards into the registry, one per PR: fee-fabrication → hallucination → turnover trio → outbound-safety policies → #6278 salvageZero-verdict-drift parity replay per migration (old vs new path over the same 30-day corpus; any changed verdict blocks)
5Annotation → golden set → regression manifest (§4 sketch); resume post-hoc judging (Q1)The labeled manifest replays green on main before it becomes a required check
6If measurement justifies: the revise (push-back-to-Clara) verdict — completeness findings only, 1 retry, deterministic re-check, never voiceSame merge gate + explicit latency budget; falls back to hold on second miss
7Voice: post-call reconciliation against the same ledger (settled doctrine, H3)Own replay over voice traces; correction via callback/SMS only

§8 Decisions — both answered

Two decisions from this investigation are raised on the Decisions tab: (1) arm the Policy Gate as the centralized layer on the staged ladder above, vs keep guards distributed; (2) build the push-back-to-Clara rewrite loop now, vs auto-heal + hold with revise-later. They compose with the four honesty-layer decisions already open on the architecture source of truth (H1 judge trigger frequency, H2 tool-execution gating, H3 voice mid-call vs post-call, H4 the credentialed nightly replay job) and Q1 (post-hoc judging paused) — this doc deliberately does not re-litigate those; stage 3 above assumes H1's recommended narrow-trigger start, and stage 5 is Q1's option C in miniature.

Both were answered by Gera on 2026-09-01, each matching the recommendation above.

  1. Arm the Policy Gate — staged. "Arm it staged: enrich the judge inputs (current inbound + reasoning), run a 60-day offline replay with hand-labeling, then enforce (repair+hold only, block reserved) on the Willows test bench, then Camellia." The §7 ladder above is therefore the adopted plan, not a proposal. Note the ordering is load-bearing rather than cautious: per §3.3 the judge cannot see an omission without the inbound message, so input enrichment is a precondition for the replay to measure anything, not merely the gentlest first step. Nothing here is built yet.
  2. No rewrite loop now. "No rewrite loop now: deterministic auto-heal + hold cover both August incidents; revisit once the armed gate’s measurement shows an omission class only a rewrite fixes." This settles the position stated in §3.2 and keeps row 6 of the §7 ladder conditional. The revisit trigger is explicit and belongs to stage 2’s replay: an omission class that repair, salvage and hold demonstrably do not cover. Absent that evidence, the revise verdict does not get built.

The four honesty-layer decisions on the architecture source of truth (H1–H4) and Q1 remain open and are unaffected by these two answers.

Appendix — verification notes, and corrections to the pre-investigation scoping

Prepared 2026-08-25 by the guard-layer investigation session (778f0ada), on Fable, at Fede's request. Sources: propflowai@6167989bc8 (origin/main), propflow-prod trace trace_de3ff842-… (read-only), docs/planning/policy-gate-spec.md, the architecture source of truth §3.1/§3.3/§4.5/§4.6/§10, PRs #1540, #5815, #6273, #6278, commits c199434602, aea19ca983, ce121fd357.
PropFlow Docs