fplkit 2.1.0__tar.gz → 2.2.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.
Files changed (294) hide show
  1. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/TOOLS.md +12 -9
  2. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/gw-prep/SKILL.md +89 -3
  3. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/gw-prep/references/output-template.md +24 -0
  4. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/gw-prep/references/rules.md +5 -2
  5. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +53 -11
  6. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/release/SKILL.md +21 -0
  7. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/squad-builder/SKILL.md +1 -1
  8. fplkit-2.2.0/.github/workflows/ci.yml +64 -0
  9. {fplkit-2.1.0 → fplkit-2.2.0}/AGENTS.md +4 -3
  10. {fplkit-2.1.0 → fplkit-2.2.0}/CHANGELOG.md +40 -0
  11. {fplkit-2.1.0 → fplkit-2.2.0}/CLAUDE.md +4 -3
  12. {fplkit-2.1.0 → fplkit-2.2.0}/CONTRIBUTING.md +12 -2
  13. {fplkit-2.1.0 → fplkit-2.2.0}/PKG-INFO +8 -1
  14. {fplkit-2.1.0 → fplkit-2.2.0}/README.md +7 -0
  15. {fplkit-2.1.0 → fplkit-2.2.0}/docs/architecture.md +34 -14
  16. {fplkit-2.1.0 → fplkit-2.2.0}/docs/command-reference.md +77 -3
  17. {fplkit-2.1.0 → fplkit-2.2.0}/docs/custom-analysis.md +50 -15
  18. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/_version.py +2 -2
  19. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/action/waiver.py +30 -4
  20. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/analysis/bench_order.py +8 -3
  21. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/analysis/captain.py +8 -3
  22. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/analysis/squad_analyzer.py +1 -1
  23. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/analysis/starting_xi.py +26 -3
  24. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/analysis/stats.py +10 -3
  25. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/analysis/transfer_eval.py +15 -4
  26. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/common.py +2 -2
  27. fplkit-2.2.0/fpl_cli/api/contract.py +77 -0
  28. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/core_insights.py +81 -21
  29. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/football_data.py +18 -2
  30. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/understat.py +35 -4
  31. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/vaastav.py +45 -5
  32. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/__init__.py +2 -0
  33. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/allocate.py +2 -2
  34. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/doctor.py +100 -18
  35. fplkit-2.2.0/fpl_cli/cli/doctor_providers.py +710 -0
  36. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/player.py +2 -2
  37. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/ratings.py +118 -9
  38. fplkit-2.2.0/fpl_cli/cli/returnees.py +798 -0
  39. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/sell_prices.py +17 -0
  40. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/stats.py +11 -7
  41. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/config/defaults.yaml +18 -0
  42. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/models/types.py +16 -0
  43. fplkit-2.2.0/fpl_cli/prompts/returnees.py +104 -0
  44. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/scraper/fpl_prices.py +49 -9
  45. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/services/player_prior.py +9 -5
  46. fplkit-2.2.0/fpl_cli/services/returnee_radar.py +1655 -0
  47. fplkit-2.2.0/fpl_cli/services/scoring/__init__.py +252 -0
  48. fplkit-2.2.0/fpl_cli/services/scoring/constants.py +493 -0
  49. fplkit-2.2.0/fpl_cli/services/scoring/data_prep.py +498 -0
  50. fplkit-2.2.0/fpl_cli/services/scoring/display.py +57 -0
  51. fplkit-2.2.0/fpl_cli/services/scoring/evaluation.py +333 -0
  52. fplkit-2.2.0/fpl_cli/services/scoring/ownership.py +241 -0
  53. fplkit-2.2.0/fpl_cli/services/scoring/shrinkage.py +213 -0
  54. fplkit-2.2.0/fpl_cli/services/scoring/signals.py +708 -0
  55. fplkit-2.2.0/fpl_cli/services/scoring/single_gw.py +550 -0
  56. fplkit-2.2.0/fpl_cli/services/scoring/value_quality.py +217 -0
  57. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/services/squad_allocator.py +12 -4
  58. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/services/team_ratings.py +67 -12
  59. fplkit-2.2.0/fpl_cli/services/team_ratings_prior.py +651 -0
  60. fplkit-2.2.0/scripts/calibrate_promoted_prior.py +283 -0
  61. fplkit-2.2.0/scripts/calibrate_quality_ceilings.py +669 -0
  62. {fplkit-2.1.0 → fplkit-2.2.0}/tests/conftest.py +15 -2
  63. fplkit-2.2.0/tests/fixtures/validate_draft_waivers/recs_clean.md +28 -0
  64. fplkit-2.2.0/tests/fixtures/validate_draft_waivers/waivers.json +19 -0
  65. fplkit-2.2.0/tests/fixtures/waiver_ranked_golden.json +1053 -0
  66. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_agents_action.py +267 -17
  67. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_agents_analysis.py +1 -1
  68. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_agents_analysis_squad.py +1 -1
  69. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_api_core_insights.py +69 -0
  70. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_api_football_data.py +14 -0
  71. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_api_understat.py +63 -0
  72. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_api_vaastav.py +84 -0
  73. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_allocate.py +4 -4
  74. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_doctor.py +48 -0
  75. fplkit-2.2.0/tests/test_cli_doctor_providers.py +359 -0
  76. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_player.py +4 -2
  77. fplkit-2.2.0/tests/test_cli_returnees.py +868 -0
  78. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_sell_prices.py +49 -0
  79. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_common_scoring.py +4 -4
  80. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_data_prep_harmonisation.py +17 -9
  81. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_player_prior.py +7 -7
  82. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_preseason.py +185 -2
  83. fplkit-2.2.0/tests/test_returnee_radar.py +1467 -0
  84. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_rolling_pts_per_m.py +1 -1
  85. fplkit-2.1.0/tests/test_player_scoring.py → fplkit-2.2.0/tests/test_scoring.py +630 -93
  86. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_scraper.py +157 -2
  87. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_squad_allocator.py +63 -2
  88. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_starting_xi_agent.py +53 -1
  89. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_stats.py +60 -0
  90. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_team_ratings.py +435 -0
  91. fplkit-2.2.0/tests/test_team_ratings_prior.py +1048 -0
  92. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_transfer_eval_agent.py +1 -1
  93. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_validate_draft_waivers.py +184 -0
  94. fplkit-2.1.0/.github/workflows/ci.yml +0 -40
  95. fplkit-2.1.0/fpl_cli/services/player_scoring.py +0 -2787
  96. fplkit-2.1.0/fpl_cli/services/team_ratings_prior.py +0 -351
  97. fplkit-2.1.0/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -20
  98. fplkit-2.1.0/tests/fixtures/validate_draft_waivers/waivers.json +0 -12
  99. fplkit-2.1.0/tests/test_team_ratings_prior.py +0 -435
  100. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/README.md +0 -0
  101. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/create-pr/SKILL.md +0 -0
  102. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/gw-prep/scripts/_bootstrap.py +0 -0
  103. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
  104. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
  105. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
  106. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
  107. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/preview-ingest/SKILL.md +0 -0
  108. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/release-notes/SKILL.md +0 -0
  109. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/squad-builder/references/output-template.md +0 -0
  110. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/squad-builder/references/rules.md +0 -0
  111. {fplkit-2.1.0 → fplkit-2.2.0}/.agents/skills/update-gw-prep/SKILL.md +0 -0
  112. {fplkit-2.1.0 → fplkit-2.2.0}/.claude/output-styles/fpl-mate.md +0 -0
  113. {fplkit-2.1.0 → fplkit-2.2.0}/.claude/settings.json +0 -0
  114. {fplkit-2.1.0 → fplkit-2.2.0}/.env.example +0 -0
  115. {fplkit-2.1.0 → fplkit-2.2.0}/.github/workflows/pr-title.yml +0 -0
  116. {fplkit-2.1.0 → fplkit-2.2.0}/.github/workflows/release.yml +0 -0
  117. {fplkit-2.1.0 → fplkit-2.2.0}/.gitignore +0 -0
  118. {fplkit-2.1.0 → fplkit-2.2.0}/LICENSE +0 -0
  119. {fplkit-2.1.0 → fplkit-2.2.0}/cliff.toml +0 -0
  120. {fplkit-2.1.0 → fplkit-2.2.0}/config/team_ratings_overrides.yaml +0 -0
  121. {fplkit-2.1.0 → fplkit-2.2.0}/docs/fpl-rules.md +0 -0
  122. {fplkit-2.1.0 → fplkit-2.2.0}/docs/images/fpl-player-demo-v1-0.png +0 -0
  123. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/__init__.py +0 -0
  124. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/__init__.py +0 -0
  125. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/action/__init__.py +0 -0
  126. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/analysis/__init__.py +0 -0
  127. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/base.py +0 -0
  128. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/data/__init__.py +0 -0
  129. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/data/fixture.py +0 -0
  130. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/data/price.py +0 -0
  131. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/data/scout.py +0 -0
  132. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/orchestration/__init__.py +0 -0
  133. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/agents/orchestration/report.py +0 -0
  134. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/__init__.py +0 -0
  135. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/dataset_fetcher.py +0 -0
  136. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/fpl.py +0 -0
  137. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/fpl_draft.py +0 -0
  138. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/historical.py +0 -0
  139. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/historical_types.py +0 -0
  140. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/providers/__init__.py +0 -0
  141. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/providers/_models.py +0 -0
  142. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/providers/anthropic.py +0 -0
  143. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/providers/openai_compat.py +0 -0
  144. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/api/providers/perplexity.py +0 -0
  145. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_banner.py +0 -0
  146. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_context.py +0 -0
  147. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_fines.py +0 -0
  148. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_fines_config.py +0 -0
  149. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_helpers.py +0 -0
  150. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_json.py +0 -0
  151. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_league_recap_data.py +0 -0
  152. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_league_recap_history.py +0 -0
  153. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_league_recap_types.py +0 -0
  154. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_plan_grid.py +0 -0
  155. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_review_analysis.py +0 -0
  156. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_review_classic.py +0 -0
  157. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_review_draft.py +0 -0
  158. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/_review_summarisation.py +0 -0
  159. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/captain.py +0 -0
  160. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/chips.py +0 -0
  161. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/credentials.py +0 -0
  162. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/differentials.py +0 -0
  163. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/fdr.py +0 -0
  164. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/fixtures.py +0 -0
  165. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/history.py +0 -0
  166. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/init.py +0 -0
  167. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/intel.py +0 -0
  168. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/league.py +0 -0
  169. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/league_recap.py +0 -0
  170. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/preview.py +0 -0
  171. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/price_changes.py +0 -0
  172. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/price_history.py +0 -0
  173. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/review.py +0 -0
  174. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/squad.py +0 -0
  175. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/status.py +0 -0
  176. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/targets.py +0 -0
  177. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/transfer_eval.py +0 -0
  178. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/waivers.py +0 -0
  179. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/cli/xg.py +0 -0
  180. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/config/fixture_predictions.yaml +0 -0
  181. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
  182. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/config/team_managers.yaml +0 -0
  183. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/constants.py +0 -0
  184. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/models/__init__.py +0 -0
  185. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/models/chip_plan.py +0 -0
  186. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/models/fixture.py +0 -0
  187. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/models/league_history.py +0 -0
  188. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/models/player.py +0 -0
  189. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/models/team.py +0 -0
  190. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/parsers/__init__.py +0 -0
  191. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/parsers/recommendations.py +0 -0
  192. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/paths.py +0 -0
  193. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/prompts/__init__.py +0 -0
  194. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/prompts/league_recap.py +0 -0
  195. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/prompts/review.py +0 -0
  196. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/prompts/scout.py +0 -0
  197. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/scraper/__init__.py +0 -0
  198. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/season.py +0 -0
  199. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/services/__init__.py +0 -0
  200. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/services/fixture_predictions.py +0 -0
  201. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/services/league_history.py +0 -0
  202. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/services/league_history_counters.py +0 -0
  203. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/services/league_history_notes.py +0 -0
  204. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/services/matchup.py +0 -0
  205. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/services/season_previews.py +0 -0
  206. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/services/team_form.py +0 -0
  207. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/templates/gw_league_recap.md.j2 +0 -0
  208. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/templates/gw_preview.md.j2 +0 -0
  209. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/templates/gw_review.md.j2 +0 -0
  210. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/utils/__init__.py +0 -0
  211. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/utils/files.py +0 -0
  212. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/utils/gameweek.py +0 -0
  213. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/utils/markdown.py +0 -0
  214. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/utils/teams.py +0 -0
  215. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/utils/text.py +0 -0
  216. {fplkit-2.1.0 → fplkit-2.2.0}/fpl_cli/utils/time.py +0 -0
  217. {fplkit-2.1.0 → fplkit-2.2.0}/pyproject.toml +0 -0
  218. {fplkit-2.1.0 → fplkit-2.2.0}/pyrightconfig.json +0 -0
  219. {fplkit-2.1.0 → fplkit-2.2.0}/requirements.lock +0 -0
  220. {fplkit-2.1.0 → fplkit-2.2.0}/tests/__init__.py +0 -0
  221. {fplkit-2.1.0 → fplkit-2.2.0}/tests/fixtures/classic_squad_fixture.md +0 -0
  222. {fplkit-2.1.0 → fplkit-2.2.0}/tests/fixtures/recommendations_fixture.md +0 -0
  223. {fplkit-2.1.0 → fplkit-2.2.0}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
  224. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_agents_base.py +0 -0
  225. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_agents_data.py +0 -0
  226. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_agents_data_price.py +0 -0
  227. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_agents_orchestration.py +0 -0
  228. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_allocate.py +0 -0
  229. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_api_client.py +0 -0
  230. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_api_dataset_fetcher.py +0 -0
  231. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_api_draft.py +0 -0
  232. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_api_historical.py +0 -0
  233. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_bench_order_script.py +0 -0
  234. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_classic_squad_extraction.py +0 -0
  235. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_banner.py +0 -0
  236. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_captain.py +0 -0
  237. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_chips.py +0 -0
  238. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_credentials.py +0 -0
  239. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_custom_analysis.py +0 -0
  240. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_differentials.py +0 -0
  241. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_fdr.py +0 -0
  242. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_fdr_blanks.py +0 -0
  243. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_fixtures.py +0 -0
  244. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_format.py +0 -0
  245. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_format_suppression.py +0 -0
  246. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_history.py +0 -0
  247. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_init.py +0 -0
  248. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_intel.py +0 -0
  249. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_json.py +0 -0
  250. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_league.py +0 -0
  251. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_league_recap.py +0 -0
  252. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_plan_grid.py +0 -0
  253. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_preview.py +0 -0
  254. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_price_changes.py +0 -0
  255. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_price_history.py +0 -0
  256. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_review.py +0 -0
  257. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_settings_warning.py +0 -0
  258. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_squad.py +0 -0
  259. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_stats.py +0 -0
  260. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_status.py +0 -0
  261. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_targets.py +0 -0
  262. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_transfer_eval.py +0 -0
  263. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_waivers.py +0 -0
  264. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_warnings_stderr.py +0 -0
  265. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_cli_xg.py +0 -0
  266. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_common.py +0 -0
  267. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_doc_consistency.py +0 -0
  268. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_fines.py +0 -0
  269. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_fines_config.py +0 -0
  270. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_fixture_predictions.py +0 -0
  271. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_fpl_prices.py +0 -0
  272. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_init.py +0 -0
  273. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_init_fines.py +0 -0
  274. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_league_history_counters.py +0 -0
  275. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_league_history_notes.py +0 -0
  276. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_league_history_store.py +0 -0
  277. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_league_recap.py +0 -0
  278. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_matchup_service.py +0 -0
  279. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_models.py +0 -0
  280. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_paths.py +0 -0
  281. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_player_resolution.py +0 -0
  282. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_providers.py +0 -0
  283. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_recommendations_parser.py +0 -0
  284. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_report_agent.py +0 -0
  285. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_report_paths.py +0 -0
  286. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_review.py +0 -0
  287. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_season.py +0 -0
  288. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_season_awareness.py +0 -0
  289. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_season_previews.py +0 -0
  290. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_sell_prices.py +0 -0
  291. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_utils_gameweek.py +0 -0
  292. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_utils_markdown.py +0 -0
  293. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_utils_text.py +0 -0
  294. {fplkit-2.1.0 → fplkit-2.2.0}/tests/test_utils_time.py +0 -0
@@ -26,7 +26,7 @@ Errors use `emit_json_error()` with `{"command", "error"}` shape.
26
26
  |---------|-------------|------|--------|-------------|---------|
27
27
  | `init` | Set up fpl-cli with your FPL IDs and optional features | No | General | No | direct-api |
28
28
  | `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 |
29
- | `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, and reports which config/data/cache directories are in use. Distinguishes broken (needs a fix today) from stale (self-corrects). Exits non-zero when something is broken | Yes | General | No | direct-api |
29
+ | `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, 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 (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 |
30
30
  | `fixtures` | Show fixtures for a gameweek | Yes | General | No | direct-api |
31
31
  | `player` | Look up a player's stats, xG, ownership and fixture run. 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 |
32
32
  | `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`. 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 |
@@ -36,15 +36,16 @@ Errors use `emit_json_error()` with `{"command", "error"}` shape.
36
36
  | `xg` | Analyse underlying stats: xG, xA, overperformers | Yes | General | No | via-agent |
37
37
  | `price-changes` | Show price changes and transfer activity | No | General | No | via-agent |
38
38
  | `price-history` | Show price trajectory and transfer momentum | Yes | General | No | direct-api |
39
+ | `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`, `quality_bar_available`, `quality_bar_applied` and `enrichment_requested`/`_available`/`_note`/`_count`; `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 |
39
40
  | `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 |
40
41
  | `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 | No | General | No | via-agent |
41
42
  | `league-recap` | Recap a completed gameweek - awards, standings, and banter. Saves to `<output dir>/<season>/gw{N}-league-recap[-draft].md`. 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). 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` | Yes | General | No | via-agent |
42
- | `captain` | Analyse and rank captain options for next gameweek. 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 | Yes | Classic | Yes | via-agent |
43
- | `differentials` | Find differential picks - high potential, low ownership. Inverted consistency bonus (volatile players score higher, phased in GW6-10) | Yes | Classic | Yes | via-agent |
44
- | `targets` | Find transfer targets - high performers across all ownership. Consistency bonus (CV-xGI percentile, phased in GW6-10) | Yes | Classic | Yes | via-agent |
45
- | `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 | Yes | General | Yes | via-agent |
46
- | `allocate` | Select mathematically optimal 15-player squad within budget | Yes | Classic | Yes | direct-api |
47
- | `waivers` | Show waiver recommendations for your draft league | Yes | Draft | Yes | via-agent |
43
+ | `captain` | Analyse and rank captain options for next gameweek. 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 |
44
+ | `differentials` | Find differential picks - high potential, low ownership. Inverted consistency bonus (volatile players score higher, 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 |
45
+ | `targets` | Find transfer targets - high performers across all ownership. Consistency bonus (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 |
46
+ | `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. 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 | General | Yes | via-agent |
47
+ | `allocate` | Select mathematically optimal 15-player squad within budget. 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; they stay in the solver pool | Yes | Classic | Yes | direct-api |
48
+ | `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. 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 | Draft | Yes | via-agent |
48
49
  | `squad` | Analyse your FPL squad health and fixtures | Yes | General | No | via-agent |
49
50
  | `squad grid` | Show squad fixture difficulty grid | Yes | General | No | via-agent |
50
51
  | `squad sell-prices` | Show squad sell prices and financial breakdown | Yes | Classic | No | direct-api |
@@ -54,7 +55,7 @@ Errors use `emit_json_error()` with `{"command", "error"}` shape.
54
55
  | `chips remove` | Remove a planned chip from a gameweek | No | Classic | No | direct-api |
55
56
  | `chips sync` | Sync chip usage from FPL API | No | Classic | No | direct-api |
56
57
  | `ratings` | Display team ratings | No | General | Yes | direct-api |
57
- | `ratings update` | Recalculate ratings from fixture results | No | General | Yes | direct-api |
58
+ | `ratings update` | Recalculate ratings from fixture results (shrunk toward the previous-season prior before GW12; seeds from the prior when no results exist yet) | No | General | Yes | direct-api |
58
59
  | `intel` | Show season preview intel collected per team, with coverage and per-gameweek decay. JSON `metadata` carries `coverage.usable_as` (`full` / `negative_filter_only` / `none`), `section_confidence`, `sections_live`/`sections_expired`, `decay_schedule`, `team_set_warning` and `warnings`. `-g/--gameweek` ages the payload to any gameweek; `--show-decay` prints the expiry schedule | Yes | General | No | direct-api |
59
60
  | `intel schema` | Print the preview file format with every field explained | No | General | No | direct-api |
60
61
  | `intel init` | Scaffold an empty preview file per Premier League team (`--force` overwrites). Stubs never count toward coverage | No | General | No | direct-api |
@@ -79,7 +80,7 @@ Skills write their own outputs (`gw{N}-recommendations.md`, `gw{N}-squad-builder
79
80
 
80
81
  | Skill | Path | Purpose | Compatibility |
81
82
  |-------|------|---------|--------------|
82
- | gw-prep | `skills/gw-prep/` | Gameweek preparation recommendations for classic and draft (embed / rederive / transfer branches; Phase B9 preview intel; Phase E post-write validation) | Full: Claude Code. Partial: Codex, Cursor, Copilot |
83
+ | gw-prep | `skills/gw-prep/` | Gameweek preparation recommendations for classic and draft (embed / rederive / transfer branches; Phase B9 preview intel; Phase B10 injury returnee radar; Phase E post-write validation) | Full: Claude Code. Partial: Codex, Cursor, Copilot |
83
84
  | update-gw-prep | `skills/update-gw-prep/` | Append GW update to existing recommendations | Full: Claude Code, Codex, Cursor, Copilot |
84
85
  | squad-builder | `skills/squad-builder/` | Build optimal 15-player squad (wildcard, free hit, season start; Phase B3 preview intel gate) | Full: Claude Code, Codex, Cursor, Copilot |
85
86
  | preview-ingest | `skills/preview-ingest/` | Convert season preview prose into structured per-team intel files, resolve player codes, verify coverage | Full: Claude Code (parallel per team), Codex, Cursor, Copilot |
@@ -107,7 +108,9 @@ Python classes in `fpl_cli/agents/` that implement `async run(context) -> AgentR
107
108
 
108
109
  **Notes:**
109
110
  - BenchOrderAgent and StartingXIAgent have no CLI command - they are invoked by gw-prep skill wrapper scripts in `.agents/skills/gw-prep/scripts/`
111
+ - Every scoring family holds players known not to be playing out of the GW1-9 shrinkage; bench and lineup order on raw scores, so for those two it changes the displayed score only
110
112
  - TransferEvalAgent is used by both `transfer-eval` CLI command and gw-prep skill
111
113
  - `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.
114
+ - 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`)
112
115
  - `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; always exits 0.
113
116
  - Both scripts 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.
@@ -13,7 +13,7 @@ compatibility:
13
13
  copilot: fallback (sequential execution)
14
14
  ---
15
15
 
16
- <!-- CLI commands composed: status, chips, chips sync, chips timing, fdr, captain, waivers, squad grid, squad sell-prices, price-history, player, stats, intel -->
16
+ <!-- CLI commands composed: status, chips, chips sync, chips timing, fdr, captain, waivers, squad grid, squad sell-prices, price-history, player, stats, intel, returnees -->
17
17
 
18
18
  # Gameweek Preparation
19
19
 
@@ -231,6 +231,31 @@ Read `metadata.coverage.usable_as` and store it as `intel_gate`:
231
231
  With partial coverage the written-up teams carry annotations and the rest carry nothing, so absence
232
232
  of a flag would read as absence of merit. Store the payload as `intel` for Phase C.
233
233
 
234
+ ### B10 -- Injury Returnee Radar
235
+
236
+ ```bash
237
+ fpl returnees --enrich --format json
238
+ ```
239
+
240
+ Flagged players -- injured, suspended, unavailable, doubtful -- whose expected return lands inside
241
+ `metadata.window` gameweeks and who clear the radar's quality bar. Each entry in `data.entries`
242
+ carries the return estimate and where it came from (`expected_return`, `return_gameweek`,
243
+ `return_source`, `escalation_basis`), a quality verdict (`quality.passed`, `quality.meets_stash`)
244
+ and what moved since the previous run (`transition`). `data.departures` says who left the
245
+ watchlist and why.
246
+
247
+ `--enrich` searches the web for fresher return timing on the players FPL's own news field is silent
248
+ or stale about, which is what supplies a date for the injured majority. It needs a Perplexity API
249
+ key; without one the command skips enrichment, records why in `metadata.enrichment_note` and still
250
+ returns the FPL-sourced watchlist -- so this step is never a reason to abort the run.
251
+
252
+ Store the payload as `returnee_radar` and inline it into both Phase C prompts as a labelled
253
+ section. C1 renders it as an informational watchlist and is barred from naming its players in
254
+ transfer recommendations; C2 renders the subset still in the waiver pool and may escalate an entry
255
+ to a stash claim. The Returning Soon section quotes this payload, which is inlined into the prompt
256
+ like every other cited source -- so its numbers are grounded the same way every other cited number
257
+ is.
258
+
234
259
  <!-- ADAPT: Add your own further supplementary data sources here. Examples:
235
260
  - `fpl preview --save --scout` generates a GW preview with fixture analysis and scout insights.
236
261
  Read the saved file and inject its content into Phase C sub-agents as additional context.
@@ -288,6 +313,13 @@ Branch on `squad_builder_result` (set in Phase A3; unset on transfer weeks):
288
313
  > ("Transfer Flow projects him starting"), never assert it. Under `negative_filter_only` use it
289
314
  > only to downgrade. A player with no intel is not a worse player.
290
315
  >
316
+ > - Returning soon: {returnee_radar} (from B10)
317
+ > **Informational only.** Render it as the Returning Soon section of the Classic output template,
318
+ > one row per entry in `data.entries`, quoting only fields present in that payload. The
319
+ > injury/suspension rule in the analysis rules above governs what may be recommended while a
320
+ > player is flagged, and its classic branch is what bars transfer recommendations -- inline
321
+ > late-change swaps included -- from naming a tracked returnee. Do not restate or relax it here.
322
+ >
291
323
  > <!-- ADAPT: Add your own further supplementary data sources here (newsletters, external reports) -->
292
324
  >
293
325
  > **Embedded Classic Squad (from squad-builder — insert this as the `### Classic Squad` section):**
@@ -302,7 +334,7 @@ Branch on `squad_builder_result` (set in Phase A3; unset on transfer weeks):
302
334
  >
303
335
  > For Phase C2.5 (`transfer_eval.py`): on embed mode, the OUT-candidate pool is the 15 players in the Player column of the embedded Starting XI and Bench tables (not the current `fpl squad grid`). Use this to evaluate whether any late-breaking swaps are justified.
304
336
  >
305
- > **Sections to produce:** Chip Timing (from B6), Momentum Alerts (from B5/B8), pFDR Overview (from B1), and the `### Classic Squad` block (embedded verbatim or swap-edited with trailing note). **Suppress:** Captain Pick top-3 table (captain is inside the embedded block), standalone Bench Order section (bench order is inside the embedded block), and Transfer Recommendations section (any swaps are applied inline in the block).
337
+ > **Sections to produce:** Chip Timing (from B6), Momentum Alerts (from B5/B8), Returning Soon (from B10), pFDR Overview (from B1), and the `### Classic Squad` block (embedded verbatim or swap-edited with trailing note). **Suppress:** Captain Pick top-3 table (captain is inside the embedded block), standalone Bench Order section (bench order is inside the embedded block), and Transfer Recommendations section (any swaps are applied inline in the block).
306
338
  >
307
339
  > **User workflow note:** The `### Classic Squad` block is the final 15-player squad the user should enter into FPL. If a trailing `> Late change:` note is present, it explains the swap — the user can revert it in the FPL site before saving if they disagree.
308
340
  >
@@ -359,6 +391,13 @@ Proceed immediately (non-interactive).
359
391
  > ("Transfer Flow projects him starting"), never assert it. Under `negative_filter_only` use it
360
392
  > only to downgrade. A player with no intel is not a worse player.
361
393
  >
394
+ > - Returning soon: {returnee_radar} (from B10)
395
+ > **Informational only.** Render it as the Returning Soon section of the Classic output template,
396
+ > one row per entry in `data.entries`, quoting only fields present in that payload. The
397
+ > injury/suspension rule in the analysis rules above governs what may be recommended while a
398
+ > player is flagged, and its classic branch is what bars transfer recommendations -- inline
399
+ > late-change swaps included -- from naming a tracked returnee. Do not restate or relax it here.
400
+ >
362
401
  > <!-- ADAPT: Add your own further supplementary data sources here (newsletters, external reports) -->
363
402
  >
364
403
  > Apply squad-builder rules from `references/rules.md` for full squad selection. Prepend the fallback banner (Variant A/B/C — see `references/output-template.md` for exact wording) on the line immediately before the Classic section heading in the output file. Do not include `squad_builder_mode` in the frontmatter — the squad was re-derived, not embedded from squad-builder.
@@ -391,7 +430,13 @@ Proceed immediately (non-interactive).
391
430
  > - Squad: {B4 output}
392
431
  > - Price movements: {B5 output}
393
432
  > - Chip timing: {B6 output}
394
- > - Stats: {stats_form}, {stats_transfer_momentum}, {stats_mid_xgi}, {stats_fwd_xgi}, {stats_def_clean_sheets} (from B8)
433
+ > - Stats: {stats_form}, {stats_transfer_momentum}, {stats_mid_xgi}, {stats_fwd_xgi}, {stats_def_clean_sheets} (from B8)>
434
+ > - Returning soon: {returnee_radar} (from B10)
435
+ > **Informational only.** Render it as the Returning Soon section of the Classic output template,
436
+ > one row per entry in `data.entries`, quoting only fields present in that payload. The
437
+ > injury/suspension rule in the analysis rules above governs what may be recommended while a
438
+ > player is flagged, and its classic branch is what bars transfer recommendations -- inline
439
+ > late-change swaps included -- from naming a tracked returnee. Do not restate or relax it here.
395
440
  >
396
441
  > **Bench Boost instructions:**
397
442
  >
@@ -436,6 +481,13 @@ Proceed immediately (non-interactive).
436
481
  > ("Transfer Flow projects him starting"), never assert it. Under `negative_filter_only` use it
437
482
  > only to downgrade. A player with no intel is not a worse player.
438
483
  >
484
+ > - Returning soon: {returnee_radar} (from B10)
485
+ > **Informational only.** Render it as the Returning Soon section of the Classic output template,
486
+ > one row per entry in `data.entries`, quoting only fields present in that payload. The
487
+ > injury/suspension rule in the analysis rules above governs what may be recommended while a
488
+ > player is flagged, and its classic branch is what bars transfer recommendations -- inline
489
+ > late-change swaps included -- from naming a tracked returnee. Do not restate or relax it here.
490
+ >
439
491
  > <!-- ADAPT: Add your own further supplementary data sources here (newsletters, external reports) -->
440
492
  >
441
493
  > Produce the **Classic** section of the output template.
@@ -463,6 +515,8 @@ Proceed immediately (non-interactive).
463
515
  >
464
516
  > **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.
465
517
  >
518
+ > **`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.
519
+ >
466
520
  > - Waivers: {B3 output}
467
521
  > - Squad: {B4 output}
468
522
  > - Stats: {stats_form}, {stats_transfer_momentum}, {stats_mid_xgi}, {stats_fwd_xgi}, {stats_def_clean_sheets} (from B8)
@@ -472,6 +526,38 @@ Proceed immediately (non-interactive).
472
526
  > ("Transfer Flow projects him starting"), never assert it. Under `negative_filter_only` use it
473
527
  > only to downgrade. A player with no intel is not a worse player.
474
528
  >
529
+ > - Returning soon: {returnee_radar} (from B10)
530
+ > Render the Returning Soon section of the Draft output template from this payload, restricted to
531
+ > tracked returnees whose `id` appears in the waivers `data.pool` list. A returnee missing from
532
+ > the pool is already owned by a rival and is not actionable — leave it out rather than listing it
533
+ > as unavailable. Quote only fields present in this payload.
534
+ >
535
+ > **STASH CLAIMS:** a Returning Soon row escalates into the Waiver Recommendations table only when
536
+ > **all four** gates below hold. Any one failing means watchlist only — no claim.
537
+ >
538
+ > 1. **Elite by prior:** `quality.meets_stash` is `true`.
539
+ > 2. **Returning soon enough:** `escalation_eligible` is `true`. The command has already applied
540
+ > `metadata.escalation_window` and the citation gate, so do not re-derive the timing yourself:
541
+ > an enrichment date counts here only when it arrived cited, and `escalation_basis` names which
542
+ > source the verdict rests on (`fpl-news` or `ai-search`).
543
+ > 3. **Claimable and position-for-position:** the returnee's `id` is in the waivers `data.pool`, and
544
+ > the player dropped for them plays the returnee's own position.
545
+ > 4. **Beats the incumbent by the configured margin:** take the drop candidate your squad analysis
546
+ > already ranks lowest at that position, run C2.5's `transfer_eval.py --out "{that player}"
547
+ > --in "{returnee}"`, and require `outlook_delta` to exceed the radar's
548
+ > `metadata.stash_upgrade_margin`, which already carries the effective configured value; both
549
+ > are quality points on the same 0-100 scale. If the script errors, or the margin is missing
550
+ > from the metadata, or the delta does not clear it, do not escalate. A drop-priority ordering
551
+ > always yields a lowest-ranked player and so can never answer "no" — the margin is what makes
552
+ > this gate refusable.
553
+ >
554
+ > Write every stash claim as a stash, never as a straight upgrade: it spends a roster slot until the
555
+ > player is fit again, and it is bought to lock the asset before a rival can claim them. Each stash
556
+ > row's Rationale must carry the expected return (`expected_return`, or `return_gameweek` where the
557
+ > date is unknown) and name the provenance of that date from `escalation_basis`. The
558
+ > injury/suspension rule in the analysis rules above is the single authority on what may be
559
+ > recommended while a player is flagged.
560
+ >
475
561
  > <!-- ADAPT: Add your own further supplementary data sources here (newsletters, external reports) -->
476
562
  >
477
563
  > Additionally enforce: every waiver swap must be position-for-position (MID out → MID in, DEF out → DEF in, etc.). Cross-position swaps are illegal under FPL Draft rules.
@@ -76,6 +76,18 @@ For each transfer, include:
76
76
  - Form summary
77
77
  - Price trend
78
78
 
79
+ ### Returning Soon
80
+
81
+ _Informational only. Omit the section when the radar payload has no entries._
82
+
83
+ | Player | Team | Pos | Quality | Expected Return | Chance | Change |
84
+ |--------|------|-----|---------|-----------------|--------|--------|
85
+ | | | | | | | |
86
+
87
+ One row per entry in the `fpl returnees` payload (`data.entries`). Quality = `quality.basis` plus whether it clears the stash bar (`quality.meets_stash`). Expected Return = `expected_return`, or `return_gameweek` where only a gameweek is known, or `Unknown`. Chance = `chance_of_playing`. Change = `transition` since the previous run, blank when nothing moved.
88
+
89
+ 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. No transfer recommendation above may name a tracked returnee (see the injury/suspension rule in `references/rules.md`).
90
+
79
91
  ### Classic Squad
80
92
 
81
93
  _Embed-mode only: the orchestrator replaces this placeholder at runtime with the `{embedded_classic_squad_block}` extracted from `gw{N}-squad-builder.md`. Do not populate this section manually — it is produced by the C1 sub-agent._
@@ -124,6 +136,18 @@ Cross-referenced against current squad holdings.
124
136
 
125
137
  Outlook = multi-GW quality delta (target score). This GW = lineup impact delta. Both from `transfer_eval.py`.
126
138
 
139
+ ### Returning Soon
140
+
141
+ _Tracked returnees still unowned in the league. Omit the section when nothing qualifies._
142
+
143
+ | Player | Team | Pos | Quality | Expected Return | Chance | Change | Verdict |
144
+ |--------|------|-----|---------|-----------------|--------|--------|---------|
145
+ | | | | | | | | |
146
+
147
+ Rows are the `fpl returnees` payload (`data.entries`) intersected with the waivers `data.pool` by `id` — 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
+
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
+
127
151
  ### Starting XI
128
152
 
129
153
  Recommended lineup with formation (via lineup engine).
@@ -45,7 +45,7 @@ When `mode = "squad-builder"`:
45
45
  4. **Fixture run** -- as with classic, weight the next 3 gameweeks of fixtures rather than just the immediate one.
46
46
  5. **Maximum 5 waiver suggestions** -- for each: player to drop, player to claim, positional context, and fixture rationale.
47
47
  6. **Position-for-position only** -- FPL Draft squads are locked at 2 GK / 5 DEF / 5 MID / 3 FWD. Every waiver swap must be position-for-position (e.g. MID out → MID in). Cross-position swaps are structurally illegal and will be rejected by Phase D1.
48
- 7. **Waiver pool discipline** -- only recommend players returned by `fpl waivers --format json`. Players appearing in other data sources (e.g. `fpl stats`) are not necessarily available; never recommend a claim not in the waiver pool.
48
+ 7. **Waiver pool discipline** -- only recommend players returned by `fpl waivers --format json`. Players appearing in other data sources (e.g. `fpl stats`) are not necessarily available; never recommend a claim not in the waiver pool. Within that output, `data.pool` establishes availability only -- it is the full unowned roster, unranked and untruncated. Ranked claims come from `data.top_targets` and `data.targets_by_position`; the one claim that may rest on `data.pool` membership alone is a stash claim for a tracked returnee, whom the waiver scoring suppresses by design.
49
49
 
50
50
  ## Transfer/Waiver Evaluation Script
51
51
 
@@ -63,7 +63,10 @@ For all formats:
63
63
  1. **Recent form** -- last 4 GW average points, minutes played (flag rotation risks below 60 mins average).
64
64
  2. **Expected stats** -- where available, prefer xG/xA-based analysis over raw goals/assists. Flag players significantly over- or under-performing their expected stats.
65
65
  3. **Set-piece involvement** -- note players on corners, free kicks, and penalties as these provide floor-raising opportunities.
66
- 4. **Injury/suspension flags** -- check player availability. Never recommend a player flagged as injured or suspended without explicitly noting the risk.
66
+ 4. **Injury/suspension flags** -- check player availability. Never recommend a player flagged as injured or suspended without explicitly noting the risk. A **tracked returnee** -- any entry in `data.entries` of the `fpl returnees` payload, excluding those whose `status` is `"d"` with a non-zero `chance_of_playing` -- is held to a tighter rule than that:
67
+ - **Draft:** a tracked returnee may be recommended only as an explicit stash claim -- a roster slot spent until the player is fit, to lock the asset before a rival claims them. Never present one as a straight upgrade for this gameweek. Every stash claim must carry the expected return (`expected_return`, or `return_gameweek` where the date is unknown) and name the provenance of that date from `escalation_basis`. SKILL.md Phase C2 holds the gates a watchlist row must clear before it becomes a claim.
68
+ - **Classic:** the returnee watchlist is informational. Transfer recommendations -- including any inline late-change swap on an embedded squad -- must not name a tracked returnee. Classic has no scarcity race: a returnee can be bought the week they are actually back.
69
+ - **Unchanged for everyone else:** a doubtful (`d`) player with a non-zero chance of playing is not a tracked returnee. They may still be recommended with the risk explicitly noted, exactly as before.
67
70
 
68
71
  ## Momentum Alerts
69
72
 
@@ -25,7 +25,7 @@ import json
25
25
  import re
26
26
  import sys
27
27
  import unicodedata
28
- from typing import TypedDict
28
+ from typing import NotRequired, TypedDict, cast
29
29
 
30
30
  from fpl_cli.utils.markdown import HeadingMatcher, find_section, leaf_body
31
31
 
@@ -58,6 +58,7 @@ class PoolEntry(TypedDict):
58
58
  player_name: str
59
59
  position: str
60
60
  team_short: str
61
+ id: NotRequired[int]
61
62
 
62
63
 
63
64
  class SquadEntry(TypedDict):
@@ -125,24 +126,65 @@ def _shape_check_squad(data: dict) -> bool:
125
126
  return False
126
127
 
127
128
 
129
+ def _entries_shape_ok(entries: object) -> bool:
130
+ """Return True if entries is a list of name/position records (empty is fine)."""
131
+ if not isinstance(entries, list):
132
+ return False
133
+ if not entries:
134
+ return True
135
+ sample = entries[0]
136
+ return isinstance(sample, dict) and "player_name" in sample and "position" in sample
137
+
138
+
128
139
  def _shape_check_waivers(data: dict) -> bool:
129
- """Return True if waivers JSON has the expected shape."""
140
+ """Return True if waivers JSON has the expected shape.
141
+
142
+ `pool` is the full unowned roster and is what membership is checked against;
143
+ `top_targets` is the ranked subset older waivers JSON carried on its own.
144
+ Either satisfies the guard, so a cached file still validates.
145
+
146
+ A `pool` key that is present but unusable deliberately fails the file rather
147
+ than falling through to `top_targets`. The two are not interchangeable: a
148
+ flagged player is kept out of `top_targets` by the availability factor no
149
+ matter how strong the case for claiming them, so checking membership
150
+ against the ranked subset reports every legitimate injury claim as a miss.
151
+ Failing here warns once and suppresses the pool check for the run, which
152
+ beats a page of false positives on exactly the claims this validates.
153
+ """
130
154
  try:
131
- targets = data["data"]["top_targets"]
132
- if not isinstance(targets, list):
133
- return False
134
- if not targets:
135
- return True
136
- sample = targets[0]
137
- return "player_name" in sample and "position" in sample
155
+ payload = data["data"]
156
+ if "pool" in payload:
157
+ return _entries_shape_ok(payload["pool"])
158
+ return _entries_shape_ok(payload["top_targets"])
138
159
  except (KeyError, TypeError):
139
160
  return False
140
161
 
141
162
 
142
163
  def build_pool(waivers_data: dict) -> dict[tuple[str, str], PoolEntry]:
143
- """Build {(position, normalised_name) -> entry} from waivers JSON."""
164
+ """Build {(position, normalised_name) -> entry} from waivers JSON.
165
+
166
+ Reads the full unowned `pool` roster, so a claim for a flagged player is
167
+ resolvable: the availability factor in the waiver score keeps such a player
168
+ out of `top_targets` no matter how strong the case for claiming them. Falls
169
+ back to `top_targets` when `pool` is missing or empty -- an older cached
170
+ file then validates as it always did rather than reporting every claim as a
171
+ miss.
172
+ """
173
+ payload = waivers_data.get("data", {})
174
+ entries = payload.get("pool")
175
+ if not isinstance(entries, list) or not entries:
176
+ entries = payload.get("top_targets")
177
+ if not isinstance(entries, list):
178
+ # The shape check clears a file on either key, so the fallback can still
179
+ # land on a malformed value. Reporting no pool costs a missed check;
180
+ # raising here would exit non-zero and break the orchestrator contract.
181
+ entries = []
182
+
144
183
  pool: dict[tuple[str, str], PoolEntry] = {}
145
- for entry in waivers_data.get("data", {}).get("top_targets", []):
184
+ for raw in entries:
185
+ if not isinstance(raw, dict):
186
+ continue
187
+ entry = cast(PoolEntry, raw)
146
188
  pos = entry.get("position", "")
147
189
  key = (pos, normalise(entry.get("player_name", "")))
148
190
  pool[key] = entry
@@ -144,6 +144,27 @@ Actions run.
144
144
  repo), update the secret, re-run the job.
145
145
  - **PyPI publish fails with OIDC errors**: check the `pypi` environment on
146
146
  the repo and the trusted-publisher config on the fplkit PyPI project.
147
+ - **PyPI publish fails on distribution metadata**
148
+ (`InvalidDistribution: Invalid distribution metadata: '2.5' is not a
149
+ valid metadata version`): toolchain drift, not a bad tag. `pyproject.toml`
150
+ requires `hatchling>=1.21` with no upper bound, so the build emits
151
+ whatever `Metadata-Version` a current hatchling produces, while
152
+ `pypa/gh-action-pypi-publish` is pinned to a SHA whose bundled twine and
153
+ packaging predate it. Bump the action pin rather than capping hatchling —
154
+ PyPI accepts the newer metadata, so the stale half is the action (v1.13.0
155
+ ships twine 6.1.0 / packaging 25.0 and rejects 2.5; v1.14.2 ships twine
156
+ 7.0.0 / packaging 26.2 and accepts it). Prove it either way by running
157
+ `twine check` on the built wheel under the pins in each action tag's
158
+ `requirements/runtime.txt`. This fails at twine's client-side check
159
+ *before* any upload, so PyPI never receives the version.
160
+ - **Fixing `release.yml` itself needs a retag, not a re-run**: a
161
+ `release`-triggered run uses the workflow file from the *tagged commit*,
162
+ so re-running a failed job replays the same broken workflow. Land the fix
163
+ on main, then `gh release delete vX.Y.Z --cleanup-tag` and re-create the
164
+ release against main so the tag moves onto the fix. Only safe while the
165
+ version is still free on PyPI — check `pip index versions fplkit` first.
166
+ Once PyPI has accepted an upload that number is gone permanently, and the
167
+ fix has to ship as the next version instead.
147
168
  - **Version source drift**: never reintroduce a hardcoded `version =
148
169
  "X.Y.Z"` in pyproject.toml — `dynamic = ["version"]` + hatch-vcs is the
149
170
  single source of truth. And `fallback-version` belongs under
@@ -309,7 +309,7 @@ Include all of this in the prompt field, populated with position-specific data:
309
309
  Run `fpl player "{name}" -f -H` **in parallel** for all candidates in your list.
310
310
  For season-start modes with no current-season data, also use `fpl history` data passed in context.
311
311
 
312
- 7. **Scoring:** Score each candidate against the mode-specific criteria from rules. Use `quality_score` and `quality_per_m` when available (null for players without Understat data — don't penalise). **Both fields are elite-within-position: never rank or compare candidates across positions by `quality_score` or `quality_per_m`.** A DEF showing 85 and a MID showing 55 is not "the DEF is better" — the ceilings differ by design. Only compare within the position you are currently filling.
312
+ 7. **Scoring:** Score each candidate against the mode-specific criteria from rules. Use `quality_score` and `quality_per_m` when available (null for players without Understat data — don't penalise). **Both fields are elite-within-position: never rank or compare candidates across positions by `quality_score` or `quality_per_m`.** A GK showing 90 is "top of the GK pool", not "better than a MID showing 85" — every position is normalised against its own calibrated ceiling, MID and FWD included. Within a position the numbers are trustworthy: elite players in every position read 80+, so a MID at 55 genuinely is mid-tier among MIDs. Only compare within the position you are currently filling.
313
313
 
314
314
  8. **Return format:** Return a structured ranked list. Per candidate:
315
315
  ```
@@ -0,0 +1,64 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ schedule:
9
+ # Date-derived logic (the July season cutover) can break main with no
10
+ # commit behind it, which push/pull_request triggers never surface.
11
+ - cron: "0 6 * * 1"
12
+ workflow_dispatch:
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ check:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: "3.11"
26
+
27
+ - name: Install dependencies
28
+ run: pip install -e ".[dev]"
29
+
30
+ - name: Lint
31
+ run: ruff check fpl_cli/ scripts/
32
+
33
+ - name: Type check
34
+ run: pyright fpl_cli/ scripts/
35
+
36
+ - name: Test
37
+ run: pytest
38
+
39
+ - name: Build package
40
+ run: pip install hatch && hatch build
41
+
42
+ provider-probe:
43
+ # The hermetic test suite pins our assumption of each provider's schema,
44
+ # not the schema -- upstream can drift with no commit behind it, which is
45
+ # exactly what the Monday cron exists for. This job is the one place CI
46
+ # talks to the live providers: it fails only on "broken" (shape drift),
47
+ # never on transient unreachability, which the probe reports as unchecked.
48
+ if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+
53
+ - uses: actions/setup-python@v5
54
+ with:
55
+ python-version: "3.11"
56
+
57
+ - name: Install package
58
+ run: pip install .
59
+
60
+ - name: Probe providers
61
+ env:
62
+ # Optional: without it the football-data checks report skipped.
63
+ FOOTBALL_DATA_API_KEY: ${{ secrets.FOOTBALL_DATA_API_KEY }}
64
+ run: fpl doctor --providers --format json
@@ -4,13 +4,13 @@ fpl-cli - CLI tool for Fantasy Premier League analysis (classic + draft). Distri
4
4
  ## Setup & Dev
5
5
  ```bash
6
6
  source .venv/bin/activate && pip install -e ".[dev]" # local only — web sessions install globally via setup script
7
- ruff check fpl_cli/ # Lint
8
- pyright fpl_cli/ # Type check
7
+ ruff check fpl_cli/ scripts/ # Lint
8
+ pyright fpl_cli/ scripts/ # Type check
9
9
  python3 -m pytest tests/ # Tests
10
10
  ```
11
11
  Run tests as `python3 -m pytest`, never bare `pytest`: in web sessions the `pytest` on PATH is a uv tool shim with its own interpreter and none of the project's dependencies, so it fails at `import pydantic`. If pytest then rejects `--disable-socket` / `--allow-unix-socket`, the dev extra is incomplete — `pip install pytest-socket`.
12
12
 
13
- Entry point: `fpl_cli/cli/__init__.py:main` (Click). Config: `fpl_cli/config/defaults.yaml`, shipped inside the package and resolved as `SHIPPED_CONFIG_DIR` (`paths.py`), + `settings.yaml` in the user config dir (overrides, deep-merged via `platformdirs`; `~/Library/Application Support/fpl-cli/` on macOS). Repo-root `config/` holds only examples and `team_ratings_overrides.yaml` — nothing there is loaded as defaults. All three writable dirs have env overrides: `FPL_CLI_CONFIG_DIR` (settings, managers, overrides; an optional `fixture_predictions.yaml` here overrides the shipped copy; an optional `previews/` dir holds season preview intel read by `fpl intel`), `FPL_CLI_DATA_DIR` (generated data: team ratings, priors, chip plan, sell prices, the append-only `league_history/` ledger and its rebuildable `league_history_counters/` projection — the ledger partitions by season instead of being discarded at rollover, the one store that outlives a season), `FPL_CLI_CACHE_DIR` (disposable). Overrides must be absolute — a relative one resolves against the cwd, so it is rejected with `UserDirError` rather than silently giving a different dir per invocation. Resolve them by calling `user_config_dir()` / `user_data_dir()` / `user_cache_dir()` at point of use — binding one to a module-level constant freezes the override at import time (before `.env` loads); `tests/test_paths.py` enforces this. Ephemeral environments (Claude Code on the web) must set config + data to a persistent workspace or generated data dies with the container; cache can stay local.
13
+ Entry point: `fpl_cli/cli/__init__.py:main` (Click). Config: `fpl_cli/config/defaults.yaml`, shipped inside the package and resolved as `SHIPPED_CONFIG_DIR` (`paths.py`), + `settings.yaml` in the user config dir (overrides, deep-merged via `platformdirs`; `~/Library/Application Support/fpl-cli/` on macOS). Repo-root `config/` holds only examples and `team_ratings_overrides.yaml` — nothing there is loaded as defaults. All three writable dirs have env overrides: `FPL_CLI_CONFIG_DIR` (settings, managers, overrides; an optional `fixture_predictions.yaml` here overrides the shipped copy; an optional `previews/` dir holds season preview intel read by `fpl intel`), `FPL_CLI_DATA_DIR` (generated data: team ratings, priors, chip plan, sell prices, the returnee radar's `returnee_snapshot.json` week-over-week baseline (season-stamped, discarded on mismatch like `player_prior.yaml`), the append-only `league_history/` ledger and its rebuildable `league_history_counters/` projection — the ledger partitions by season instead of being discarded at rollover, the one store that outlives a season), `FPL_CLI_CACHE_DIR` (disposable). Overrides must be absolute — a relative one resolves against the cwd, so it is rejected with `UserDirError` rather than silently giving a different dir per invocation. Resolve them by calling `user_config_dir()` / `user_data_dir()` / `user_cache_dir()` at point of use — binding one to a module-level constant freezes the override at import time (before `.env` loads); `tests/test_paths.py` enforces this. Ephemeral environments (Claude Code on the web) must set config + data to a persistent workspace or generated data dies with the container; cache can stay local.
14
14
 
15
15
  ## Architecture
16
16
  Agents inherit `fpl_cli/agents/base.py:Agent`, implement `async run(context: dict | None) -> AgentResult`. Organised in `agents/{data,analysis,action,orchestration}/`. AgentResult statuses: SUCCESS, PARTIAL, FAILED, PENDING_APPROVAL.
@@ -76,6 +76,7 @@ For a complete inventory of CLI commands, analysis agents, and skills with JSON
76
76
  - Draft has no captains, no budget, no transfers and no chips — acquisition is via waivers and free agents
77
77
 
78
78
  ## Rules
79
+ - This repo is public: never put real manager or league names, entry/league IDs, or FPL account details into commits, PR titles or bodies, issues, or CHANGELOG.md. Use placeholders when quoting generated reports from the configured `reports.output_dir`. Footballer and club names are fine — they are public data. Before pushing, check the branch for a pre-existing leak and flag it rather than pushing over it.
79
80
  - Verify before asserting: don't state that a command, file, function, or data point exists without checking first (read the file, run the command, grep for the name)
80
81
  - Find-and-replace: review each replacement in context - don't blindly replace substrings in unrelated identifiers
81
82
  - Removing/replacing X: new implementation must have zero dependencies on X
@@ -1,6 +1,46 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
+ ## [2.1.0] - 2026-08-23
5
+
6
+ ### Bug Fixes
7
+
8
+ - survive a keyring backend that panics below Python
9
+ - validate pre-season teams and guard pFDR against unrated seasons
10
+ - consistent non-zero exit codes on command failure, friendly pre-season squad 404 (#47)
11
+ - drop stale committed team_ratings.yaml seed
12
+ - rank promoted teams against the PL, not the Championship
13
+ - config dirs must be absolute, and gated commands name their gate (#46)
14
+ - layer team_managers.yaml over the shipped copy per club
15
+ - make team ratings and manager config season-aware (#56)
16
+ - stop gw-prep squad validation failing when a heading names the formation
17
+ - keep the new-signing flag alive until real minutes exist, not until the window shuts
18
+ - stop gw-prep validators mislocating sections on drifted headings
19
+ - stop review inventing fines, positions and rolled transfers at gameweek 1
20
+ - keep the status classic section alive pre-season and stop last-place fines misreading a partial league table
21
+ - report exact classic league size and rank in status instead of page-one figures
22
+ - keep league recap accurate for large leagues and gameweek 1
23
+ - cap the managers named in a tied league recap bench-haul award
24
+ - pin PuLP below 4.0 so squad allocation keeps working (#80)
25
+ - stop a new season's reports overwriting the previous season's (#90)
26
+
27
+ ### Features
28
+
29
+ - add FPL_CLI_DATA_DIR override so the data dir survives ephemeral environments
30
+ - user config dir copy of fixture_predictions.yaml overrides shipped copy
31
+ - add `fpl intel` for season preview intel with per-gameweek decay
32
+ - add preview-ingest skill for turning preview prose into intel files
33
+ - wire season preview intel into squad-builder
34
+ - wire season preview intel into gw-prep and update-gw-prep
35
+ - point-in-time headline numbers and league positions for replays (#75)
36
+ - record every gameweek to a durable league history store (#76)
37
+ - surface streaks and season phase in every output (#77)
38
+ - add `fpl doctor` to surface stale config and dead IDs (#98)
39
+
40
+ ### Performance
41
+
42
+ - stop fpl player and fpl stats fetching data they discard (#96)
43
+
4
44
  ## [2.0.0] - 2026-05-25
5
45
 
6
46
  ### Bug Fixes
@@ -4,13 +4,13 @@ fpl-cli - CLI tool for Fantasy Premier League analysis (classic + draft). Distri
4
4
  ## Setup & Dev
5
5
  ```bash
6
6
  source .venv/bin/activate && pip install -e ".[dev]" # local only — web sessions install globally via setup script
7
- ruff check fpl_cli/ # Lint
8
- pyright fpl_cli/ # Type check
7
+ ruff check fpl_cli/ scripts/ # Lint
8
+ pyright fpl_cli/ scripts/ # Type check
9
9
  python3 -m pytest tests/ # Tests
10
10
  ```
11
11
  Run tests as `python3 -m pytest`, never bare `pytest`: in web sessions the `pytest` on PATH is a uv tool shim with its own interpreter and none of the project's dependencies, so it fails at `import pydantic`. If pytest then rejects `--disable-socket` / `--allow-unix-socket`, the dev extra is incomplete — `pip install pytest-socket`.
12
12
 
13
- Entry point: `fpl_cli/cli/__init__.py:main` (Click). Config: `fpl_cli/config/defaults.yaml`, shipped inside the package and resolved as `SHIPPED_CONFIG_DIR` (`paths.py`), + `settings.yaml` in the user config dir (overrides, deep-merged via `platformdirs`; `~/Library/Application Support/fpl-cli/` on macOS). Repo-root `config/` holds only examples and `team_ratings_overrides.yaml` — nothing there is loaded as defaults. All three writable dirs have env overrides: `FPL_CLI_CONFIG_DIR` (settings, managers, overrides; an optional `fixture_predictions.yaml` here overrides the shipped copy; an optional `previews/` dir holds season preview intel read by `fpl intel`), `FPL_CLI_DATA_DIR` (generated data: team ratings, priors, chip plan, sell prices, the append-only `league_history/` ledger and its rebuildable `league_history_counters/` projection — the ledger partitions by season instead of being discarded at rollover, the one store that outlives a season), `FPL_CLI_CACHE_DIR` (disposable). Overrides must be absolute — a relative one resolves against the cwd, so it is rejected with `UserDirError` rather than silently giving a different dir per invocation. Resolve them by calling `user_config_dir()` / `user_data_dir()` / `user_cache_dir()` at point of use — binding one to a module-level constant freezes the override at import time (before `.env` loads); `tests/test_paths.py` enforces this. Ephemeral environments (Claude Code on the web) must set config + data to a persistent workspace or generated data dies with the container; cache can stay local.
13
+ Entry point: `fpl_cli/cli/__init__.py:main` (Click). Config: `fpl_cli/config/defaults.yaml`, shipped inside the package and resolved as `SHIPPED_CONFIG_DIR` (`paths.py`), + `settings.yaml` in the user config dir (overrides, deep-merged via `platformdirs`; `~/Library/Application Support/fpl-cli/` on macOS). Repo-root `config/` holds only examples and `team_ratings_overrides.yaml` — nothing there is loaded as defaults. All three writable dirs have env overrides: `FPL_CLI_CONFIG_DIR` (settings, managers, overrides; an optional `fixture_predictions.yaml` here overrides the shipped copy; an optional `previews/` dir holds season preview intel read by `fpl intel`), `FPL_CLI_DATA_DIR` (generated data: team ratings, priors, chip plan, sell prices, the returnee radar's `returnee_snapshot.json` week-over-week baseline (season-stamped, discarded on mismatch like `player_prior.yaml`), the append-only `league_history/` ledger and its rebuildable `league_history_counters/` projection — the ledger partitions by season instead of being discarded at rollover, the one store that outlives a season), `FPL_CLI_CACHE_DIR` (disposable). Overrides must be absolute — a relative one resolves against the cwd, so it is rejected with `UserDirError` rather than silently giving a different dir per invocation. Resolve them by calling `user_config_dir()` / `user_data_dir()` / `user_cache_dir()` at point of use — binding one to a module-level constant freezes the override at import time (before `.env` loads); `tests/test_paths.py` enforces this. Ephemeral environments (Claude Code on the web) must set config + data to a persistent workspace or generated data dies with the container; cache can stay local.
14
14
 
15
15
  ## Architecture
16
16
  Agents inherit `fpl_cli/agents/base.py:Agent`, implement `async run(context: dict | None) -> AgentResult`. Organised in `agents/{data,analysis,action,orchestration}/`. AgentResult statuses: SUCCESS, PARTIAL, FAILED, PENDING_APPROVAL.
@@ -76,6 +76,7 @@ For a complete inventory of CLI commands, analysis agents, and skills with JSON
76
76
  - Draft has no captains, no budget, no transfers and no chips — acquisition is via waivers and free agents
77
77
 
78
78
  ## Rules
79
+ - This repo is public: never put real manager or league names, entry/league IDs, or FPL account details into commits, PR titles or bodies, issues, or CHANGELOG.md. Use placeholders when quoting generated reports from the configured `reports.output_dir`. Footballer and club names are fine — they are public data. Before pushing, check the branch for a pre-existing leak and flag it rather than pushing over it.
79
80
  - Verify before asserting: don't state that a command, file, function, or data point exists without checking first (read the file, run the command, grep for the name)
80
81
  - Find-and-replace: review each replacement in context - don't blindly replace substrings in unrelated identifiers
81
82
  - Removing/replacing X: new implementation must have zero dependencies on X