fplkit 2.8.0__tar.gz → 2.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/TOOLS.md +20 -11
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/gw-prep/SKILL.md +61 -16
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/gw-prep/references/output-template.md +2 -2
- fplkit-2.9.0/.agents/skills/gw-prep/scripts/claimable_returnees.py +175 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +107 -40
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/squad-builder/SKILL.md +1 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/.claude/output-styles/fpl-mate.md +1 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/AGENTS.md +4 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/CHANGELOG.md +18 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/CLAUDE.md +4 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/PKG-INFO +34 -6
- {fplkit-2.8.0 → fplkit-2.9.0}/README.md +33 -5
- {fplkit-2.8.0 → fplkit-2.9.0}/docs/architecture.md +29 -14
- {fplkit-2.8.0 → fplkit-2.9.0}/docs/command-reference.md +374 -26
- {fplkit-2.8.0 → fplkit-2.9.0}/docs/custom-analysis.md +7 -5
- {fplkit-2.8.0 → fplkit-2.9.0}/docs/fpl-rules.md +4 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/_version.py +2 -2
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/action/waiver.py +187 -20
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/analysis/bench_order.py +22 -4
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/analysis/captain.py +7 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/analysis/starting_xi.py +4 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/analysis/stats.py +32 -10
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/analysis/transfer_eval.py +3 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/common.py +31 -8
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/data/price.py +2 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/orchestration/report.py +24 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/core_insights.py +208 -33
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/fpl.py +32 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/fpl_draft.py +242 -12
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/historical_types.py +10 -2
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/providers/_models.py +16 -3
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/providers/anthropic.py +64 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/understat.py +45 -4
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/vaastav.py +2 -2
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_context.py +13 -0
- fplkit-2.9.0/fpl_cli/cli/_gameweek_status.py +72 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_helpers.py +24 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_league_recap_data.py +436 -29
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_league_recap_history.py +153 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_league_recap_types.py +435 -2
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_plan_grid.py +1 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_review_draft.py +31 -13
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_review_summarisation.py +255 -99
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_synthesis_retry.py +18 -13
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/allocate.py +13 -5
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/doctor.py +6 -4
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/doctor_providers.py +387 -41
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/fdr.py +26 -5
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/league_recap.py +258 -21
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/player.py +57 -32
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/preview.py +15 -6
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/price_history.py +3 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/returnees.py +14 -6
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/review.py +23 -6
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/sell_prices.py +109 -8
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/squad.py +1 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/stats.py +49 -4
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/status.py +62 -3
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/transfer_eval.py +14 -12
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/config/defaults.yaml +6 -0
- fplkit-2.9.0/fpl_cli/models/gameweek_status.py +89 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/models/player.py +14 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/models/types.py +16 -5
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/parsers/recommendations.py +17 -4
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/prompts/league_recap.py +73 -33
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/prompts/league_recap_checks.py +207 -19
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/scraper/fpl_prices.py +52 -1
- fplkit-2.9.0/fpl_cli/services/draft_provenance.py +854 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/league_history.py +5 -2
- fplkit-2.9.0/fpl_cli/services/league_history_drafts.py +332 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/league_history_notes.py +121 -35
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/player_prior.py +113 -9
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/returnee_radar.py +15 -2
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/scoring/__init__.py +35 -0
- fplkit-2.9.0/fpl_cli/services/scoring/coverage.py +435 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/scoring/data_prep.py +53 -3
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/scoring/evaluation.py +49 -12
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/scoring/shrinkage.py +2 -4
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/scoring/signals.py +74 -37
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/scoring/single_gw.py +3 -0
- fplkit-2.9.0/fpl_cli/services/sell_prices.py +170 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/templates/gw_league_recap.md.j2 +43 -5
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/templates/gw_review.md.j2 +2 -2
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/utils/files.py +3 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/utils/markdown.py +5 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/conftest.py +174 -9
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/fixtures/waiver_ranked_golden.json +34 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_agents_action.py +643 -11
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_agents_analysis.py +44 -2
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_agents_analysis_squad.py +54 -4
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_agents_data.py +10 -7
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_agents_data_price.py +9 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_allocate.py +4 -4
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_api_core_insights.py +346 -16
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_api_draft.py +246 -14
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_api_historical.py +2 -2
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_api_understat.py +37 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_api_vaastav.py +4 -4
- fplkit-2.9.0/tests/test_claimable_returnees.py +202 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_allocate.py +53 -2
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_captain.py +8 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_differentials.py +13 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_doctor.py +3 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_doctor_providers.py +437 -19
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_fdr.py +53 -9
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_format_suppression.py +18 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_fpl_block.py +2 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_history.py +3 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_json_contract.py +244 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_league_recap.py +981 -7
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_notice_streams.py +2 -2
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_plan_grid.py +8 -8
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_player.py +161 -5
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_preview.py +90 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_price_history.py +26 -8
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_returnees.py +30 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_review.py +624 -17
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_sell_prices.py +75 -2
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_squad.py +1 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_stats.py +130 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_status.py +175 -7
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_synthesis_retry.py +44 -13
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_transfer_eval.py +69 -33
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_common.py +101 -47
- fplkit-2.9.0/tests/test_draft_provenance.py +1063 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_fpl_prices.py +34 -0
- fplkit-2.9.0/tests/test_gameweek_status.py +172 -0
- fplkit-2.9.0/tests/test_league_history_drafts.py +693 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_league_history_notes.py +139 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_league_recap.py +1828 -51
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_models.py +12 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_player_prior.py +136 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_providers.py +146 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_recommendations_parser.py +24 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_report_agent.py +103 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_returnee_radar.py +49 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_scoring.py +313 -21
- fplkit-2.9.0/tests/test_scoring_coverage.py +422 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_scraper.py +52 -0
- fplkit-2.9.0/tests/test_sell_prices.py +207 -0
- fplkit-2.9.0/tests/test_sell_prices_service.py +192 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_starting_xi_agent.py +22 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_stats.py +117 -14
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_transfer_eval_agent.py +21 -1
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_utils_markdown.py +8 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_validate_draft_waivers.py +194 -8
- fplkit-2.8.0/tests/test_sell_prices.py +0 -76
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/README.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/build-test-plan/SKILL.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/build-test-plan/references/plan-template.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/build-test-plan/references/workspace-env.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/create-pr/SKILL.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/gw-prep/references/entity-normalisation.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/gw-prep/references/rules.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/gw-prep/scripts/_bootstrap.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/gw-prep/scripts/normalise_entities.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/preview-ingest/SKILL.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/release/SKILL.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/release-notes/SKILL.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/squad-builder/references/output-template.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/squad-builder/references/rules.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.agents/skills/update-gw-prep/SKILL.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.claude/settings.json +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.env.example +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.github/workflows/ci.yml +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.github/workflows/pr-title.yml +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.github/workflows/release.yml +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/.gitignore +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/CONTRIBUTING.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/LICENSE +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/cliff.toml +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/config/team_ratings_overrides.yaml +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/docs/images/fpl-player-demo-v1-0.png +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/action/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/analysis/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/base.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/data/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/data/fixture.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/data/scout.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/agents/orchestration/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/contract.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/dataset_fetcher.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/football_data.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/historical.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/providers/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/providers/_http.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/providers/openai_compat.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/api/providers/perplexity.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_banner.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_fines.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_fines_config.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_json.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_review_analysis.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/_review_classic.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/captain.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/chips.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/credentials.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/differentials.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/fixtures.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/history.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/init.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/intel.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/league.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/league_fines.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/price_changes.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/ratings.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/targets.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/waivers.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/cli/xg.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/config/fixture_predictions.yaml +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/config/team_managers.yaml +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/constants.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/models/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/models/chip_plan.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/models/fixture.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/models/league_history.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/models/team.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/parsers/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/paths.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/prompts/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/prompts/returnees.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/prompts/review.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/prompts/scout.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/scraper/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/season.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/fixture_predictions.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/league_history_counters.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/league_history_fines.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/matchup.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/player_clubs.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/scoring/constants.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/scoring/display.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/scoring/ownership.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/scoring/value_quality.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/season_previews.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/squad_allocator.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/team_form.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/team_ratings.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/services/team_ratings_prior.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/templates/gw_preview.md.j2 +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/utils/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/utils/gameweek.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/utils/teams.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/utils/text.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/fpl_cli/utils/time.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/pyproject.toml +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/pyrightconfig.json +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/scripts/backtest_early_season_prior_blend.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/scripts/calibrate_promoted_prior.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/scripts/calibrate_quality_ceilings.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/__init__.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/fixtures/classic_squad_fixture.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/fixtures/recommendations_fixture.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_agents_base.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_agents_orchestration.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_api_client.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_api_dataset_fetcher.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_api_football_data.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_bench_order_script.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_calibrate_quality_ceilings.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_classic_squad_extraction.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_banner.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_chips.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_context_settings.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_credentials.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_custom_analysis.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_failure_parity.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_failure_streams.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_fdr_blanks.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_fixtures.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_format.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_init.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_intel.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_json.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_league.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_league_fines.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_price_changes.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_settings_warning.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_targets.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_waivers.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_warnings_stderr.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_cli_xg.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_common_scoring.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_data_prep_harmonisation.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_dependencies.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_doc_consistency.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_fines.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_fines_config.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_fixture_predictions.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_formation_guidance_sync.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_gw_prep_script_bootstrap.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_init.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_init_fines.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_league_history_counters.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_league_history_fines.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_league_history_store.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_matchup_service.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_normalise_entities.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_output_style_sync.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_paths.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_player_clubs.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_player_resolution.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_preseason.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_prompt_tables.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_report_paths.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_review.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_rolling_pts_per_m.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_season.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_season_awareness.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_season_previews.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_skill_effort_levels.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_skill_fdr_quality_signals.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_skill_stats_thresholds.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_squad_allocator.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_starting_xi_script.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_team_ratings.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_team_ratings_prior.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_transfer_eval_script.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_utils_gameweek.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_utils_text.py +0 -0
- {fplkit-2.8.0 → fplkit-2.9.0}/tests/test_utils_time.py +0 -0
|
@@ -28,17 +28,25 @@ Table mode is the mirror image: the table on stdout, and warnings, progress and
|
|
|
28
28
|
for a nonzero exit on stderr — so a script reading table output can silence the commentary
|
|
29
29
|
with `2>/dev/null` without losing the table, whichever command it runs.
|
|
30
30
|
|
|
31
|
+
Every scoring command's `metadata.warnings` can carry a feed coverage entry — one of
|
|
32
|
+
`understat_coverage_low`, `adjusted_npxg_coverage_low`, `consistency_coverage_low`,
|
|
33
|
+
`history_prior_coverage_low`, `gw_history_coverage_low`, `fixture_ratings_coverage_low` —
|
|
34
|
+
saying a data feed reached too little of the player pool and what the scores fall back to;
|
|
35
|
+
`fpl waivers`, and the draft columns of `fpl player` and `fpl stats`, add `draft_squads_unread`
|
|
36
|
+
for a rival squad the ownership read could not fetch. Read them before trusting a ranking:
|
|
37
|
+
see [Feed coverage warnings](../docs/command-reference.md#json-output).
|
|
38
|
+
|
|
31
39
|
## CLI Commands
|
|
32
40
|
|
|
33
41
|
| Command | Description | JSON | Format | Experimental | Pattern |
|
|
34
42
|
|---------|-------------|------|--------|-------------|---------|
|
|
35
43
|
| `init` | Set up fpl-cli with your FPL IDs and optional features | No | General | No | direct-api |
|
|
36
|
-
| `status` | Show FPL gameweek status and upcoming deadlines. JSON `metadata` carries `gameweek`, `format` and `season` (hyphenated label, e.g. `2026-27`) - `season` is the directory segment skills must use when writing alongside fpl-cli's own reports | Yes | General | No | direct-api |
|
|
37
|
-
| `doctor` | Check the setup for dead IDs, stale data, and config problems: resolves each configured ID against the live API (reporting the team/league name back so a wrong-but-valid ID is visible), verifies per-team data files (team ratings, managers, season preview intel, finances, player prior) describe the current season's clubs, reports whether the calibrated quality ceilings were measured against the newest completed season (the one season-derived artefact frozen in code, so a rollover with no recalibration leaves the scoring scale anchored to a cohort that has turned over), and reports which config/data/cache directories are in use. With `--providers`, probes the external data sources instead: live shape-and-volume checks against the FPL and Draft APIs, the vaastav and Core-Insights datasets, Understat, and football-data.org, including that every club resolves across sources and that players actually join by name (both Understat checks call the enrichment's own gate and matcher on the payload the scoring commands scan, so probe and runtime cannot disagree; a club resolving does not mean its players' names line up) and that the Core-Insights per-gameweek files parse to records through the parsers the scoring commands use (the drift that otherwise surfaces as plausible but wrong output). Distinguishes broken (needs a fix today) from stale (self-corrects). Exits non-zero when something is broken | Yes | General | No | direct-api |
|
|
44
|
+
| `status` | Show FPL gameweek status and upcoming deadlines. JSON `metadata` carries `gameweek`, `format` and `season` (hyphenated label, e.g. `2026-27`) - `season` is the directory segment skills must use when writing alongside fpl-cli's own reports. `data.gameweek_info` carries `finished` beside `bonus_confirmed` and `league_tables_updated`, read from FPL's processing feed (`event-status`) for each game shown (the classic one alone when no format is configured): `true` only when every game's feed says so, `false` as soon as one says otherwise, and `null` otherwise - always `null` while the gameweek is in progress (the feed can report the days played so far as settled), and also whenever some game's feed could not answer and none said no, so with both formats `null` can mean one confirmed and the other was unreadable. `finished` comes hours before the league-table refresh (issue #424), so a skill gating a league recap should wait on `league_tables_updated` being `true`, and table mode prints both beside "Finished" | Yes | General | No | direct-api |
|
|
45
|
+
| `doctor` | Check the setup for dead IDs, stale data, and config problems: resolves each configured ID against the live API (reporting the team/league name back so a wrong-but-valid ID is visible), verifies per-team data files (team ratings, managers, season preview intel, finances, player prior) describe the current season's clubs, reports whether the calibrated quality ceilings were measured against the newest completed season (the one season-derived artefact frozen in code, so a rollover with no recalibration leaves the scoring scale anchored to a cohort that has turned over), and reports which config/data/cache directories are in use. With `--providers`, probes the external data sources instead: live shape-and-volume checks against the FPL and Draft APIs (including that the draft game's club list agrees with the main game's by club code, since every draft path translates club ids through it), the vaastav and Core-Insights datasets, Understat, and football-data.org, including that every club resolves across sources and that players actually join by name (both Understat checks call the enrichment's own gate and matcher on the payload the scoring commands scan, so probe and runtime cannot disagree; a club resolving does not mean its players' names line up) and that the Core-Insights per-gameweek files parse to records through the parsers the scoring commands use (the drift that otherwise surfaces as plausible but wrong output). Distinguishes broken (needs a fix today) from stale (self-corrects, or waits on an upstream fix the tool works around). Exits non-zero when something is broken | Yes | General | No | direct-api |
|
|
38
46
|
| `fixtures` | Show fixtures for a gameweek. With custom analysis on, FDR is the venue-aware ATK/DEF mean `fpl fdr` and `fpl preview` show, so one match reads the same on all three; `-m/--mode` picks `difference` (default) or `opponent`, an unrated club scores a neutral 4.0 rather than the FPL API's 1-5 difficulty, and when the ratings cannot support difficulty at all, table mode warns on stderr and JSON adds a `team_ratings_unusable` entry to `metadata.warnings` (always present, empty or not). With it off, the raw FPL API 1-5 difficulty, and an explicitly passed `-m` reports on stderr that it does not apply. JSON `metadata` carries `custom_analysis`, `fdr_scale` (`team_ratings_1_7` / `fpl_api_1_5`) and `fdr_mode` (null when it does not apply) | Yes | General | No | direct-api |
|
|
39
47
|
| `player` | Look up a player's stats, xG, ownership and fixture run. Before GW10 `quality_score` blends last season's pedigree in and JSON `metadata.warnings` says so (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded), same as `fpl stats --value`; `understat_team_unmatched` joins it for any club Understat carried no players for, whose whole squad loses xG enrichment. JSON includes `ep_next` (predicted pts next GW) and `ep_this` (current GW), emitting `null` when FPL has no projection; panel shows `xPts` (ep_next) when available, omits the segment when FPL has no projection. `--detail` (`-d`) shows GW-by-GW performance and, for FWD/MID with sufficient history, xGI sustainability (per-match GI-xGI divergence -> form modifier). With `custom_analysis`: JSON adds `info.adjusted_npxg_per_90` (fixture-adjusted) and `info.raw_npxg_per_90` (Understat season avg); panel shows `adj. npxG/90: X.XXX (raw: Y.YYY)` | Yes | General | No | direct-api |
|
|
40
48
|
| `stats` | List players with filtering and sorting. `--value` adds quality/value per £m columns; `--window N` sets rolling lookback (3-10) for `rolling_pts_per_m`. Before GW10, `--value` quality scores blend last season's pts/90 pedigree with this season's observation and the command says so (stderr notice quoting the observation's weight band; JSON `metadata.warnings` code `early_season_prior_informed`); when last season's history cannot be loaded they are pure observation and the notice says that instead (`early_season_small_sample`, before GW6). `understat_team_unmatched` is raised once per club Understat carried no players for, whose whole squad scores without xG. GK ceilings scale with the calendar-possible sample, reaching full scale at GW6. Sortable by `ep_next`/`ep_this` (FPL predicted points; `ep_this` is only meaningful before the gameweek's first kickoff — FPL rolls it forward to `ep_next` as each player's match finishes); both in JSON output (emit `null` when FPL has no projection; table renders `—`). Players with no projection sort to the bottom in either direction | Yes | General | No | direct-api |
|
|
41
|
-
| `history` | Show historical player performance across seasons. A provider that refuses exits 1 in both output formats, carrying the upstream's own reason: the error envelope used to say only `Failed to fetch historical data` while the cause went to stderr, and table mode printed the cause to stdout and exited 0 | Yes | General | No | direct-api |
|
|
49
|
+
| `history` | Show historical player performance across seasons. JSON `seasons[].team_code` is the permanent club code, not a season's 1-20 team id; `seasons[].team` carries the same number and is deprecated. A provider that refuses exits 1 in both output formats, carrying the upstream's own reason: the error envelope used to say only `Failed to fetch historical data` while the cause went to stderr, and table mode printed the cause to stdout and exited 0 | Yes | General | No | direct-api |
|
|
42
50
|
| `league` | Show live league standings for Classic and Draft leagues. Classic positions are competition-ranked from cumulative totals — managers level on points share a place and the next distinct total skips the places they consumed — matching `fpl review` and the league history ledger rather than the standings API's sequential rank, which splits such a tie on fewest transfers season-to-date, a figure no column shows. Draft keeps the league's own rank, which already applies the head-to-head points-for tie-break. The Best/Worst GW Performers lists (top three; bottom five classic, bottom three draft) are the ones `fpl review` prints: managers level on the gameweek share a place (`3=`) and a tie at the cut keeps every manager in it, a hit reads `29 net pts (33 gross, -4 hit)`, and a classic user above the bottom five gets a `Your GW rank: R of N` line beside the list | No | General | No | direct-api |
|
|
43
51
|
| `fdr` | Analyse fixture difficulty - easy runs, blanks, doubles. Three payloads, not one: the default call runs the agent and returns the full analysis plus `data.ratings_warning` (the team-ratings quality verdict, and with `--my-squad` also `data.predictions_stale` / `data.prediction_warnings` / `data.squad_exposure`); with custom analysis off it returns raw 1-5 FPL API difficulty (`data.easy_fixture_runs` only, `metadata.custom_analysis: false`, no ratings and so no warning); `--blanks` bypasses the agent for a schedule-only payload carrying neither the FDR analysis nor `ratings_warning`. `--blanks` mixes confirmed fixtures with predictions; when those predictions are stale, table mode warns on stderr and JSON adds a `fixture_predictions_stale` entry to `metadata.warnings` | Yes | General | No | via-agent |
|
|
44
52
|
| `xg` | Analyse underlying stats: xG, xA, overperformers. The minutes floor and the window scale to the gameweeks played (`min(60 x window, 45 x gameweeks played)`, so 90 minutes over 2 GWs rather than an unreachable 360 over 6); the configured bar binds from GW9, or GW11 for `--all`. JSON `metadata` carries `window_label`, `gameweeks_played` and `min_minutes`, and `metadata.warnings` an `early_season_minutes_floor` notice while the bar is scaled, plus `understat_team_unmatched` for any club Understat carried no players for. When nothing qualifies, `data.empty_reason` says whether the floor (`below_minutes_floor`) or the data (`no_minutes_played`) caused it | Yes | General | No | via-agent |
|
|
@@ -46,18 +54,18 @@ with `2>/dev/null` without losing the table, whichever command it runs.
|
|
|
46
54
|
| `price-history` | Show price trajectory and transfer momentum. A trend provider that refuses exits 1 in both output formats with the upstream's own reason — the error envelope used to say only `Failed to fetch price history data` while the table path named the cause | Yes | General | No | direct-api |
|
|
47
55
|
| `returnees` | Track injured and suspended players due back soon, so a returnee can be claimed in draft before they are fit or planned around in classic. Reads the FPL availability news, filters to returns inside a window (`--window N`, 1-38) and through a source-aware quality bar, and diffs against the stored watchlist for week-over-week changes. Most flagged players carry no parseable return date, so date-unknown entries are the norm rather than the exception; `--enrich` searches the web for fresher timing on those and on stale-news entries (needs a Perplexity key; skips with a note without one) and shows it beside the FPL news, never over it. `--all` bypasses the quality bar and deliberately does not persist the snapshot. JSON `metadata` carries `window`, `escalation_window`, `stash_upgrade_margin`, `transitions_available`, `transitions_baseline_gameweek` (the gameweek `transition` is measured against — every run in a gameweek diffs against the last state stored before it, so a repeat run reports the same changes), `quality_bar_available`, `quality_bar_applied` and `enrichment_requested`/`_available`/`_note`/`_count`/`_rate_limited` (true when the search provider rate-limited the run even after retrying — the one failure a re-run is likely to clear); `data` is `{entries, departures}`, each entry carrying `quality.basis`/`quality.meets_stash`, `expected_return`/`return_gameweek`/`return_source`, `escalation_eligible`/`escalation_basis`, `transition`, and `enrichment` (null unless enriched) | Yes | General | No | direct-api |
|
|
48
56
|
| `preview` | Run full pre-gameweek analysis and generate report. `--save` writes to `<output dir>/<season>/gw{N}-preview.md`; `--scout` writes to `<research dir>/ai-scout-reports/<season>/gw{N}-scout-preview[-referenced].md` | No | General | No | via-agent |
|
|
49
|
-
| `review` | Review a completed gameweek - squad performance and standings. `--save` writes to `<output dir>/<season>/gw{N}-review.md`; `--compare-recs` reads `gw{N}-recommendations.md` from the same season directory and grades the captain, each transfer and each waiver or free-agent move against it; the captain line sets the recommended captain against the armband in captain points (raw score times that week's multiplier, Triple Captain included), the armband being the vice's when the captain played no minutes, and adds the change to the team total when the recommended captain was in the side that scored; one who was not in the squad or played no minutes is labelled so rather than priced at 0; on a wildcard or free hit week, where the recommendation is a squad, each player brought in is graded against the recommended 15 instead, and a recommended name that fits more than one player is left ungraded with a warning on stderr rather than matched to the first; a part of the file it could not read (the captain, the transfers or the waivers) is left ungraded the same way, the warning naming the file, rather than grading every move unadvised, and a file nothing could be read from is not compared at all. Under `--summarise` the personal analysis is handed next gameweek's fixtures — every club's opponent and venue plus each squad player's positional FDR (1-7 team ratings with custom analysis on, the FPL API's 1-5 difficulty with it off, carrying the same scale footer and unrated-club caveat the FDR tables print, plus any warning that the ratings cannot support difficulty) — so its "Next Week" calls are grounded in fixtures rather than in one week of points; with no next gameweek to look at, that section is narrowed to fixture-independent observations and points at `fpl gw-prep`, and a gameweek already played is marked retrospective. The answer is then checked back against that block — a cited FDR the block never printed, or any fixture claim made without one — and against the sections its prompt asked for: an answer missing a `## ` heading, ending mid-sentence, or that the provider cut off at its token ceiling is retried once, told what the last draft lacked (a refused answer is flagged but never retried, and a retry that errors keeps the first attempt), and a still-incomplete one is warned about on stderr (detail in `data/debug/synthesis_corrections.txt` under `--debug`) and carries the same list as a callout in the saved report. The summary is an add-on the review does not depend on: the two provider roles resolve independently, and a role with no usable key is skipped with the reason on stderr (`Community narrative skipped: ...` / `Personal analysis skipped: ...`) while the review prints, saves and exits 0 — so one key produces that half rather than nothing, and a saved report names each skipped half in a warning callout of its own. A gameweek it cannot resolve - one still being played, an id the season does not have, a season with nothing finished yet - is refused with the specific reason on stderr and exit 1; stdout carries the review or nothing | No | General | No | via-agent |
|
|
50
|
-
| `league-recap` | Recap a completed gameweek - awards, standings, and banter. Saves to `<output dir>/<season>/gw{N}-league-recap[-draft].md`, opening on a fixed `# Gameweek N Recap: <league name>` title the report writes itself, editorial or not; the editorial is asked for a `## ` headline beneath it and never a title, and whatever heading it opens with anyway is demoted to that level or, when it only restates the title, dropped, so the model's heading is never the document's (issue #349). Records every run into the league history ledger (`<data dir>/league_history/`) as a side effect, backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. The editorial is told to write each race as its own clause and never fold two into one ("respectively"), and is checked against the races afterwards (issue #357): a manager it names as beaten to, or winning, a contested player in a role that player's line does not give them triggers one regeneration told which claims were wrong (a retry that errors keeps the first attempt), and a kept editorial that still fails is saved as written with the wrong claims named on stderr, in a `synthesis_contested_misattribution` JSON warning, and in a callout above the editorial in the report. The same retry and reporting cover two checks on either format (issue #359): a manager's net overall, pre-hit swing or another move's swing pinned on one named move (`synthesis_net_misattribution`), and a tie word ("joint-lowest", "tied for last", "joint-seventh") on a gameweek score or league position no two managers hold (`synthesis_unsupported_tie`); and a third on draft (issue #390): the net overall, or the other kind's figure, said of one kind of a manager's moves ("his waiver moves netted +3 overall" of a -1 waiver and a +4 free-agent pickup, `synthesis_kind_net_misattribution`), for which a roster line that mixes kinds now gives each kind's net beside the net overall. The prompt's GW Standings section names the week's highest and lowest scores, every shared score and position (Pos, and Prev where any is shared) and any season totals the league's tie-break placed apart, so "joint" has a line to be held to, and the roster lines call the manager's net "overall". Biggest Bench Haul (both formats) ranks on the best legal XI's gain over the XI that counted — one keeper, the eight legal formations, the doubled player held in place — never on the bench's raw sum, which counted a keeper who could not have played and outfielders no formation had room for (issue #350); its detail names the swap, a manager with bench points but no better XI is no candidate, and the raw sum stays on the row as `bench_points` (the API's `points_on_bench`) because the coarse tier can record nothing else. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) and `first_capture_store_path` (always present; the partition directory on a first capture, `null` after). Fines are ruled *and recorded* against every captured gameweek, backfilled ones included: the detailed replay rules every configured rule, the coarse tier rules only what cohort points support (`last-place`, `below-threshold`) and records that `red-card` went unruled. `last-place` is ruled against every manager level on the gameweek's lowest score rather than whichever of them the cohort's arrival order reached first; because rulings are frozen at capture and the ledger is append-only, gameweeks recorded before that was true are swept on every run and the recorded ruling appended against the managers they left out, with no flag and no request (`league_history_tied_last_place_repaired`) — and where the row data cannot settle who was level, since the gameweek does not record whether it was ruled on gross or net points, the manager is named by `league_history_tied_last_place_ambiguous` rather than fined on a guess. Season totals print under `Season Fines` on console and as a `# Season Fines` report section, but only at the two milestone gameweeks (GW19 and the finale) - every other week those surfaces stay a this-week view and `league-fines` answers the season question on demand. The `## Season Fines` prompt section and `metadata.season_fines` are ungated: the editorial gets the running totals every week as optional colour (never a required beat), and a scripted consumer sees no calendar-driven appearance and disappearance. Bottom-half gameweeks and green-arrow droughts no longer surface as streaks at all (their run only drives the season count). Streak conditions also keep reset-surviving season occurrence counts ("their sixth gameweek win of the season"), each surfaced on its own rule (see [Command Reference](../docs/command-reference.md#league-recap) for the per-count table): wins and last places on multiples of 3 plus any second-half first, captain blanks and waiver hauls/backfires on multiples of 5, hits on multiples of 3, weeks on top on multiples of 5, bottom-half gameweeks on multiples of 10 in the second half only, and green-arrow droughts on an unbroken run of 5 or 10 (gameweeks spent leading the league never count towards a drought — the leader could not have climbed). Some counts carry same-week peers alongside the milestone — past a fixed floor for counts that stay small, or within a relative window for counts that climb all season. A firing count reaches the report's `## Season Counts` subsection and the editorial's League History section; at the two milestone gameweeks both surfaces carry every nonzero count as the season set-piece. Every line states its span and any not-judged gameweeks beside the number, and every nonzero count is emitted in `metadata.notes_pack.season_count_entries` regardless. A recap of the most recently finished gameweek keeps working once the next gameweek has started: the league table describes that later gameweek from its deadline onward, so the run takes the replay path (figures from each manager's own gameweek history, positions and cumulative totals derived rather than read off the table) and raises a `league_standings_moved_on` warning, instead of reconciling two different gameweeks' numbers against each other and exiting 1 with the ledger row unwritten. `--summarise` with no usable synthesis key no longer aborts the run: the recap, report and ledger capture all proceed, and a `synthesis_provider_unavailable` warning says the editorial was skipped; an editorial the provider cut off at its token ceiling is regenerated once, told it was cut off (a refused editorial is never regenerated, even with a wrong claim in it), and one the provider stopped early on that is still the one kept is written out as it stands with the reason on stderr, a `synthesis_stopped_early` warning in JSON, and a line in the report's warning callout above the editorial (issue #372). A gameweek that cannot be resolved at all exits 1 in both output formats - it used to exit 1 under `--format json` with a generic `"Could not resolve a gameweek to recap."` and 0 in table mode - and the resolver's own reason (`Gameweek 7 is not yet finished`) now reaches the error envelope and the stderr prose alike. On the league's opening gameweek only (GW1, or its own `start_event`), each classic manager's FPL-wide prior-season record — seasons played, last season's points/rank/top-% finish, best season — is fetched from the manager-history endpoint (one request per manager, that gameweek only, and only with `--save`/`--output` or `--summarise`; a league created after GW1 shares the fetch with its league-start offset) and rendered as a `# Prior Seasons` report section and a locked `## Prior Seasons` prompt section with an explicit count; tenure is phrased FPL-wide because entry and league IDs are reissued each season, and a manager with no record or a failed fetch is named as such rather than omitted. Nothing of it reaches the ledger or the JSON rows; draft has no such endpoint and no such section | Yes | General | No | via-agent |
|
|
57
|
+
| `review` | Review a completed gameweek - squad performance and standings. `--save` writes to `<output dir>/<season>/gw{N}-review.md`; `--compare-recs` reads `gw{N}-recommendations.md` from the same season directory and grades the captain, each transfer and each waiver or free-agent move against it; transfer rows that sell the same player are read as ranked alternatives for one sale and graded once, as followed if any candidate was bought and as one miss otherwise; waiver rows that share a drop are read as one conditional chain, each claim keeping its own row: the move made is credited to the claim it filled wherever that claim sits in the chain, a claim below the one that landed is marked not needed rather than not executed, a claim skipped above one that was made is a miss, and a chain abandoned part-way is one miss where it was left, the claims below that marked as fallbacks not reached; the captain line sets the recommended captain against the armband in captain points (raw score times that week's multiplier, Triple Captain included), the armband being the vice's when the captain played no minutes, and adds the change to the team total when the recommended captain was in the side that scored; one who was not in the squad or played no minutes is labelled so rather than priced at 0; on a wildcard or free hit week, where the recommendation is a squad, each player brought in is graded against the recommended 15 instead, and a recommended name that fits more than one player is left ungraded with a warning on stderr rather than matched to the first; a part of the file it could not read (the captain, the transfers or the waivers) is left ungraded the same way, the warning naming the file, rather than grading every move unadvised, and a file nothing could be read from is not compared at all. Under `--summarise` the personal analysis is handed next gameweek's fixtures — every club's opponent and venue plus each squad player's positional FDR (1-7 team ratings with custom analysis on, the FPL API's 1-5 difficulty with it off, carrying the same scale footer and unrated-club caveat the FDR tables print, plus any warning that the ratings cannot support difficulty) — so its "Next Week" calls are grounded in fixtures rather than in one week of points; with no next gameweek to look at, that section is narrowed to fixture-independent observations and points at `fpl gw-prep`, and a gameweek already played is marked retrospective. The answer is then checked back against that block — a cited FDR the block never printed, or any fixture claim made without one — and against the sections its prompt asked for: an answer missing a `## ` heading, ending mid-sentence, or that the provider cut off at its token ceiling is retried once, told what the last draft lacked (a refused answer is flagged but never retried, and a retry that errors keeps the first attempt), and a still-incomplete one is warned about on stderr (detail in `data/debug/synthesis_corrections.txt` under `--debug`, where `synthesis_meta.json` also records how many answers were judged, which was kept, and the tokens they spent between them) and carries the same list as a callout in the saved report. The summary is an add-on the review does not depend on: the two provider roles resolve independently, and a role with no usable key is skipped with the reason on stderr (`Community narrative skipped: ...` / `Personal analysis skipped: ...`) while the review prints, saves and exits 0 — so one key produces that half rather than nothing, and a saved report names each skipped half in a warning callout of its own. A gameweek it cannot resolve - one still being played, an id the season does not have, a season with nothing finished yet - is refused with the specific reason on stderr and exit 1; stdout carries the review or nothing | No | General | No | via-agent |
|
|
58
|
+
| `league-recap` | Recap a completed gameweek - awards, standings, and banter. Saves to `<output dir>/<season>/gw{N}-league-recap[-draft].md`, opening on a fixed `# Gameweek N Recap: <league name>` title the report writes itself, editorial or not; the editorial is asked for a `## ` headline beneath it and never a title, and whatever heading it opens with anyway is demoted to that level or, when it only restates the title, dropped, so the model's heading is never the document's (issue #349). Records every run into the league history ledger (`<data dir>/league_history/`) as a side effect, for draft alongside each held draft's picks (`drafts/draft-{id}.json` in the draft partition, saved once while the API still serves them, since a scheduled redraft drops them for good; `league_history_draft_picks_conflict`, `_lost` (once per draft), `_unsaved` and `_unchecked` name a disagreement with the API, a draft held before anything saved it, a run that could not save, and a `league.drafts` list it could not read), backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. The editorial is told to write each race as its own clause and never fold two into one ("respectively"), and is checked against the races afterwards (issue #357): a manager it names as beaten to, or winning, a contested player in a role that player's line does not give them triggers one regeneration told which claims were wrong (a retry that errors keeps the first attempt), and a kept editorial that still fails is saved as written with the wrong claims named on stderr, in a `synthesis_contested_misattribution` JSON warning, and in a callout above the editorial in the report. The same retry and reporting cover two checks on either format (issue #359): a manager's net overall, pre-hit swing or another move's swing pinned on one named move (`synthesis_net_misattribution`), and a tie word ("joint-lowest", "tied for last", "joint-seventh") on a gameweek score or league position no two managers hold (`synthesis_unsupported_tie`); and a third on draft (issue #390): the net overall, or the other kind's figure, said of one kind of a manager's moves ("his waiver moves netted +3 overall" of a -1 waiver and a +4 free-agent pickup, `synthesis_kind_net_misattribution`), for which a roster line that mixes kinds now gives each kind's net beside the net overall. A fourth, on draft (issue #459), holds the editorial to not reciting a race as a list: a paragraph naming the race's player and three or more of its beaten managers (`synthesis_contested_roll_call`) earns the same retry and reporting, but as a broken brief rather than a contradiction of the data, since the sentence is true, so the retry, stderr and the report callout say that instead. The prompt's GW Standings section names the week's highest and lowest scores, every shared score and position (Pos, and Prev where any is shared) and any season totals the league's tie-break placed apart, so "joint" has a line to be held to, and the roster lines call the manager's net "overall". Biggest Bench Haul (both formats) ranks on the best legal XI's gain over the XI that counted — one keeper, the eight legal formations, the doubled player held in place — never on the bench's raw sum, which counted a keeper who could not have played and outfielders no formation had room for (issue #350); its detail names the swap, a manager with bench points but no better XI is no candidate, and the raw sum stays on the row as `bench_points` (the API's `points_on_bench`) because the coarse tier can record nothing else. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `synthesis_attempts` (answers judged: 1, or 2 after a retry), `synthesis_shipped_attempt` (which of them shipped) and `synthesis_usage` (input and output tokens summed over every answer judged; all three `null` when no editorial came back), `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) `first_capture_store_path` (always present; the partition directory on a first capture, `null` after) and `draft_picks_saved` (always present; the draft-picks snapshots this run wrote, empty on classic). Fines are ruled *and recorded* against every captured gameweek, backfilled ones included: the detailed replay rules every configured rule, the coarse tier rules only what cohort points support (`last-place`, `below-threshold`) and records that `red-card` went unruled. `last-place` is ruled against every manager level on the gameweek's lowest score rather than whichever of them the cohort's arrival order reached first; because rulings are frozen at capture and the ledger is append-only, gameweeks recorded before that was true are swept on every run and the recorded ruling appended against the managers they left out, with no flag and no request (`league_history_tied_last_place_repaired`) — and where the row data cannot settle who was level, since the gameweek does not record whether it was ruled on gross or net points, the manager is named by `league_history_tied_last_place_ambiguous` rather than fined on a guess. Season totals print under `Season Fines` on console and as a `# Season Fines` report section, but only at the two milestone gameweeks (GW19 and the finale) - every other week those surfaces stay a this-week view and `league-fines` answers the season question on demand. The `## Season Fines` prompt section and `metadata.season_fines` are ungated: the editorial gets the running totals every week as optional colour (never a required beat), and a scripted consumer sees no calendar-driven appearance and disappearance. Bottom-half gameweeks and green-arrow droughts no longer surface as streaks at all (their run only drives the season count). Streak conditions also keep reset-surviving season occurrence counts ("their sixth gameweek win of the season"), each surfaced on its own rule (see [Command Reference](../docs/command-reference.md#league-recap) for the per-count table): wins and last places on multiples of 3 plus any second-half first, captain blanks and waiver hauls/backfires on multiples of 5, hits on multiples of 3, weeks on top on multiples of 5, bottom-half gameweeks on multiples of 10 in the second half only, and green-arrow droughts on an unbroken run of 5 or 10 (gameweeks spent leading the league never count towards a drought — the leader could not have climbed). Some counts carry same-week peers alongside the milestone — past a fixed floor for counts that stay small, or within a relative window for counts that climb all season. A firing count reaches the report's `## Season Counts` subsection and the editorial's League History section; at the two milestone gameweeks both surfaces carry every nonzero count as the season set-piece. Every line states its span and any not-judged gameweeks beside the number, and every nonzero count is emitted in `metadata.notes_pack.season_count_entries` regardless. A recap of the most recently finished gameweek keeps working once the next gameweek has started: the league table describes that later gameweek from its deadline onward, so the run takes the replay path (figures from each manager's own gameweek history, positions and cumulative totals derived rather than read off the table) and raises a `league_standings_moved_on` warning, instead of reconciling two different gameweeks' numbers against each other and exiting 1 with the ledger row unwritten. `--summarise` with no usable synthesis key no longer aborts the run: the recap, report and ledger capture all proceed, and a `synthesis_provider_unavailable` warning says the editorial was skipped; an editorial the provider cut off at its token ceiling is regenerated once, told it was cut off (a refused editorial is never regenerated, even with a wrong claim in it), and one the provider stopped early on that is still the one kept is written out as it stands with the reason on stderr, a `synthesis_stopped_early` warning in JSON, and a line in the report's warning callout above the editorial (issue #372). A gameweek that cannot be resolved at all exits 1 in both output formats - it used to exit 1 under `--format json` with a generic `"Could not resolve a gameweek to recap."` and 0 in table mode - and the resolver's own reason (`Gameweek 7 is not yet finished`) now reaches the error envelope and the stderr prose alike. On the league's opening gameweek only (GW1, or its own `start_event`), each classic manager's FPL-wide prior-season record — seasons played, last season's points/rank/top-% finish, best season — is fetched from the manager-history endpoint (one request per manager, that gameweek only, and only with `--save`/`--output` or `--summarise`; a league created after GW1 shares the fetch with its league-start offset) and rendered as a `# Prior Seasons` report section and a locked `## Prior Seasons` prompt section with an explicit count; tenure is phrased FPL-wide because entry and league IDs are reissued each season, and a manager with no record or a failed fetch is named as such rather than omitted. Nothing of it reaches the ledger or the JSON rows; draft has no such endpoint and no such section. Every recap, classic and draft, also carries a Standout Players section (issue #455), read off the squads and live points the bench haul already uses: the week's top scorers in any manager's squad and its costliest blanks (2 points or fewer; in classic the one that counted for the most managers first, in draft the best by FPL's pre-season draft rank and, on a live recap from GW19, by season points per game, since the rank is never updated; a draft player neither can rank is left off, and no line states the rank or average, which only the heading and JSON carry), each with the managers he counted for, how long a blank played when it was under the hour, and any who left him on the bench, as one sentence per player that the console, a `# Standout Players` report section and a `## Standout Players` prompt section all print; `--format json` carries it as `metadata.standout_players`, every player row with the main game's `id` (under `--draft` too) and `code` beside it. A draft recap's lines also say how each player came to the manager they name (issue #458): the draft pick, the waiver or free-agent move, or the trade, the latest as of the recapped gameweek (a `-g` replay shows its own week), from a replay of the league's draft, accepted transactions and processed trades that must end where FPL's own ownership list does and match the week's squads, with the draft's picks read from the choices feed or, once it serves another draft, the saved snapshot, or, where neither has them, who drafted whom rebuilt from each manager's squad at the draft's gameweek with the earlier moves undone (the line then says "who drafted him" with no round, `round_unknown` in the JSON, and a `draft_rounds_unknown` notice replaces the warning). If the history cannot be shown complete (or a read fails, a failed trades read included once a trade has happened) the lines read without it and a `draft_provenance_unavailable` warning says why; draft rows carry `acquisition` in the JSON, classic rows do not. Before a live capture collects, it reads the recapped game's processing feed (`event-status/` on `FPLClient`, `pl/event-status` on `FPLDraftClient`, both `get_gameweek_status()`): when the feed's match days for the recapped gameweek show bonus unconfirmed or the league tables still "Updating" (`GameweekStatus.still_processing()`), it warns on stderr, in a `league_tables_updating` JSON warning and in a callout at the top of a saved report that positions and totals may be stale, and records the gameweek anyway, since a later run supersedes it and a missed draft gameweek cannot be recaptured; a reconciliation failure in that window keeps its own diagnosis and adds that it may be timing. Only a definite signal warns: an unreadable, slow (5s), reshaped or unrecognised feed is a debug log, and a replay never reads it (issue #424) | Yes | General | No | via-agent |
|
|
51
59
|
| `league-fines` | Show who has been fined this season and how much of it was actually ruled, folded out of the league history ledger. Reads local files only - no network - so it works for any season still on disk (`--season 2025-26`). Nothing is re-ruled: a fine is counted exactly as its gameweek recorded it — including one `league-recap` appended against a manager who tied for last and was left out at capture. Counts, never money (`penalty` is free text; settlement and escalation are not modelled). Every gameweek that could not be ruled is named beneath the table - never captured, unreadable, captured before rulings were recorded, captured with no rules configured, or captured at a fidelity that could not rule a given rule. `--format json` emits one entry per recorded manager, fined or not, each with `counts` (zero included), `total`, `fined_gameweeks`, `ruled_gameweeks`, `unruled_gameweeks`, `first_recorded_gameweek`, `last_recorded_gameweek` and `is_fully_ruled`; `metadata` carries `season`, `fpl_format`, `league_id`, `gameweek`, `start_gameweek`, `rule_types`, `total_fines` and `qualifiers`. Exits 1 with the shared error envelope when no league id is configured for the format | Yes | General | No | direct-api |
|
|
52
60
|
| `captain` | Analyse and rank captain options for next gameweek. Your squad is the default, so an absent `classic_entry_id` exits 1 with an `error` envelope naming `--global` rather than returning the global list as though it were your options; `metadata.my_squad_mode` says which list the payload holds, and `metadata.warnings` carries `captain_global_fallback` when there is no squad to rank yet (before the first deadline) or `captain_no_next_gameweek` when there is no next gameweek at all (finished season, or the next one unpublished). An entry ID that does not resolve is a failure, not a fallback, worded exactly as `squad` words it (`get_own_squad_picks`, `agents/common.py`). JSON candidates include `adjusted_npxg_per_90` and `raw_npxg_per_90` when fixture adjustment is active. Consistency tiebreaker (CV-xGI percentile) phased in GW6-10. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the GW1-9 shrinkage rather than pulled toward the position mean | Yes | Classic | Yes | via-agent |
|
|
53
61
|
| `differentials` | Find differential picks - high potential, low ownership. Inverted consistency bonus (volatile players score higher, phased in GW6-10). Before GW10 the score's quality baseline carries the prior blend (last season's pedigree weighted by confidence) rather than position-mean shrinkage, so a quiet-starting elite is not ranked below a one-game wonder; the ownership, matchup and consistency bonuses stay pure observation; JSON `metadata.warnings` carries the early-season notice while the blend is live (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded) plus `understat_team_unmatched` for any club Understat carried no players for, table mode prints them to stderr. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the blend rather than handed last season's standing. The gameweek stamped into JSON `metadata` is fetched after the agent succeeds, not before it runs, so an unreachable API is reported as the agent's failure in both output formats rather than as a client error under `--format json` and an agent error in table mode | Yes | Classic | Yes | via-agent |
|
|
54
62
|
| `targets` | Find transfer targets - high performers across all ownership. Consistency bonus (CV-xGI percentile, phased in GW6-10). Before GW10 the score's quality baseline carries the prior blend (last season's pedigree weighted by confidence) rather than position-mean shrinkage, so a quiet-starting elite is not ranked below a one-game wonder; the matchup and consistency bonuses stay pure observation, and the score agrees with `fpl transfer-eval`'s Outlook column; JSON `metadata.warnings` carries the early-season notice while the blend is live (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded) plus `understat_team_unmatched` for any club Understat carried no players for, table mode prints them to stderr. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the blend rather than handed last season's standing | Yes | Classic | Yes | via-agent |
|
|
55
63
|
| `transfer-eval` | Compare transfer OUT player against IN candidates. JSON includes `adjusted_npxg_per_90` and `raw_npxg_per_90` per player when fixture adjustment is active. Before GW10 `quality_score` carries the value family's prior blend (matching `fpl player` and `fpl stats --value`) and `target_score` the target family's (matching `fpl targets`); the single-GW `lineup_score` keeps position-mean shrinkage. JSON `metadata.warnings` carries the early-season notice naming both blended columns, or only `target_score` when no Understat match produced a quality score. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of whichever device applies, rather than pulled toward the position mean or handed last season's standing. A name two players answer to exactly (`Henderson`) exits 1 with an `error` envelope naming both, rather than resolving to whichever the API lists first; `Name (TEAM)` or a player ID picks one | Yes | General | Yes | via-agent |
|
|
56
64
|
| `allocate` | Select mathematically optimal 15-player squad within budget. At `--horizon >= 2` the solver ranks on the value family's GW1-9 prior blend (last season's pedigree weighted by confidence); players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of it rather than handed last season's standing, and stay in the solver pool; JSON `metadata.warnings` carries the early-season notice at `--horizon >= 2` (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded), plus `understat_team_unmatched` for any club Understat carried no players for, whose squad therefore scored without xG | Yes | Classic | Yes | direct-api |
|
|
57
|
-
| `waivers` | Show waiver recommendations for your draft league. JSON `data` carries `top_targets` and `targets_by_position` (ranked claims) plus `pool` — the full unowned roster, one `{id, player_name, position, team_short}` row each, unranked and untruncated, so a consumer can answer "is this player claimable at all" without inferring it from the ranked lists. Keeper scores carry the GK signal block (saves/90, defensive quality, clean-sheet rate), read from the main-game player each draft element is joined to on `code` because the draft bootstrap publishes none of it, so draft keepers rank on what separates them rather than on form and ppg alone. Before GW10 the score's quality baseline carries the prior blend (last season's pedigree weighted by confidence) rather than position-mean shrinkage; the matchup, position-need, team-stacking and consistency terms stay pure observation; JSON `metadata.warnings` carries the early-season notice while the blend is live, and reports `early_season_small_sample` both when history could not be loaded and when no draft element found a main-game counterpart to take a prior from. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the blend rather than handed last season's standing. No `draft_league_id` configured exits 1 in both output formats — table mode used to warn and exit 0 | Yes | Draft | Yes | via-agent |
|
|
65
|
+
| `waivers` | Show waiver recommendations for your draft league. JSON `data` carries `top_targets` and `targets_by_position` (ranked claims) plus `pool` — the full unowned roster, one `{id, code, player_name, position, team_short}` row each, unranked and untruncated, so a consumer can answer "is this player claimable at all" without inferring it from the ranked lists. On every player row the command emits `id` is the draft game's element id and `code` the cross-game key: the gw-prep skill joins the returnee radar's main-game rows to the pool on `code`, since a mid-season addition holds a different id in each game (#418). Keeper scores carry the GK signal block (saves/90, defensive quality, clean-sheet rate), read from the main-game player each draft element is joined to on `code` because the draft bootstrap publishes none of it, so draft keepers rank on what separates them rather than on form and ppg alone. Defender scores carry DC/90 through the same join, since the draft bootstrap has defensive contribution only as a season total; every row's `dc_per_90` (pool, `current_squad`, `recent_releases`) is the main-game rate, weighted only in the defender score and display-only for other positions, and a defender the join misses stays at 0. A keeper or defender in the unowned pool that the join misses is reported as one `draft_player_unjoined` entry in `metadata.warnings` (stderr in table mode) with the count of each and the first three names, since the player ranks against an anchor calibrated with the term it lacks. Before GW10 the score's quality baseline carries the prior blend (last season's pedigree weighted by confidence) rather than position-mean shrinkage; the matchup, position-need, team-stacking and consistency terms stay pure observation; JSON `metadata.warnings` carries the early-season notice while the blend is live, and reports `early_season_small_sample` both when history could not be loaded and when no draft element found a main-game counterpart to take a prior from. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the blend rather than handed last season's standing. Each row's `team_id` is the main game's team id, translated from the draft game's own club numbering by club code (null for a club the main game does not list, which `metadata.warnings` reports as `draft_club_untranslated`; the row keeps the club's draft name), since fixtures, form and FDR are all keyed by it. No `draft_league_id` configured exits 1 in both output formats — table mode used to warn and exit 0 | Yes | Draft | Yes | via-agent |
|
|
58
66
|
| `squad` | Analyse your FPL squad health and fixtures. Exits 1 with an `error` envelope when no entry ID is configured for the active format. `--classic` / `--draft` (mutually exclusive) pin the format instead of inferring it from the configured IDs, so a caller that means classic gets the missing-ID error rather than the draft roster; with neither, single-format configs still auto-select. A picks 404 is checked against `entry/<id>/` before it is reported as "no squad submitted yet", so a reissued or wrong entry ID says so; `squad grid` and `captain` share that diagnosis through `agents/common.py` | Yes | General | No | via-agent |
|
|
59
67
|
| `squad grid` | Show squad fixture difficulty grid. Exits 1 with an `error` envelope when no entry ID is configured for the active format, or when the squad fetch fails. Takes the same `--classic` / `--draft` pinning as `squad`, before or after the subcommand, and the same stale-entry diagnosis. A `--watch` name that does not resolve is non-fatal either way: unmatched, or matching two players exactly, it warns on stderr and is skipped, and the grid still renders | Yes | General | No | via-agent |
|
|
60
|
-
| `squad sell-prices` | Show squad sell prices and financial breakdown. Nothing cached and no `--refresh`, a failed scrape, draft format, or a scrape the extraction heuristics distrust each exit 1 with an `error` envelope under `--format json`; table mode still renders a suspect scrape under a `Squad Budget (Suspect)` panel, since a reader can judge it and `fpl allocate --sell-prices` cannot | Yes | Classic | No | direct-api |
|
|
68
|
+
| `squad sell-prices` | Show squad sell prices and financial breakdown. A cached run recomputes each sell price at today's price, applying FPL's rule only to a player whose cached sell price it reproduces at the price stored with the scrape; JSON rows carry `scraped_sell_price` beside `sell_price`, and a player it cannot check keeps the scraped price under a `sell_prices_as_scraped` warning, on stderr in both modes (also given, with exit 0, when today's prices cannot be fetched). `transfer-eval` budgets on the same recomputed price and warns when the OUT player's is unchecked or not cached; `allocate --sell-prices` repeats the file's warning. Nothing cached and no `--refresh`, a failed scrape, draft format, or a scrape the extraction heuristics distrust each exit 1 with an `error` envelope under `--format json`; table mode still renders a suspect scrape under a `Squad Budget (Suspect)` panel, since a reader can judge it and `fpl allocate --sell-prices` cannot | Yes | Classic | No | direct-api |
|
|
61
69
|
| `chips` | View and plan FPL chip usage | Yes | Classic | No | direct-api |
|
|
62
70
|
| `chips timing` | Recommend chip timing based on blank/double GW exposure. No `classic_entry_id` configured, and no completed gameweek to measure exposure against, each exit 1 in both output formats — table mode used to print the reason and exit 0. The missing-ID message is the shared one `fpl squad` and `fpl squad grid` give, and a failed fixture agent reports the agent's own reason in the same words either way (`Fixture agent failed: <reason>`), where the envelope and the terminal used to word it differently and neither passed the reason on | Yes | Classic | No | via-agent |
|
|
63
71
|
| `chips add` | Plan a chip for a gameweek | No | Classic | No | direct-api |
|
|
@@ -117,17 +125,18 @@ Python classes in `fpl_cli/agents/` that implement `async run(context) -> AgentR
|
|
|
117
125
|
| ReportAgent | `agents/orchestration/report.py` | Orchestration | `preview`, `review`, `league-recap` | - |
|
|
118
126
|
|
|
119
127
|
**Notes:**
|
|
120
|
-
- BenchOrderAgent and StartingXIAgent have no CLI command - they are invoked by gw-prep skill wrapper scripts in `.agents/skills/gw-prep/scripts
|
|
128
|
+
- BenchOrderAgent and StartingXIAgent have no CLI command - they are invoked by gw-prep skill wrapper scripts in `.agents/skills/gw-prep/scripts/`. Both payloads carry `warnings` as `{code, message}` entries: the feed coverage codes above, and for bench order `bench_cover_missing` per position the bench cannot cover
|
|
121
129
|
- Every scoring family holds players known not to be playing out of its GW1-9 early-season device — the prior blend for the value family (`quality_score`, `fpl allocate`) and the ownership family (`targets`, `differentials`, `waivers`, transfer-eval's Outlook), position-mean shrinkage for the single-GW family (`captain`, `bench-order`, `starting-xi`, transfer-eval's This GW); bench and lineup order on raw scores, so for those two it changes the displayed score only
|
|
122
130
|
- TransferEvalAgent is used by both `transfer-eval` CLI command and gw-prep skill
|
|
123
|
-
- `bench_order.py`, `starting_xi.py` and `transfer_eval.py` (`.agents/skills/gw-prep/scripts/`) take player names, accept the `Name (TEAM)` disambiguator, and exit 1 naming both candidates when a name matches two players exactly (Dean and Jordan Henderson are both `Henderson`) rather than resolving to whichever the API lists first. Pass `Name (CLUB)` from the squad grid's `player`/`team` pair (issue #180). All three share that resolution with the `transfer-eval` CLI command through `resolve_player_or_report` / `resolve_players_or_report` (`fpl_cli/models/player.py`)
|
|
131
|
+
- `bench_order.py`, `starting_xi.py` and `transfer_eval.py` (`.agents/skills/gw-prep/scripts/`) take player names, accept the `Name (TEAM)` disambiguator, and exit 1 naming both candidates when a name matches two players exactly (Dean and Jordan Henderson are both `Henderson`) rather than resolving to whichever the API lists first. Pass `Name (CLUB)` from the squad grid's `player`/`team` pair (issue #180). All three share that resolution with the `transfer-eval` CLI command through `resolve_player_or_report` / `resolve_players_or_report` (`fpl_cli/models/player.py`). Every player row they emit carries the main-game `id` and `code`, the cross-game key, beside it (#419), so a phase that combines one with a draft payload joins on `code`
|
|
124
132
|
- Every gw-prep script that runs an agent (`bench_order.py`, `starting_xi.py`, `transfer_eval.py`) runs it inside `json_output_mode()`, so a phase can parse stdout from byte 0 on the success path as well as the failure one. Until #226 the agent's two progress lines landed ahead of the payload and only the error envelopes were clean — the reverse of what a lenient reader would guess. `Agent.log` now writes to stderr for every agent, so the same holds for `fpl transfer-eval --format json`; `tests/test_cli_json_contract.py` guards all four
|
|
125
133
|
- `extract_classic_squad.py` (`.agents/skills/gw-prep/scripts/`) — deterministic Classic Squad block extractor used by gw-prep Phase A3 + Phase E. JSON stdout; read-only; emits TypedDict-annotated payloads.
|
|
126
|
-
- gw-prep Phase B10 runs `fpl returnees --enrich --format json` and inlines the payload into both Phase C prompts, rendering a Returning Soon section in the classic and draft output templates. In draft a row can escalate into a stash claim; in classic the watchlist is informational and transfer recommendations may not name a tracked returnee (`references/rules.md`)
|
|
134
|
+
- gw-prep Phase B10 runs `fpl returnees --enrich --format json` and inlines the payload into both Phase C prompts, rendering a Returning Soon section in the classic and draft output templates. In draft, `claimable_returnees.py` first narrows the payload to the entries still in the waiver pool, joined on `code` (#418), and a row of that list can escalate into a stash claim; in classic the watchlist is informational and transfer recommendations may not name a tracked returnee (`references/rules.md`)
|
|
127
135
|
- gw-prep Phase B1 and squad-builder Phase B each run the `fpl fdr` analysis call alongside `fpl fdr --blanks --format json`, and read the quality signals both return -- `data.ratings_warning`, `metadata.custom_analysis`, the blanks payload's `metadata.warnings`, plus `data.predictions_stale` / `data.prediction_warnings` where `--my-squad` resolved a squad. `--blanks` bypasses the fixture agent for a schedule-only payload carrying none of the analysis and none of those fields, so the skills that issued only it were promising pFDR they never received (gw-prep's pFDR Overview, squad-builder's per-position `{ATK|DEF} column` blocks) and silently accepting whatever rating quality they got (issue #135). Whatever fires becomes `data_caveat`, inlined into every sub-agent prompt and written to the output's Data Quality section, messages quoted verbatim so the remedy they name (`fpl ratings update`, `fpl init`) survives. gw-prep passes `--my-squad` (it also wants squad blank/double exposure); squad-builder deliberately does not, since it is replacing the 15 and its season-start modes have no squad to resolve. Enforced by `tests/test_skill_fdr_quality_signals.py`
|
|
128
136
|
- StatsAgent (`fpl xg`, `fpl preview`) clamps its own window and minutes floor to the gameweeks played, scaling the floor by the same 45 minutes per gameweek the skills apply to `fpl stats --min-minutes` below -- off a different anchor, though: the skills scale off `(N - 1)` for the upcoming gameweek, this off the gameweeks the API reports finished, which diverges under a postponement and only downwards. The default 360-minute floor is arithmetically impossible until four gameweeks have finished and out of reach for all but ever-presents for several more, so it emptied every table through the opening weeks (issue #227); the configured bar binds again from 8 finished gameweeks, 10 for `--all`. `data.min_minutes` / `data.gameweeks_played` / `data.window_label` report what was applied, `data.empty_reason` says whether the floor or the data emptied the result, and `warnings` carries `early_season_minutes_floor` while the bar is scaled. A floor a caller passes explicitly (`fpl targets --min-minutes`, `fpl differentials --min-minutes`) is applied verbatim. Every surface renders the reason where its reader is: `fpl xg`, `fpl targets` and `fpl differentials` in place of their tables, `fpl preview` under a Performance Stats heading that now carries the analysed window rather than a hardcoded six gameweeks (console, `templates/gw_preview.md.j2`, and ReportAgent's inline fallback alike). Enforced by `tests/test_stats.py::TestStatsAgentMinutesFloor` and `::TestStatsAgentWindowPrefilter`
|
|
129
137
|
- gw-prep Phase B8 and squad-builder Phase B derive the `fpl stats --min-minutes` floor from the current gameweek (`min(cap, (N - 1) * 45)`, caps 315/450) rather than hardcoding it. A fixed floor is unreachable until GW8/GW11 and empties the result silently -- `fpl stats` returns a well-formed envelope with `"data": []`. Both also rank on `ep_next` before GW6 -- the positional sorts (xGI, form, total_points, ppg) order mostly on sample that early -- and label each block with the sort and floor it used, so a downstream agent knows what ordered it; an empty block is annotated rather than left bare. Both also say what that sort is *not*: before FPL's fixture factor moves off 1.0, `ep_next` tracks `form` almost exactly (at GW4, every row of all four positional shortlists), so the ordering is a form ordering with doubtful players scaled down by chance of playing, and a sub-agent must not be told it is a projection rather than observed output (issue #236). Enforced by `tests/test_skill_stats_thresholds.py`
|
|
138
|
+
- `claimable_returnees.py` (`.agents/skills/gw-prep/scripts/`) — joins the `fpl returnees` payload to the `fpl waivers` pool on `code` (gw-prep Phase B10, draft runs) and emits the radar narrowed to the entries still unowned, with the rest under `excluded` as `owned` or `no-code`. The radar's `id` is the main game's element id and the pool's the draft's, and the two differ for mid-season additions, so the C2 sub-agent is handed a claimable list rather than left to match two payloads itself (#418). JSON stdout; read-only; `ok: false` with no entries when the pool has no usable `code` (an older fpl-cli) or the radar has no `data.entries` (a failed `fpl returnees` run), with a warning naming which side failed, so the stash path fails closed and an unreadable radar never reads as nobody returning (#432); exits 0 whatever it finds — the one non-zero exit is the startup guard below.
|
|
130
139
|
- `validate_draft_waivers.py` (`.agents/skills/gw-prep/scripts/`) — cross-checks the Draft waiver table against the live waiver pool and squad grid (gw-prep Phase D1). JSON stdout; read-only; exits 0 whatever the validation finds — the one non-zero exit is the startup guard below.
|
|
131
140
|
- `normalise_entities.py` (`.agents/skills/gw-prep/scripts/`) — decodes HTML-escaped text in a report assembled from sub-agent output and reports the entity references it deliberately leaves alone. JSON stdout; rewrites its `--file` in place; shared by gw-prep Phase D, squad-builder Phase E and update-gw-prep Phase D, all of which warn rather than block on its findings. Contract and warn posture live once, in `.agents/skills/gw-prep/references/entity-normalisation.md`.
|
|
132
141
|
- Every script under `.agents/skills/gw-prep/scripts/` imports the `fpl_cli` package, so it runs on the interpreter fpl-cli is installed on — every SKILL.md that calls one (gw-prep, squad-builder, update-gw-prep) writes that interpreter as `[YOUR_PYTHON]`, and squad-builder/update-gw-prep reach gw-prep's `scripts/` cross-skill via `[YOUR_SKILLS_DIR]` since `${CLAUDE_SKILL_DIR}` resolves to the *calling* skill's own directory (issue #221). A standalone `fpl` on `PATH` (uv tool, pipx) provides the command but not the package; `_bootstrap.py` catches that case ahead of the fpl_cli imports and exits 1 with `{"error": true, "messages": [...]}` on stdout, which the phases parsing that stdout surface as a warning instead of a traceback
|
|
133
|
-
- The extractor and the waiver validator locate markdown sections via `fpl_cli.utils.markdown` (`HeadingMatcher`, `find_section`, `section_body`, `leaf_body`, `fence_flags`), tolerating LLM heading drift (qualifiers, case, leading annotations, opt-in aliases) without matching a different heading that shares a prefix. Also used by `fpl_cli/prompts/review.py` for the GW Narrative section boundary. Both scripts read the tables inside those sections with `iter_tables`, the reader `fpl review --compare-recs` uses, so a `| - | - |` placeholder row never ends a table early. The waiver validator also cleans its cells with `split_name_team`, so a `(LIV, MID)` suffix comes off a name as `(LIV)` does, skips the same placeholder cells as the review (`No claim`, `-`), and reports rows under Waiver Recommendations that
|
|
142
|
+
- The extractor and the waiver validator locate markdown sections via `fpl_cli.utils.markdown` (`HeadingMatcher`, `find_section`, `section_body`, `leaf_body`, `fence_flags`), tolerating LLM heading drift (qualifiers, case, leading annotations, opt-in aliases) without matching a different heading that shares a prefix. Also used by `fpl_cli/prompts/review.py` for the GW Narrative section boundary. Both scripts read the tables inside those sections with `iter_tables`, the reader `fpl review --compare-recs` uses, so a `| - | - |` placeholder row never ends a table early. The waiver validator also cleans its cells with `split_name_team`, so a `(LIV, MID)` suffix comes off a name as `(LIV)` does, skips the same placeholder cells as the review (`No claim`, `-`), picks the waiver table by its `Drop`/`Claim` headers rather than its place, and reports rows under Waiver Recommendations that it did not take in (via `pipe_rows` and the table's `span`, counting fenced and blockquoted rows too) rather than passing them unchecked, naming the first few: when no waiver row was read at all it fails closed with a `waiver-table-unreadable` flag that blocks Phase D1, and when some were it warns with `waiver-rows-unread`. An empty waiver table is a week with no claims, so beside it only rows no table took in block. `tests/test_validate_draft_waivers.py` holds Phase D1's stop-or-proceed prose in SKILL.md to the flag types the validator emits. It still matches the template's column names exactly, where the review takes aliases: it checks gw-prep's fresh output, not older files.
|
|
@@ -174,6 +174,8 @@ Store the analysis as `pfdr`, the schedule as `blanks_schedule` and the draft va
|
|
|
174
174
|
|
|
175
175
|
`data_caveat` goes into every Phase C prompt and into the report's `## Notes` section in Phase D, so a reader can see why the recommendations leaned away from fixtures.
|
|
176
176
|
|
|
177
|
+
**Feed coverage entries from the scoring payloads join `data_caveat` too.** Every scoring command gathered below (`fpl captain`, `fpl waivers`, `fpl targets`, `fpl differentials`, `fpl transfer-eval`, `fpl stats --value`) and the `bench_order.py` / `starting_xi.py` / `transfer_eval.py` scripts carry `warnings` entries whose `code` ends in `_coverage_low` when a data feed reached too little of the player pool -- Understat down, Core-Insights match files missing, per-gameweek history throttled, last season's history gone, clubs unrated -- and `fpl waivers` (with the draft columns of `fpl stats`) carries `draft_squads_unread` when a rival squad could not be read. Each message says what the scores fell back to. Add every such entry to `data_caveat` verbatim, once per distinct `code` across all payloads, and treat the affected scores the way the Data quality rule treats pFDR: still shown, not decisive on their own.
|
|
178
|
+
|
|
177
179
|
### B2 -- Captain Candidates (classic only - skip if format is "draft")
|
|
178
180
|
|
|
179
181
|
```bash
|
|
@@ -335,13 +337,37 @@ returns the FPL-sourced watchlist -- so this step is never a reason to abort the
|
|
|
335
337
|
own retries; the answers it did get are cached, so re-run the command once before settling for the
|
|
336
338
|
partial payload.
|
|
337
339
|
|
|
338
|
-
Store the payload as `returnee_radar` and inline it into
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
to a stash claim. The Returning Soon section quotes this payload, which is inlined into the prompt
|
|
340
|
+
Store the payload as `returnee_radar` and inline it into the C1 prompt as a labelled section. C1
|
|
341
|
+
renders it as an informational watchlist and is barred from naming its players in transfer
|
|
342
|
+
recommendations. The Returning Soon section quotes this payload, which is inlined into the prompt
|
|
342
343
|
like every other cited source -- so its numbers are grounded the same way every other cited number
|
|
343
344
|
is.
|
|
344
345
|
|
|
346
|
+
On a draft run (format `draft` or `both`), join the radar to the waiver pool here rather than
|
|
347
|
+
leaving the match to the C2 sub-agent: the radar's `id` is the main game's element id and the
|
|
348
|
+
pool's is the draft's, and the two differ for players added mid-season, so a model matching the two
|
|
349
|
+
payloads by eye is the step that failed in #418. Write B10's payload to
|
|
350
|
+
`/tmp/gw-prep-returnees-{N}.json` and B3's to `/tmp/gw-prep-waivers-{N}.json` (the file Phase D1
|
|
351
|
+
reads), then:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
[YOUR_PYTHON] "${CLAUDE_SKILL_DIR}/scripts/claimable_returnees.py" \
|
|
355
|
+
--returnees-json /tmp/gw-prep-returnees-{N}.json \
|
|
356
|
+
--waivers-json /tmp/gw-prep-waivers-{N}.json
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Store its output as `returnee_radar_draft`. It carries the radar's `metadata` and the subset of its
|
|
360
|
+
`entries` whose `code` matches a pool row -- joined on `code`, never on `id` -- with everyone else
|
|
361
|
+
under `excluded` and why (`owned`, or `no-code`). `ok: false` means the join could not run and
|
|
362
|
+
`entries` is then empty, so no stash claim can be made from it; `warnings` says which side failed:
|
|
363
|
+
`radar-unreadable` when the radar has no `data.entries` (for example the `{command, error}` envelope
|
|
364
|
+
of a failed `fpl returnees` run), `pool-unreadable` or `pool-without-code` when the pool could not be
|
|
365
|
+
read (no `data.pool`, or rows without `code` from an older fpl-cli), `envelope-unreadable` when
|
|
366
|
+
either file is not a JSON object at all. An empty list with `ok: false`
|
|
367
|
+
is never "nobody is returning". If the script fails (exit 1) treat it the same way: Returning Soon
|
|
368
|
+
says the radar and pool could not be joined, lists nobody, and C2 makes no stash claim. Never fall
|
|
369
|
+
back to matching the raw payloads yourself.
|
|
370
|
+
|
|
345
371
|
<!-- ADAPT: Add your own further supplementary data sources here. Examples:
|
|
346
372
|
- `fpl preview --save --scout` generates a GW preview with fixture analysis and scout insights.
|
|
347
373
|
Read the saved file and inject its content into Phase C sub-agents as additional context.
|
|
@@ -625,7 +651,7 @@ Agent tool parameters:
|
|
|
625
651
|
>
|
|
626
652
|
> **WAIVER POOL IS AUTHORITATIVE:** `fpl waivers` output is the only source for available players. All other data (stats, form tables, squad context) is for analysis only. Never recommend a claim not present in the waivers output — Phase D1 will flag pool misses as a warning. Cross-position recommendations (e.g. dropping a MID to claim a DEF) are structurally illegal and will be blocked by Phase D1.
|
|
627
653
|
>
|
|
628
|
-
> **`data.pool` is an availability roster, not a shortlist.** It lists every unowned player in the league — one `{id, player_name, position, team_short}` row each, unranked and untruncated — so membership answers only "may this player be claimed at all". Ranked claims come from `data.top_targets` and `data.targets_by_position`; presence in `data.pool` is never on its own a reason to recommend someone. The single claim allowed to rest on pool membership alone is a stash claim for a tracked returnee, which the waiver scoring suppresses by design and which therefore cannot reach the ranked targets.
|
|
654
|
+
> **`data.pool` is an availability roster, not a shortlist.** It lists every unowned player in the league — one `{id, code, player_name, position, team_short}` row each, unranked and untruncated — so membership answers only "may this player be claimed at all". Its `id` is the draft game's element id; `code` is the key every other command's player rows share, and the only field to match another payload on. Ranked claims come from `data.top_targets` and `data.targets_by_position`; presence in `data.pool` is never on its own a reason to recommend someone. The single claim allowed to rest on pool membership alone is a stash claim for a tracked returnee, which the waiver scoring suppresses by design and which therefore cannot reach the ranked targets.
|
|
629
655
|
>
|
|
630
656
|
> - Waivers: {B3 output}
|
|
631
657
|
> - Squad: {B4 output}
|
|
@@ -637,11 +663,20 @@ Agent tool parameters:
|
|
|
637
663
|
> ("Transfer Flow projects him starting"), never assert it. Under `negative_filter_only` use it
|
|
638
664
|
> only to downgrade. A player with no intel is not a worse player.
|
|
639
665
|
>
|
|
640
|
-
> - Returning soon: {
|
|
641
|
-
>
|
|
642
|
-
>
|
|
643
|
-
>
|
|
644
|
-
>
|
|
666
|
+
> - Returning soon: {returnee_radar_draft} (from B10, the radar joined to the waiver pool by
|
|
667
|
+
> `claimable_returnees.py`)
|
|
668
|
+
> Render the Returning Soon section of the Draft output template from its `entries`. Every entry
|
|
669
|
+
> there is a tracked returnee whose `code` matches a row in the waivers `data.pool`, so each one
|
|
670
|
+
> is claimable and no returnee outside the list is. Do not re-derive membership from the waivers
|
|
671
|
+
> payload, and never match on `id`: the radar's `id` is the main game's element id and the pool's
|
|
672
|
+
> is the draft's, and the two differ for players added mid-season. The `excluded` list names the
|
|
673
|
+
> returnees a rival already owns — leave them out rather than listing them as unavailable. If `ok`
|
|
674
|
+
> is false the join could not run: name what failed from `warnings` in the section ("the
|
|
675
|
+
> returnee radar could not be read" for `radar-unreadable`, "the waiver pool could not be read"
|
|
676
|
+
> for `pool-unreadable` or `pool-without-code`; both when both appear; "the returnee data could
|
|
677
|
+
> not be read" for any other warning type, or none), list nobody, and make no stash claim.
|
|
678
|
+
> Never render an empty section as nobody returning when `ok` is false. Quote only fields
|
|
679
|
+
> present in this payload.
|
|
645
680
|
>
|
|
646
681
|
> **STASH CLAIMS:** a Returning Soon row escalates into the Waiver Recommendations table only when
|
|
647
682
|
> **all four** gates below hold. Any one failing means watchlist only — no claim.
|
|
@@ -651,8 +686,9 @@ Agent tool parameters:
|
|
|
651
686
|
> `metadata.escalation_window` and the citation gate, so do not re-derive the timing yourself:
|
|
652
687
|
> an enrichment date counts here only when it arrived cited, and `escalation_basis` names which
|
|
653
688
|
> source the verdict rests on (`fpl-news` or `ai-search`).
|
|
654
|
-
> 3. **Claimable and position-for-position:** the returnee
|
|
655
|
-
> the
|
|
689
|
+
> 3. **Claimable and position-for-position:** the returnee is one of `returnee_radar_draft`'s
|
|
690
|
+
> `entries` (the script has already joined on `code`; never re-match on `id`), and the player
|
|
691
|
+
> dropped for them plays the returnee's own position.
|
|
656
692
|
> 4. **Beats the incumbent by the configured margin:** take the drop candidate your squad analysis
|
|
657
693
|
> already ranks lowest at that position, run C2.5's `transfer_eval.py --out "{that player}"
|
|
658
694
|
> --in "{returnee}"`, and require `outlook_delta` to exceed the radar's
|
|
@@ -745,7 +781,7 @@ Present a brief summary to the user:
|
|
|
745
781
|
|
|
746
782
|
_Runs after Phase D file write, before Phase E. Not embed-gated._
|
|
747
783
|
|
|
748
|
-
1. Write Phase B's waivers JSON (from orchestrator context, B3 output) to `/tmp/gw-prep-waivers-{N}.json
|
|
784
|
+
1. Write Phase B's waivers JSON (from orchestrator context, B3 output) to `/tmp/gw-prep-waivers-{N}.json` (B10 already wrote it on a draft run; writing it again is harmless).
|
|
749
785
|
2. Write Phase B's draft squad-grid JSON (from orchestrator context, B4 draft output) to `/tmp/gw-prep-squad-grid-{N}.json`.
|
|
750
786
|
- For draft-only runs, this is the `fpl squad grid --format json` output from B4.
|
|
751
787
|
- For `both` format runs, this is the `fpl squad grid --draft --format json` output from B4.
|
|
@@ -759,7 +795,7 @@ _Runs after Phase D file write, before Phase E. Not embed-gated._
|
|
|
759
795
|
```
|
|
760
796
|
|
|
761
797
|
Parse stdout as JSON: `{"ok": bool, "flags": [...], "warnings": [...]}`.
|
|
762
|
-
If the script cannot be found or exits non-zero unexpectedly, emit a warning and proceed to Phase E (fail-open for infrastructure errors, fail-closed only for confirmed rule violations). A non-zero exit whose stdout parses as `{"error": true, ...}` is the script reporting its own startup failure — quote `messages[0]` verbatim in that warning; it names the cause (a wrong interpreter is the common one).
|
|
798
|
+
If the script cannot be found or exits non-zero unexpectedly, emit a warning and proceed to Phase E (fail-open for infrastructure errors, fail-closed only for confirmed rule violations and for a waiver table the validator could not read at all). A non-zero exit whose stdout parses as `{"error": true, ...}` is the script reporting its own startup failure — quote `messages[0]` verbatim in that warning; it names the cause (a wrong interpreter is the common one).
|
|
763
799
|
|
|
764
800
|
4. **Managed block update:** locate the sentinel block in the recommendations file:
|
|
765
801
|
```
|
|
@@ -774,6 +810,7 @@ _Runs after Phase D file write, before Phase E. Not embed-gated._
|
|
|
774
810
|
> - {one line per flag/warning, e.g. "Row 1: cross-position-claim (Drop: João Pedro FWD → Claim: Pedro Porro DEF)"}
|
|
775
811
|
<!-- phase-d1:end -->
|
|
776
812
|
```
|
|
813
|
+
For `waiver-table-unreadable` and `waiver-rows-unread`, the line names the unchecked rows rather than giving a count: `{type} ({rows} line(s) unchecked): {lines joined with "; "}`, then ` and {rows - len(lines)} more` when `rows` exceeds the lines quoted. Quote each line as it comes, without adding code spans: the lines are LLM-written rows that can carry backticks of their own.
|
|
777
814
|
- If both are empty: remove the block entirely (including sentinels) if present.
|
|
778
815
|
|
|
779
816
|
5. **Split posture:**
|
|
@@ -783,8 +820,16 @@ _Runs after Phase D file write, before Phase E. Not embed-gated._
|
|
|
783
820
|
> Edit the file to fix the flagged row(s), then re-run `/gw-prep` to continue.
|
|
784
821
|
|
|
785
822
|
**Stop the pipeline.** Do not proceed to Phase E. Do not mark the run as complete.
|
|
786
|
-
-
|
|
787
|
-
>
|
|
823
|
+
- Any flag with `type == "waiver-table-unreadable"` present → emit a **red error** in chat. The validator read no waiver row at all -- rows with no `Drop`/`Claim` table to hold them, or an empty one beside rows no table took in -- so no claim was checked for a cross-position swap; this fails closed like the swap itself:
|
|
824
|
+
> ❌ Phase D1 blocked: the Waiver Recommendations table in `gw{N}-recommendations.md` could not be read, so no claim in it was checked for a cross-position swap. Unread line(s):
|
|
825
|
+
> - {line} (one per entry in `lines`, as it comes; then "and {rows - len(lines)} more" if `rows` is larger)
|
|
826
|
+
> A waiver table needs `Drop` and `Claim` columns, a `|---|` row straight under its header, no blank line inside it, nothing stacked beneath it, and no code fence or blockquote around it. Fix the table, then re-run `/gw-prep` to continue.
|
|
827
|
+
|
|
828
|
+
**Stop the pipeline.** Do not proceed to Phase E. Do not mark the run as complete.
|
|
829
|
+
- Else, if a warning has `type == "waiver-rows-unread"` → the table was read and its rows checked, but other rows beside it were not. Emit this in-chat warning, followed by the pool-miss one below if any `waiver-not-in-pool` flags are present too:
|
|
830
|
+
> ⚠️ Phase D1: {rows} row(s) under Waiver Recommendations in `gw{N}-recommendations.md` were not read as waiver rows, so they were not checked for cross-position swaps:
|
|
831
|
+
> - {line} (one per entry in `lines`, as it comes; then "and {rows - len(lines)} more" if `rows` is larger)
|
|
832
|
+
> A table needs a `|---|` row straight under its header, no blank line inside it, and nothing stacked beneath it. Fix the table and re-run `/gw-prep` before submitting waivers.
|
|
788
833
|
|
|
789
834
|
Proceed to Phase E.
|
|
790
835
|
- Else (only warnings and/or `waiver-not-in-pool` flags) → emit an in-chat warning:
|
|
@@ -144,7 +144,7 @@ _Tracked returnees still unowned in the league. Omit the section when nothing qu
|
|
|
144
144
|
|--------|------|-----|---------|-----------------|--------|--------|---------|
|
|
145
145
|
| | | | | | | | |
|
|
146
146
|
|
|
147
|
-
Rows are the `fpl returnees` payload
|
|
147
|
+
Rows are the `entries` of `returnee_radar_draft`: the `fpl returnees` payload joined to the waivers `data.pool` on `code` by `claimable_returnees.py`, never by `id` (the radar's `id` is a main-game element id, the pool's a draft one, and they differ for mid-season additions) — a returnee already owned by a rival is not listed at all. Columns as Classic. Verdict = `Stash` when every escalation gate in SKILL.md Phase C2 is met, in which case the player also appears in Waiver Recommendations above with their expected return and its `escalation_basis` in the Rationale; otherwise `Watch`.
|
|
148
148
|
|
|
149
149
|
Every number here comes from the radar payload inlined into the sub-agent prompt, so the Momentum Alerts grounding rule is satisfied by citing it — but nothing beyond those fields may be asserted.
|
|
150
150
|
|
|
@@ -195,7 +195,7 @@ The **Data quality** sub-section carries `data_caveat` from gw-prep Phase B1 --
|
|
|
195
195
|
|
|
196
196
|
Every recommendation in this file that leans on fixture difficulty rests on those ratings, so this belongs with the recommendations rather than only in the terminal scrollback the run scrolled past.
|
|
197
197
|
|
|
198
|
-
**Read back by `fpl review --compare-recs`.** The review grades what was done against this file, through the column headers of Transfer Recommendations (`Out`, `In`) and Waiver Recommendations (`Drop`, `Claim`), the `**Recommended captain:**` line, and on embed-mode runs the Classic Squad's `#### Starting XI` and `#### Bench` tables. Write them exactly as above, with the club code in brackets after each player (`Tzolis (ARS)`) and the initial left on (`B.Fernandes`, not `Fernandes`): a renamed column leaves every move in that table ungraded, and a bare surname cannot be told apart from a same-club namesake. When there is nothing to recommend, say so in words ("Roll the free transfer", "No waiver claims this week") rather than leaving the table's rows blank: a blank table reads the same as one the review could not parse. `tests/test_recommendations_parser.py` parses this template, so change it and `fpl_cli/parsers/recommendations.py` together.
|
|
198
|
+
**Read back by `fpl review --compare-recs`.** The review grades what was done against this file, through the column headers of Transfer Recommendations (`Out`, `In`) and Waiver Recommendations (`Drop`, `Claim`), the `**Recommended captain:**` line, and on embed-mode runs the Classic Squad's `#### Starting XI` and `#### Bench` tables. Write them exactly as above, with the club code in brackets after each player (`Tzolis (ARS)`) and the initial left on (`B.Fernandes`, not `Fernandes`): a renamed column leaves every move in that table ungraded, and a bare surname cannot be told apart from a same-club namesake. Rows that share an Out player are read as ranked alternatives for one sale, so list each candidate on its own row with the same Out cell; an alternative between two different sales ("sell Tzolis, or else Ballard") reads as two separate moves and is graded as both. Waiver rows that share a Drop are read as one conditional chain, in the order listed, so write each fallback claim as its own row with the same Drop cell, below the claim it backs up: the claim that landed is graded as followed, and the claims below it are not counted as misses. When there is nothing to recommend, say so in words ("Roll the free transfer", "No waiver claims this week") rather than leaving the table's rows blank: a blank table reads the same as one the review could not parse. `tests/test_recommendations_parser.py` parses this template, so change it and `fpl_cli/parsers/recommendations.py` together.
|
|
199
199
|
|
|
200
200
|
---
|
|
201
201
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Join the returnee radar to the draft waiver pool on `code`.
|
|
3
|
+
|
|
4
|
+
Reads `fpl returnees --format json` and `fpl waivers --format json` and emits
|
|
5
|
+
the radar narrowed to the tracked returnees a draft manager can actually
|
|
6
|
+
claim: those whose `code` matches a row in the waivers `data.pool`.
|
|
7
|
+
|
|
8
|
+
This is the one step of the Draft stash path that used to be left to the
|
|
9
|
+
sub-agent reading two payloads, and when it did the two had no key in common
|
|
10
|
+
(#418): the radar's `id` is the main game's element id, the pool's is the
|
|
11
|
+
draft's, and a player added mid-season holds a different one in each. Done
|
|
12
|
+
here the join is deterministic and testable, and the sub-agent receives a
|
|
13
|
+
list it cannot mis-read.
|
|
14
|
+
|
|
15
|
+
Emits JSON to stdout:
|
|
16
|
+
{"ok": bool,
|
|
17
|
+
"metadata": {...the radar's, plus "pool_size"...},
|
|
18
|
+
"entries": [...the claimable radar entries, unchanged...],
|
|
19
|
+
"excluded": [{"web_name": ..., "code": ..., "reason": "owned" | "no-code"}],
|
|
20
|
+
"warnings": [{"type": ...}]}
|
|
21
|
+
|
|
22
|
+
`ok` is false only when the join could not run -- no usable `data.pool`, a
|
|
23
|
+
pool whose rows carry no `code` (an older fpl-cli), or a radar with no
|
|
24
|
+
`data.entries` (such as the `{command, error}` envelope a failed `fpl
|
|
25
|
+
returnees` run saves) -- and `entries` is then empty: a join that cannot run
|
|
26
|
+
offers nothing to claim rather than everything, so the stash path fails
|
|
27
|
+
closed. `warnings` says which side failed (`radar-unreadable`, or
|
|
28
|
+
`pool-unreadable` / `pool-without-code`), so an unreadable radar is never
|
|
29
|
+
reported as nobody returning (#432). Exit code is 0 whatever the join finds; the
|
|
30
|
+
startup guard in `_bootstrap` is the one non-zero exit, and reports itself as
|
|
31
|
+
{"error": true, "messages": [...]}.
|
|
32
|
+
|
|
33
|
+
Runs on the interpreter fpl-cli is installed on (activate its venv first,
|
|
34
|
+
or invoke that venv's Python directly).
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
python3 claimable_returnees.py \\
|
|
38
|
+
--returnees-json /tmp/returnees.json \\
|
|
39
|
+
--waivers-json /tmp/waivers.json
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import argparse
|
|
45
|
+
import json
|
|
46
|
+
import sys
|
|
47
|
+
from typing import Any, TypedDict
|
|
48
|
+
|
|
49
|
+
# Imported first and for its side effect: _bootstrap reaches for the fpl_cli
|
|
50
|
+
# package on its own, turning a wrong interpreter into this script's JSON error
|
|
51
|
+
# envelope instead of a ModuleNotFoundError traceback.
|
|
52
|
+
import _bootstrap # noqa: F401 — import guard, see module docstring
|
|
53
|
+
|
|
54
|
+
from fpl_cli.models.player import usable_code
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Excluded(TypedDict):
|
|
58
|
+
web_name: str
|
|
59
|
+
code: int | None
|
|
60
|
+
reason: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class JoinWarning(TypedDict):
|
|
64
|
+
type: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class JoinResult(TypedDict):
|
|
68
|
+
ok: bool
|
|
69
|
+
metadata: dict[str, Any]
|
|
70
|
+
entries: list[dict[str, Any]]
|
|
71
|
+
excluded: list[Excluded]
|
|
72
|
+
warnings: list[JoinWarning]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def pool_codes(waivers: dict[str, Any]) -> tuple[set[int] | None, JoinWarning | None]:
|
|
76
|
+
"""The `code` of every row in the waivers `data.pool`, or None with the reason.
|
|
77
|
+
|
|
78
|
+
None means the join cannot run: there is no pool list to read, or there
|
|
79
|
+
is one and not a row of it carries `code`, which is what `fpl waivers`
|
|
80
|
+
emitted before #418. An empty pool is readable and matches nobody.
|
|
81
|
+
"""
|
|
82
|
+
data = waivers.get("data")
|
|
83
|
+
pool = data.get("pool") if isinstance(data, dict) else None
|
|
84
|
+
if not isinstance(pool, list):
|
|
85
|
+
return None, JoinWarning(type="pool-unreadable")
|
|
86
|
+
rows = [row for row in pool if isinstance(row, dict)]
|
|
87
|
+
codes = {code for row in rows if (code := usable_code(row.get("code"))) is not None}
|
|
88
|
+
if rows and not codes:
|
|
89
|
+
return None, JoinWarning(type="pool-without-code")
|
|
90
|
+
return codes, None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def radar_entries(radar: dict[str, Any]) -> tuple[list[dict[str, Any]], JoinWarning | None]:
|
|
94
|
+
"""The radar's `data.entries`, or an empty list with the reason."""
|
|
95
|
+
data = radar.get("data")
|
|
96
|
+
entries = data.get("entries") if isinstance(data, dict) else None
|
|
97
|
+
if not isinstance(entries, list):
|
|
98
|
+
return [], JoinWarning(type="radar-unreadable")
|
|
99
|
+
return [entry for entry in entries if isinstance(entry, dict)], None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def join(radar: dict[str, Any], waivers: dict[str, Any]) -> JoinResult:
|
|
103
|
+
"""Narrow the radar to the entries whose `code` is in the waiver pool."""
|
|
104
|
+
metadata = radar.get("metadata")
|
|
105
|
+
result: JoinResult = {
|
|
106
|
+
"ok": True,
|
|
107
|
+
"metadata": dict(metadata) if isinstance(metadata, dict) else {},
|
|
108
|
+
"entries": [],
|
|
109
|
+
"excluded": [],
|
|
110
|
+
"warnings": [],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
entries, radar_warning = radar_entries(radar)
|
|
114
|
+
codes, pool_warning = pool_codes(waivers)
|
|
115
|
+
result["warnings"].extend(w for w in (radar_warning, pool_warning) if w is not None)
|
|
116
|
+
if codes is not None:
|
|
117
|
+
result["metadata"]["pool_size"] = len(codes)
|
|
118
|
+
if radar_warning is not None or codes is None:
|
|
119
|
+
# Nothing to claim rather than everything: the stash path fails closed,
|
|
120
|
+
# and an unreadable radar is a failure, not an empty watchlist (#432).
|
|
121
|
+
result["ok"] = False
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
for entry in entries:
|
|
125
|
+
code = usable_code(entry.get("code"))
|
|
126
|
+
web_name = str(entry.get("web_name", ""))
|
|
127
|
+
if code is None:
|
|
128
|
+
result["excluded"].append(Excluded(web_name=web_name, code=None, reason="no-code"))
|
|
129
|
+
elif code in codes:
|
|
130
|
+
result["entries"].append(entry)
|
|
131
|
+
else:
|
|
132
|
+
result["excluded"].append(Excluded(web_name=web_name, code=code, reason="owned"))
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _run(returnees_json: str, waivers_json: str) -> None:
|
|
137
|
+
with open(returnees_json, encoding="utf-8") as f:
|
|
138
|
+
radar = json.load(f)
|
|
139
|
+
with open(waivers_json, encoding="utf-8") as f:
|
|
140
|
+
waivers = json.load(f)
|
|
141
|
+
if not isinstance(radar, dict) or not isinstance(waivers, dict):
|
|
142
|
+
# Neither file is an envelope at all; say so in the same shape rather
|
|
143
|
+
# than raise, since the orchestrator parses stdout either way.
|
|
144
|
+
json.dump(
|
|
145
|
+
JoinResult(ok=False, metadata={}, entries=[], excluded=[], warnings=[JoinWarning(type="envelope-unreadable")]),
|
|
146
|
+
sys.stdout,
|
|
147
|
+
)
|
|
148
|
+
return
|
|
149
|
+
json.dump(join(radar, waivers), sys.stdout)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main() -> None:
|
|
153
|
+
parser = argparse.ArgumentParser(
|
|
154
|
+
description="Narrow the returnee radar to the players still in the draft waiver pool, joined on code.",
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"--returnees-json",
|
|
158
|
+
required=True,
|
|
159
|
+
metavar="PATH",
|
|
160
|
+
dest="returnees_json",
|
|
161
|
+
help="Path to fpl returnees --format json output.",
|
|
162
|
+
)
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"--waivers-json",
|
|
165
|
+
required=True,
|
|
166
|
+
metavar="PATH",
|
|
167
|
+
dest="waivers_json",
|
|
168
|
+
help="Path to fpl waivers --format json output.",
|
|
169
|
+
)
|
|
170
|
+
args = parser.parse_args()
|
|
171
|
+
_run(args.returnees_json, args.waivers_json)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
main()
|