Releases: jbdrak/ssb-for-agents
Release list
v2.9.3
Highlights
- scan tier filter — tennis fallback now honors
-t;-t 1scans stop listing TIER 2 plays. - massive JSON cut — unresolved rows are summarized past 50 (total/byReason/sample) instead of a ~12 MB dump on broad scans.
- budget actually spendable — the odds-history gate was serial (~10 calls/sec), capping every scan regardless of budget. Now parallel (
PP_ODDS_HISTORY_CONCURRENCY, default 3). Real upstream 429s still halt the gate with cooldown. - Also in this line: per-pair hydration budget bounded by the caller's limit (v2.9.2).
3093/3093 tests, lint/types/format clean. See CHANGELOG.md.
GitHub-only release; npm publish is not wired (no NPM_TOKEN).
v2.9.2
Highlights
- fix — mixed
quick_screenscans (pp scan mlb wnba ncaaf -n 5) timed out past 150s with 1000+ "scan scope aborted" and never emitted JSON. The per-pair hydration budget (133 games/pair) flooded the serial odds-history gate. Now bounded by the caller's limit: ~33s, 0 aborts, honest output. - fix — EV-first discovery pass capped per-pair the same way; direct/targeted scans keep the full pass.
- Deps:
@types/node,eslint,globals,actions/setup-pythonbumps (#46, #47).
Full suite 3083/3083, lint/types/format clean. See CHANGELOG.md for details.
Note: published as a GitHub release only; npm publish is not wired up (no NPM_TOKEN).
v2.1.7
v2.1.6
2.1.6
Consensus-preservation fix in extractScreenRows — every main-line screen row was silently cascading to consensusBookCount: 0 / TIER 4 / PASS because the full per-book odds map was being clobbered on the expanded row. Symptom: live screen / get_play_details / recommended_bets / sharp_plays calls all returned consensusBookCount: 0, consensusEdge: null, executionQuality: "unknown", marketBookCount: 0, supportBookCount: 0, screenScore: 0, gatePassed: false with gateReason: "score 0.00 below 2.05 gate", and consequently riskScore: 10, kaiCall: "PASS", confidenceTier: "TIER 4". The same symptom pattern as the 2026-06-15 upstream consensus outage — but the upstream /odds_history_new and /screen are healthy, so the bug was local.
Root cause
extractScreenRows (lib/screen-parser.js) expands a normalized upstream row (selections: { null: { ...lifted fields, odds: {full map} } }, defaultKey: null) into per-book rows by spreading the row and then setting odds: bookOdds.odds1 (a number). The explicit odds override clobbered the lifted full map. Combined with normalizeRow setting selections: undefined (because no other selection keys exist), the expanded row had no way for the ranker to find the full map. The ranker (expandScreenRow in lib/screen-ranker.js) gates on row?.selections && (row?.book || row?.sportsbook); with selections: undefined the main path was skipped, the early-return on missing preferredOdds fired, and the row passed through with every consensus-derived field at zero.
Fix
lib/screen-parser.jsextractScreenRows(isNormalizedNonPropbranch) — preserve the full lifted odds map on the expanded row asallBookOddsbefore overridingoddswith the per-book number.lib/screen-ranker.jsexpandScreenRow— whenrow.selectionsis undefined butrow.allBookOddsis present, reconstruct theselections: { null: { ...lifted fields, odds: row.allBookOdds } }shape the existing main path already understands.- 3 new regression tests in
test/propprofessor-analysis.test.js(live-shape fixture mirroring the actual/screenpayload, v2.1.2 fallback preservation, per-bookoddscontract preserved).
Live impact
| Field | Before | After |
|---|---|---|
consensusBookCount |
0 | 5–19 |
consensusStrength |
"none" |
"strong" |
consensusEdge |
null |
0.4–5.2pp |
executionQuality |
"unknown" |
"best" / "playable" / "bad" |
marketBookCount |
0 | 4–19 |
supportBookCount |
0 | 4–19 |
bestAvailableOdds |
null |
real value (e.g. 117 for a +116 side) |
screenScore |
0 | 2.4–11.8 |
gatePassed |
false ("score 0.00 below 2.05 gate") |
true ("score X.XX passed 2.05 gate") |
riskScore |
10 | 4–8 |
kaiCall |
"PASS" |
"CONSIDER" / "BET" (when signals warrant) |
confidenceTier |
"TIER 4" |
"TIER 1"–"TIER 3" |
tierTrajectory |
all-zero placeholder | real trend / volatility / data points |
scoreBreakdown |
all-zero | real consensus / movement / sport / freshness |
"No consensus data" warning |
always present | gone (only "No line history" + freshness remain when applicable) |
What I'm NOT touching (separate concerns)
< 2gate inresolveHistoryForEntity— correct (you need 2+ points for movement). With consensus now flowing, rows with sparse history rank on consensus + freshness + sport score instead of being silently killed.freshnessFallbackUsed: truefor the screen call itself — separate upstream timestamp issue.
Full Changelog: v2.1.5...v2.1.6
What's Changed
- chore(deps-dev): bump prettier from 3.8.3 to 3.8.4 in the npm-minor-and-patch group across 1 directory by @dependabot[bot] in #30
- chore(deps-dev): bump eslint from 9.39.4 to 10.5.0 by @dependabot[bot] in #31
Full Changelog: v2.1.5...v2.1.6
v2.1.5
v2.1.4 — Warning was broken, now it's not
v2.1.4 — Warning was broken, now it's not
Hotfix on top of v2.1.3. v2.1.3 shipped the line-history backfill and a degraded-data warning. Live testing caught that the warning was silently filtering out every Puck Line / Run Line / Total Goals / Total Runs row — the check used r.line1 == null as a defense-in-depth moneyline guard, but the ranker doesn't surface line1 on the output for Puck Line rows (the normalizeRow step only lifts selections.null.*, and Puck Line defaultKey is "-1" / "-3.5", not "null"). The check evaluated undefined == null === true and excluded every line-based row. Test data included line1 so unit tests passed; live MCP calls never got the warning. Fixed in this release.
What changed for users
- The v2.1.3 "Line values missing from upstream" warning now actually fires for non-moneyline markets. If you're using a Puck Line / Run Line / Point Spread / Total screen, you'll see the degraded-state warning instead of silent
line: nulleverywhere.
What's under the hood
buildDegradedDataWarningsinlib/propprofessor-mcp-ranked-screen.jsno longer inspectsr.line1orr.linedirectly. It relies onr.lineFieldMissingCount > 0as the primary signal (the backfill code inresolveHistoryForEntityalready guards onfallbackLine !== null, which is naturally null for moneylines, so the count is naturally 0 there) plus a defense-in-depthmarket === "moneyline"check.- Regression test in
test/propprofessor-mcp-ranked-screen.test.jsmirrors the real ranked-row shape (noline1, large backfill count). The old tests hadline1: -1set on the synthetic data, which masked the production bug.
Honest framing
This is the second time a unit test passed but live data didn't. v2.1.3's release notes claimed the degraded-data warning would fire; it did not, because the test data didn't match the live shape. Live testing caught it. v2.1.4 closes the loop. v2.1.3 was technically correct (backfill works) but the user-facing warning was broken.
Also updates the v2.1.1 release page to add an "Honest framing" section about the incomplete line-history tracking in that release, cross-referencing v2.1.3 and v2.1.4.
Try it
make install to upgrade, or npm install from a clean clone.
Then ask your agent: "Show me the line history on tonight's NHL Puck Line." You should now see the degraded-data warning in the response.
Full diff in CHANGELOG.md.
v2.1.3 — Line-history backfill + degraded-data warning
v2.1.3 — Line-history backfill + degraded-data warning
The PropProfessor backend's /odds_history endpoint doesn't return a line field per entry — only odds, start_ts, end_ts, and liquidity. For line-based markets (Puck Line, Run Line, Point Spread, Total Goals/Runs/Rounds) that meant every history entry had line: null and the MCP couldn't track line movement. v2.1.1 / v2.1.2 shipped the spread-alias fix but the underlying line-history data was missing upstream. v2.1.3 closes the loop: self-consistent entries + a visible degraded-state warning.
What changed for users
- Line-based markets now show non-null
linevalues in their history entries. The MCP backfills the current line (from the screen response) into every history entry that was missing it. YourlineHistoryarrays are now self-consistent. - Degraded-state warning in the response. When the MCP is operating on backfilled data,
resultMeta.warningsnow reads: "Line values missing from upstream history for N/M non-moneyline rows (K entries backfilled from current line). Line-movement detection is degraded for this slate." You see the degraded state explicitly instead of a silentline: nulleverywhere.
What's under the hood
lib/propprofessor-history.js:resolveHistoryForEntityreads the matched row's current line and backfills it into any history entry whereentry.line === null. Tracks the count aslineFieldMissingCount. Moneylines (legitimateline: null) are not backfilled.lib/propprofessor-screen-history.js: propagates the backfill count throughhydrateScreenRowsWithHistory.lib/propprofessor-mcp-ranked-screen.js:buildDegradedDataWarningsemits a warning when non-moneyline rows had line values backfilled.- 6 new regression tests covering: backfill from current line, no-backfill when upstream provides line, no-backfill on moneylines, warning fires correctly, warning suppressed for moneylines, warning suppressed when no backfill happened.
Honest framing
The v2.1.1 / v2.1.2 release notes claimed a "spread-alias regression fix" that resolved MARKET_ALIASES.spread / .handicap to per-league canonical names. The alias resolution is correct. The line-movement tracking it was implicitly claiming to enable is not — the upstream data doesn't carry per-entry line values. v2.1.3 makes this honest: the alias works, the line-history entries are self-consistent, and the degraded state is visible. The upstream fix is tracked separately.
Try it
make install to upgrade, or npm install from a clean clone.
Then ask your agent: "Show me the line history on tonight's NHL Puck Line." You should see the entries populated and (if the slate is thin) a warning explaining the backfill.
Full diff in CHANGELOG.md.
v2.1.2 — UFC card now actually shows up
v2.1.2 — UFC card now actually shows up
Hotfix shipped ~2 hours after v2.1.1. A live test against tonight's UFC card caught a bug where screen_ranked returned 0 rows for any non-major league (UFC, Soccer) — the default focus book was being set to Pinnacle, and Pinnacle doesn't post UFC moneylines. Algorithm, tier system, and tool surface unchanged.
What changed for users
screen_rankednow returns UFC data. Callingscreen_ranked({league: "UFC"})(and by extensionufc_card,all_slates, andrecommended_betsfor UFC) now surfaces the actual UFC card — currently the Gaethje/Topuria, Chandler/Ruffy, and Aswell/Bolanos fights. Before this fix, the same call returned an empty slate, making it look like the data feed was missing UFC events when it wasn't.
What's under the hood
screen_rankedwas defaulting the focus book to the league preset's first preferred book (Pinnacle for most leagues).extractScreenRowsthen applied afocusPlaysfilter that dropped every row whose odds didn't include Pinnacle — which is every UFC row, since Pinnacle doesn't post UFC moneylines. Fix: only set the focus book when the user explicitly passesbooks; otherwise expand to all books in the payload.- Defensive follow-up:
extractScreenRowsnow falls back to "all books in the row" if the requested focus book has no odds in that row — the focus-book feature was always meant as a best-effort preference, not a hard filter. - 1 new regression test in
test/handler-integration.test.jsasserts the fix against a fixture mirroring the live 2026-06-14 data shape (Pinnacle absent, BetOnline/Caesars/FanDuel/DraftKings present).
Honest framing
Even with the fix, the UFC card tonight still returns all TIER 4 / kaiCall: PASS — the system correctly identifies that the pregame moneylines don't have enough sharp-book consensus or line history to warrant a BET call. If you want signal on the card, try find_best_price for line shopping or wait until closer to tip when more line history accumulates.
Try it
Same as before. make install to upgrade, or npm install from a clean clone.
Then ask your agent: "Show me tonight's UFC card."
Full diff in CHANGELOG.md.
v2.1.1 — Fantasy Optimizer + tighter auth
v2.1.1 — Fantasy Optimizer + tighter auth
Fantasy picks land in the MCP, the spread-market bug from June 12 gets a permanent regression test, and the auth file that holds your PropProfessor cookies is now locked down to owner-only access.
What changed for users
- Fantasy Optimizer tool — new
fantasy_optimizerMCP tool for DFS-style fantasy picks. Requires a paid PropProfessor subscription with Fantasy Optimizer access. Query by league, fantasy app, market, or sport. - Auth file permissions tightened —
pp-query login,installAuthFile, and the token cache now write0o600(owner-only) andchmodto enforce it on existing files. June 8 SEC-003 fix. - Spread-alias regression fix —
MARKET_ALIASES.spreadand.handicapfor NBA/WNBA/NCAAB/NCAAF/NFL/Soccer now correctly resolve to the per-league canonical name (e.g."Point Spread"for NBA). Previously these markets returned empty payloads.
What's under the hood
- New
fantasy_optimizertool inlib/propprofessor-fantasy-optimizer.js, wired through the standard handler pipeline. lib/propprofessor-shared-utils.jsnow exportsresolveMarketName(input, league)— the central alias resolver used by all market-aware tools.pp-query loginandinstallAuthFilewrite mode 0o600 on new and existing files (chmodSyncfor the existing-file path).- 19 new tests across fantasy optimizer, auth permissions, market aliases, and xurl player-name sanitization.
Honest framing (added 2026-06-14)
The spread-alias fix is correct for the alias resolution itself, but the underlying line-history tracking was incomplete in this release. The PropProfessor backend's /odds_history endpoint does not return a line field per entry, so even when the alias resolved correctly, the MCP couldn't track line movement on spread markets. v2.1.3 ships a defensive line-value backfill + degraded-data warning, and v2.1.4 hotfixes a bug in that warning check that was silently filtering out Puck Line / Run Line / Total rows. If you're using v2.1.1 with line-based markets, please upgrade to v2.1.4.
Full diff in CHANGELOG.md.
v2.0.1 — Pre-directory polish
v2.0.1 — Pre-directory polish
Docs-only patch. The README's v1.6.1 polish checklist (repo description, Mermaid diagram, FAQ, docs map, install path verification) was already comprehensive from prior cycles. v2.0.1 ships two targeted fixes for drift.
What changed for users
- Nothing functional. Tool surface, algorithm, tier system unchanged.
What's under the hood
- FAQ updated — TIER 1 hit rate sample size corrected from the v1.5.5-era 580-play count to the current 575-play backtest. Framing tightened to "sits around chance (~50%)" so the honest scope is the lead, not a footnote.
- Status section updated — "Latest release" now names v2.0.0 specifically with a one-line description, so directory visitors landing on the README see the most recent release at a glance instead of clicking through.
- Install path verified — server boots clean, NDJSON framing works end-to-end,
initialize+tools/listreturns all 23 tools,npm link --dry-runconfirmspp-mcp/pp-querybinaries would install.
Try it
Same as before.