fplkit 2.4.0__tar.gz → 2.4.1__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 (314) hide show
  1. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/TOOLS.md +21 -14
  2. fplkit-2.4.1/.agents/skills/build-test-plan/SKILL.md +332 -0
  3. fplkit-2.4.1/.agents/skills/build-test-plan/references/plan-template.md +202 -0
  4. fplkit-2.4.1/.agents/skills/build-test-plan/references/workspace-env.md +101 -0
  5. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/gw-prep/SKILL.md +13 -9
  6. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/gw-prep/references/entity-normalisation.md +13 -3
  7. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/gw-prep/references/output-template.md +1 -1
  8. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/gw-prep/scripts/_bootstrap.py +21 -3
  9. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/gw-prep/scripts/bench_order.py +18 -17
  10. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/gw-prep/scripts/starting_xi.py +17 -16
  11. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/gw-prep/scripts/transfer_eval.py +19 -21
  12. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/squad-builder/SKILL.md +6 -3
  13. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/update-gw-prep/SKILL.md +4 -1
  14. {fplkit-2.4.0 → fplkit-2.4.1}/AGENTS.md +5 -0
  15. {fplkit-2.4.0 → fplkit-2.4.1}/CHANGELOG.md +27 -0
  16. {fplkit-2.4.0 → fplkit-2.4.1}/CLAUDE.md +5 -0
  17. {fplkit-2.4.0 → fplkit-2.4.1}/PKG-INFO +9 -1
  18. {fplkit-2.4.0 → fplkit-2.4.1}/README.md +8 -0
  19. {fplkit-2.4.0 → fplkit-2.4.1}/docs/architecture.md +21 -13
  20. {fplkit-2.4.0 → fplkit-2.4.1}/docs/command-reference.md +116 -17
  21. {fplkit-2.4.0 → fplkit-2.4.1}/docs/custom-analysis.md +7 -1
  22. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/_version.py +2 -2
  23. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/analysis/captain.py +97 -34
  24. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/analysis/stats.py +161 -14
  25. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/base.py +11 -2
  26. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/common.py +89 -0
  27. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/orchestration/report.py +24 -8
  28. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/fpl.py +16 -0
  29. fplkit-2.4.1/fpl_cli/api/understat.py +790 -0
  30. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/__init__.py +7 -0
  31. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_context.py +127 -18
  32. fplkit-2.4.1/fpl_cli/cli/_fines_config.py +146 -0
  33. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_helpers.py +56 -2
  34. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_json.py +55 -11
  35. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_league_recap_data.py +15 -5
  36. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_league_recap_history.py +60 -7
  37. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_plan_grid.py +25 -4
  38. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_review_analysis.py +13 -2
  39. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_review_classic.py +20 -5
  40. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_review_summarisation.py +18 -3
  41. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/allocate.py +11 -3
  42. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/captain.py +28 -13
  43. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/chips.py +10 -8
  44. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/differentials.py +14 -1
  45. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/doctor.py +2 -2
  46. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/doctor_providers.py +13 -8
  47. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/fdr.py +7 -8
  48. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/fixtures.py +2 -4
  49. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/init.py +26 -14
  50. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/intel.py +2 -2
  51. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/league.py +7 -6
  52. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/league_fines.py +17 -7
  53. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/league_recap.py +14 -5
  54. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/player.py +20 -5
  55. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/preview.py +17 -7
  56. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/price_history.py +1 -1
  57. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/ratings.py +29 -12
  58. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/returnees.py +17 -9
  59. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/review.py +10 -6
  60. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/sell_prices.py +14 -16
  61. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/squad.py +101 -28
  62. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/stats.py +20 -6
  63. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/status.py +19 -8
  64. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/targets.py +7 -0
  65. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/transfer_eval.py +33 -22
  66. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/waivers.py +8 -5
  67. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/xg.py +35 -5
  68. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/config/defaults.yaml +3 -0
  69. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/prompts/league_recap.py +137 -7
  70. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/prompts/review.py +116 -15
  71. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/scraper/fpl_prices.py +29 -4
  72. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/season.py +3 -0
  73. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/league_history.py +55 -3
  74. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/league_history_counters.py +20 -16
  75. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/league_history_fines.py +42 -7
  76. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/league_history_notes.py +5 -8
  77. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/player_prior.py +11 -6
  78. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/returnee_radar.py +172 -45
  79. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/scoring/data_prep.py +11 -3
  80. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/team_ratings.py +6 -2
  81. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/team_ratings_prior.py +353 -31
  82. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/templates/gw_preview.md.j2 +5 -2
  83. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/templates/gw_review.md.j2 +3 -1
  84. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/utils/text.py +17 -1
  85. {fplkit-2.4.0 → fplkit-2.4.1}/pyproject.toml +19 -3
  86. {fplkit-2.4.0 → fplkit-2.4.1}/tests/conftest.py +142 -0
  87. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_agents_analysis.py +153 -0
  88. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_agents_base.py +28 -2
  89. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_api_understat.py +453 -6
  90. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_allocate.py +56 -3
  91. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_captain.py +62 -2
  92. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_chips.py +20 -11
  93. fplkit-2.4.1/tests/test_cli_context_settings.py +63 -0
  94. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_custom_analysis.py +91 -0
  95. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_differentials.py +36 -0
  96. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_doctor.py +7 -1
  97. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_doctor_providers.py +51 -4
  98. fplkit-2.4.1/tests/test_cli_failure_streams.py +247 -0
  99. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_fdr.py +51 -33
  100. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_fixtures.py +7 -7
  101. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_format_suppression.py +10 -9
  102. fplkit-2.4.1/tests/test_cli_fpl_block.py +210 -0
  103. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_intel.py +7 -3
  104. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_json_contract.py +166 -32
  105. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_league.py +2 -2
  106. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_league_fines.py +83 -1
  107. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_league_recap.py +246 -6
  108. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_plan_grid.py +4 -4
  109. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_player.py +64 -11
  110. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_preview.py +41 -4
  111. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_returnees.py +91 -13
  112. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_review.py +188 -1
  113. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_sell_prices.py +28 -1
  114. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_squad.py +233 -2
  115. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_stats.py +65 -0
  116. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_status.py +72 -2
  117. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_targets.py +30 -0
  118. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_waivers.py +1 -1
  119. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_warnings_stderr.py +3 -3
  120. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_xg.py +107 -3
  121. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_common.py +105 -0
  122. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_data_prep_harmonisation.py +30 -0
  123. fplkit-2.4.1/tests/test_doc_consistency.py +162 -0
  124. fplkit-2.4.1/tests/test_fines_config.py +224 -0
  125. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_init.py +11 -0
  126. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_league_history_fines.py +71 -0
  127. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_league_history_store.py +66 -0
  128. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_league_recap.py +233 -1
  129. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_player_prior.py +39 -0
  130. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_report_agent.py +147 -0
  131. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_returnee_radar.py +452 -58
  132. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_review.py +256 -1
  133. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_scoring.py +10 -0
  134. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_scraper.py +45 -0
  135. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_skill_fdr_quality_signals.py +2 -2
  136. fplkit-2.4.1/tests/test_skill_stats_thresholds.py +144 -0
  137. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_stats.py +321 -11
  138. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_team_ratings.py +45 -0
  139. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_team_ratings_prior.py +641 -12
  140. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_utils_text.py +16 -1
  141. fplkit-2.4.0/fpl_cli/api/understat.py +0 -418
  142. fplkit-2.4.0/fpl_cli/cli/_fines_config.py +0 -76
  143. fplkit-2.4.0/tests/test_doc_consistency.py +0 -89
  144. fplkit-2.4.0/tests/test_fines_config.py +0 -99
  145. fplkit-2.4.0/tests/test_skill_stats_thresholds.py +0 -75
  146. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/README.md +0 -0
  147. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/create-pr/SKILL.md +0 -0
  148. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/gw-prep/references/rules.md +0 -0
  149. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
  150. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/gw-prep/scripts/normalise_entities.py +0 -0
  151. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +0 -0
  152. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/preview-ingest/SKILL.md +0 -0
  153. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/release/SKILL.md +0 -0
  154. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/release-notes/SKILL.md +0 -0
  155. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/squad-builder/references/output-template.md +0 -0
  156. {fplkit-2.4.0 → fplkit-2.4.1}/.agents/skills/squad-builder/references/rules.md +0 -0
  157. {fplkit-2.4.0 → fplkit-2.4.1}/.claude/output-styles/fpl-mate.md +0 -0
  158. {fplkit-2.4.0 → fplkit-2.4.1}/.claude/settings.json +0 -0
  159. {fplkit-2.4.0 → fplkit-2.4.1}/.env.example +0 -0
  160. {fplkit-2.4.0 → fplkit-2.4.1}/.github/workflows/ci.yml +0 -0
  161. {fplkit-2.4.0 → fplkit-2.4.1}/.github/workflows/pr-title.yml +0 -0
  162. {fplkit-2.4.0 → fplkit-2.4.1}/.github/workflows/release.yml +0 -0
  163. {fplkit-2.4.0 → fplkit-2.4.1}/.gitignore +0 -0
  164. {fplkit-2.4.0 → fplkit-2.4.1}/CONTRIBUTING.md +0 -0
  165. {fplkit-2.4.0 → fplkit-2.4.1}/LICENSE +0 -0
  166. {fplkit-2.4.0 → fplkit-2.4.1}/cliff.toml +0 -0
  167. {fplkit-2.4.0 → fplkit-2.4.1}/config/team_ratings_overrides.yaml +0 -0
  168. {fplkit-2.4.0 → fplkit-2.4.1}/docs/fpl-rules.md +0 -0
  169. {fplkit-2.4.0 → fplkit-2.4.1}/docs/images/fpl-player-demo-v1-0.png +0 -0
  170. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/__init__.py +0 -0
  171. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/__init__.py +0 -0
  172. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/action/__init__.py +0 -0
  173. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/action/waiver.py +0 -0
  174. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/analysis/__init__.py +0 -0
  175. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/analysis/bench_order.py +0 -0
  176. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
  177. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/analysis/starting_xi.py +0 -0
  178. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
  179. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/data/__init__.py +0 -0
  180. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/data/fixture.py +0 -0
  181. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/data/price.py +0 -0
  182. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/data/scout.py +0 -0
  183. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/agents/orchestration/__init__.py +0 -0
  184. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/__init__.py +0 -0
  185. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/contract.py +0 -0
  186. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/core_insights.py +0 -0
  187. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/dataset_fetcher.py +0 -0
  188. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/football_data.py +0 -0
  189. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/fpl_draft.py +0 -0
  190. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/historical.py +0 -0
  191. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/historical_types.py +0 -0
  192. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/providers/__init__.py +0 -0
  193. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/providers/_http.py +0 -0
  194. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/providers/_models.py +0 -0
  195. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/providers/anthropic.py +0 -0
  196. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/providers/openai_compat.py +0 -0
  197. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/providers/perplexity.py +0 -0
  198. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/api/vaastav.py +0 -0
  199. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_banner.py +0 -0
  200. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_fines.py +0 -0
  201. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_league_recap_types.py +0 -0
  202. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/_review_draft.py +0 -0
  203. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/credentials.py +0 -0
  204. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/history.py +0 -0
  205. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/cli/price_changes.py +0 -0
  206. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/config/fixture_predictions.yaml +0 -0
  207. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
  208. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/config/team_managers.yaml +0 -0
  209. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/constants.py +0 -0
  210. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/models/__init__.py +0 -0
  211. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/models/chip_plan.py +0 -0
  212. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/models/fixture.py +0 -0
  213. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/models/league_history.py +0 -0
  214. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/models/player.py +0 -0
  215. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/models/team.py +0 -0
  216. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/models/types.py +0 -0
  217. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/parsers/__init__.py +0 -0
  218. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/parsers/recommendations.py +0 -0
  219. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/paths.py +0 -0
  220. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/prompts/__init__.py +0 -0
  221. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/prompts/returnees.py +0 -0
  222. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/prompts/scout.py +0 -0
  223. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/scraper/__init__.py +0 -0
  224. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/__init__.py +0 -0
  225. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/fixture_predictions.py +0 -0
  226. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/matchup.py +0 -0
  227. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/scoring/__init__.py +0 -0
  228. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/scoring/constants.py +0 -0
  229. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/scoring/display.py +0 -0
  230. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/scoring/evaluation.py +0 -0
  231. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/scoring/ownership.py +0 -0
  232. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/scoring/shrinkage.py +0 -0
  233. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/scoring/signals.py +0 -0
  234. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/scoring/single_gw.py +0 -0
  235. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/scoring/value_quality.py +0 -0
  236. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/season_previews.py +0 -0
  237. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/squad_allocator.py +0 -0
  238. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/services/team_form.py +0 -0
  239. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/templates/gw_league_recap.md.j2 +0 -0
  240. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/utils/__init__.py +0 -0
  241. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/utils/files.py +0 -0
  242. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/utils/gameweek.py +0 -0
  243. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/utils/markdown.py +0 -0
  244. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/utils/teams.py +0 -0
  245. {fplkit-2.4.0 → fplkit-2.4.1}/fpl_cli/utils/time.py +0 -0
  246. {fplkit-2.4.0 → fplkit-2.4.1}/pyrightconfig.json +0 -0
  247. {fplkit-2.4.0 → fplkit-2.4.1}/requirements.lock +0 -0
  248. {fplkit-2.4.0 → fplkit-2.4.1}/scripts/backtest_early_season_prior_blend.py +0 -0
  249. {fplkit-2.4.0 → fplkit-2.4.1}/scripts/calibrate_promoted_prior.py +0 -0
  250. {fplkit-2.4.0 → fplkit-2.4.1}/scripts/calibrate_quality_ceilings.py +0 -0
  251. {fplkit-2.4.0 → fplkit-2.4.1}/tests/__init__.py +0 -0
  252. {fplkit-2.4.0 → fplkit-2.4.1}/tests/fixtures/classic_squad_fixture.md +0 -0
  253. {fplkit-2.4.0 → fplkit-2.4.1}/tests/fixtures/recommendations_fixture.md +0 -0
  254. {fplkit-2.4.0 → fplkit-2.4.1}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
  255. {fplkit-2.4.0 → fplkit-2.4.1}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
  256. {fplkit-2.4.0 → fplkit-2.4.1}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
  257. {fplkit-2.4.0 → fplkit-2.4.1}/tests/fixtures/waiver_ranked_golden.json +0 -0
  258. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_agents_action.py +0 -0
  259. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_agents_analysis_squad.py +0 -0
  260. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_agents_data.py +0 -0
  261. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_agents_data_price.py +0 -0
  262. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_agents_orchestration.py +0 -0
  263. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_allocate.py +0 -0
  264. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_api_client.py +0 -0
  265. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_api_core_insights.py +0 -0
  266. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_api_dataset_fetcher.py +0 -0
  267. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_api_draft.py +0 -0
  268. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_api_football_data.py +0 -0
  269. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_api_historical.py +0 -0
  270. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_api_vaastav.py +0 -0
  271. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_bench_order_script.py +0 -0
  272. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_classic_squad_extraction.py +0 -0
  273. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_banner.py +0 -0
  274. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_credentials.py +0 -0
  275. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_fdr_blanks.py +0 -0
  276. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_format.py +0 -0
  277. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_history.py +0 -0
  278. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_init.py +0 -0
  279. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_json.py +0 -0
  280. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_price_changes.py +0 -0
  281. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_price_history.py +0 -0
  282. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_settings_warning.py +0 -0
  283. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_cli_transfer_eval.py +0 -0
  284. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_common_scoring.py +0 -0
  285. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_fines.py +0 -0
  286. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_fixture_predictions.py +0 -0
  287. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_fpl_prices.py +0 -0
  288. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_gw_prep_script_bootstrap.py +0 -0
  289. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_init_fines.py +0 -0
  290. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_league_history_counters.py +0 -0
  291. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_league_history_notes.py +0 -0
  292. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_matchup_service.py +0 -0
  293. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_models.py +0 -0
  294. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_normalise_entities.py +0 -0
  295. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_paths.py +0 -0
  296. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_player_resolution.py +0 -0
  297. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_preseason.py +0 -0
  298. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_providers.py +0 -0
  299. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_recommendations_parser.py +0 -0
  300. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_report_paths.py +0 -0
  301. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_rolling_pts_per_m.py +0 -0
  302. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_season.py +0 -0
  303. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_season_awareness.py +0 -0
  304. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_season_previews.py +0 -0
  305. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_sell_prices.py +0 -0
  306. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_squad_allocator.py +0 -0
  307. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_starting_xi_agent.py +0 -0
  308. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_starting_xi_script.py +0 -0
  309. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_transfer_eval_agent.py +0 -0
  310. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_transfer_eval_script.py +0 -0
  311. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_utils_gameweek.py +0 -0
  312. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_utils_markdown.py +0 -0
  313. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_utils_time.py +0 -0
  314. {fplkit-2.4.0 → fplkit-2.4.1}/tests/test_validate_draft_waivers.py +0 -0
@@ -24,35 +24,39 @@ which envelope you got. Warnings and progress go to stderr; the failure message
24
24
  in `error`, not on stderr. Full contract:
25
25
  [Command Reference](../docs/command-reference.md#json-output).
26
26
 
27
+ Table mode is the mirror image: the table on stdout, and warnings, progress and the reason
28
+ for a nonzero exit on stderr — so a script reading table output can silence the commentary
29
+ with `2>/dev/null` without losing the table, whichever command it runs.
30
+
27
31
  ## CLI Commands
28
32
 
29
33
  | Command | Description | JSON | Format | Experimental | Pattern |
30
34
  |---------|-------------|------|--------|-------------|---------|
31
35
  | `init` | Set up fpl-cli with your FPL IDs and optional features | No | General | No | direct-api |
32
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 |
33
- | `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 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 |
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, 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 Understat club check calls the enrichment's own gate on the payload the scoring commands scan, so probe and runtime cannot disagree) 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 |
34
38
  | `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 |
35
- | `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`. 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 |
36
- | `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). 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 |
39
+ | `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
+ | `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 |
37
41
  | `history` | Show historical player performance across seasons | Yes | General | No | direct-api |
38
42
  | `league` | Show live league standings for Classic and Draft leagues | No | General | No | direct-api |
39
43
  | `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 |
40
- | `xg` | Analyse underlying stats: xG, xA, overperformers | Yes | General | No | via-agent |
44
+ | `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 |
41
45
  | `price-changes` | Show price changes and transfer activity | No | General | No | via-agent |
42
46
  | `price-history` | Show price trajectory and transfer momentum | Yes | General | No | direct-api |
43
- | `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`/`_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 |
47
+ | `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 |
44
48
  | `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 |
45
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 | No | General | No | via-agent |
46
50
  | `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). A rebuild keeps the name, club 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. 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. 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. 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. `--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 | Yes | General | No | via-agent |
47
51
  | `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. 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 |
48
- | `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 |
49
- | `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), table mode prints it 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 |
50
- | `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), table mode prints it 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 |
52
+ | `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
+ | `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 | Yes | Classic | Yes | via-agent |
54
+ | `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 |
51
55
  | `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 |
52
- | `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) | Yes | Classic | Yes | direct-api |
56
+ | `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 |
53
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 | Yes | Draft | Yes | via-agent |
54
- | `squad` | Analyse your FPL squad health and fixtures. Exits 1 with an `error` envelope when no entry ID is configured for the active format | Yes | General | No | via-agent |
55
- | `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. 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 |
58
+ | `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
+ | `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 |
56
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 |
57
61
  | `chips` | View and plan FPL chip usage | Yes | Classic | No | direct-api |
58
62
  | `chips timing` | Recommend chip timing based on blank/double GW exposure | Yes | Classic | No | via-agent |
@@ -60,7 +64,7 @@ in `error`, not on stderr. Full contract:
60
64
  | `chips remove` | Remove a planned chip from a gameweek | No | Classic | No | direct-api |
61
65
  | `chips sync` | Sync chip usage from FPL API | No | Classic | No | direct-api |
62
66
  | `ratings` | Display team ratings | No | General | Yes | direct-api |
63
- | `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 |
67
+ | `ratings update` | Recalculate ratings from fixture results (shrunk toward the previous-season prior before GW12; seeds from the prior when no results exist yet). Wherever it applies the prior it names the clubs rated from Championship results or the flat promoted estimate and points at `team_ratings_prior.yaml`, whose `inputs` block records each club's basis, matches and the rates it was ranked on | No | General | Yes | direct-api |
64
68
  | `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 |
65
69
  | `intel schema` | Print the preview file format with every field explained | No | General | No | direct-api |
66
70
  | `intel init` | Scaffold an empty preview file per Premier League team (`--force` overwrites). Stubs never count toward coverage | No | General | No | direct-api |
@@ -92,6 +96,7 @@ Skills write their own outputs (`gw{N}-recommendations.md`, `gw{N}-squad-builder
92
96
  | release-notes | `skills/release-notes/` | Draft release notes and suggest the next semver (read-only preview; never tags or publishes) | Full: Claude Code, Codex, Cursor, Copilot |
93
97
  | release | `skills/release/` | Cut a release end-to-end: preflight, notes via release-notes, approval gate, publish GitHub release (tag drives PyPI publish + changelog automation) | Full: Claude Code (local), Codex, Cursor, Copilot. Partial: Claude Code web (publish step handed to user) |
94
98
  | create-pr | `skills/create-pr/` | Open a PR for the current branch: conventional-commit title (the changelog line) + why-first prose body in the fpl-cli PR description style | Full: Claude Code, Codex, Cursor, Copilot |
99
+ | build-test-plan | `skills/build-test-plan/` | Build a runnable test plan for the user-facing changes shipped between two releases, written for a live-data session in the fpl-workspace vault (deep per-change research, carries forward the previous plan's BLOCKED/FAIL items) | Full: Claude Code (parallel research sub-agents). Partial: Codex, Cursor, Copilot |
95
100
 
96
101
  ## Analysis Agents
97
102
 
@@ -116,11 +121,13 @@ Python classes in `fpl_cli/agents/` that implement `async run(context) -> AgentR
116
121
  - 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
117
122
  - TransferEvalAgent is used by both `transfer-eval` CLI command and gw-prep skill
118
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`)
124
+ - 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
119
125
  - `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.
120
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`)
121
127
  - 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`
122
- - 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 reads the ordering as a projection rather than as observed output; an empty block is annotated rather than left bare. Enforced by `tests/test_skill_stats_thresholds.py`
128
+ - 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
+ - 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`
123
130
  - `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.
124
131
  - `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`.
125
- - Every script under `.agents/skills/gw-prep/scripts/` imports the `fpl_cli` package, so it runs on the interpreter fpl-cli is installed on — gw-prep's SKILL.md writes that interpreter as `[YOUR_PYTHON]` (squad-builder and update-gw-prep still spell their `normalise_entities.py` call `python3`, correct only under an activated venv). 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
132
+ - 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
126
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.
@@ -0,0 +1,332 @@
1
+ ---
2
+ name: build-test-plan
3
+ description: >
4
+ Build a runnable test plan covering every user-facing fpl-cli change
5
+ shipped between two releases, written for a Claude Code session in the
6
+ fpl-workspace vault (live FPL API, real leagues, LLM keys). Use whenever
7
+ the user says "build a test plan", "test plan for the release", "what
8
+ needs testing since v2.2", "verify the last release", or asks how to
9
+ check that shipped changes actually work against live data. Also the
10
+ natural follow-up after cutting a release — offer it once a release is
11
+ published.
12
+ model: opus
13
+ compatibility:
14
+ claude-code: full (parallel research sub-agents; GitHub MCP on the web, gh locally)
15
+ codex: partial (sequential research — no sub-agent spawning)
16
+ cursor: partial (sequential research)
17
+ copilot: partial (sequential research)
18
+ ---
19
+
20
+ <!-- Runs in the fpl-cli repo; writes into the fpl-workspace vault. -->
21
+
22
+ # Build Test Plan
23
+
24
+ Turn a range of fpl-cli releases into a runbook another Claude Code
25
+ session can execute against live data in the fpl-workspace vault.
26
+
27
+ This skill runs **in the fpl-cli repo**, where the git history, PR bodies,
28
+ diffs and docs live. The plan it writes runs **in fpl-workspace**, which is
29
+ where the FPL credentials, LLM keys, real leagues and generated data are.
30
+ That split is the whole reason the plan has to be written down rather than
31
+ improvised: the session that knows what changed is not the session that can
32
+ exercise it.
33
+
34
+ Two things stay true throughout:
35
+
36
+ - **The plan targets released `fplkit`.** The vault installs the CLI from
37
+ PyPI, so a test written against an unreleased commit on `main` cannot be
38
+ run there. Unreleased work is noted, not tested.
39
+ - **fpl-cli is public; the vault is private.** The plan lives in the vault,
40
+ but write it so it *could* be public: no manager or league names, no
41
+ entry/league IDs. Refer to `classic-<league id>`, "your classic league".
42
+ Real names belong only in the Results log the runner fills in.
43
+
44
+ ## Step 1 — Fix the range
45
+
46
+ ```bash
47
+ git fetch origin main --tags
48
+ git tag --sort=-creatordate | head -10
49
+ ```
50
+
51
+ The default range is **the end of the last test plan → the newest release
52
+ tag**. Find where the last one stopped:
53
+
54
+ ```bash
55
+ ls /home/user/fpl-workspace/docs/test-plans/
56
+ ```
57
+
58
+ If the vault isn't cloned, attach `rossgroomio/fpl-workspace` and clone it
59
+ — the plan lands there and the environment facts come from there.
60
+
61
+ Then classify what sits beyond the newest tag:
62
+
63
+ ```bash
64
+ git log <newest tag>..origin/main --oneline --no-decorate
65
+ ```
66
+
67
+ If those commits are `docs:`/`chore:`/`ci:` only, say so in the plan's
68
+ opening paragraph — it tells the runner that testing the released version
69
+ covers the whole range, which is worth stating because it is not obvious.
70
+ If unreleased *code* sits on main, the honest options are to cut a release
71
+ first or to scope the plan to the released range and list the unreleased
72
+ work as untested. Put that to the user rather than silently picking.
73
+
74
+ Confirm the range with the user before researching. Getting it wrong wastes
75
+ the most expensive step.
76
+
77
+ ## Step 2 — Inventory the changes
78
+
79
+ ```bash
80
+ git log <from>..<to> --oneline --no-decorate
81
+ sed -n '/## \[<to version>\]/,/## \[<from version>\]/p' CHANGELOG.md
82
+ ```
83
+
84
+ Squash-merged subjects carry their PR number as `(#N)` — that is the handle
85
+ for the research step.
86
+
87
+ Keep `feat:`, `fix:`, `refactor:`, `perf:`. Drop `chore:`/`docs:`/`ci:`/
88
+ `test:`/`style:` — but skim their subjects first. A user-visible change
89
+ mislabelled `chore:` is invisible to the changelog *and* to this plan, and
90
+ that is exactly the change nobody thinks to test.
91
+
92
+ ## Step 3 — Carry forward the unfinished business
93
+
94
+ Two sources feed this, and a plan that reads only the first will miss work
95
+ the user has already specified.
96
+
97
+ ### The previous plan's Results log
98
+
99
+ Three kinds of thing carry forward:
100
+
101
+ - **BLOCKED tests** — deferred because the season state didn't allow them
102
+ ("only GW1 complete, defer the replay test"). Check whether the condition
103
+ has since cleared. If it has, the test is now runnable and belongs in the
104
+ new plan verbatim.
105
+ - **FAILs** — check whether the range under test fixes them. Match on PR
106
+ subject and number: a FAIL that a shipped `fix:` claims to address needs
107
+ a test that reproduces the *original* failure and confirms it is gone.
108
+ A FAIL nothing has addressed carries forward unchanged, so it doesn't
109
+ quietly become accepted behaviour.
110
+ - **Post-plan findings** — the observations recorded below the table. These
111
+ are usually the sharpest bugs in the whole document, because they came
112
+ from a human looking at real output. Same treatment as FAILs.
113
+
114
+ This loop is real: most of v2.3.0 exists because the v2.0→v2.2 run found
115
+ those bugs. A plan that doesn't close its predecessor's loop lets fixes ship
116
+ unverified.
117
+
118
+ Carry the *substance* across, not a pointer to it. A runner who has to open
119
+ the previous plan to understand a test has been handed two documents to
120
+ execute instead of one, and mid-run is the worst moment to go looking. So
121
+ state what the old test found and what changed, inline, in the new test.
122
+
123
+ Plans all live together in `docs/test-plans/`, so where provenance is
124
+ genuinely useful — tracing a claim, or flagging that this plan contradicts
125
+ its predecessor — name the file (`v2.0-to-v2.2.md`) and let co-location do
126
+ the rest. No paths, no links.
127
+
128
+ ### The vault's open issues
129
+
130
+ The Results log records what a run found. The vault's issue tracker records
131
+ what the user decided to do about it, which is not the same set — and it is
132
+ where deferred testing work actually queues:
133
+
134
+ ```
135
+ mcp__github__issue_read / list_issues on rossgroomio/fpl-workspace,
136
+ state OPEN — the `testing` label first, then the rest
137
+ ```
138
+
139
+ **Read the comments, not just the body.** A deferred-items issue grows by
140
+ comment as later sessions hit the same wall, and the comments carry the
141
+ best material in the whole tracker: fully-specified tests with the exact
142
+ invocation and expected values, the data state at the time of testing, and
143
+ the condition that would unblock each one. Fold those in close to as
144
+ written — they are better specified than anything drafted fresh, and the
145
+ person who wrote them was looking at real output.
146
+
147
+ Two things to notice while you're there:
148
+
149
+ - **Tests with no home.** Work deferred from a version that never got its
150
+ own plan has nowhere to be logged, and the issue usually says so. The
151
+ plan you are writing is that home if its range covers the change.
152
+ - **Environment blockers.** An open issue about the vault's own setup — a
153
+ broken helper script, a wrong interpreter, a missing key — is not a
154
+ change under test, but it will make a test fail confusingly and get
155
+ misattributed to fpl-cli. Surface those in the preflight.
156
+
157
+ Cite the issue number against each carried test (`carried from
158
+ workspace#34`), and have the wrap-up tell the runner to close those items
159
+ as they are recorded. An issue that stays open after its tests pass is how
160
+ the same work gets queued twice.
161
+
162
+ ## Step 4 — Research each change
163
+
164
+ This is where plan quality is won or lost. A test built from a commit
165
+ subject alone reads "verify `fpl doctor --providers` works" and cannot
166
+ fail. A test built from the diff reads "expect all six probes, each
167
+ asserting shape **and** volume: 20 teams, 38 gameweeks, every stat field
168
+ the tool reads present" — and that one catches a regression.
169
+
170
+ **Group before you fan out.** Cluster the commits by the surface they touch
171
+ (league-recap and its ledger; `doctor`; scoring and data quality; JSON
172
+ envelopes). Themes make better research units than individual PRs, because
173
+ several PRs usually move one surface together and the tests want to be
174
+ written against the surface's final state, not each intermediate step.
175
+
176
+ Then spawn one research sub-agent per theme, in a single batch so they run
177
+ concurrently. Brief each one:
178
+
179
+ > Research the fpl-cli changes in <theme> shipped between <from> and <to>,
180
+ > so a tester with a live FPL account can verify them. Commits: <list with
181
+ > PR numbers>.
182
+ >
183
+ > For each change, read: the PR body and any linked issue (via GitHub MCP
184
+ > `pull_request_read` / `issue_read`, or `gh pr view <N>`), the diff
185
+ > (`git show <sha>`), the relevant section of `docs/command-reference.md`,
186
+ > and the tests the PR added under `tests/`.
187
+ >
188
+ > Report back, per change:
189
+ > - The exact CLI invocation that exercises it, flags included.
190
+ > - Falsifiable expectations — specific counts, field names, value ranges,
191
+ > exit codes, which stream output lands on. Name the actual values
192
+ > (how many probes, which warning codes, which JSON keys), not "the
193
+ > documented ones".
194
+ > - The repro from the issue, for a `fix:` — the condition that used to
195
+ > break is what the test recreates.
196
+ > - Degradation and edge paths the change introduces or touches: missing
197
+ > data, corrupt file, unreachable provider, invalid input, exit codes.
198
+ > - Whether the current season state can exercise it at all, and if not,
199
+ > the condition that would unblock it.
200
+ >
201
+ > Ground every specific in a file you actually read. Do not describe
202
+ > behaviour you inferred from a commit subject.
203
+
204
+ Sub-agent findings are a draft, not the plan. Reconcile them yourself:
205
+ drop duplicates where two themes touch the same command, and check that
206
+ the specifics they quote really appear in the docs or diff before those
207
+ numbers become assertions the runner will trust.
208
+
209
+ ## What makes a test worth writing
210
+
211
+ The reference plan (`docs/test-plans/v2.0-to-v2.2.md` in the vault) is the
212
+ quality bar. What separates its useful tests from filler:
213
+
214
+ **It can fail.** Every expectation is something that would read differently
215
+ if the fix regressed. "Renders without crashing" is worth one line for a
216
+ crash fix and nothing otherwise; "row count equals the real league size"
217
+ caught a real pagination bug.
218
+
219
+ **It tests the bug, not the fix.** For a `fix:`, recreate the condition
220
+ from the issue and assert the old symptom is absent. Testing that the new
221
+ code path runs tells you nothing about whether the bug is gone.
222
+
223
+ **It uses the observable surface.** The runner has a released binary, not
224
+ the source. Commands, files on disk, exit codes, stdout vs stderr — never
225
+ internal functions.
226
+
227
+ **It builds a fixture before it gives up.** When live data can't reach a
228
+ code path, a scratch fixture often can: point `FPL_CLI_DATA_DIR` at a
229
+ throwaway copy of the store and doctor a row to the shape the bug needed,
230
+ or feed the matching function inputs that disagree. This reaches repair
231
+ and migration paths that a healthy live store never exercises, and it
232
+ costs the runner a `cp -r` they throw away afterwards. Reach for it before
233
+ reaching for BLOCKED — and keep it off the real store, which for the
234
+ ledger is irreplaceable.
235
+
236
+ **It mines the environment for fixtures it already has.** The vault's own
237
+ data is full of them. Players in captured ledger rows who have since
238
+ changed club are a ready-made test that a re-run doesn't restamp recorded
239
+ history — verify what each row *should* say from that gameweek's live
240
+ data, tabulate it, and any drift is an unambiguous FAIL. Look for this
241
+ before inventing setup.
242
+
243
+ **It says when it genuinely can't run.** Some things no fixture reaches:
244
+ a blank gameweek that hasn't happened, a week-over-week comparison with no
245
+ prior week. Write the test anyway, mark it BLOCKED with the condition that
246
+ unblocks it, and the next plan picks it up via Step 3. A test that
247
+ silently can't run is worse than one marked BLOCKED, because it looks like
248
+ coverage.
249
+
250
+ **It covers degradation.** Missing config, corrupt store, provider down,
251
+ bad gameweek number. Make corruption tests reversible and say how to
252
+ restore (`git checkout -- <path>`) — the runner is working on real data
253
+ they want to keep.
254
+
255
+ **It names the channel.** This tool's JSON contract has moved between
256
+ stdout and stderr more than once. Where a test asserts on output, say which
257
+ stream, and have the runner capture stderr separately
258
+ (`2>/tmp/<id>-err.txt`).
259
+
260
+ **It earns its place.** Plan length tracks the range. Two releases and
261
+ twenty user-facing changes justify something like the reference plan's
262
+ ~35 tests across four parts; a patch-only range with four fixes wants six
263
+ or seven. Padding a small range with smoke tests of untouched commands
264
+ buys no coverage and teaches the runner to skim.
265
+
266
+ ## Step 5 — Order the parts
267
+
268
+ Group tests into parts by surface, then order the parts by **what the user
269
+ gets from running them**, not by release order:
270
+
271
+ 1. Anything that produces artifacts worth having this week (a recap, a
272
+ ledger capture) goes first, because a plan often gets run across two
273
+ sessions and the first session should produce the thing with a deadline.
274
+ 2. Health checks next (`fpl doctor`) — they surface environment problems
275
+ that would otherwise show up as confusing failures later.
276
+ 3. Everything else, grouped by surface.
277
+
278
+ Say the ordering rationale in the preamble. A runner who understands why
279
+ Part 1 is first will keep it first when the session runs short.
280
+
281
+ Number tests with a per-part letter prefix (`P1`–`P4` preflight, `R1`–`R10`
282
+ recap, `D1`–`D4` doctor…) so the Results log stays readable and a follow-up
283
+ session can reference a single test unambiguously.
284
+
285
+ ## Step 6 — Write the plan
286
+
287
+ Read `references/plan-template.md` for the exact document shape, and
288
+ `references/workspace-env.md` for the environment facts that go into the
289
+ preflight and the cost/safety notes. Write the plan to:
290
+
291
+ ```
292
+ /home/user/fpl-workspace/docs/test-plans/v<from>-to-<to>.md
293
+ ```
294
+
295
+ Name it for the range's ends. Trim a trailing `.0` only when the two ends
296
+ differ in major.minor — `v2.2.0`→`v2.4.0` becomes `v2.2-to-v2.4.md`,
297
+ matching `v2.0-to-v2.2.md`. When both ends sit in the same minor, keep
298
+ full patch versions on both, or the name reads as a range it isn't
299
+ (`v2.3.0`→`v2.3.2` is `v2.3.0-to-v2.3.2.md`, never `v2.3-to-v2.3.2.md`).
300
+
301
+ Pre-seed the Results log with one empty row per test ID. It is the runner's
302
+ worksheet and the next plan's Step 3 input, so every ID needs a row waiting
303
+ for it.
304
+
305
+ Then add a **coverage map**: every in-range PR number against the test IDs
306
+ that exercise it. Write it by walking the Step 2 inventory, not by
307
+ summarising what you already wrote — the point is to catch the change you
308
+ researched and then lost track of while grouping. A PR with no test ID
309
+ next to it is either a gap to fill or a deliberate omission to justify in
310
+ one line, and either way the runner can see it. This is cheap to produce
311
+ and the only mechanism that makes coverage auditable from the artifact
312
+ itself.
313
+
314
+ Vault convention: no blank lines before or after headings.
315
+
316
+ ## Step 7 — Land it
317
+
318
+ Commit the plan in the vault on its own branch:
319
+
320
+ ```bash
321
+ cd /home/user/fpl-workspace
322
+ git checkout -b claude/test-plan-v<to>
323
+ git add docs/test-plans/<file>
324
+ git commit -m "docs: add v<from> to v<to> test plan"
325
+ ```
326
+
327
+ Ask before pushing — the vault is the user's own repo and they may want to
328
+ read the plan first.
329
+
330
+ Then summarise here: the range, how many tests in how many parts, which
331
+ carried forward from the last plan, and anything you had to mark BLOCKED
332
+ on season state so the user knows what the run won't cover.