CedarCopilot wants to merge 1 commit into staging from fix/stage-inference-terminal-lock
Live on prod, no production signal yet
Strict CloudWatch fallback found 54 recent prod failure log lines for [UpdateExternalCrmWorkflow] Driver update failed (Step updateOpportunityField failed: HTTP 400: [{\), but the same failure had 96 log lines in the comparable pre-deploy baseline. Libra is not blaming this PR.
Libra has verdicts on 0 of 1 tracked behaviors on prod; 1 is still being checked. Libra checks hourly for 3 days after each deploy.
Two defects in stage inference, both surfaced against Warp's live Salesforce OpportunityHistory during the data audit. Both cause the inferred stage to misreport where a deal actually got to.
Once a deal is won / lost / dq it is over. But post-close traffic , onboarding calls, support threads, invoices , kept getting reclassified as an earlier selling stage, walking a closed deal backwards.
Terminal now only yields to another terminal, so a genuine reopen (won → lost) still lands while ordinary post-close noise does not.
The terminal check sits before the off-ramp branches on purpose: otherwise a non-terminal off-ramp becomes an unlock hatch back into the ladder (terminal → no-show → discovery).
A missed meeting is recoverable , reps rebook it , so it has to be a reversible side state. Treated as an ordinary linear stage it did two wrong things at once: it read as forward progress, and at a high enumOrder it permanently froze the arc.
In Warp's book there are 41 No Show → Demo Scheduled (S1) transitions, which would have frozen 35 in-scope deals, 8 of them won.
3 new cases , terminal lock, terminal→terminal reopen, no-show recovery. 17 pass (verified on a clean staging base, not just in my working tree).
Test Files 1 passed (1)
Tests 17 passed (17)
Deliberately just the two fixes. The Warp audit tooling and deliverables are a separate PR so these two logic changes get read on their own.
🤖 Generated with Claude Code
This PR makes terminal CRM stages resist later nonterminal event noise while preserving terminal-to-terminal transitions, and treats no-show stages as reversible off-ramps.
The PR should not merge until the mail-side off-ramp classifier is updated so Active Pipeline filtering agrees with the new server treatment of no-show stages.
The terminal reconciliation logic is covered and consistent with the stated behavior, but the server-only no-show classification change causes the mail Active Pipeline preset to retain deals that the server now classifies as parked off-ramps.
Files Needing Attention: apps/server/src/services/crm/stage-inference.ts and apps/mail/modules/crm/utils/stage-terminality.ts
| Filename | Overview |
|---|---|
| apps/server/src/services/crm/stage-inference.ts | Adds the intended terminal lock and no-show off-ramp behavior, but updating only the server classifier leaves the mail-side mirror inconsistent. |
| apps/server/src/services/crm/tests/stage-inference.test.ts | Adds focused reconciliation and classifier coverage for the new server behavior, though it does not cover parity with the mail classifier. |
### Issue 1
apps/server/src/services/crm/stage-inference.ts:88-89
**No-show classifiers diverge**
When an organization uses a “No Show” stage with th
Libra has not measured any production surfaces for this change yet.