fplkit 2.9.0__tar.gz → 2.11.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 (356) hide show
  1. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/README.md +2 -0
  2. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/TOOLS.md +6 -5
  3. fplkit-2.11.0/.agents/skills/ab-brief/SKILL.md +401 -0
  4. fplkit-2.11.0/.agents/skills/ab-brief/references/brief-template.md +65 -0
  5. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/create-pr/SKILL.md +16 -1
  6. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/release/SKILL.md +1 -1
  7. {fplkit-2.9.0 → fplkit-2.11.0}/.claude/output-styles/fpl-mate.md +2 -2
  8. {fplkit-2.9.0 → fplkit-2.11.0}/.github/workflows/ci.yml +1 -1
  9. {fplkit-2.9.0 → fplkit-2.11.0}/AGENTS.md +8 -5
  10. {fplkit-2.9.0 → fplkit-2.11.0}/CHANGELOG.md +60 -0
  11. {fplkit-2.9.0 → fplkit-2.11.0}/CLAUDE.md +8 -5
  12. {fplkit-2.9.0 → fplkit-2.11.0}/CONTRIBUTING.md +23 -1
  13. {fplkit-2.9.0 → fplkit-2.11.0}/PKG-INFO +42 -3
  14. {fplkit-2.9.0 → fplkit-2.11.0}/README.md +40 -2
  15. {fplkit-2.9.0 → fplkit-2.11.0}/docs/architecture.md +30 -15
  16. {fplkit-2.9.0 → fplkit-2.11.0}/docs/command-reference.md +419 -33
  17. {fplkit-2.9.0 → fplkit-2.11.0}/docs/custom-analysis.md +5 -1
  18. {fplkit-2.9.0 → fplkit-2.11.0}/docs/fpl-rules.md +1 -1
  19. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/_version.py +2 -2
  20. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/action/waiver.py +83 -28
  21. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/common.py +120 -20
  22. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/orchestration/report.py +44 -14
  23. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/fpl.py +29 -1
  24. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/fpl_draft.py +14 -4
  25. fplkit-2.11.0/fpl_cli/api/premier_league.py +231 -0
  26. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_context.py +5 -5
  27. fplkit-2.11.0/fpl_cli/cli/_draft_provenance.py +303 -0
  28. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_fines.py +1 -1
  29. fplkit-2.11.0/fpl_cli/cli/_fines_config.py +391 -0
  30. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_league_recap_data.py +390 -142
  31. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_league_recap_history.py +103 -1
  32. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_league_recap_types.py +476 -25
  33. fplkit-2.11.0/fpl_cli/cli/_league_recap_vouchers.py +453 -0
  34. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/doctor.py +1 -1
  35. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/doctor_providers.py +99 -6
  36. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/init.py +43 -0
  37. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/league_recap.py +472 -26
  38. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/player.py +52 -27
  39. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/preview.py +1 -3
  40. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/review.py +3 -3
  41. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/stats.py +46 -29
  42. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/config/defaults.yaml +15 -3
  43. fplkit-2.11.0/fpl_cli/constants.py +11 -0
  44. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/models/chip_plan.py +4 -4
  45. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/models/player.py +24 -8
  46. fplkit-2.11.0/fpl_cli/models/season_month.py +67 -0
  47. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/models/types.py +2 -1
  48. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/prompts/league_recap.py +390 -32
  49. fplkit-2.11.0/fpl_cli/prompts/league_recap_checks.py +3888 -0
  50. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/season.py +26 -1
  51. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/draft_provenance.py +164 -20
  52. fplkit-2.11.0/fpl_cli/services/draft_rebuild_cache.py +113 -0
  53. fplkit-2.11.0/fpl_cli/services/league_history_months.py +355 -0
  54. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/league_history_notes.py +615 -22
  55. fplkit-2.11.0/fpl_cli/services/league_history_vouchers.py +419 -0
  56. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/scoring/__init__.py +0 -2
  57. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/scoring/constants.py +0 -3
  58. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/templates/gw_league_recap.md.j2 +49 -29
  59. fplkit-2.11.0/fpl_cli/utils/ranking.py +27 -0
  60. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/utils/text.py +17 -1
  61. {fplkit-2.9.0 → fplkit-2.11.0}/pyproject.toml +1 -0
  62. {fplkit-2.9.0 → fplkit-2.11.0}/tests/conftest.py +88 -1
  63. fplkit-2.11.0/tests/fixtures/premier_league_awards/season-2025.json +1406 -0
  64. fplkit-2.11.0/tests/fixtures/premier_league_awards/season-2026.json +260 -0
  65. {fplkit-2.9.0 → fplkit-2.11.0}/tests/fixtures/waiver_ranked_golden.json +192 -159
  66. fplkit-2.11.0/tests/test_ab_brief_skill.py +60 -0
  67. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_agents_action.py +220 -17
  68. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_api_client.py +34 -0
  69. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_api_draft.py +43 -0
  70. fplkit-2.11.0/tests/test_api_premier_league.py +176 -0
  71. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_chips.py +4 -0
  72. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_doctor_providers.py +164 -1
  73. fplkit-2.11.0/tests/test_cli_draft_provenance.py +330 -0
  74. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_league_recap.py +1494 -25
  75. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_player.py +252 -3
  76. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_review.py +5 -4
  77. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_stats.py +112 -1
  78. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_status.py +1 -1
  79. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_common.py +171 -8
  80. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_draft_provenance.py +292 -1
  81. fplkit-2.11.0/tests/test_fines_config.py +477 -0
  82. fplkit-2.11.0/tests/test_harness.py +25 -0
  83. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_init_fines.py +94 -0
  84. fplkit-2.11.0/tests/test_league_history_months.py +557 -0
  85. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_league_history_notes.py +687 -9
  86. fplkit-2.11.0/tests/test_league_history_vouchers.py +789 -0
  87. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_league_recap.py +4140 -168
  88. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_models.py +10 -1
  89. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_season.py +50 -0
  90. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_utils_text.py +9 -0
  91. fplkit-2.9.0/fpl_cli/cli/_fines_config.py +0 -146
  92. fplkit-2.9.0/fpl_cli/constants.py +0 -3
  93. fplkit-2.9.0/fpl_cli/prompts/league_recap_checks.py +0 -1471
  94. fplkit-2.9.0/tests/test_fines_config.py +0 -224
  95. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/build-test-plan/SKILL.md +0 -0
  96. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/build-test-plan/references/plan-template.md +0 -0
  97. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/build-test-plan/references/workspace-env.md +0 -0
  98. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/SKILL.md +0 -0
  99. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/references/entity-normalisation.md +0 -0
  100. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/references/output-template.md +0 -0
  101. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/references/rules.md +0 -0
  102. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/scripts/_bootstrap.py +0 -0
  103. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
  104. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/scripts/claimable_returnees.py +0 -0
  105. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +0 -0
  106. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/scripts/normalise_entities.py +0 -0
  107. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
  108. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
  109. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +0 -0
  110. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/preview-ingest/SKILL.md +0 -0
  111. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/release-notes/SKILL.md +0 -0
  112. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/squad-builder/SKILL.md +0 -0
  113. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/squad-builder/references/output-template.md +0 -0
  114. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/squad-builder/references/rules.md +0 -0
  115. {fplkit-2.9.0 → fplkit-2.11.0}/.agents/skills/update-gw-prep/SKILL.md +0 -0
  116. {fplkit-2.9.0 → fplkit-2.11.0}/.claude/settings.json +0 -0
  117. {fplkit-2.9.0 → fplkit-2.11.0}/.env.example +0 -0
  118. {fplkit-2.9.0 → fplkit-2.11.0}/.github/workflows/pr-title.yml +0 -0
  119. {fplkit-2.9.0 → fplkit-2.11.0}/.github/workflows/release.yml +0 -0
  120. {fplkit-2.9.0 → fplkit-2.11.0}/.gitignore +0 -0
  121. {fplkit-2.9.0 → fplkit-2.11.0}/LICENSE +0 -0
  122. {fplkit-2.9.0 → fplkit-2.11.0}/cliff.toml +0 -0
  123. {fplkit-2.9.0 → fplkit-2.11.0}/config/team_ratings_overrides.yaml +0 -0
  124. {fplkit-2.9.0 → fplkit-2.11.0}/docs/images/fpl-player-demo-v1-0.png +0 -0
  125. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/__init__.py +0 -0
  126. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/__init__.py +0 -0
  127. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/action/__init__.py +0 -0
  128. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/analysis/__init__.py +0 -0
  129. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/analysis/bench_order.py +0 -0
  130. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/analysis/captain.py +0 -0
  131. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
  132. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/analysis/starting_xi.py +0 -0
  133. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/analysis/stats.py +0 -0
  134. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
  135. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/base.py +0 -0
  136. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/data/__init__.py +0 -0
  137. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/data/fixture.py +0 -0
  138. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/data/price.py +0 -0
  139. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/data/scout.py +0 -0
  140. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/agents/orchestration/__init__.py +0 -0
  141. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/__init__.py +0 -0
  142. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/contract.py +0 -0
  143. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/core_insights.py +0 -0
  144. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/dataset_fetcher.py +0 -0
  145. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/football_data.py +0 -0
  146. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/historical.py +0 -0
  147. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/historical_types.py +0 -0
  148. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/providers/__init__.py +0 -0
  149. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/providers/_http.py +0 -0
  150. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/providers/_models.py +0 -0
  151. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/providers/anthropic.py +0 -0
  152. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/providers/openai_compat.py +0 -0
  153. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/providers/perplexity.py +0 -0
  154. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/understat.py +0 -0
  155. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/api/vaastav.py +0 -0
  156. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/__init__.py +0 -0
  157. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_banner.py +0 -0
  158. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_gameweek_status.py +0 -0
  159. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_helpers.py +0 -0
  160. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_json.py +0 -0
  161. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_plan_grid.py +0 -0
  162. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_review_analysis.py +0 -0
  163. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_review_classic.py +0 -0
  164. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_review_draft.py +0 -0
  165. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_review_summarisation.py +0 -0
  166. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/_synthesis_retry.py +0 -0
  167. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/allocate.py +0 -0
  168. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/captain.py +0 -0
  169. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/chips.py +0 -0
  170. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/credentials.py +0 -0
  171. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/differentials.py +0 -0
  172. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/fdr.py +0 -0
  173. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/fixtures.py +0 -0
  174. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/history.py +0 -0
  175. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/intel.py +0 -0
  176. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/league.py +0 -0
  177. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/league_fines.py +0 -0
  178. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/price_changes.py +0 -0
  179. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/price_history.py +0 -0
  180. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/ratings.py +0 -0
  181. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/returnees.py +0 -0
  182. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/sell_prices.py +0 -0
  183. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/squad.py +0 -0
  184. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/status.py +0 -0
  185. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/targets.py +0 -0
  186. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/transfer_eval.py +0 -0
  187. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/waivers.py +0 -0
  188. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/cli/xg.py +0 -0
  189. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/config/fixture_predictions.yaml +0 -0
  190. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
  191. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/config/team_managers.yaml +0 -0
  192. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/models/__init__.py +0 -0
  193. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/models/fixture.py +0 -0
  194. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/models/gameweek_status.py +0 -0
  195. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/models/league_history.py +0 -0
  196. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/models/team.py +0 -0
  197. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/parsers/__init__.py +0 -0
  198. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/parsers/recommendations.py +0 -0
  199. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/paths.py +0 -0
  200. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/prompts/__init__.py +0 -0
  201. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/prompts/returnees.py +0 -0
  202. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/prompts/review.py +0 -0
  203. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/prompts/scout.py +0 -0
  204. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/scraper/__init__.py +0 -0
  205. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/scraper/fpl_prices.py +0 -0
  206. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/__init__.py +0 -0
  207. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/fixture_predictions.py +0 -0
  208. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/league_history.py +0 -0
  209. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/league_history_counters.py +0 -0
  210. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/league_history_drafts.py +0 -0
  211. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/league_history_fines.py +0 -0
  212. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/matchup.py +0 -0
  213. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/player_clubs.py +0 -0
  214. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/player_prior.py +0 -0
  215. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/returnee_radar.py +0 -0
  216. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/scoring/coverage.py +0 -0
  217. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/scoring/data_prep.py +0 -0
  218. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/scoring/display.py +0 -0
  219. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/scoring/evaluation.py +0 -0
  220. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/scoring/ownership.py +0 -0
  221. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/scoring/shrinkage.py +0 -0
  222. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/scoring/signals.py +0 -0
  223. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/scoring/single_gw.py +0 -0
  224. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/scoring/value_quality.py +0 -0
  225. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/season_previews.py +0 -0
  226. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/sell_prices.py +0 -0
  227. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/squad_allocator.py +0 -0
  228. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/team_form.py +0 -0
  229. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/team_ratings.py +0 -0
  230. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/services/team_ratings_prior.py +0 -0
  231. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/templates/gw_preview.md.j2 +0 -0
  232. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/templates/gw_review.md.j2 +0 -0
  233. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/utils/__init__.py +0 -0
  234. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/utils/files.py +0 -0
  235. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/utils/gameweek.py +0 -0
  236. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/utils/markdown.py +0 -0
  237. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/utils/teams.py +0 -0
  238. {fplkit-2.9.0 → fplkit-2.11.0}/fpl_cli/utils/time.py +0 -0
  239. {fplkit-2.9.0 → fplkit-2.11.0}/pyrightconfig.json +0 -0
  240. {fplkit-2.9.0 → fplkit-2.11.0}/scripts/backtest_early_season_prior_blend.py +0 -0
  241. {fplkit-2.9.0 → fplkit-2.11.0}/scripts/calibrate_promoted_prior.py +0 -0
  242. {fplkit-2.9.0 → fplkit-2.11.0}/scripts/calibrate_quality_ceilings.py +0 -0
  243. {fplkit-2.9.0 → fplkit-2.11.0}/tests/__init__.py +0 -0
  244. {fplkit-2.9.0 → fplkit-2.11.0}/tests/fixtures/classic_squad_fixture.md +0 -0
  245. {fplkit-2.9.0 → fplkit-2.11.0}/tests/fixtures/recommendations_fixture.md +0 -0
  246. {fplkit-2.9.0 → fplkit-2.11.0}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
  247. {fplkit-2.9.0 → fplkit-2.11.0}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
  248. {fplkit-2.9.0 → fplkit-2.11.0}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
  249. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_agents_analysis.py +0 -0
  250. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_agents_analysis_squad.py +0 -0
  251. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_agents_base.py +0 -0
  252. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_agents_data.py +0 -0
  253. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_agents_data_price.py +0 -0
  254. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_agents_orchestration.py +0 -0
  255. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_allocate.py +0 -0
  256. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_api_core_insights.py +0 -0
  257. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_api_dataset_fetcher.py +0 -0
  258. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_api_football_data.py +0 -0
  259. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_api_historical.py +0 -0
  260. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_api_understat.py +0 -0
  261. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_api_vaastav.py +0 -0
  262. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_bench_order_script.py +0 -0
  263. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_calibrate_quality_ceilings.py +0 -0
  264. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_claimable_returnees.py +0 -0
  265. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_classic_squad_extraction.py +0 -0
  266. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_allocate.py +0 -0
  267. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_banner.py +0 -0
  268. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_captain.py +0 -0
  269. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_context_settings.py +0 -0
  270. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_credentials.py +0 -0
  271. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_custom_analysis.py +0 -0
  272. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_differentials.py +0 -0
  273. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_doctor.py +0 -0
  274. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_failure_parity.py +0 -0
  275. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_failure_streams.py +0 -0
  276. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_fdr.py +0 -0
  277. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_fdr_blanks.py +0 -0
  278. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_fixtures.py +0 -0
  279. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_format.py +0 -0
  280. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_format_suppression.py +0 -0
  281. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_fpl_block.py +0 -0
  282. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_history.py +0 -0
  283. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_init.py +0 -0
  284. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_intel.py +0 -0
  285. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_json.py +0 -0
  286. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_json_contract.py +0 -0
  287. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_league.py +0 -0
  288. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_league_fines.py +0 -0
  289. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_notice_streams.py +0 -0
  290. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_plan_grid.py +0 -0
  291. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_preview.py +0 -0
  292. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_price_changes.py +0 -0
  293. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_price_history.py +0 -0
  294. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_returnees.py +0 -0
  295. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_sell_prices.py +0 -0
  296. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_settings_warning.py +0 -0
  297. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_squad.py +0 -0
  298. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_synthesis_retry.py +0 -0
  299. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_targets.py +0 -0
  300. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_transfer_eval.py +0 -0
  301. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_waivers.py +0 -0
  302. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_warnings_stderr.py +0 -0
  303. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_cli_xg.py +0 -0
  304. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_common_scoring.py +0 -0
  305. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_data_prep_harmonisation.py +0 -0
  306. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_dependencies.py +0 -0
  307. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_doc_consistency.py +0 -0
  308. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_fines.py +0 -0
  309. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_fixture_predictions.py +0 -0
  310. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_formation_guidance_sync.py +0 -0
  311. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_fpl_prices.py +0 -0
  312. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_gameweek_status.py +0 -0
  313. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_gw_prep_script_bootstrap.py +0 -0
  314. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_init.py +0 -0
  315. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_league_history_counters.py +0 -0
  316. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_league_history_drafts.py +0 -0
  317. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_league_history_fines.py +0 -0
  318. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_league_history_store.py +0 -0
  319. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_matchup_service.py +0 -0
  320. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_normalise_entities.py +0 -0
  321. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_output_style_sync.py +0 -0
  322. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_paths.py +0 -0
  323. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_player_clubs.py +0 -0
  324. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_player_prior.py +0 -0
  325. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_player_resolution.py +0 -0
  326. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_preseason.py +0 -0
  327. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_prompt_tables.py +0 -0
  328. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_providers.py +0 -0
  329. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_recommendations_parser.py +0 -0
  330. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_report_agent.py +0 -0
  331. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_report_paths.py +0 -0
  332. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_returnee_radar.py +0 -0
  333. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_review.py +0 -0
  334. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_rolling_pts_per_m.py +0 -0
  335. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_scoring.py +0 -0
  336. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_scoring_coverage.py +0 -0
  337. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_scraper.py +0 -0
  338. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_season_awareness.py +0 -0
  339. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_season_previews.py +0 -0
  340. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_sell_prices.py +0 -0
  341. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_sell_prices_service.py +0 -0
  342. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_skill_effort_levels.py +0 -0
  343. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_skill_fdr_quality_signals.py +0 -0
  344. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_skill_stats_thresholds.py +0 -0
  345. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_squad_allocator.py +0 -0
  346. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_starting_xi_agent.py +0 -0
  347. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_starting_xi_script.py +0 -0
  348. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_stats.py +0 -0
  349. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_team_ratings.py +0 -0
  350. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_team_ratings_prior.py +0 -0
  351. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_transfer_eval_agent.py +0 -0
  352. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_transfer_eval_script.py +0 -0
  353. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_utils_gameweek.py +0 -0
  354. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_utils_markdown.py +0 -0
  355. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_utils_time.py +0 -0
  356. {fplkit-2.9.0 → fplkit-2.11.0}/tests/test_validate_draft_waivers.py +0 -0
@@ -34,6 +34,8 @@ The four product skills (gw-prep, squad-builder, update-gw-prep, preview-ingest)
34
34
 
35
35
  These levels are a judgement, not a measurement. `max` is untested here and probably not worth its time and cost; a gameweek of gw-prep run twice, once at these levels and once with classic at `max`, is the experiment that would settle it. Change a level in the block and in its row together: `tests/test_skill_effort_levels.py` reads each skill's frontmatter and every `Agent tool parameters` block, and holds each row of this table to the block its Where cell names (`orchestrator` is the frontmatter; the other labels are the sub-agent blocks in document order), so a swapped, missing or misspelt row fails there rather than being dropped.
36
36
 
37
+ The maintainer skills (release, release-notes, create-pr, build-test-plan, ab-brief) set no effort level and are not held to this table: they run once per release or PR, interactively, and follow the session's effort. ab-brief is the exception worth knowing, since the effort that matters is the vault session's, not its own, and it says in its brief which level that session should start at.
38
+
37
39
  How far each key reaches, quoted from the Claude Code docs as read on 14 October 2026:
38
40
 
39
41
  - `model` in the frontmatter ([skills, Frontmatter reference](https://code.claude.com/docs/en/skills#frontmatter-reference)): "The override applies for the rest of the current turn and isn't saved to settings. The session model resumes when you send your next prompt." So a follow-up question about the output ("why sell X?") is answered on the session model.
@@ -42,10 +42,10 @@ see [Feed coverage warnings](../docs/command-reference.md#json-output).
42
42
  |---------|-------------|------|--------|-------------|---------|
43
43
  | `init` | Set up fpl-cli with your FPL IDs and optional features | No | General | No | direct-api |
44
44
  | `status` | Show FPL gameweek status and upcoming deadlines. JSON `metadata` carries `gameweek`, `format` and `season` (hyphenated label, e.g. `2026-27`) - `season` is the directory segment skills must use when writing alongside fpl-cli's own reports. `data.gameweek_info` carries `finished` beside `bonus_confirmed` and `league_tables_updated`, read from FPL's processing feed (`event-status`) for each game shown (the classic one alone when no format is configured): `true` only when every game's feed says so, `false` as soon as one says otherwise, and `null` otherwise - always `null` while the gameweek is in progress (the feed can report the days played so far as settled), and also whenever some game's feed could not answer and none said no, so with both formats `null` can mean one confirmed and the other was unreadable. `finished` comes hours before the league-table refresh (issue #424), so a skill gating a league recap should wait on `league_tables_updated` being `true`, and table mode prints both beside "Finished" | Yes | General | No | direct-api |
45
- | `doctor` | Check the setup for dead IDs, stale data, and config problems: resolves each configured ID against the live API (reporting the team/league name back so a wrong-but-valid ID is visible), verifies per-team data files (team ratings, managers, season preview intel, finances, player prior) describe the current season's clubs, reports whether the calibrated quality ceilings were measured against the newest completed season (the one season-derived artefact frozen in code, so a rollover with no recalibration leaves the scoring scale anchored to a cohort that has turned over), and reports which config/data/cache directories are in use. With `--providers`, probes the external data sources instead: live shape-and-volume checks against the FPL and Draft APIs (including that the draft game's club list agrees with the main game's by club code, since every draft path translates club ids through it), the vaastav and Core-Insights datasets, Understat, and football-data.org, including that every club resolves across sources and that players actually join by name (both Understat checks call the enrichment's own gate and matcher on the payload the scoring commands scan, so probe and runtime cannot disagree; a club resolving does not mean its players' names line up) and that the Core-Insights per-gameweek files parse to records through the parsers the scoring commands use (the drift that otherwise surfaces as plausible but wrong output). Distinguishes broken (needs a fix today) from stale (self-corrects, or waits on an upstream fix the tool works around). Exits non-zero when something is broken | Yes | General | No | direct-api |
45
+ | `doctor` | Check the setup for dead IDs, stale data, and config problems: resolves each configured ID against the live API (reporting the team/league name back so a wrong-but-valid ID is visible), verifies per-team data files (team ratings, managers, season preview intel, finances, player prior) describe the current season's clubs, reports whether the calibrated quality ceilings were measured against the newest completed season (the one season-derived artefact frozen in code, so a rollover with no recalibration leaves the scoring scale anchored to a cohort that has turned over), and reports which config/data/cache directories are in use. With `--providers`, probes the external data sources instead: live shape-and-volume checks against the FPL and Draft APIs (including that the draft game's club list agrees with the main game's by club code, since every draft path translates club ids through it), the vaastav and Core-Insights datasets, Understat, football-data.org, and the Premier League's awards feed the draft Player of the Month voucher reads (run through the recap's own parser, with its winners joined on FPL codes), including that every club resolves across sources and that players actually join by name (both Understat checks call the enrichment's own gate and matcher on the payload the scoring commands scan, so probe and runtime cannot disagree; a club resolving does not mean its players' names line up) and that the Core-Insights per-gameweek files parse to records through the parsers the scoring commands use (the drift that otherwise surfaces as plausible but wrong output). Distinguishes broken (needs a fix today) from stale (self-corrects, or waits on an upstream fix the tool works around). Exits non-zero when something is broken | Yes | General | No | direct-api |
46
46
  | `fixtures` | Show fixtures for a gameweek. With custom analysis on, FDR is the venue-aware ATK/DEF mean `fpl fdr` and `fpl preview` show, so one match reads the same on all three; `-m/--mode` picks `difference` (default) or `opponent`, an unrated club scores a neutral 4.0 rather than the FPL API's 1-5 difficulty, and when the ratings cannot support difficulty at all, table mode warns on stderr and JSON adds a `team_ratings_unusable` entry to `metadata.warnings` (always present, empty or not). With it off, the raw FPL API 1-5 difficulty, and an explicitly passed `-m` reports on stderr that it does not apply. JSON `metadata` carries `custom_analysis`, `fdr_scale` (`team_ratings_1_7` / `fpl_api_1_5`) and `fdr_mode` (null when it does not apply) | Yes | General | No | direct-api |
47
- | `player` | Look up a player's stats, xG, ownership and fixture run. Before GW10 `quality_score` blends last season's pedigree in and JSON `metadata.warnings` says so (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded), same as `fpl stats --value`; `understat_team_unmatched` joins it for any club Understat carried no players for, whose whole squad loses xG enrichment. JSON includes `ep_next` (predicted pts next GW) and `ep_this` (current GW), emitting `null` when FPL has no projection; panel shows `xPts` (ep_next) when available, omits the segment when FPL has no projection. `--detail` (`-d`) shows GW-by-GW performance and, for FWD/MID with sufficient history, xGI sustainability (per-match GI-xGI divergence -> form modifier). With `custom_analysis`: JSON adds `info.adjusted_npxg_per_90` (fixture-adjusted) and `info.raw_npxg_per_90` (Understat season avg); panel shows `adj. npxG/90: X.XXX (raw: Y.YYY)` | Yes | General | No | direct-api |
48
- | `stats` | List players with filtering and sorting. `--value` adds quality/value per £m columns; `--window N` sets rolling lookback (3-10) for `rolling_pts_per_m`. Before GW10, `--value` quality scores blend last season's pts/90 pedigree with this season's observation and the command says so (stderr notice quoting the observation's weight band; JSON `metadata.warnings` code `early_season_prior_informed`); when last season's history cannot be loaded they are pure observation and the notice says that instead (`early_season_small_sample`, before GW6). `understat_team_unmatched` is raised once per club Understat carried no players for, whose whole squad scores without xG. GK ceilings scale with the calendar-possible sample, reaching full scale at GW6. Sortable by `ep_next`/`ep_this` (FPL predicted points; `ep_this` is only meaningful before the gameweek's first kickoff — FPL rolls it forward to `ep_next` as each player's match finishes); both in JSON output (emit `null` when FPL has no projection; table renders `—`). Players with no projection sort to the bottom in either direction | Yes | General | No | direct-api |
47
+ | `player` | Look up a player's stats, xG, ownership and fixture run. Before GW10 `quality_score` blends last season's pedigree in and JSON `metadata.warnings` says so (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded), same as `fpl stats --value`; `understat_team_unmatched` joins it for any club Understat carried no players for, whose whole squad loses xG enrichment. JSON includes `ep_next` (predicted pts next GW) and `ep_this` (current GW), emitting `null` when FPL has no projection; panel shows `xPts` (ep_next) when available, omits the segment when FPL has no projection. `--detail` (`-d`) shows GW-by-GW performance and, for FWD/MID with sufficient history, xGI sustainability (per-match GI-xGI divergence -> form modifier). With `custom_analysis`: JSON adds `info.adjusted_npxg_per_90` (fixture-adjusted) and `info.raw_npxg_per_90` (Understat season avg); panel shows `adj. npxG/90: X.XXX (raw: Y.YYY)`. Under a draft league (format not classic) the `Draft:` line says how the player came to stand where he does, from a replay of the league's draft, waivers and trades (issue #460): `Owned by Alice (round-3 pick)`, `Owned by Bob (GW4 waiver pickup; Cam's round-5 pick)`, `Available (released by Dan in GW5; Dan's round-9 pick)`, `Available (undrafted)`, round unknown where the draft's picks are lost, with a `Draft history:` line beneath when the chain has a step the line does not name; JSON keeps `info.draft_ownership` and adds `info.draft` (owner, acquisition, gameweek, drafter, round/pick, counterparty, full `history`), null with a `draft_provenance_unavailable` warning when the history cannot be shown complete | Yes | General | No | direct-api |
48
+ | `stats` | List players with filtering and sorting. `--value` adds quality/value per £m columns; `--window N` sets rolling lookback (3-10) for `rolling_pts_per_m`. Before GW10, `--value` quality scores blend last season's pts/90 pedigree with this season's observation and the command says so (stderr notice quoting the observation's weight band; JSON `metadata.warnings` code `early_season_prior_informed`); when last season's history cannot be loaded they are pure observation and the notice says that instead (`early_season_small_sample`, before GW6). `understat_team_unmatched` is raised once per club Understat carried no players for, whose whole squad scores without xG. GK ceilings scale with the calendar-possible sample, reaching full scale at GW6. Sortable by `ep_next`/`ep_this` (FPL predicted points; `ep_this` is only meaningful before the gameweek's first kickoff — FPL rolls it forward to `ep_next` as each player's match finishes); both in JSON output (emit `null` when FPL has no projection; table renders `—`). Players with no projection sort to the bottom in either direction. Under a draft league (format not classic) the table adds a Draft column: the owner, tagged with how they came by him (`R3` round-3 pick, `GW4 W` waiver, `GW9 FA` free agent, `GW7 T` trade; issue #460), or `Available`; table only, JSON carries no draft column | Yes | General | No | direct-api |
49
49
  | `history` | Show historical player performance across seasons. JSON `seasons[].team_code` is the permanent club code, not a season's 1-20 team id; `seasons[].team` carries the same number and is deprecated. A provider that refuses exits 1 in both output formats, carrying the upstream's own reason: the error envelope used to say only `Failed to fetch historical data` while the cause went to stderr, and table mode printed the cause to stdout and exited 0 | Yes | General | No | direct-api |
50
50
  | `league` | Show live league standings for Classic and Draft leagues. Classic positions are competition-ranked from cumulative totals — managers level on points share a place and the next distinct total skips the places they consumed — matching `fpl review` and the league history ledger rather than the standings API's sequential rank, which splits such a tie on fewest transfers season-to-date, a figure no column shows. Draft keeps the league's own rank, which already applies the head-to-head points-for tie-break. The Best/Worst GW Performers lists (top three; bottom five classic, bottom three draft) are the ones `fpl review` prints: managers level on the gameweek share a place (`3=`) and a tie at the cut keeps every manager in it, a hit reads `29 net pts (33 gross, -4 hit)`, and a classic user above the bottom five gets a `Your GW rank: R of N` line beside the list | No | General | No | direct-api |
51
51
  | `fdr` | Analyse fixture difficulty - easy runs, blanks, doubles. Three payloads, not one: the default call runs the agent and returns the full analysis plus `data.ratings_warning` (the team-ratings quality verdict, and with `--my-squad` also `data.predictions_stale` / `data.prediction_warnings` / `data.squad_exposure`); with custom analysis off it returns raw 1-5 FPL API difficulty (`data.easy_fixture_runs` only, `metadata.custom_analysis: false`, no ratings and so no warning); `--blanks` bypasses the agent for a schedule-only payload carrying neither the FDR analysis nor `ratings_warning`. `--blanks` mixes confirmed fixtures with predictions; when those predictions are stale, table mode warns on stderr and JSON adds a `fixture_predictions_stale` entry to `metadata.warnings` | Yes | General | No | via-agent |
@@ -55,14 +55,14 @@ see [Feed coverage warnings](../docs/command-reference.md#json-output).
55
55
  | `returnees` | Track injured and suspended players due back soon, so a returnee can be claimed in draft before they are fit or planned around in classic. Reads the FPL availability news, filters to returns inside a window (`--window N`, 1-38) and through a source-aware quality bar, and diffs against the stored watchlist for week-over-week changes. Most flagged players carry no parseable return date, so date-unknown entries are the norm rather than the exception; `--enrich` searches the web for fresher timing on those and on stale-news entries (needs a Perplexity key; skips with a note without one) and shows it beside the FPL news, never over it. `--all` bypasses the quality bar and deliberately does not persist the snapshot. JSON `metadata` carries `window`, `escalation_window`, `stash_upgrade_margin`, `transitions_available`, `transitions_baseline_gameweek` (the gameweek `transition` is measured against — every run in a gameweek diffs against the last state stored before it, so a repeat run reports the same changes), `quality_bar_available`, `quality_bar_applied` and `enrichment_requested`/`_available`/`_note`/`_count`/`_rate_limited` (true when the search provider rate-limited the run even after retrying — the one failure a re-run is likely to clear); `data` is `{entries, departures}`, each entry carrying `quality.basis`/`quality.meets_stash`, `expected_return`/`return_gameweek`/`return_source`, `escalation_eligible`/`escalation_basis`, `transition`, and `enrichment` (null unless enriched) | Yes | General | No | direct-api |
56
56
  | `preview` | Run full pre-gameweek analysis and generate report. `--save` writes to `<output dir>/<season>/gw{N}-preview.md`; `--scout` writes to `<research dir>/ai-scout-reports/<season>/gw{N}-scout-preview[-referenced].md` | No | General | No | via-agent |
57
57
  | `review` | Review a completed gameweek - squad performance and standings. `--save` writes to `<output dir>/<season>/gw{N}-review.md`; `--compare-recs` reads `gw{N}-recommendations.md` from the same season directory and grades the captain, each transfer and each waiver or free-agent move against it; transfer rows that sell the same player are read as ranked alternatives for one sale and graded once, as followed if any candidate was bought and as one miss otherwise; waiver rows that share a drop are read as one conditional chain, each claim keeping its own row: the move made is credited to the claim it filled wherever that claim sits in the chain, a claim below the one that landed is marked not needed rather than not executed, a claim skipped above one that was made is a miss, and a chain abandoned part-way is one miss where it was left, the claims below that marked as fallbacks not reached; the captain line sets the recommended captain against the armband in captain points (raw score times that week's multiplier, Triple Captain included), the armband being the vice's when the captain played no minutes, and adds the change to the team total when the recommended captain was in the side that scored; one who was not in the squad or played no minutes is labelled so rather than priced at 0; on a wildcard or free hit week, where the recommendation is a squad, each player brought in is graded against the recommended 15 instead, and a recommended name that fits more than one player is left ungraded with a warning on stderr rather than matched to the first; a part of the file it could not read (the captain, the transfers or the waivers) is left ungraded the same way, the warning naming the file, rather than grading every move unadvised, and a file nothing could be read from is not compared at all. Under `--summarise` the personal analysis is handed next gameweek's fixtures — every club's opponent and venue plus each squad player's positional FDR (1-7 team ratings with custom analysis on, the FPL API's 1-5 difficulty with it off, carrying the same scale footer and unrated-club caveat the FDR tables print, plus any warning that the ratings cannot support difficulty) — so its "Next Week" calls are grounded in fixtures rather than in one week of points; with no next gameweek to look at, that section is narrowed to fixture-independent observations and points at `fpl gw-prep`, and a gameweek already played is marked retrospective. The answer is then checked back against that block — a cited FDR the block never printed, or any fixture claim made without one — and against the sections its prompt asked for: an answer missing a `## ` heading, ending mid-sentence, or that the provider cut off at its token ceiling is retried once, told what the last draft lacked (a refused answer is flagged but never retried, and a retry that errors keeps the first attempt), and a still-incomplete one is warned about on stderr (detail in `data/debug/synthesis_corrections.txt` under `--debug`, where `synthesis_meta.json` also records how many answers were judged, which was kept, and the tokens they spent between them) and carries the same list as a callout in the saved report. The summary is an add-on the review does not depend on: the two provider roles resolve independently, and a role with no usable key is skipped with the reason on stderr (`Community narrative skipped: ...` / `Personal analysis skipped: ...`) while the review prints, saves and exits 0 — so one key produces that half rather than nothing, and a saved report names each skipped half in a warning callout of its own. A gameweek it cannot resolve - one still being played, an id the season does not have, a season with nothing finished yet - is refused with the specific reason on stderr and exit 1; stdout carries the review or nothing | No | General | No | via-agent |
58
- | `league-recap` | Recap a completed gameweek - awards, standings, and banter. Saves to `<output dir>/<season>/gw{N}-league-recap[-draft].md`, opening on a fixed `# Gameweek N Recap: <league name>` title the report writes itself, editorial or not; the editorial is asked for a `## ` headline beneath it and never a title, and whatever heading it opens with anyway is demoted to that level or, when it only restates the title, dropped, so the model's heading is never the document's (issue #349). Records every run into the league history ledger (`<data dir>/league_history/`) as a side effect, for draft alongside each held draft's picks (`drafts/draft-{id}.json` in the draft partition, saved once while the API still serves them, since a scheduled redraft drops them for good; `league_history_draft_picks_conflict`, `_lost` (once per draft), `_unsaved` and `_unchecked` name a disagreement with the API, a draft held before anything saved it, a run that could not save, and a `league.drafts` list it could not read), backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. The editorial is told to write each race as its own clause and never fold two into one ("respectively"), and is checked against the races afterwards (issue #357): a manager it names as beaten to, or winning, a contested player in a role that player's line does not give them triggers one regeneration told which claims were wrong (a retry that errors keeps the first attempt), and a kept editorial that still fails is saved as written with the wrong claims named on stderr, in a `synthesis_contested_misattribution` JSON warning, and in a callout above the editorial in the report. The same retry and reporting cover two checks on either format (issue #359): a manager's net overall, pre-hit swing or another move's swing pinned on one named move (`synthesis_net_misattribution`), and a tie word ("joint-lowest", "tied for last", "joint-seventh") on a gameweek score or league position no two managers hold (`synthesis_unsupported_tie`); and a third on draft (issue #390): the net overall, or the other kind's figure, said of one kind of a manager's moves ("his waiver moves netted +3 overall" of a -1 waiver and a +4 free-agent pickup, `synthesis_kind_net_misattribution`), for which a roster line that mixes kinds now gives each kind's net beside the net overall. A fourth, on draft (issue #459), holds the editorial to not reciting a race as a list: a paragraph naming the race's player and three or more of its beaten managers (`synthesis_contested_roll_call`) earns the same retry and reporting, but as a broken brief rather than a contradiction of the data, since the sentence is true, so the retry, stderr and the report callout say that instead. The prompt's GW Standings section names the week's highest and lowest scores, every shared score and position (Pos, and Prev where any is shared) and any season totals the league's tie-break placed apart, so "joint" has a line to be held to, and the roster lines call the manager's net "overall". Biggest Bench Haul (both formats) ranks on the best legal XI's gain over the XI that counted — one keeper, the eight legal formations, the doubled player held in place — never on the bench's raw sum, which counted a keeper who could not have played and outfielders no formation had room for (issue #350); its detail names the swap, a manager with bench points but no better XI is no candidate, and the raw sum stays on the row as `bench_points` (the API's `points_on_bench`) because the coarse tier can record nothing else. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `synthesis_attempts` (answers judged: 1, or 2 after a retry), `synthesis_shipped_attempt` (which of them shipped) and `synthesis_usage` (input and output tokens summed over every answer judged; all three `null` when no editorial came back), `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) `first_capture_store_path` (always present; the partition directory on a first capture, `null` after) and `draft_picks_saved` (always present; the draft-picks snapshots this run wrote, empty on classic). Fines are ruled *and recorded* against every captured gameweek, backfilled ones included: the detailed replay rules every configured rule, the coarse tier rules only what cohort points support (`last-place`, `below-threshold`) and records that `red-card` went unruled. `last-place` is ruled against every manager level on the gameweek's lowest score rather than whichever of them the cohort's arrival order reached first; because rulings are frozen at capture and the ledger is append-only, gameweeks recorded before that was true are swept on every run and the recorded ruling appended against the managers they left out, with no flag and no request (`league_history_tied_last_place_repaired`) — and where the row data cannot settle who was level, since the gameweek does not record whether it was ruled on gross or net points, the manager is named by `league_history_tied_last_place_ambiguous` rather than fined on a guess. Season totals print under `Season Fines` on console and as a `# Season Fines` report section, but only at the two milestone gameweeks (GW19 and the finale) - every other week those surfaces stay a this-week view and `league-fines` answers the season question on demand. The `## Season Fines` prompt section and `metadata.season_fines` are ungated: the editorial gets the running totals every week as optional colour (never a required beat), and a scripted consumer sees no calendar-driven appearance and disappearance. Bottom-half gameweeks and green-arrow droughts no longer surface as streaks at all (their run only drives the season count). Streak conditions also keep reset-surviving season occurrence counts ("their sixth gameweek win of the season"), each surfaced on its own rule (see [Command Reference](../docs/command-reference.md#league-recap) for the per-count table): wins and last places on multiples of 3 plus any second-half first, captain blanks and waiver hauls/backfires on multiples of 5, hits on multiples of 3, weeks on top on multiples of 5, bottom-half gameweeks on multiples of 10 in the second half only, and green-arrow droughts on an unbroken run of 5 or 10 (gameweeks spent leading the league never count towards a drought — the leader could not have climbed). Some counts carry same-week peers alongside the milestone — past a fixed floor for counts that stay small, or within a relative window for counts that climb all season. A firing count reaches the report's `## Season Counts` subsection and the editorial's League History section; at the two milestone gameweeks both surfaces carry every nonzero count as the season set-piece. Every line states its span and any not-judged gameweeks beside the number, and every nonzero count is emitted in `metadata.notes_pack.season_count_entries` regardless. A recap of the most recently finished gameweek keeps working once the next gameweek has started: the league table describes that later gameweek from its deadline onward, so the run takes the replay path (figures from each manager's own gameweek history, positions and cumulative totals derived rather than read off the table) and raises a `league_standings_moved_on` warning, instead of reconciling two different gameweeks' numbers against each other and exiting 1 with the ledger row unwritten. `--summarise` with no usable synthesis key no longer aborts the run: the recap, report and ledger capture all proceed, and a `synthesis_provider_unavailable` warning says the editorial was skipped; an editorial the provider cut off at its token ceiling is regenerated once, told it was cut off (a refused editorial is never regenerated, even with a wrong claim in it), and one the provider stopped early on that is still the one kept is written out as it stands with the reason on stderr, a `synthesis_stopped_early` warning in JSON, and a line in the report's warning callout above the editorial (issue #372). A gameweek that cannot be resolved at all exits 1 in both output formats - it used to exit 1 under `--format json` with a generic `"Could not resolve a gameweek to recap."` and 0 in table mode - and the resolver's own reason (`Gameweek 7 is not yet finished`) now reaches the error envelope and the stderr prose alike. On the league's opening gameweek only (GW1, or its own `start_event`), each classic manager's FPL-wide prior-season record — seasons played, last season's points/rank/top-% finish, best season — is fetched from the manager-history endpoint (one request per manager, that gameweek only, and only with `--save`/`--output` or `--summarise`; a league created after GW1 shares the fetch with its league-start offset) and rendered as a `# Prior Seasons` report section and a locked `## Prior Seasons` prompt section with an explicit count; tenure is phrased FPL-wide because entry and league IDs are reissued each season, and a manager with no record or a failed fetch is named as such rather than omitted. Nothing of it reaches the ledger or the JSON rows; draft has no such endpoint and no such section. Every recap, classic and draft, also carries a Standout Players section (issue #455), read off the squads and live points the bench haul already uses: the week's top scorers in any manager's squad and its costliest blanks (2 points or fewer; in classic the one that counted for the most managers first, in draft the best by FPL's pre-season draft rank and, on a live recap from GW19, by season points per game, since the rank is never updated; a draft player neither can rank is left off, and no line states the rank or average, which only the heading and JSON carry), each with the managers he counted for, how long a blank played when it was under the hour, and any who left him on the bench, as one sentence per player that the console, a `# Standout Players` report section and a `## Standout Players` prompt section all print; `--format json` carries it as `metadata.standout_players`, every player row with the main game's `id` (under `--draft` too) and `code` beside it. A draft recap's lines also say how each player came to the manager they name (issue #458): the draft pick, the waiver or free-agent move, or the trade, the latest as of the recapped gameweek (a `-g` replay shows its own week), from a replay of the league's draft, accepted transactions and processed trades that must end where FPL's own ownership list does and match the week's squads, with the draft's picks read from the choices feed or, once it serves another draft, the saved snapshot, or, where neither has them, who drafted whom rebuilt from each manager's squad at the draft's gameweek with the earlier moves undone (the line then says "who drafted him" with no round, `round_unknown` in the JSON, and a `draft_rounds_unknown` notice replaces the warning). If the history cannot be shown complete (or a read fails, a failed trades read included once a trade has happened) the lines read without it and a `draft_provenance_unavailable` warning says why; draft rows carry `acquisition` in the JSON, classic rows do not. Before a live capture collects, it reads the recapped game's processing feed (`event-status/` on `FPLClient`, `pl/event-status` on `FPLDraftClient`, both `get_gameweek_status()`): when the feed's match days for the recapped gameweek show bonus unconfirmed or the league tables still "Updating" (`GameweekStatus.still_processing()`), it warns on stderr, in a `league_tables_updating` JSON warning and in a callout at the top of a saved report that positions and totals may be stale, and records the gameweek anyway, since a later run supersedes it and a missed draft gameweek cannot be recaptured; a reconciliation failure in that window keeps its own diagnosis and adds that it may be timing. Only a definite signal warns: an unreadable, slow (5s), reshaped or unrecognised feed is a debug log, and a replay never reads it (issue #424) | Yes | General | No | via-agent |
58
+ | `league-recap` | Recap a completed gameweek - awards, standings, and banter. Saves to `<output dir>/<season>/gw{N}-league-recap[-draft].md`, opening on a fixed `# Gameweek N Recap: <league name>` title the report writes itself, editorial or not; the editorial is asked for a `## ` headline beneath it and never a title, and whatever heading it opens with anyway is demoted to that level or, when it only restates the title, dropped, so the model's heading is never the document's (issue #349). Records every run into the league history ledger (`<data dir>/league_history/`) as a side effect, for draft alongside each held draft's picks (`drafts/draft-{id}.json` in the draft partition, saved once while the API still serves them, since a scheduled redraft drops them for good; `league_history_draft_picks_conflict`, `_lost` (once per draft), `_unsaved` and `_unchecked` name a disagreement with the API, a draft held before anything saved it, a run that could not save, and a `league.drafts` list it could not read), backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. The editorial is told to write each race as its own clause and never fold two into one ("respectively"), and is checked against the races afterwards (issue #357): a manager it names as beaten to, or winning, a contested player in a role that player's line does not give them triggers one regeneration told which claims were wrong (a retry that errors keeps the first attempt), and a kept editorial that still fails is saved as written with the wrong claims named on stderr, in a `synthesis_contested_misattribution` JSON warning, and in a callout above the editorial in the report. The same retry and reporting cover five checks on either format: a manager's net overall, pre-hit swing or another move's swing pinned on one named move (`synthesis_net_misattribution`, issue #359), a tie word ("joint-lowest", "tied for last", "joint-seventh") on a gameweek score or league position no two managers hold, or on the season's highest or lowest score where its season record was scored only once, a place in a milestone's highs or lows table nobody shares, or a manager's own season best or worst nobody equalled this gameweek (`synthesis_unsupported_tie`, issues #359, #494 and #496), one of this gameweek's scores given the league's best or worst of the season where the season-record line names another ("a new season low of 30", "the week of the season: 65 points"), unless a manager owns it ("his own season low") (`synthesis_unsupported_season_record`, issue #496), a placing on the week the manager's gameweek score does not earn, said of the week ("third on the week") or set beside that score ("finished eighth on 31", a league position read as the week's finish) (`synthesis_wrong_gameweek_placing`, issue #486), and an escape from last place given to a manager who finished there ("He escaped the wooden spoon" of the week's lowest scorer, or "clear of the foot of the table" of the manager bottom of it) (`synthesis_escape_misattribution`, issue #488); and a fourth on draft (issue #390): the net overall, or the other kind's figure, said of one kind of a manager's moves ("his waiver claims netted +3 overall" of a -1 waiver and a +4 free-agent pickup, `synthesis_kind_net_misattribution`; a generic waiver word such as "his waiver work" may mean all of a manager's moves, as the waiver awards are, and may carry the net overall, issue #539), for which a roster line that mixes kinds now gives each kind's net beside the net overall. A fifth, on draft (issue #459), holds the editorial to not reciting a race as a list: a paragraph naming the race's player and three or more of its beaten managers (`synthesis_contested_roll_call`) earns the same retry and reporting, but as a broken brief rather than a contradiction of the data, since the sentence is true, so the retry, stderr and the report callout say that instead; so does a fine's forfeit written out word for word more than once (`synthesis_penalty_repeated`, issue #503), which the prompt allows once, headline included, every other mention in other words or, where the `fines:` block or a rule sets `penalty_phrasings`, only those (a bare "the forfeit" counts, a short aside alluding to it may be in the model's own words, and only the headline may pun on it in its own words, never into more of it or a different act), while the Fines list and the ledger keep the configured `penalty`; and so does the brief's own wording read out to the league (`synthesis_brief_echoed`, issue #517): a prompt section named as a section, "exact phrase", "at most once", "official wording" or the "Naming the forfeit" label; and so does a false claim about who was fined this gameweek (`synthesis_fine_claim_contradicted`, issue #516): a manager called the only one without a fine where the Fines section's not-fined list holds more than them (or the only one on the week's highest or lowest score while another on it went unfined), or "a double fine", "fined twice" or "two fines" for a manager with other than two fine lines. The prompt's GW Standings section names the week's highest and lowest scores, every shared score and position (Pos, and Prev where any is shared) and any season totals the league's tie-break placed apart, so "joint" has a line to be held to, and the roster lines call the manager's net "overall". Biggest Bench Haul (both formats) ranks on the best legal XI's gain over the XI that counted — one keeper, the eight legal formations, the doubled player held in place — never on the bench's raw sum, which counted a keeper who could not have played and outfielders no formation had room for (issue #350); its detail names the swap, a manager with bench points but no better XI is no candidate, and the raw sum stays on the row as `bench_points` (the API's `points_on_bench`) because the coarse tier can record nothing else. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. A `--save` whose report cannot be written exits 1 with `Failed to save report: <reason>` on stderr; under `--format json` that is the `{command, error}` envelope, with the codes of any capture warnings named in the message and any editorial printed on stderr first (issue #474). `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `synthesis_attempts` (answers judged: 1, or 2 after a retry), `synthesis_shipped_attempt` (which of them shipped) and `synthesis_usage` (input and output tokens summed over every answer judged; all three `null` when no editorial came back), `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) `first_capture_store_path` (always present; the partition directory on a first capture, `null` after) and `draft_picks_saved` (always present; the draft-picks snapshots this run wrote, empty on classic). Fines are ruled *and recorded* against every captured gameweek, backfilled ones included: the detailed replay rules every configured rule, the coarse tier rules only what cohort points support (`last-place`, `below-threshold`) and records that `red-card` went unruled. `last-place` is ruled against every manager level on the gameweek's lowest score rather than whichever of them the cohort's arrival order reached first; because rulings are frozen at capture and the ledger is append-only, gameweeks recorded before that was true are swept on every run and the recorded ruling appended against the managers they left out, with no flag and no request (`league_history_tied_last_place_repaired`) — and where the row data cannot settle who was level, since the gameweek does not record whether it was ruled on gross or net points, the manager is named by `league_history_tied_last_place_ambiguous` rather than fined on a guess. Season totals print under `Season Fines` on console and as a `# Season Fines` report section, but only at the two milestone gameweeks (GW19 and the finale) - every other week those surfaces stay a this-week view and `league-fines` answers the season question on demand. The `## Season Fines` prompt section and `metadata.season_fines` are ungated: the editorial gets the running totals every week as optional colour (never a required beat), and a scripted consumer sees no calendar-driven appearance and disappearance. Bottom-half gameweeks and green-arrow droughts no longer surface as streaks at all (their run only drives the season count). Streak conditions also keep reset-surviving season occurrence counts ("their sixth gameweek win of the season"), each surfaced on its own rule (see [Command Reference](../docs/command-reference.md#league-recap) for the per-count table): wins and last places on multiples of 3 plus any second-half first, captain blanks and waiver hauls/backfires on multiples of 5, hits on multiples of 3, weeks on top on multiples of 5, bottom-half gameweeks on multiples of 10 in the second half only, and green-arrow droughts on an unbroken run of 5 or 10 (gameweeks spent leading the league never count towards a drought — the leader could not have climbed). Some counts carry same-week peers alongside the milestone — past a fixed floor for counts that stay small, or within a relative window for counts that climb all season. A firing count reaches the report's `## Season Counts` subsection and the editorial's League History section; at the two milestone gameweeks both surfaces carry every nonzero count as the season set-piece. Every line states its span and any not-judged gameweeks beside the number, and every nonzero count is emitted in `metadata.notes_pack.season_count_entries` regardless. The editorial's League History section also states the league's highest and lowest gameweek score this season with who scored each and when, counted as the standings count the week (gross, or net under `use_net_points`) and qualified by any gameweek not every score of which was recorded, and the system prompt allows a season superlative ("the week of the season") only where that line names it (issue #494); prompt-only, absent on the league's first gameweek, and in `metadata.notes_pack.season_record_entries` with `points` and `holders`. It also states a manager's own highest or lowest gameweek score this season when this gameweek set or equalled it, beside the record before it ("above their previous highest of 61 in GW9"), once the manager has ten recorded gameweeks of their own behind it; the system prompt allows "their best week of the season" only where such a line says so (issue #496). Prompt-only too, and in `metadata.notes_pack.personal_record_entries` for every manager every week, with `points`, `holders`, `previous_points` and `previous_holders`. At GW19 and the finale the report's League History section adds Season Highs and Lows, two tables ranking every manager's own highest gameweek score highest first and lowest worst first, places shared on a tie, which the editorial also gets, in `metadata.notes_pack.personal_record_tables`. A recap of the most recently finished gameweek keeps working once the next gameweek has started: the league table describes that later gameweek from its deadline onward, so the run takes the replay path (figures from each manager's own gameweek history, positions and cumulative totals derived rather than read off the table) and raises a `league_standings_moved_on` warning, instead of reconciling two different gameweeks' numbers against each other and exiting 1 with the ledger row unwritten. `--summarise` with no usable synthesis key no longer aborts the run: the recap, report and ledger capture all proceed, and a `synthesis_provider_unavailable` warning says the editorial was skipped; an editorial the provider cut off at its token ceiling is regenerated once, told it was cut off (a refused editorial is never regenerated, even with a wrong claim in it), and one the provider stopped early on that is still the one kept is written out as it stands with the reason on stderr, a `synthesis_stopped_early` warning in JSON, and a line in the report's warning callout above the editorial (issue #372). A gameweek that cannot be resolved at all exits 1 in both output formats - it used to exit 1 under `--format json` with a generic `"Could not resolve a gameweek to recap."` and 0 in table mode - and the resolver's own reason (`Gameweek 7 is not yet finished`) now reaches the error envelope and the stderr prose alike. On the league's opening gameweek only (GW1, or its own `start_event`), each classic manager's FPL-wide prior-season record — seasons played, last season's points/rank/top-% finish, best season — is fetched from the manager-history endpoint (one request per manager, that gameweek only, and only with `--save`/`--output` or `--summarise`; a league created after GW1 shares the fetch with its league-start offset) and rendered as a `# Prior Seasons` report section and a locked `## Prior Seasons` prompt section with an explicit count; tenure is phrased FPL-wide because entry and league IDs are reissued each season, and a manager with no record or a failed fetch is named as such rather than omitted. Nothing of it reaches the ledger or the JSON rows; draft has no such endpoint and no such section. Every recap, classic and draft, also carries a Standout Players section (issue #455), read off the squads and live points the bench haul already uses: the week's top scorers in any manager's squad and its costliest blanks (2 points or fewer; in classic the one that counted for the most managers first, in draft the best by FPL's pre-season draft rank and, on a live recap from GW19, by season points per game, since the rank is never updated; a draft player neither can rank is left off, and no line states the rank or average, which only the heading and JSON carry), each with the managers he counted for, how long a blank played when it was under the hour, and any who left him on the bench, as one sentence per player that the console and, with `--summarise`, a `## Standout Players` prompt section print, never the saved report, where the editorial already tells the same week in prose (issue #482); `--format json` carries it as `metadata.standout_players`, every player row with the main game's `id` (under `--draft` too) and `code` beside it. A draft recap's lines also say how each player came to the manager they name (issue #458): the draft pick, the waiver or free-agent move, or the trade, the latest as of the recapped gameweek (a `-g` replay shows its own week), from a replay of the league's draft, accepted transactions and processed trades that must end where FPL's own ownership list does and match the week's squads, with the draft's picks read from the choices feed or, once it serves another draft, the saved snapshot, or, where neither has them, who drafted whom rebuilt from each manager's squad at the draft's gameweek with the earlier moves undone (the line then says "who drafted him" with no round, `round_unknown` in the JSON, and a `draft_rounds_unknown` notice replaces the warning). If the history cannot be shown complete (or a read fails, a failed trades read included once a trade has happened) the lines read without it and a `draft_provenance_unavailable` warning says why; draft rows carry `acquisition` in the JSON, classic rows do not. On the gameweek that closes one of FPL's months (bootstrap `phases`, read by `FPLClient.get_months()`; a draft league uses the main game's), the recap adds Manager of the Month (the month's top scorer, named with the rest of the top three) and Dud of the Month (its lowest) to the console, the report's Awards and, with `--summarise`, the editorial's, summed from the ledger the way FPL's own month table sums it (net of hits, except a hit for the month's first gameweek, which FPL never charges), ties shared, and both withheld with the reason when any manager in the month's table lacks a score (or, on classic, a hit) for one of its gameweeks, or when the recap reads only part of the league (classic standings past one page); derived every run, never stored (issue #425). The halfway (GW19) and final (GW38) recaps list each completed month's awards under `Monthly Honours` on console and as a `# Monthly Honours` report section, the printed season fines table's rhythm; the editorial gets the same list on those two recaps and on every month's last gameweek, and `metadata.monthly_honours` carries it every week. The month a classic recap closes is cross-checked against FPL's own month table (`?phase=N`) before either award is placed, a disagreement withholding both everywhere and raising `league_month_table_disagrees`; the editorial's tie check reads a month tie against that month's own table; months the bootstrap does not list readably skip both with `league_months_unavailable`. A draft league that sets a `player-of-the-month` entry under `fines.draft_vouchers` also gets the Premier League's Player of the Month (issue #492), read from the Premier League's own awards feed (`PremierLeagueClient`, whose player id is FPL's `code`) and announced once, by the first recap of the latest finished gameweek after the award is out (so a skipped recap still gets it, and a replay of an older gameweek never claims it or asks the feed; the announcing recap writes a record under the draft partition's `awards/`): an Awards line naming who held him for the month's last gameweek, bench included, read off the ledger's squads every run (with who held him when, where he changed hands), and a Fines section line saying that manager may hand out one fine, the voucher's `penalty`, to anyone they choose. A grant, never a ruling, so `league-fines` never counts it. Nobody holding him at the month's end means no voucher, said so; a month-end gameweek that cannot say (a missing or unknown row, an unidentified pick, a league read in part) withholds it with the reason, and the first latest-gameweek recap that can say settles it, once; an unreadable or reshaped feed, or a winner whose code FPL does not know, is "not checked" with `player_of_the_month_unchecked`, never "no award"; the voucher's `overrides` (month to code) win over the feed. `metadata.player_of_the_month` carries every completed month's winner (`player` with `code`, no `id`), owner, holders and announcement gameweek every week; classic never shows it. Before a live capture collects, it reads the recapped game's processing feed (`event-status/` on `FPLClient`, `pl/event-status` on `FPLDraftClient`, both `get_gameweek_status()`): when the feed's match days for the recapped gameweek show bonus unconfirmed or the league tables still "Updating" (`GameweekStatus.still_processing()`), it warns on stderr, in a `league_tables_updating` JSON warning and in a callout at the top of a saved report that positions and totals may be stale, and records the gameweek anyway, since a later run supersedes it and a missed draft gameweek cannot be recaptured; a reconciliation failure in that window keeps its own diagnosis and adds that it may be timing. Only a definite signal warns: an unreadable, slow (5s), reshaped or unrecognised feed is a debug log, and a replay never reads it (issue #424) | Yes | General | No | via-agent |
59
59
  | `league-fines` | Show who has been fined this season and how much of it was actually ruled, folded out of the league history ledger. Reads local files only - no network - so it works for any season still on disk (`--season 2025-26`). Nothing is re-ruled: a fine is counted exactly as its gameweek recorded it — including one `league-recap` appended against a manager who tied for last and was left out at capture. Counts, never money (`penalty` is free text; settlement and escalation are not modelled). Every gameweek that could not be ruled is named beneath the table - never captured, unreadable, captured before rulings were recorded, captured with no rules configured, or captured at a fidelity that could not rule a given rule. `--format json` emits one entry per recorded manager, fined or not, each with `counts` (zero included), `total`, `fined_gameweeks`, `ruled_gameweeks`, `unruled_gameweeks`, `first_recorded_gameweek`, `last_recorded_gameweek` and `is_fully_ruled`; `metadata` carries `season`, `fpl_format`, `league_id`, `gameweek`, `start_gameweek`, `rule_types`, `total_fines` and `qualifiers`. Exits 1 with the shared error envelope when no league id is configured for the format | Yes | General | No | direct-api |
60
60
  | `captain` | Analyse and rank captain options for next gameweek. Your squad is the default, so an absent `classic_entry_id` exits 1 with an `error` envelope naming `--global` rather than returning the global list as though it were your options; `metadata.my_squad_mode` says which list the payload holds, and `metadata.warnings` carries `captain_global_fallback` when there is no squad to rank yet (before the first deadline) or `captain_no_next_gameweek` when there is no next gameweek at all (finished season, or the next one unpublished). An entry ID that does not resolve is a failure, not a fallback, worded exactly as `squad` words it (`get_own_squad_picks`, `agents/common.py`). JSON candidates include `adjusted_npxg_per_90` and `raw_npxg_per_90` when fixture adjustment is active. Consistency tiebreaker (CV-xGI percentile) phased in GW6-10. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the GW1-9 shrinkage rather than pulled toward the position mean | Yes | Classic | Yes | via-agent |
61
61
  | `differentials` | Find differential picks - high potential, low ownership. Inverted consistency bonus (volatile players score higher, phased in GW6-10). Before GW10 the score's quality baseline carries the prior blend (last season's pedigree weighted by confidence) rather than position-mean shrinkage, so a quiet-starting elite is not ranked below a one-game wonder; the ownership, matchup and consistency bonuses stay pure observation; JSON `metadata.warnings` carries the early-season notice while the blend is live (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded) plus `understat_team_unmatched` for any club Understat carried no players for, table mode prints them to stderr. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the blend rather than handed last season's standing. The gameweek stamped into JSON `metadata` is fetched after the agent succeeds, not before it runs, so an unreachable API is reported as the agent's failure in both output formats rather than as a client error under `--format json` and an agent error in table mode | Yes | Classic | Yes | via-agent |
62
62
  | `targets` | Find transfer targets - high performers across all ownership. Consistency bonus (CV-xGI percentile, phased in GW6-10). Before GW10 the score's quality baseline carries the prior blend (last season's pedigree weighted by confidence) rather than position-mean shrinkage, so a quiet-starting elite is not ranked below a one-game wonder; the matchup and consistency bonuses stay pure observation, and the score agrees with `fpl transfer-eval`'s Outlook column; JSON `metadata.warnings` carries the early-season notice while the blend is live (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded) plus `understat_team_unmatched` for any club Understat carried no players for, table mode prints them to stderr. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the blend rather than handed last season's standing | Yes | Classic | Yes | via-agent |
63
63
  | `transfer-eval` | Compare transfer OUT player against IN candidates. JSON includes `adjusted_npxg_per_90` and `raw_npxg_per_90` per player when fixture adjustment is active. Before GW10 `quality_score` carries the value family's prior blend (matching `fpl player` and `fpl stats --value`) and `target_score` the target family's (matching `fpl targets`); the single-GW `lineup_score` keeps position-mean shrinkage. JSON `metadata.warnings` carries the early-season notice naming both blended columns, or only `target_score` when no Understat match produced a quality score. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of whichever device applies, rather than pulled toward the position mean or handed last season's standing. A name two players answer to exactly (`Henderson`) exits 1 with an `error` envelope naming both, rather than resolving to whichever the API lists first; `Name (TEAM)` or a player ID picks one | Yes | General | Yes | via-agent |
64
64
  | `allocate` | Select mathematically optimal 15-player squad within budget. At `--horizon >= 2` the solver ranks on the value family's GW1-9 prior blend (last season's pedigree weighted by confidence); players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of it rather than handed last season's standing, and stay in the solver pool; JSON `metadata.warnings` carries the early-season notice at `--horizon >= 2` (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded), plus `understat_team_unmatched` for any club Understat carried no players for, whose squad therefore scored without xG | Yes | Classic | Yes | direct-api |
65
- | `waivers` | Show waiver recommendations for your draft league. JSON `data` carries `top_targets` and `targets_by_position` (ranked claims) plus `pool` — the full unowned roster, one `{id, code, player_name, position, team_short}` row each, unranked and untruncated, so a consumer can answer "is this player claimable at all" without inferring it from the ranked lists. On every player row the command emits `id` is the draft game's element id and `code` the cross-game key: the gw-prep skill joins the returnee radar's main-game rows to the pool on `code`, since a mid-season addition holds a different id in each game (#418). Keeper scores carry the GK signal block (saves/90, defensive quality, clean-sheet rate), read from the main-game player each draft element is joined to on `code` because the draft bootstrap publishes none of it, so draft keepers rank on what separates them rather than on form and ppg alone. Defender scores carry DC/90 through the same join, since the draft bootstrap has defensive contribution only as a season total; every row's `dc_per_90` (pool, `current_squad`, `recent_releases`) is the main-game rate, weighted only in the defender score and display-only for other positions, and a defender the join misses stays at 0. A keeper or defender in the unowned pool that the join misses is reported as one `draft_player_unjoined` entry in `metadata.warnings` (stderr in table mode) with the count of each and the first three names, since the player ranks against an anchor calibrated with the term it lacks. Before GW10 the score's quality baseline carries the prior blend (last season's pedigree weighted by confidence) rather than position-mean shrinkage; the matchup, position-need, team-stacking and consistency terms stay pure observation; JSON `metadata.warnings` carries the early-season notice while the blend is live, and reports `early_season_small_sample` both when history could not be loaded and when no draft element found a main-game counterpart to take a prior from. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the blend rather than handed last season's standing. Each row's `team_id` is the main game's team id, translated from the draft game's own club numbering by club code (null for a club the main game does not list, which `metadata.warnings` reports as `draft_club_untranslated`; the row keeps the club's draft name), since fixtures, form and FDR are all keyed by it. No `draft_league_id` configured exits 1 in both output formats — table mode used to warn and exit 0 | Yes | Draft | Yes | via-agent |
65
+ | `waivers` | Show waiver recommendations for your draft league. JSON `data` carries `top_targets` and `targets_by_position` (ranked claims) plus `pool` — the full unowned roster, one `{id, code, player_name, position, team_short}` row each, unranked and untruncated, so a consumer can answer "is this player claimable at all" without inferring it from the ranked lists. On every player row the command emits `id` is the draft game's element id and `code` the cross-game key: the gw-prep skill joins the returnee radar's main-game rows to the pool on `code`, since a mid-season addition holds a different id in each game (#418). Keeper scores carry the GK signal block (saves/90, defensive quality, clean-sheet rate), read from the main-game player each draft element is joined to on `code` because the draft bootstrap publishes none of it, so draft keepers rank on what separates them rather than on form and ppg alone. Defender scores carry DC/90 through the same join, since the draft bootstrap has defensive contribution only as a season total; every row's `dc_per_90` (pool, `current_squad`, `recent_releases`) is the main-game rate, weighted only in the defender score and display-only for other positions, and a defender the join misses stays at 0. The join also carries every player's history, prior, adjusted npxG and consistency signals, so a player in the unowned pool that the join misses is reported as one `draft_player_unjoined` entry in `metadata.warnings` (stderr in table mode) naming the terms live that gameweek, with the count per position and the first three names in ranked order, since his score is built from a different mix of terms (keepers and defenders also lose their position-specific term, and count even when no shared term is live) and can read high or low; an empty main-game player list is one sentence instead of counts. 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. Every row outside `pool` carries `appearances`, derived from total points over ppg as the main game derives it, which the minutes factor reads: from GW6 a player with none loses his per-90 and matchup terms. Each row's `team_id` is the main game's team id, translated from the draft game's own club numbering by club code (null for a club the main game does not list, which `metadata.warnings` reports as `draft_club_untranslated`; the row keeps the club's draft name), since fixtures, form and FDR are all keyed by it. No `draft_league_id` configured exits 1 in both output formats — table mode used to warn and exit 0 | Yes | Draft | Yes | via-agent |
66
66
  | `squad` | Analyse your FPL squad health and fixtures. Exits 1 with an `error` envelope when no entry ID is configured for the active format. `--classic` / `--draft` (mutually exclusive) pin the format instead of inferring it from the configured IDs, so a caller that means classic gets the missing-ID error rather than the draft roster; with neither, single-format configs still auto-select. A picks 404 is checked against `entry/<id>/` before it is reported as "no squad submitted yet", so a reissued or wrong entry ID says so; `squad grid` and `captain` share that diagnosis through `agents/common.py` | Yes | General | No | via-agent |
67
67
  | `squad grid` | Show squad fixture difficulty grid. Exits 1 with an `error` envelope when no entry ID is configured for the active format, or when the squad fetch fails. Takes the same `--classic` / `--draft` pinning as `squad`, before or after the subcommand, and the same stale-entry diagnosis. A `--watch` name that does not resolve is non-fatal either way: unmatched, or matching two players exactly, it warns on stderr and is skipped, and the grid still renders | Yes | General | No | via-agent |
68
68
  | `squad sell-prices` | Show squad sell prices and financial breakdown. A cached run recomputes each sell price at today's price, applying FPL's rule only to a player whose cached sell price it reproduces at the price stored with the scrape; JSON rows carry `scraped_sell_price` beside `sell_price`, and a player it cannot check keeps the scraped price under a `sell_prices_as_scraped` warning, on stderr in both modes (also given, with exit 0, when today's prices cannot be fetched). `transfer-eval` budgets on the same recomputed price and warns when the OUT player's is unchecked or not cached; `allocate --sell-prices` repeats the file's warning. Nothing cached and no `--refresh`, a failed scrape, draft format, or a scrape the extraction heuristics distrust each exit 1 with an `error` envelope under `--format json`; table mode still renders a suspect scrape under a `Squad Budget (Suspect)` panel, since a reader can judge it and `fpl allocate --sell-prices` cannot | Yes | Classic | No | direct-api |
@@ -105,6 +105,7 @@ Skills write their own outputs (`gw{N}-recommendations.md`, `gw{N}-squad-builder
105
105
  | 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) |
106
106
  | 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 |
107
107
  | 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 |
108
+ | ab-brief | `skills/ab-brief/` | Write the brief a fresh fpl-workspace session runs to A/B a reviewed league-recap prompt or check change (gate on review, classify fresh runs vs corpus replay, pin both arms' SHAs, per-shape expectations), subscribe to the PR, and act on the `ab-handback` review the vault session posts back | Full: Claude Code (PR subscription wakes the session). Partial: Codex, Cursor, Copilot (no subscription) |
108
109
 
109
110
  ## Analysis Agents
110
111
 
@@ -0,0 +1,401 @@
1
+ ---
2
+ name: ab-brief
3
+ description: >
4
+ Write the brief a fresh fpl-workspace (vault) session runs to A/B a
5
+ league-recap prompt or editorial-check change, once its fpl-cli PR has
6
+ been through code review, and act on the handback review that session
7
+ posts on the PR. Use when the user says "A/B brief", "prompt for a vault
8
+ session", "give me a prompt for a fresh vault session to A/B this", or
9
+ "write the brief for the A/B"; as the step the create-pr skill names
10
+ after review for a PR touching fpl_cli/prompts/ or a data section the
11
+ model is shown; and when a review carrying the ab-handback marker lands
12
+ on the PR.
13
+ model: opus
14
+ compatibility:
15
+ claude-code: full (GitHub MCP on the web, gh locally; subscribe_pr_activity for the wake-up)
16
+ codex: partial (no PR subscription, so the user says when the handback is posted)
17
+ cursor: partial (no PR subscription)
18
+ copilot: partial (no PR subscription)
19
+ ---
20
+
21
+ <!-- Runs in the fpl-cli repo; its brief runs in a fresh fpl-workspace session. -->
22
+
23
+ # A/B Brief
24
+
25
+ Hand a league-recap A/B to a dedicated vault session, and close the loop
26
+ when its results come back.
27
+
28
+ A change to a prompt, a data section the model is shown, or an editorial
29
+ check is verified by the vault's harness (`.claude/scripts/recap_ab/` in
30
+ fpl-workspace) before it merges: CLAUDE.md's prompt-change rule. The work
31
+ splits across two sessions, and this skill is the seam between them:
32
+
33
+ - **The fpl-cli session owns the code.** It writes the change, takes it
34
+ through code review, runs this skill, and makes every fix the round
35
+ turns up.
36
+ - **The vault session owns the test.** It has the FPL credentials, LLM
37
+ keys, real leagues and the drafts corpus, and it never changes fpl-cli.
38
+
39
+ The forward leg is copy and paste: this skill prints a brief, the user
40
+ pastes it into a fresh vault session. The return leg is a PR review: the
41
+ vault session posts one comment-only review on the fpl-cli PR, which wakes
42
+ this session because Step 6 subscribed it to the PR first.
43
+
44
+ **This session never runs the A/B or the replay, not even "just to
45
+ check".** Twice in October an fpl-cli session did the vault's work itself,
46
+ once three whole rounds and once a corpus replay the user had to stop.
47
+ That overlap is what the split exists to end: two sessions testing one PR
48
+ produce two sets of numbers and no owner for either.
49
+
50
+ Everything here is written for a public repo. The skill, the PR, its
51
+ commits and the handback carry no manager names, entry IDs, manager_keys
52
+ or league IDs; examples use Manager A, Manager B. The brief itself goes to
53
+ a private session, but write it to the same standard so a pasted fragment
54
+ can never leak.
55
+
56
+ ## Step 1: The gate
57
+
58
+ Refuse to write a brief, and say exactly what is missing, unless all three
59
+ hold. A brief written early pins a SHA the review is about to move, and
60
+ the vault session then spends a round on code nobody will ship.
61
+
62
+ 1. **The branch has an open PR.** `gh pr view --json number,state,headRefOid`
63
+ locally, or the GitHub MCP `list_pull_requests` with
64
+ `head: rossgroomio:<branch>` on the web.
65
+ 2. **A code review has been posted, and every thread on it is resolved or
66
+ replied to.** Read the threads (`pull_request_read` with
67
+ `get_review_comments`, or `gh api graphql`). An unresolved thread whose
68
+ last comment is a reply counts as answered. A thread deliberately left
69
+ open as the A/B merge gate is fine: name it, because Step 5 resolves it
70
+ when the gate clears. A review carrying the ab-handback marker is an
71
+ earlier round's result, not a code review, and does not satisfy this.
72
+ 3. **The head you will pin includes the review fixes.** Each fix reply
73
+ names a commit; check every one is in the head
74
+ (`git merge-base --is-ancestor <fix sha> <head sha>`). A reply saying
75
+ "pushing next" with no such commit on the branch means the fix is not
76
+ there yet.
77
+
78
+ Report the CI state on that head (`get_check_runs`, or `gh pr checks`) but
79
+ do not block on it. Briefs in October went out with CI still running and
80
+ it never mattered: the round takes longer than CI, and a red CI is a code
81
+ problem this session fixes on its own clock.
82
+
83
+ ## Step 2: Classify the change
84
+
85
+ From the diff against the merge-base (Step 3), say which test the change
86
+ needs:
87
+
88
+ - **Fresh model runs** when what the model is shown, or the model itself,
89
+ changes: either prompt (`fpl_cli/prompts/league_recap.py`'s system or
90
+ user prompt), a data section the model is shown (what a
91
+ `format_recap_*` function emits, or the recap data keys it reads, often
92
+ set in `fpl_cli/cli/_league_recap_data.py` or `league_recap.py`), the
93
+ synthesis model or its settings (`llm.synthesis` in
94
+ `fpl_cli/config/defaults.yaml`, `fpl_cli/api/providers/`), or the retry
95
+ loop (`fpl_cli/cli/_synthesis_retry.py`).
96
+ - **A replay of the saved drafts corpus** when the editorial checks change
97
+ (`fpl_cli/prompts/league_recap_checks.py`). Never model runs for a check
98
+ change alone: the corpus already holds the drafts, and a replay judges
99
+ them in seconds where fresh runs cost an hour and add noise.
100
+ - **Both** when it does both.
101
+
102
+ Name every check whose `retry_instruction` changed or is new. A replay
103
+ shows which drafts would now retry, never how the model reads the new
104
+ wording; only fresh runs show that, so a new or reworded retry tips a
105
+ check-only change towards fresh runs too.
106
+
107
+ This is an expectation, not the verdict. The vault's `triage.sh` dry-runs
108
+ both builds and is authoritative; the brief states what you expect and
109
+ tells the vault session to stop if triage says otherwise. A disagreement
110
+ is itself a finding: either the diff does something its author did not
111
+ see, or the classification above has a gap.
112
+
113
+ ## Step 3: Gather
114
+
115
+ Collect everything the vault session cannot cheaply rediscover. It starts
116
+ with no knowledge of the PR, and every fact it has to dig out of fpl-cli
117
+ is time spent and a chance to guess.
118
+
119
+ **Both arms as full 40-character SHAs.** Control is the PR's merge-base
120
+ with main, treatment is the PR head:
121
+
122
+ ```bash
123
+ git fetch origin main <branch>
124
+ git merge-base origin/main origin/<branch> # control
125
+ git rev-parse origin/<branch> # treatment
126
+ git log --oneline <control>..origin/main # has main moved on?
127
+ git diff --stat <control> origin/main -- fpl_cli/
128
+ ```
129
+
130
+ Copy the SHAs out of command output, never type one. One October brief
131
+ went out with placeholder SHAs, and one session typed a SHA from memory
132
+ and broke a uvx run. If main has moved on, say whether those commits are
133
+ docs, tests or CI only (an empty `fpl_cli/` diffstat). If they touch
134
+ `fpl_cli/`, stop and put it to the user: the PR should probably merge main
135
+ first, so the round tests what will ship.
136
+
137
+ **Only what you have read in full.** Every expectation in the brief about
138
+ a file, probe, earlier change or count comes from reading the whole thing,
139
+ never a preview, a diffstat or an inference. The first real brief told the
140
+ vault session that a probe wrote the forfeit once, from the probe's first
141
+ 600 characters (it wrote it twice), and that an earlier PR had changed the
142
+ prompt, from a diffstat that included console code (it had not). Each cost
143
+ the round a stop. Where you could not check, write it as a question the
144
+ dry run or the corpus answers, not as an expectation.
145
+
146
+ **What the change is meant to do, per sentence shape, and what counts as
147
+ a regression.** Quote real sentence shapes in full from the reports the
148
+ issue cites or the PR's tests, with every manager name replaced by
149
+ Manager A, Manager B. Never trim a sentence into a test-shaped fragment:
150
+ one October fix brief did, and the PR's test passed while the real
151
+ sentence was still missed. For each shape, say what treatment should write
152
+ instead (or that it should be unchanged) and what would be a regression,
153
+ and name the file each shape was quoted from so the vault session can find
154
+ it. If an expectation you want sits uneasily with the PR's own rule (puns
155
+ wanted in headlines, while the prompt's "no others" covers the headline
156
+ too), put that to the user before writing the brief: left in, it becomes a
157
+ definitional call the vault session makes mid-round, and the call moves
158
+ the count. The brief that listed expected changes per shape ran fastest and cleanest
159
+ of all the October rounds, because grading became a comparison against a
160
+ written expectation instead of a judgement made from scratch.
161
+
162
+ **The baseline.** The previous round's numbers for the same measure, from
163
+ the linked issue or earlier PR bodies, numbers only. If no round has
164
+ measured it, say so and say where the first measurement will come from
165
+ (often control's own runs, or a replay of older control drafts), and do
166
+ not predict its size: the first brief expected "a clear share" of repeats
167
+ in control and the round found 1 in 10, which left the vault session
168
+ deciding whether a wrong guess was a stop. "Better" is then stated against
169
+ whatever control measures, not against a guess.
170
+
171
+ **The gameweek.** The most recently finished one per
172
+ `fpl status --format json`, unless the PR's evidence came from a specific
173
+ gameweek (an issue quoting one week's recap), in which case name that
174
+ gameweek and say why. The evidence week is where the failure is known to
175
+ show, so it is the strongest test.
176
+
177
+ **Run counts.** For fresh runs where the gate is a frequency, 40 draft and
178
+ 4 to 5 classic per arm. A round of 8 draft runs per arm in October was too
179
+ small to call first time, and the re-run cost more than doing 40 once.
180
+ Draft is the gate by default because it has the most checks and the most
181
+ variance; where the change's evidence lives in classic (a classic-only
182
+ section, a failure the issue saw only in classic), raise classic and say
183
+ why. Replay rounds need no run counts.
184
+
185
+ **The round name** for `RECAP_AB_ROUND`, as `pr<N>-gw<GW>` (`pr507-gw8`).
186
+
187
+ **For a replay: which corpus rounds, and what recheck data.** List the
188
+ rounds on the vault's main (`.claude/scripts/recap_ab/corpus/`, via the
189
+ GitHub MCP `get_file_contents` on `rossgroomio/fpl-workspace` or a clone).
190
+ Read the README's "Rechecking saved drafts": a check that reads a recap
191
+ data key older snapshots lack judges those drafts on less than its build
192
+ computes, so the replay needs `RECAP_AB_RECHECK_DATA` carrying that key for
193
+ each round's gameweek and format, merged with any round's own
194
+ `recheck-data.json`. Say what the key should hold and where its value
195
+ comes from. Name the rounds whose snapshots already carry the key (a round
196
+ recorded on this PR's own build does), since the override would replace
197
+ their values. Read each probe you expect a result from in full.
198
+
199
+ The replay's control will not reproduce a round recorded on an older build
200
+ wherever a check changed in between. For each round, run
201
+ `git log --oneline <round's recorded sha>..<control> --
202
+ fpl_cli/prompts/league_recap_checks.py` and name the expected
203
+ non-reproductions with their PR, giving every SHA involved in full: the
204
+ first round's vault session met one such miss, guessed the SHA of the
205
+ commit that explained it, and broke a replay before looking it up.
206
+
207
+ **Whether a second control exists:** an earlier corpus round, at the same
208
+ gameweek, holding control model runs (dry runs and probes alone cannot be
209
+ pooled), whose control prompts are byte-identical to this control's. One
210
+ October round pooled with its predecessor this way and halved the control
211
+ runs it needed. Read what changed between the two builds in the code that
212
+ writes the prompt (`fpl_cli/prompts/league_recap.py` and the recap data it
213
+ reads), not a diffstat: `fpl_cli/cli/` also holds the console and saved
214
+ report, which the model never sees. If you cannot tell, say the dry runs
215
+ decide and leave it there. If no such round exists, say so, so the vault
216
+ session does not go looking.
217
+
218
+ ## Step 4: Write the brief
219
+
220
+ Fill in `references/brief-template.md` in its order; the template carries
221
+ the wording that must survive verbatim in spirit, and the reasons stay
222
+ here so the brief stays short enough to read before the round starts.
223
+
224
+ - **The opening line** points the vault session at the harness README
225
+ first. The harness changed after most October rounds; a session working
226
+ from memory of an earlier round runs an older procedure.
227
+ - **The role boundary** is near the top and absolute. In two October
228
+ rounds the vault session pushed fixes to the fpl-cli PR branch, and one
229
+ opened an fpl-cli PR. Both times the user asked it to, which is why the
230
+ boundary holds even against the user's own request mid-round: a fix made
231
+ in the vault session is invisible to the fpl-cli session that owns the
232
+ PR, which then pushes over it or reviews code it never saw.
233
+ - **A fresh `origin/main` checkout of the vault.** A leftover branch from
234
+ an earlier round once caused git and stop-hook noise that read as a
235
+ broken test.
236
+ - **Stop conditions at every step that has an expectation** (triage, dry
237
+ run, anything in the brief that does not match). "If anything in this
238
+ brief doesn't match what you find, stop and ask me" caught a missing
239
+ corpus in one round. A brief is written from one repo about another; a
240
+ stop costs a message, a wrong round costs an hour. Stops are for set-up
241
+ facts (triage, dry-run diffs, corpus contents, SHAs), never for results:
242
+ a number that differs from what the brief hoped for is the finding, and
243
+ the brief should not predict one in the first place.
244
+ - **Landing** is spelled out because every October session needed a
245
+ follow-up message to land its round. Never write a blanket "don't commit
246
+ anything": one October brief did, and that round's drafts were lost
247
+ before the next round needed them. The rule is that anything naming
248
+ managers stays under `data/debug/`, and the corpus is the one tracked
249
+ exception.
250
+ - **Out-of-scope findings and harness gaps are listed, not fixed.** One
251
+ round spent about an hour building unscoped harness and kept it only
252
+ because the user asked. A small harness fix the round cannot run
253
+ without may ride the round's vault PR; anything bigger is a finding.
254
+ - **Scratch paths outside the harness's own scripts** are written
255
+ `${TMPDIR:-/tmp}/...`, the fallback the harness's `common.sh` already
256
+ uses, never a bare `$TMPDIR/...`. A fresh vault session may not set
257
+ `TMPDIR`; the pr545 brief's `cp -R "$FPL_CLI_CONFIG_DIR"
258
+ "$TMPDIR/pr545-config"` expanded to a path at the filesystem root, so
259
+ the session improvised `/tmp` and logged a harness gap that was really
260
+ a brief gap.
261
+ - **The handback** (below) is required in full, because in October the
262
+ whole vault transcript, progress chatter included, was pasted back and
263
+ the fpl-cli session had to dig the results out.
264
+
265
+ Before printing, read the brief once as the vault session will: every SHA
266
+ is 40 characters and came from command output, no `<placeholder>` or
267
+ "TBD" is left, every number has a source, every expectation comes from
268
+ something you read in full, and no manager name or ID appears anywhere in
269
+ it.
270
+
271
+ ## The handback
272
+
273
+ One comment-only pull request review on the fpl-cli PR (event `COMMENT`,
274
+ no inline comments, never `APPROVE` or `REQUEST_CHANGES`), posted once,
275
+ after the vault PR is open if the round has one. It is a review rather than
276
+ a plain comment because only the review reaches the fpl-cli session: in
277
+ #507's first round a plain handback comment landed and the subscribed
278
+ session never woke, while in its second the same text posted as a
279
+ comment-only review woke it. Its first line is the marker, an HTML comment
280
+ GitHub does not render:
281
+
282
+ ```
283
+ <!-- ab-handback pr=<N> head=<40-char sha the round tested> round=<round> -->
284
+ ```
285
+
286
+ Both sessions post as the same GitHub identity, and a session watching a
287
+ PR is told to skip echoes of what it posted itself. The marker is how this
288
+ session tells the handback apart from its own reviews and comments: it
289
+ never writes one, so a review carrying it is an A/B result to act on. It also carries
290
+ the head the round actually tested, which Step 5 checks.
291
+
292
+ Then a heading, `## A/B handback for <round>, from the fpl-workspace
293
+ session`, and two blocks:
294
+
295
+ - **Block A, "PR body".** First-person prose in the create-pr skill's
296
+ style and length budget (up to three short paragraphs, plus caveats in
297
+ italics), numbers only, ready to go into the PR body as written.
298
+ - **Block B, "Findings".** A verdict line (`ship`, `fix then re-run
299
+ fresh`, `fix then replay`, or `don't ship`), the vault PR link if one was
300
+ opened, then one entry per finding: kind (regression, check false
301
+ positive, missed catch, out of scope, harness gap); check code if any;
302
+ the sentence shape in full with managers as Manager A, Manager B; what
303
+ should have happened; and the test shape to add.
304
+
305
+ The review lands in a public repo, and GitHub keeps edit history and
306
+ sends notification emails, so a name in it cannot be taken back. The vault
307
+ session writes it to a file under `data/debug/`, runs the vault's
308
+ `.claude/scripts/recap_ab/handback_check.py` over it, and posts that
309
+ exact file as the review body only on exit 0 (the GitHub MCP
310
+ `pull_request_review_write`, or `gh pr review <N> --comment --body-file`).
311
+ The script reads every manager the vault's
312
+ ledger has recorded and fails (exit 1) on any spelling of a full name, any
313
+ part of one no other manager shares, an entry ID, manager_key or league
314
+ ID, and on a footballer who shares a manager's name, which has to be
315
+ reworded by club and position; there is no allow-list. Exit 2 means it
316
+ could not check, which is not a pass. If the script is missing or exits 2,
317
+ the vault session does not post; it prints the handback in chat and says
318
+ why, and the user carries it across. It never edits the review once
319
+ posted: a correction is a second short comment-only review, opening with
320
+ the same marker and a heading that says it corrects the first, through the
321
+ same check.
322
+
323
+ ## Step 5: Closing the loop
324
+
325
+ When the handback lands (a review event wakes this session; otherwise
326
+ the check-in finds it or the user says it has been posted; read it from
327
+ the PR's reviews, and its comments too in case an older brief's session
328
+ posted a plain comment):
329
+
330
+ 1. **Check the marker.** Its `pr` is this PR and its `head` is the head
331
+ the brief pinned, or a later head the brief allowed ("if the PR has
332
+ moved on, use its current head and say so"). For a later head, read
333
+ what changed between the two: if it changes what the model sees or the
334
+ checks, the round tested something other than what was briefed, so say
335
+ so to the user before acting on the numbers. A review or comment with
336
+ no marker is not a handback, whoever wrote it.
337
+ 2. **Put Block A into the PR body**, replacing whatever caveat said the
338
+ A/B had not run, keeping the body inside the create-pr style.
339
+ 3. **Treat each Block B finding like a review comment.** Fix and push
340
+ (`chore(review): ...` commits, per CLAUDE.md), or say on the PR why
341
+ not, in one comment covering every finding you are not fixing. Never
342
+ reply to the handback review itself: the vault session is not
343
+ listening, and a reply there reads as a thread for nobody.
344
+ 4. **Re-run Step 2 on the new head.** If the fixes change what the model
345
+ sees, write a fresh-run brief for the new head; if they touch only the
346
+ checks, a replay brief; either way the next round's handback comes back
347
+ the same way, and this session stays subscribed. A verdict of
348
+ `fix then re-run fresh` or `fix then replay` is the vault session's
349
+ expectation, and Step 2 on the actual fixes decides. Scope the round by
350
+ the diff, not by the finding: a system-prompt line reaches every arm,
351
+ so a fix aimed at the phrasings arm alone still re-runs the plain
352
+ treatment arm when it touches the system prompt (#507's first fix did).
353
+ An earlier round's arm is reused only where its build is unchanged and
354
+ the new dry runs show the same data sections, and a follow-up round
355
+ whose predecessor's vault PR is still open continues on that PR's
356
+ branch under the same round name with new labels, so both rounds are
357
+ graded together and land together.
358
+ 5. **If no fix is needed**, say the A/B gate is cleared, and resolve the
359
+ thread left open as the gate (reply first, saying which round cleared
360
+ it). Merging stays the user's call.
361
+ 6. **Out-of-scope findings** become issues only once the user agrees: list
362
+ them for the user with a one-line proposal each.
363
+
364
+ A `don't ship` verdict goes to the user before anything else: it may mean
365
+ the approach is wrong, which is their decision, not a fix to push.
366
+
367
+ ## Step 6: Deliver
368
+
369
+ 1. **Subscribe this session to the PR's activity before printing**
370
+ (`subscribe_pr_activity`), so the handback review wakes it, and say so.
371
+ Subscribing after printing races a fast replay round. The wake has
372
+ worked once, so also arm one backstop `send_later` check-in about two
373
+ hours after delivery that reads the PR's reviews for the marker; re-arm
374
+ it twice at most, then tell the user once that the next message from
375
+ them will bring you back. Where either tool is missing (a local session,
376
+ another agent), tell the user to come back here and say when the
377
+ handback is posted.
378
+ 2. **Print the brief in one fenced block** for copy and paste, with a
379
+ fence longer than any fence inside it (`~~~~` around a brief that holds
380
+ ```` ``` ```` blocks), so the copy comes out whole.
381
+ 3. **Above the block, not inside it, say how to start the vault session:**
382
+ a fresh session on `rossgroomio/fpl-workspace`, Opus, at xhigh effort
383
+ for a round with fresh runs and high for a replay alone. October's
384
+ fresh rounds ran at xhigh; a replay at high took under four minutes.
385
+
386
+ ## Common mistakes
387
+
388
+ - Running triage, a dry run or a replay here "to check the brief": that
389
+ is the overlap this skill exists to end
390
+ - A placeholder or hand-typed SHA, or a short one
391
+ - A sentence shape trimmed to the fragment a test matches
392
+ - "Don't commit anything" in place of the landing rules
393
+ - A scratch path written `$TMPDIR/...` with no `:-/tmp` fallback
394
+ - Printing the brief before subscribing to the PR
395
+ - A brief that asks for the handback as a plain comment, which does not
396
+ wake the subscribed session
397
+ - An expectation built from a preview, a diffstat or a guess at a count
398
+ - Treating the handback as an echo of this session's own review, or
399
+ editing the PR body with the vault session's transcript instead of
400
+ Block A
401
+ - A manager name in the skill, the brief, the PR or a commit