feat(tui): reach the asset-admission gate from the dashboard (screen / propose / discover) - #176
Merged
Merged
Conversation
…verdict `DATA_DERIVED_FAILURES` claimed to hold every failure class "DOWNSTREAM of having no cached history", but omitted `history` itself. So a product with zero cached candles was reported as `history: 0 daily bars, need 1460` -- which reads as "this asset is too young" when the truth is "we have never fetched it". A candidate that was never fetched was indistinguishable, by its failure list alone, from one that genuinely IS too young. Adds `history` to the tag set, and moves the split + its explanation out of the two call sites that each kept their own copy (`assets holdings --screen` and `assets propose`) into `screen.py`, beside the tag set that decides it: split_failures(facts, result) -> (about_the_asset, about_our_cache) missing_history_lines(product_id, not_assessable) -> list[str] The split only ever fires at EXACTLY zero bars, so a genuinely short history stays a real verdict -- pinned by a counterpart test, so the fix cannot decay into a blanket silencer. `keel assets screen`'s output is deliberately untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rkflow `keel/commands/admission.py` is to the allowlist-admission workflow what `keel/commands/insights.py` is to the insights overlay: a curses-free, network-free report layer the TUI will merely style. - `build_screen_report`/`render_screen_report` screen the configured allowlist through an INJECTED `screen_fn` (`keel.cli._screen_product` in production), so no candidate source can drift onto a laxer path -- and route the zero-bars case through `screen.split_failures`/`missing_history_lines` rather than reimplementing it. - `build_propose_view`/`render_propose_view` locate the newest `*.json` shortlist and reuse `keel/proposer.py`'s `parse_proposal`/`build_proposal_report`/ `render_proposal_report` verbatim. NEVER raises: a missing directory, absent shortlist, unreadable file, bad JSON or malformed proposal each become a calm, actionable status -- a dashboard must not be killed by a stray file on disk. - `build_discover_report`/`render_discover_report` are PURE over already-fetched venue products. Taking the product list as an argument, rather than building a broker, is what lets the caller gate the one network-touching action behind an explicit keypress. Adds `Config.proposals_dir` (default `~/keel/proposals`, `~` expanded at use, not at parse) so the shortlist location is configuration rather than a user-specific absolute path hardcoded in library code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oard Makes the halal asset-admission gate reachable from the operator dashboard instead of CLI-only. Each overlay styles `keel/commands/admission.py`'s already-built report -- the same discipline `i` insights keeps toward `keel/commands/insights.py`. - `s` screen -- the allowlist's admission verdicts. OFFLINE, DB reads only. - `p` propose -- screens the newest `*.json` in `config.proposals_dir`. OFFLINE. - `d` discover -- proposes NEW candidates from the venue's product list. `d` is the second deliberate network exception after `f`, and is gated harder than `f` is: opening the overlay makes NO call and renders an ARMED, not-yet-run explanation of what it is about to do. Only Enter, pressed inside the overlay, makes the one `list_products` call. The result is then HELD -- later polls repaint it without re-fetching -- and closing discards it, so reopening is armed again. None of the three attests, admits or trades. `attest` stays CLI-only: it is the human judgment the whole gate rests on, and must not be a keystroke. Also fixes a docstring that was already untrue: `tui_cmd` claimed the TUI "never touches the network except when explicitly asked (`f`)", while `run_live` has all along made an automatic ~30s `get_accounts` balance read -- which `run_live`'s own docstring acknowledged, so the two contradicted each other. It now enumerates all three touches. `run_live`'s help and insights branches each hand-rolled an identical scroll chain; rather than copy it three more times, both now share one pure `_scroll_offset` helper, with their behaviour unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lings do Review of the new TUI overlays surfaced that `keel assets screen` was the last caller still printing `✗ history: 0 daily bars, need 1460` for a product nobody had ever fetched -- the "too young" misreading the rest of this branch removes. It is the SIBLING of the new `s` screen overlay: both screen `_default_sim_products(config)` through `_screen_product`. Leaving only one able to explain an empty cache would hand an operator two different stories about the same allowlist depending on which surface they happened to look at -- the drift `_screen_product` exists to prevent, applied to the reporting rather than the verdict. Also bounds the discover overlay's `list_products` call with `_DISCOVER_TIMEOUT_SEC`. The operator is blocked on that one with the screen frozen behind a "contacting venue" frame, so a hung venue must become a retryable `discover failed:` line rather than a dashboard whose only exit is Ctrl-C. (`f` fetch stays deliberately unbounded -- it legitimately runs for minutes, and is documented as freezing the dashboard.) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Aug 7, 2026
…176 made (#177) An adversarial review of #176 confirmed the network gating and read-only guarantees, and found ten things to fix. This is all ten. The two real bugs: - `build_propose_view` guarded `source.read_text()` with `except OSError`, but `UnicodeDecodeError` subclasses **ValueError**. A UTF-16LE+BOM shortlist -- valid JSON, and what a scout run on Windows writes -- escaped both handlers and broke the function's own "Never raises. Every failure mode is FAIL-SOFT" contract: the TUI overlay repainted `propose read failed: 'utf-8' codec can't decode byte 0xff...` every poll forever, naming no file and no next step, and `keel assets propose --from` exited on a raw traceback. Both now take the existing `unreadable` fail-soft path, which names the file. The two docstrings that claimed an exception "can only come from `open_state()`, never from the shortlist read itself" said something that was false then and is still not the whole truth now (`build_propose_view` screens every parsed candidate, so a locked DB surfaces there too) -- both corrected. - `assets propose --json` emitted `sc.result.failures` raw, so at zero cached bars the payload carried `history: 0 daily bars < 1460 required` unflagged while every human surface suppressed that exact line. Same report, two surfaces, two different answers to "is this asset too young?". `--json` now applies the same `split_failures` the renderers do, and says so explicitly: `failures` / `not_assessable` / `missing_history`. The single-admission-path property (design constraint 4) was convention-only exactly where it is wired: replacing `_screen_product` with an always-ADMIT stub in `_do_screen_report` OR `_do_propose_view` left all 2062 tests green, because the overlay tests only asserted that a title paints and Esc closes. Two `run_live` tests now seed an unattested asset with ample history and liquidity and assert the overlay paints REJECT plus `attestation: MISSING` -- a verdict only the real gate produces. Both stubs now die. Honesty fixes to comments that asserted more than the code can know: - `split_failures` claimed a shallow-but-non-empty cache means the asset "really is too young". `MarketFacts` carries no first-bar timestamp, so it cannot tell that from `keel fetch --years 2`, an aborted fetch, or a venue not serving the full window. Reworded to say so, and to tell the operator to check the fetch window first. - `missing_history_lines` said a zero-bar asset "is not too young" -- same overclaim, opposite direction. It now refuses to rule either way. - "the SECOND deliberate network exception" appeared in the module docstring, the help screen's Safety notes and the operator-facing ARMED overlay, forgetting the ~30s live-balance refresh that has been firing since v3. It is the THIRD of exactly three, as `tui_cmd`'s docstring already said. The help's "Live balance" section now also says the refresh is itself a live venue call. - `run_live` said "Seven modes"; there are six. Coverage for things that were correct but untested, each verified to die under a mutation: `config.proposals_dir` resolution and `~` expansion (the default is `~/keel/proposals`, inside the live deployment root, so a silent regression means reading a right-looking wrong place); `_DISCOVER_TIMEOUT_SEC`'s wiring, pinned per-call so collapsing it into the balance timeout is caught; `missing_history_lines`' semantic sentence and its promise never to restate a suppressed failure verbatim. `test_run_live_discover_closing_discards_the_held_result` is renamed to `..._reopening_after_a_run_is_armed_not_stale`. Deleting the close-branch clear alone leaves it green -- verified -- because the normal-mode `d` branch clears too, and nothing observable from outside `run_live` can separate them, since `mode` only becomes `discover` via that branch. The name now matches what it pins. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Aug 7, 2026
Patch: no schema change, no rail change, nothing that alters what the agent trades or when. Ships #176, #177 and #178. The reason to cut this rather than wait is #176's fix to `DATA_DERIVED_FAILURES`, which is a reporting bug live on the deployment right now: `history` was missing from the tag set that marks a failure as downstream of an empty cache, so `keel assets propose` and `keel assets holdings --screen` printed `x history: 0 daily bars < 1460 required` directly beneath their own note saying the verdict was about missing data rather than about the asset. An asset nobody had ever fetched was reported as too young to trade. Also in: the TUI can now reach the admission gate (`screen`/`propose`/`discover`, read-only, `discover` network-gated behind an explicit keypress like `f`); the non-UTF-8 shortlist hole that broke `build_propose_view`'s documented never-raises contract; and the header now shows the patch segment, so 0.5.1 and 0.5.3 no longer render identically as `v0.5`. uv.lock relocked in the SAME commit, per 0.5.0-0.5.2. Verified with `uv sync --frozen`, which accepted the lock and rewrote nothing. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The allowlist-admission workflow — the halal asset gate — was CLI-only. This puts it on the operator dashboard: three new overlays,
sscreen,ppropose,ddiscover, alongside the existingiinsights.Nothing here admits, attests, or trades.
keel assets atteststays deliberately CLI-only: it is the one step in this gate that rests on human judgment rather than code, and it must not be a keystroke.The overlays
sscreen — the current allowlist's admission verdicts, with the reason for each. OFFLINE, DB reads only.ppropose — screens the newest*.jsonshortlist inconfig.proposals_dir(new setting, default~/keel/proposals,~expanded at use rather than baked into aConfig). OFFLINE. A missing directory, absent shortlist, unreadable file, bad JSON or malformed proposal each render as a calm, actionable explanation — never an error that takes the dashboard down. Invalid candidate entries are reported asINVALID, never dropped.ddiscover — proposes NEW candidates from the venue's own product list.Each reuses
keel/proposer.pyand the existing_screen_productgate verbatim rather than growing a second rendering or admission path.dis gated harder thanfisDiscover is the second deliberate network exception in a dashboard whose contract is read-only and offline. Pressing
dmakes no call at all — it opens ARMED, rendering an explanation of what running it will do and that it is a live venue call. Only Enter, pressed inside the overlay, makes the onelist_productsrequest. The result is then HELD: later polls repaint it without re-fetching, and closing discards it so reopening is armed again. It is bounded by a timeout, because the operator is blocked on it with the screen frozen — a hung venue becomes a retryablediscover failed:line, not a dashboard whose only exit is Ctrl-C.Two tests pin this by driving the real loop: zero
list_productscalls acrossd+ several polls + close, and exactly one per Enter with no re-fetch on subsequent polls.The correctness fix this uncovered
DATA_DERIVED_FAILURESclaimed to hold every failure class "DOWNSTREAM of having no cached history" but omittedhistoryitself. So a product with zero cached candles was reported ashistory: 0 daily bars, need 1460— which reads as "this asset is too young" when the truth is "we have never fetched it". A candidate never fetched was indistinguishable, by its failure list alone, from one that genuinely is too young. Since screening an unfetched asset is the normal first step of admitting one, this was the common case, not the edge case.The split and its explanation now live once, in
screen.pybeside the tag set that decides them:assets holdings --screen,assets propose,assets screenand the new TUI overlay all route through them, replacing three independent copies of the same logic. A zero-bar product now says "no local history — runkeel fetch --products Xfirst" and names what is unassessable until then, instead of asserting something false about the asset.The suppression fires at exactly zero bars, so an asset with some history that still falls short is still reported as genuinely too young — pinned by counterpart tests on every surface, so the fix cannot decay into a blanket silencer.
keel assets screenwas initially left out of that migration and a review caught it. It is the sibling of the newsoverlay — both screen the same product set through the same gate — so leaving only one able to explain an empty cache would have handed an operator two different stories about the same allowlist depending on which surface they looked at.Also
tui_cmd's docstring claimed the TUI "never touches the network except when explicitly asked (f)", whilerun_livehas all along made an automatic ~30sget_accountsbalance read — whichrun_live's own docstring acknowledged. The two contradicted each other before this branch. The docstring now enumerates all three network touches honestly.run_live's help and insights branches each hand-rolled an identical scroll chain. Rather than copy it three more times, both now share one pure_scroll_offset, behaviour unchanged.Verification
ruffclean,mypyclean, 2062 tests passing (1976 onmain, +86).Beyond running the suite, the two constraints that carry the safety weight were mutation-tested: removing
historyfrom the tag set fails 5 tests, and making discover fetch on open or on poll fails 4. Both were confirmed to fail for the right reason before the fix existed.