FOS-3 — the new exit-4 escalation can be masked by yesterday's failed BUY, and its only test reads the source as text: the one exposure-left-on alarm has two ways to stay silent
status: open · raised panel run 21 (2026-09-11, change-review catch on PR #154 — does NOT count toward the frontier cadence) · class: loudness & safety (G2, but the exit-4 path is trading-contract, not observability) · effort ~0.75 pd · horizon: live daily — the AVB streak proves failed de-risking orders are not hypothetical
Plain-language summary for an owner reading one paragraph. Last week's
IGD-1 leg-3 ruling built the escalation this shop had been missing: thales run now exits 4 — redding CI — when a de-risking sell fails for a symbol
that was NOT already failing, so a NEW stuck exit pages instead of joining
the furniture. Two defects in the build, found by this run's change review.
First, "already failing" is side-blind: if symbol X had a failed BUY
yesterday (a symbol halt or one broker-wide buying-power rejection day
poisons every symbol at once) and X's SELL fails today for the first
time, today's exposure-left-on event exits 0 and lands only in the digest —
the ruling's letter is met, its rationale (a failed buy leaves no exposure;
the sell does) is not. Second, the only test pinning the escalation reads
cli.py AS TEXT and asserts substrings — it fails if the code is deleted,
but passes if the code is present and neutered (an exception before the
raise, a later try-wrap, reordering that makes it unreachable). The single
alarm for "the book cannot get out of a name" is guarded by a detector that
cannot fail in its primary failure mode.
Mechanism — ground truth verified this run at HEAD fa55f0d
src/thales/execution/failed_streaks.py:73-78—failed_symbols_onreturns every failed symbol for the prior session with no side condition;fresh_derisking_failures(:111-116) filters today's de-risking failures onsymbol not in failed_prev_session. A prior-day failed buy therefore suppresses a first-ever failed sell. The existing test (:71 region) only exercises a sell-historyprev_failed— the buy-masking case was never considered. Contrast: the unreadable-history fallback (cli.py:1152-1156) correctly fails safe (unknown → all NEW), so the side-blindness is an oversight in otherwise fail-safe-aware code.tests/test_execution/test_failed_streaks.py:83-92— readssrc/thales/cli.pyas a string and asserts substrings between"Orders FAILED:"and"Exit 4: state is persisted". Its own docstring admits why ("the run command builds a live broker"). A blankettry/exceptaroundcli.py:1143-1176, an exception between computingfresh(:1157) andraise typer.Exit(4)(:1176), or reordering all pass green. The behavioral pieces offailed_streaks.pyARE tested; the escalation wiring is not.- The converse defect direction errs loud, not silent — a HALT day (no
orders attempted) resets
prev_failed, so a standing streak re-escalates as NEW after any halt — and is accepted as-is.
Fix shape (propose-only)
Make the masking set side-aware: only prior de-risking failures mask
(at failed_symbols_on or its call site). Build a thin injectable-broker
harness for cli.run (SimulatedBroker + tmp state dir + Typer CliRunner —
src/thales/execution/simulated.py already exists, so this is real work,
not a wish) and delete the text-grep test.
Pre-registered kill criterion / done-when
Two behavioral tests, both red today:
- Symbol X fails a BUY on session N−1 and a first-ever SELL on session N → run exits 4 (currently 0).
- CliRunner-invoked
thales runwith an injected broker exits 4 on a fresh de-risking failure, 0 on a standing one, and treats unreadable history as all-NEW — verified by mutation: wrapping the escalation block intry/exceptor removing the raise must red the suite.
Done when the text-grep test no longer exists and the behavioral tests run in the pre-trade gate (exit-4 is the run command's contract, not observability). Keeping the grep test "alongside" does not satisfy this row.