fplkit 2.7.3__tar.gz → 2.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fplkit-2.8.0/.agents/README.md +51 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/TOOLS.md +5 -5
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/gw-prep/SKILL.md +21 -4
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/gw-prep/references/output-template.md +2 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/gw-prep/scripts/extract_classic_squad.py +20 -50
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/gw-prep/scripts/validate_draft_waivers.py +48 -50
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/preview-ingest/SKILL.md +11 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/squad-builder/SKILL.md +5 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/update-gw-prep/SKILL.md +13 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/AGENTS.md +1 -1
- {fplkit-2.7.3 → fplkit-2.8.0}/CHANGELOG.md +23 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/CLAUDE.md +1 -1
- {fplkit-2.7.3 → fplkit-2.8.0}/PKG-INFO +1 -1
- {fplkit-2.7.3 → fplkit-2.8.0}/docs/architecture.md +8 -6
- {fplkit-2.7.3 → fplkit-2.8.0}/docs/command-reference.md +51 -11
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/_version.py +2 -2
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/providers/__init__.py +4 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/providers/_models.py +34 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_helpers.py +155 -1
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_league_recap_types.py +22 -2
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_review_analysis.py +39 -32
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_review_classic.py +46 -87
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_review_draft.py +45 -72
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_review_summarisation.py +437 -185
- fplkit-2.8.0/fpl_cli/cli/_synthesis_retry.py +147 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/league.py +16 -79
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/league_recap.py +58 -58
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/preview.py +15 -2
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/review.py +145 -36
- fplkit-2.8.0/fpl_cli/parsers/recommendations.py +474 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/prompts/league_recap.py +15 -3
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/prompts/league_recap_checks.py +453 -93
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/prompts/review.py +53 -2
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/templates/gw_league_recap.md.j2 +19 -1
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/templates/gw_review.md.j2 +54 -8
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/utils/markdown.py +197 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/utils/text.py +22 -6
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_classic_squad_extraction.py +44 -0
- fplkit-2.8.0/tests/test_cli_league.py +376 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_league_recap.py +215 -1
- fplkit-2.8.0/tests/test_cli_notice_streams.py +313 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_preview.py +72 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_review.py +1162 -56
- fplkit-2.8.0/tests/test_cli_synthesis_retry.py +202 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_league_recap.py +285 -9
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_providers.py +24 -0
- fplkit-2.8.0/tests/test_recommendations_parser.py +965 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_report_agent.py +256 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_review.py +16 -0
- fplkit-2.8.0/tests/test_skill_effort_levels.py +181 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_utils_markdown.py +123 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_utils_text.py +5 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_validate_draft_waivers.py +150 -1
- fplkit-2.7.3/.agents/README.md +0 -27
- fplkit-2.7.3/fpl_cli/parsers/recommendations.py +0 -145
- fplkit-2.7.3/tests/test_cli_league.py +0 -161
- fplkit-2.7.3/tests/test_recommendations_parser.py +0 -297
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/build-test-plan/SKILL.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/build-test-plan/references/plan-template.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/build-test-plan/references/workspace-env.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/create-pr/SKILL.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/gw-prep/references/entity-normalisation.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/gw-prep/references/rules.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/gw-prep/scripts/_bootstrap.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/gw-prep/scripts/bench_order.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/gw-prep/scripts/normalise_entities.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/gw-prep/scripts/starting_xi.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/gw-prep/scripts/transfer_eval.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/release/SKILL.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/release-notes/SKILL.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/squad-builder/references/output-template.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.agents/skills/squad-builder/references/rules.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.claude/output-styles/fpl-mate.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.claude/settings.json +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.env.example +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.github/workflows/ci.yml +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.github/workflows/pr-title.yml +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.github/workflows/release.yml +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/.gitignore +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/CONTRIBUTING.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/LICENSE +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/README.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/cliff.toml +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/config/team_ratings_overrides.yaml +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/docs/custom-analysis.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/docs/fpl-rules.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/docs/images/fpl-player-demo-v1-0.png +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/action/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/action/waiver.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/analysis/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/analysis/bench_order.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/analysis/captain.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/analysis/squad_analyzer.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/analysis/starting_xi.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/analysis/stats.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/analysis/transfer_eval.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/base.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/common.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/data/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/data/fixture.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/data/price.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/data/scout.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/orchestration/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/agents/orchestration/report.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/contract.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/core_insights.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/dataset_fetcher.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/football_data.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/fpl.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/fpl_draft.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/historical.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/historical_types.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/providers/_http.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/providers/anthropic.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/providers/openai_compat.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/providers/perplexity.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/understat.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/api/vaastav.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_banner.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_context.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_fines.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_fines_config.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_json.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_league_recap_data.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_league_recap_history.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/_plan_grid.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/allocate.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/captain.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/chips.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/credentials.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/differentials.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/doctor.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/doctor_providers.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/fdr.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/fixtures.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/history.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/init.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/intel.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/league_fines.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/player.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/price_changes.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/price_history.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/ratings.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/returnees.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/sell_prices.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/squad.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/stats.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/status.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/targets.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/transfer_eval.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/waivers.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/cli/xg.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/config/defaults.yaml +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/config/fixture_predictions.yaml +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/config/previews/EXAMPLE.yaml +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/config/team_managers.yaml +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/constants.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/models/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/models/chip_plan.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/models/fixture.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/models/league_history.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/models/player.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/models/team.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/models/types.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/parsers/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/paths.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/prompts/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/prompts/returnees.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/prompts/scout.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/scraper/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/scraper/fpl_prices.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/season.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/fixture_predictions.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/league_history.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/league_history_counters.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/league_history_fines.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/league_history_notes.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/matchup.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/player_clubs.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/player_prior.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/returnee_radar.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/scoring/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/scoring/constants.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/scoring/data_prep.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/scoring/display.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/scoring/evaluation.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/scoring/ownership.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/scoring/shrinkage.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/scoring/signals.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/scoring/single_gw.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/scoring/value_quality.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/season_previews.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/squad_allocator.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/team_form.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/team_ratings.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/services/team_ratings_prior.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/templates/gw_preview.md.j2 +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/utils/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/utils/files.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/utils/gameweek.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/utils/teams.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/fpl_cli/utils/time.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/pyproject.toml +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/pyrightconfig.json +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/scripts/backtest_early_season_prior_blend.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/scripts/calibrate_promoted_prior.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/scripts/calibrate_quality_ceilings.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/__init__.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/conftest.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/fixtures/classic_squad_fixture.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/fixtures/recommendations_fixture.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/fixtures/validate_draft_waivers/recs_clean.md +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/fixtures/validate_draft_waivers/squad_grid.json +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/fixtures/validate_draft_waivers/waivers.json +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/fixtures/waiver_ranked_golden.json +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_agents_action.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_agents_analysis.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_agents_analysis_squad.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_agents_base.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_agents_data.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_agents_data_price.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_agents_orchestration.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_allocate.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_api_client.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_api_core_insights.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_api_dataset_fetcher.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_api_draft.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_api_football_data.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_api_historical.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_api_understat.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_api_vaastav.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_bench_order_script.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_calibrate_quality_ceilings.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_allocate.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_banner.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_captain.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_chips.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_context_settings.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_credentials.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_custom_analysis.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_differentials.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_doctor.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_doctor_providers.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_failure_parity.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_failure_streams.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_fdr.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_fdr_blanks.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_fixtures.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_format.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_format_suppression.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_fpl_block.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_history.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_init.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_intel.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_json.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_json_contract.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_league_fines.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_plan_grid.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_player.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_price_changes.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_price_history.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_returnees.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_sell_prices.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_settings_warning.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_squad.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_stats.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_status.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_targets.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_transfer_eval.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_waivers.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_warnings_stderr.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_cli_xg.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_common.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_common_scoring.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_data_prep_harmonisation.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_dependencies.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_doc_consistency.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_fines.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_fines_config.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_fixture_predictions.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_formation_guidance_sync.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_fpl_prices.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_gw_prep_script_bootstrap.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_init.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_init_fines.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_league_history_counters.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_league_history_fines.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_league_history_notes.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_league_history_store.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_matchup_service.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_models.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_normalise_entities.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_output_style_sync.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_paths.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_player_clubs.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_player_prior.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_player_resolution.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_preseason.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_prompt_tables.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_report_paths.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_returnee_radar.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_rolling_pts_per_m.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_scoring.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_scraper.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_season.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_season_awareness.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_season_previews.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_sell_prices.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_skill_fdr_quality_signals.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_skill_stats_thresholds.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_squad_allocator.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_starting_xi_agent.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_starting_xi_script.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_stats.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_team_ratings.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_team_ratings_prior.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_transfer_eval_agent.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_transfer_eval_script.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_utils_gameweek.py +0 -0
- {fplkit-2.7.3 → fplkit-2.8.0}/tests/test_utils_time.py +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# .agents/
|
|
2
|
+
|
|
3
|
+
Agent skills for fpl-cli. Each subdirectory in `skills/` is a self-contained skill with a `SKILL.md` entry point.
|
|
4
|
+
|
|
5
|
+
## Discovery
|
|
6
|
+
|
|
7
|
+
Claude Code discovers skills via the `.claude/skills/` symlink, which points here. Other agent tools can read `skills/` directly.
|
|
8
|
+
|
|
9
|
+
For a complete inventory of CLI commands, analysis agents, and skills, see [TOOLS.md](TOOLS.md).
|
|
10
|
+
|
|
11
|
+
## Adapting Skills
|
|
12
|
+
|
|
13
|
+
Skills are showcase examples - they work out of the box but are designed to be customised:
|
|
14
|
+
|
|
15
|
+
- `<!-- ADAPT: ... -->` comments mark sections you'll want to change for your setup
|
|
16
|
+
- Output paths use `[YOUR_OUTPUT_DIR]` placeholders - replace with your preferred location
|
|
17
|
+
- Script invocations use a `[YOUR_PYTHON]` placeholder - replace with the interpreter fpl-cli is installed on. Helper scripts import the `fpl_cli` package, which a standalone `fpl` on `PATH` (uv tool, pipx) does not provide; get it wrong and they exit 1 with a JSON error envelope naming the cause
|
|
18
|
+
- Supplementary data sources (newsletters, external reports) are noted but not required
|
|
19
|
+
- All CLI data gathering uses `--format json` for structured output
|
|
20
|
+
|
|
21
|
+
## Model and effort
|
|
22
|
+
|
|
23
|
+
The four product skills (gw-prep, squad-builder, update-gw-prep, preview-ingest) pin a model and an effort level in their `SKILL.md` frontmatter, and every `Agent tool parameters` block pins its own pair, so a run does not depend on whatever the session happened to be set to (issue #393). The skills carry the reasons and point here; this table is the one place the levels are written down outside the blocks themselves.
|
|
24
|
+
|
|
25
|
+
| Where | Model | Effort | Why |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| gw-prep orchestrator | opus | high | Gathers the data and assembles the report; the frontmatter also sets the level the Phase C prompts are built at |
|
|
28
|
+
| gw-prep C1 classic analysis | opus | high | Weighs transfers, hits, chips and captaincy against each other: the most trade-off-heavy step in the repo |
|
|
29
|
+
| gw-prep C2 draft analysis | sonnet | medium | No budget, chips or captains; the call is waivers and the lineup |
|
|
30
|
+
| squad-builder orchestrator and Phase D assembly | opus | high | Assembly enforces every cross-position constraint and settles the trade-offs the position agents only surface |
|
|
31
|
+
| squad-builder Phase C position research | opus | medium | Gathering and ranking within one position; the trade-offs are made in Phase D |
|
|
32
|
+
| update-gw-prep orchestrator and sub-agent | opus | high | Revises the classic and draft recommendations against new information, so it inherits the classic analysis's trade-offs |
|
|
33
|
+
| preview-ingest orchestrator and per-team agents | opus | medium | Extraction against a fixed schema: reading, not weighing |
|
|
34
|
+
|
|
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
|
+
|
|
37
|
+
How far each key reaches, quoted from the Claude Code docs as read on 14 October 2026:
|
|
38
|
+
|
|
39
|
+
- `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.
|
|
40
|
+
- `effort` in the frontmatter (same table): "Effort level when this skill is active. Overrides the session effort level." The docs do not say whether it reverts when the skill finishes.
|
|
41
|
+
- `effort` in an `Agent tool parameters` block is the per-invocation parameter ([sub-agents, Choose an effort level](https://code.claude.com/docs/en/sub-agents#choose-an-effort-level)): "When you ask Claude to run a non-fork subagent at a specific effort level, it can also pass an `effort` parameter for that invocation. The parameter overrides the `effort` field and stays in effect when the subagent is resumed. The `CLAUDE_CODE_EFFORT_LEVEL` environment variable takes precedence over both. The per-invocation parameter requires Claude Code v2.1.292 or later."
|
|
42
|
+
- claude.ai skill uploads and the Skills API ([skills, Using skill frontmatter outside Claude Code](https://code.claude.com/docs/en/skills#using-skill-frontmatter-outside-claude-code)) accept only the Agent Skills spec keys, `name`, `description`, `license`, `compatibility`, `metadata` and `allowed-tools`, and fail on anything else. `model` already fell outside that set, so `effort` adds no new break; strip both before uploading.
|
|
43
|
+
- Codex, Cursor and Copilot (named in each skill's `compatibility` block) read neither key, so they run at whatever their session is set to.
|
|
44
|
+
|
|
45
|
+
## Private Skills
|
|
46
|
+
|
|
47
|
+
To add a private skill that shouldn't be committed, create it under `skills/` and add the path to `.gitignore`:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
.agents/skills/my-private-skill/
|
|
51
|
+
```
|
|
@@ -39,15 +39,15 @@ with `2>/dev/null` without losing the table, whichever command it runs.
|
|
|
39
39
|
| `player` | Look up a player's stats, xG, ownership and fixture run. Before GW10 `quality_score` blends last season's pedigree in and JSON `metadata.warnings` says so (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded), same as `fpl stats --value`; `understat_team_unmatched` joins it for any club Understat carried no players for, whose whole squad loses xG enrichment. JSON includes `ep_next` (predicted pts next GW) and `ep_this` (current GW), emitting `null` when FPL has no projection; panel shows `xPts` (ep_next) when available, omits the segment when FPL has no projection. `--detail` (`-d`) shows GW-by-GW performance and, for FWD/MID with sufficient history, xGI sustainability (per-match GI-xGI divergence -> form modifier). With `custom_analysis`: JSON adds `info.adjusted_npxg_per_90` (fixture-adjusted) and `info.raw_npxg_per_90` (Understat season avg); panel shows `adj. npxG/90: X.XXX (raw: Y.YYY)` | Yes | General | No | direct-api |
|
|
40
40
|
| `stats` | List players with filtering and sorting. `--value` adds quality/value per £m columns; `--window N` sets rolling lookback (3-10) for `rolling_pts_per_m`. Before GW10, `--value` quality scores blend last season's pts/90 pedigree with this season's observation and the command says so (stderr notice quoting the observation's weight band; JSON `metadata.warnings` code `early_season_prior_informed`); when last season's history cannot be loaded they are pure observation and the notice says that instead (`early_season_small_sample`, before GW6). `understat_team_unmatched` is raised once per club Understat carried no players for, whose whole squad scores without xG. GK ceilings scale with the calendar-possible sample, reaching full scale at GW6. Sortable by `ep_next`/`ep_this` (FPL predicted points; `ep_this` is only meaningful before the gameweek's first kickoff — FPL rolls it forward to `ep_next` as each player's match finishes); both in JSON output (emit `null` when FPL has no projection; table renders `—`). Players with no projection sort to the bottom in either direction | Yes | General | No | direct-api |
|
|
41
41
|
| `history` | Show historical player performance across seasons. A provider that refuses exits 1 in both output formats, carrying the upstream's own reason: the error envelope used to say only `Failed to fetch historical data` while the cause went to stderr, and table mode printed the cause to stdout and exited 0 | Yes | General | No | direct-api |
|
|
42
|
-
| `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 | No | General | No | direct-api |
|
|
42
|
+
| `league` | Show live league standings for Classic and Draft leagues. Classic positions are competition-ranked from cumulative totals — managers level on points share a place and the next distinct total skips the places they consumed — matching `fpl review` and the league history ledger rather than the standings API's sequential rank, which splits such a tie on fewest transfers season-to-date, a figure no column shows. Draft keeps the league's own rank, which already applies the head-to-head points-for tie-break. The Best/Worst GW Performers lists (top three; bottom five classic, bottom three draft) are the ones `fpl review` prints: managers level on the gameweek share a place (`3=`) and a tie at the cut keeps every manager in it, a hit reads `29 net pts (33 gross, -4 hit)`, and a classic user above the bottom five gets a `Your GW rank: R of N` line beside the list | No | General | No | direct-api |
|
|
43
43
|
| `fdr` | Analyse fixture difficulty - easy runs, blanks, doubles. Three payloads, not one: the default call runs the agent and returns the full analysis plus `data.ratings_warning` (the team-ratings quality verdict, and with `--my-squad` also `data.predictions_stale` / `data.prediction_warnings` / `data.squad_exposure`); with custom analysis off it returns raw 1-5 FPL API difficulty (`data.easy_fixture_runs` only, `metadata.custom_analysis: false`, no ratings and so no warning); `--blanks` bypasses the agent for a schedule-only payload carrying neither the FDR analysis nor `ratings_warning`. `--blanks` mixes confirmed fixtures with predictions; when those predictions are stale, table mode warns on stderr and JSON adds a `fixture_predictions_stale` entry to `metadata.warnings` | Yes | General | No | via-agent |
|
|
44
44
|
| `xg` | Analyse underlying stats: xG, xA, overperformers. The minutes floor and the window scale to the gameweeks played (`min(60 x window, 45 x gameweeks played)`, so 90 minutes over 2 GWs rather than an unreachable 360 over 6); the configured bar binds from GW9, or GW11 for `--all`. JSON `metadata` carries `window_label`, `gameweeks_played` and `min_minutes`, and `metadata.warnings` an `early_season_minutes_floor` notice while the bar is scaled, plus `understat_team_unmatched` for any club Understat carried no players for. When nothing qualifies, `data.empty_reason` says whether the floor (`below_minutes_floor`) or the data (`no_minutes_played`) caused it | Yes | General | No | via-agent |
|
|
45
45
|
| `price-changes` | Show price changes and transfer activity | No | General | No | via-agent |
|
|
46
46
|
| `price-history` | Show price trajectory and transfer momentum. A trend provider that refuses exits 1 in both output formats with the upstream's own reason — the error envelope used to say only `Failed to fetch price history data` while the table path named the cause | Yes | General | No | direct-api |
|
|
47
47
|
| `returnees` | Track injured and suspended players due back soon, so a returnee can be claimed in draft before they are fit or planned around in classic. Reads the FPL availability news, filters to returns inside a window (`--window N`, 1-38) and through a source-aware quality bar, and diffs against the stored watchlist for week-over-week changes. Most flagged players carry no parseable return date, so date-unknown entries are the norm rather than the exception; `--enrich` searches the web for fresher timing on those and on stale-news entries (needs a Perplexity key; skips with a note without one) and shows it beside the FPL news, never over it. `--all` bypasses the quality bar and deliberately does not persist the snapshot. JSON `metadata` carries `window`, `escalation_window`, `stash_upgrade_margin`, `transitions_available`, `transitions_baseline_gameweek` (the gameweek `transition` is measured against — every run in a gameweek diffs against the last state stored before it, so a repeat run reports the same changes), `quality_bar_available`, `quality_bar_applied` and `enrichment_requested`/`_available`/`_note`/`_count`/`_rate_limited` (true when the search provider rate-limited the run even after retrying — the one failure a re-run is likely to clear); `data` is `{entries, departures}`, each entry carrying `quality.basis`/`quality.meets_stash`, `expected_return`/`return_gameweek`/`return_source`, `escalation_eligible`/`escalation_basis`, `transition`, and `enrichment` (null unless enriched) | Yes | General | No | direct-api |
|
|
48
48
|
| `preview` | Run full pre-gameweek analysis and generate report. `--save` writes to `<output dir>/<season>/gw{N}-preview.md`; `--scout` writes to `<research dir>/ai-scout-reports/<season>/gw{N}-scout-preview[-referenced].md` | No | General | No | via-agent |
|
|
49
|
-
| `review` | Review a completed gameweek - squad performance and standings. `--save` writes to `<output dir>/<season>/gw{N}-review.md`; `--compare-recs` reads `gw{N}-recommendations.md` from the same season directory. 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
|
|
50
|
-
| `league-recap` | Recap a completed gameweek - awards, standings, and banter. Saves to `<output dir>/<season>/gw{N}-league-recap[-draft].md`, opening on a fixed `# Gameweek N Recap: <league name>` title the report writes itself, editorial or not; the editorial is asked for a `## ` headline beneath it and never a title, and whatever heading it opens with anyway is demoted to that level or, when it only restates the title, dropped, so the model's heading is never the document's (issue #349). Records every run into the league history ledger (`<data dir>/league_history/`) as a side effect, backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. The editorial is told to write each race as its own clause and never fold two into one ("respectively"), and is checked against the races afterwards (issue #357): a manager it names as beaten to, or winning, a contested player in a role that player's line does not give them triggers one regeneration told which claims were wrong (a retry that errors keeps the first attempt), and a kept editorial that still fails is saved as written with the wrong claims named on stderr, in a `synthesis_contested_misattribution` JSON warning, and in a callout above the editorial in the report. The same retry and reporting cover two checks on either format (issue #359): a manager's net overall, pre-hit swing or another move's swing pinned on one named move (`synthesis_net_misattribution`), and a tie word ("joint-lowest", "tied for last", "joint-seventh") on a gameweek score or league position no two managers hold (`synthesis_unsupported_tie`). The prompt's GW Standings section names the week's highest and lowest scores, every shared score and position (Pos, and Prev where any is shared) and any season totals the league's tie-break placed apart, so "joint" has a line to be held to, and the roster lines call the manager's net "overall". Biggest Bench Haul (both formats) ranks on the best legal XI's gain over the XI that counted — one keeper, the eight legal formations, the doubled player held in place — never on the bench's raw sum, which counted a keeper who could not have played and outfielders no formation had room for (issue #350); its detail names the swap, a manager with bench points but no better XI is no candidate, and the raw sum stays on the row as `bench_points` (the API's `points_on_bench`) because the coarse tier can record nothing else. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) and `first_capture_store_path` (always present; the partition directory on a first capture, `null` after). Fines are ruled *and recorded* against every captured gameweek, backfilled ones included: the detailed replay rules every configured rule, the coarse tier rules only what cohort points support (`last-place`, `below-threshold`) and records that `red-card` went unruled. `last-place` is ruled against every manager level on the gameweek's lowest score rather than whichever of them the cohort's arrival order reached first; because rulings are frozen at capture and the ledger is append-only, gameweeks recorded before that was true are swept on every run and the recorded ruling appended against the managers they left out, with no flag and no request (`league_history_tied_last_place_repaired`) — and where the row data cannot settle who was level, since the gameweek does not record whether it was ruled on gross or net points, the manager is named by `league_history_tied_last_place_ambiguous` rather than fined on a guess. Season totals print under `Season Fines` on console and as a `# Season Fines` report section, but only at the two milestone gameweeks (GW19 and the finale) - every other week those surfaces stay a this-week view and `league-fines` answers the season question on demand. The `## Season Fines` prompt section and `metadata.season_fines` are ungated: the editorial gets the running totals every week as optional colour (never a required beat), and a scripted consumer sees no calendar-driven appearance and disappearance. Bottom-half gameweeks and green-arrow droughts no longer surface as streaks at all (their run only drives the season count). Streak conditions also keep reset-surviving season occurrence counts ("their sixth gameweek win of the season"), each surfaced on its own rule (see [Command Reference](../docs/command-reference.md#league-recap) for the per-count table): wins and last places on multiples of 3 plus any second-half first, captain blanks and waiver hauls/backfires on multiples of 5, hits on multiples of 3, weeks on top on multiples of 5, bottom-half gameweeks on multiples of 10 in the second half only, and green-arrow droughts on an unbroken run of 5 or 10 (gameweeks spent leading the league never count towards a drought — the leader could not have climbed). Some counts carry same-week peers alongside the milestone — past a fixed floor for counts that stay small, or within a relative window for counts that climb all season. A firing count reaches the report's `## Season Counts` subsection and the editorial's League History section; at the two milestone gameweeks both surfaces carry every nonzero count as the season set-piece. Every line states its span and any not-judged gameweeks beside the number, and every nonzero count is emitted in `metadata.notes_pack.season_count_entries` regardless. A recap of the most recently finished gameweek keeps working once the next gameweek has started: the league table describes that later gameweek from its deadline onward, so the run takes the replay path (figures from each manager's own gameweek history, positions and cumulative totals derived rather than read off the table) and raises a `league_standings_moved_on` warning, instead of reconciling two different gameweeks' numbers against each other and exiting 1 with the ledger row unwritten. `--summarise` with no usable synthesis key no longer aborts the run: the recap, report and ledger capture all proceed, and a `synthesis_provider_unavailable` warning says the editorial was skipped; an editorial the provider stopped early on is written out as it stands with the reason on stderr and a `synthesis_stopped_early` warning in JSON. A gameweek that cannot be resolved at all exits 1 in both output formats - it used to exit 1 under `--format json` with a generic `"Could not resolve a gameweek to recap."` and 0 in table mode - and the resolver's own reason (`Gameweek 7 is not yet finished`) now reaches the error envelope and the stderr prose alike. On the league's opening gameweek only (GW1, or its own `start_event`), each classic manager's FPL-wide prior-season record — seasons played, last season's points/rank/top-% finish, best season — is fetched from the manager-history endpoint (one request per manager, that gameweek only, and only with `--save`/`--output` or `--summarise`; a league created after GW1 shares the fetch with its league-start offset) and rendered as a `# Prior Seasons` report section and a locked `## Prior Seasons` prompt section with an explicit count; tenure is phrased FPL-wide because entry and league IDs are reissued each season, and a manager with no record or a failed fetch is named as such rather than omitted. Nothing of it reaches the ledger or the JSON rows; draft has no such endpoint and no such section | Yes | General | No | via-agent |
|
|
49
|
+
| `review` | Review a completed gameweek - squad performance and standings. `--save` writes to `<output dir>/<season>/gw{N}-review.md`; `--compare-recs` reads `gw{N}-recommendations.md` from the same season directory and grades the captain, each transfer and each waiver or free-agent move against it; the captain line sets the recommended captain against the armband in captain points (raw score times that week's multiplier, Triple Captain included), the armband being the vice's when the captain played no minutes, and adds the change to the team total when the recommended captain was in the side that scored; one who was not in the squad or played no minutes is labelled so rather than priced at 0; on a wildcard or free hit week, where the recommendation is a squad, each player brought in is graded against the recommended 15 instead, and a recommended name that fits more than one player is left ungraded with a warning on stderr rather than matched to the first; a part of the file it could not read (the captain, the transfers or the waivers) is left ungraded the same way, the warning naming the file, rather than grading every move unadvised, and a file nothing could be read from is not compared at all. Under `--summarise` the personal analysis is handed next gameweek's fixtures — every club's opponent and venue plus each squad player's positional FDR (1-7 team ratings with custom analysis on, the FPL API's 1-5 difficulty with it off, carrying the same scale footer and unrated-club caveat the FDR tables print, plus any warning that the ratings cannot support difficulty) — so its "Next Week" calls are grounded in fixtures rather than in one week of points; with no next gameweek to look at, that section is narrowed to fixture-independent observations and points at `fpl gw-prep`, and a gameweek already played is marked retrospective. The answer is then checked back against that block — a cited FDR the block never printed, or any fixture claim made without one — and against the sections its prompt asked for: an answer missing a `## ` heading, ending mid-sentence, or that the provider cut off at its token ceiling is retried once, told what the last draft lacked (a refused answer is flagged but never retried, and a retry that errors keeps the first attempt), and a still-incomplete one is warned about on stderr (detail in `data/debug/synthesis_corrections.txt` under `--debug`) and carries the same list as a callout in the saved report. The summary is an add-on the review does not depend on: the two provider roles resolve independently, and a role with no usable key is skipped with the reason on stderr (`Community narrative skipped: ...` / `Personal analysis skipped: ...`) while the review prints, saves and exits 0 — so one key produces that half rather than nothing, and a saved report names each skipped half in a warning callout of its own. A gameweek it cannot resolve - one still being played, an id the season does not have, a season with nothing finished yet - is refused with the specific reason on stderr and exit 1; stdout carries the review or nothing | No | General | No | via-agent |
|
|
50
|
+
| `league-recap` | Recap a completed gameweek - awards, standings, and banter. Saves to `<output dir>/<season>/gw{N}-league-recap[-draft].md`, opening on a fixed `# Gameweek N Recap: <league name>` title the report writes itself, editorial or not; the editorial is asked for a `## ` headline beneath it and never a title, and whatever heading it opens with anyway is demoted to that level or, when it only restates the title, dropped, so the model's heading is never the document's (issue #349). Records every run into the league history ledger (`<data dir>/league_history/`) as a side effect, backfilling classic gaps at a coarse tier automatically; `--backfill-detail` rebuilds earlier gameweeks in full (one request per manager per gameweek). A rebuild keeps the name and position already recorded for each player instead of restamping them from today's bootstrap, and never drops a code the gameweek already resolved; it reads the earliest recorded row rather than the newest, so a gameweek an older version already restamped is repaired rather than re-copied. The club is not carried but *derived*, from the gameweek's own fixtures: FPL's live data records which fixtures each player's club played, so a double gameweek settles his club outright and a single narrows it to two, with one `element-summary` request spent only where today's club is absent from that pair (proof he has moved); today's club being one of the two is left unanswered rather than assumed, so a recorded club still wins there. That reaches the two cases a carry structurally cannot — a first capture and a coarse-to-detailed upgrade, neither of which has a recorded squad — and it supersedes a stale recorded club rather than preserving it, with `league_history_club_rederived` naming how many were replaced. A club that blanked has no fixture to intersect and falls back to the recorded club, then today's; name and position are not derivable from fixtures at all and stay carry-only. Everything the gameweek itself answers (points, cards, pick flags, blank status) is still re-derived, and a `league_history_identity_carried` warning names how many players were kept. A rebuild likewise never writes away a league position or cumulative total it cannot re-derive: draft has no per-manager history endpoint and its standings describe a later gameweek, so a replayed draft gameweek keeps the figures already recorded (`league_history_standings_carried`) instead of nulling them, which used to cost the ledger every position that gameweek held and collapse the streaks built on them. Where they were already lost, every run restores them from the ledger itself — the last superseded line that recorded them (a gameweek captured repeatedly while it was still settling holds a stale position in its first capture and the settled one in its last), or a cumulative total re-summed from earlier gameweeks — with no flag and no request (`league_history_standings_repaired`); a draft gameweek's table is derivable from `gross_points` alone at the league's start gameweek. A draft row also records the waiver claims its manager lost to a rival (`lost_claims`, kept apart from `transactions` because nothing moved — no points, no net, and never the denial that is the cascade of the manager's own accepted claim), so an outbid manager is not written into the permanent record as one who never tried; a gameweek captured before that field existed reads it back as unrecorded rather than as "lost none", is named by the coverage report, and is re-recorded by `--backfill-detail` while the league's transaction feed still serves it; a replay that finds fewer claims than the gameweek recorded keeps the recorded list (`league_history_claims_carried`). Those claims also make the waiver wire's races visible (issue #330): the editorial's waiver roster closes with every player more than one manager claimed, who won him and who was beaten to him at what priority, and a week where at least three managers claimed the same player (`MOST_CONTESTED_MIN_CLAIMANTS`) and one of them can be seen to have won him gets a Most Contested award beside Waiver Genius and Waiver Disaster — derived from the manager rows by `contested_draft_claims()`, never recorded, never a report section of its own, and absent from `--format json`, whose rows already carry the `lost_claims` and `transactions` they are regrouped from. The editorial is told to write each race as its own clause and never fold two into one ("respectively"), and is checked against the races afterwards (issue #357): a manager it names as beaten to, or winning, a contested player in a role that player's line does not give them triggers one regeneration told which claims were wrong (a retry that errors keeps the first attempt), and a kept editorial that still fails is saved as written with the wrong claims named on stderr, in a `synthesis_contested_misattribution` JSON warning, and in a callout above the editorial in the report. The same retry and reporting cover two checks on either format (issue #359): a manager's net overall, pre-hit swing or another move's swing pinned on one named move (`synthesis_net_misattribution`), and a tie word ("joint-lowest", "tied for last", "joint-seventh") on a gameweek score or league position no two managers hold (`synthesis_unsupported_tie`); and a third on draft (issue #390): the net overall, or the other kind's figure, said of one kind of a manager's moves ("his waiver moves netted +3 overall" of a -1 waiver and a +4 free-agent pickup, `synthesis_kind_net_misattribution`), for which a roster line that mixes kinds now gives each kind's net beside the net overall. The prompt's GW Standings section names the week's highest and lowest scores, every shared score and position (Pos, and Prev where any is shared) and any season totals the league's tie-break placed apart, so "joint" has a line to be held to, and the roster lines call the manager's net "overall". Biggest Bench Haul (both formats) ranks on the best legal XI's gain over the XI that counted — one keeper, the eight legal formations, the doubled player held in place — never on the bench's raw sum, which counted a keeper who could not have played and outfielders no formation had room for (issue #350); its detail names the swap, a manager with bench points but no better XI is no candidate, and the raw sum stays on the row as `bench_points` (the API's `points_on_bench`) because the coarse tier can record nothing else. Coverage gaps and capture warnings go to stderr; the command exits 0 even when the store is unreadable. `--format json` emits one row per manager (the captured ledger row shape) plus `metadata.coverage`, `season_phase`, `notes_pack`, `synthesis_summary`, `warnings` (always present, each entry a stable `code` plus rewritable `message` - codes listed in [Command Reference](../docs/command-reference.md#league-history)) and `first_capture_store_path` (always present; the partition directory on a first capture, `null` after). Fines are ruled *and recorded* against every captured gameweek, backfilled ones included: the detailed replay rules every configured rule, the coarse tier rules only what cohort points support (`last-place`, `below-threshold`) and records that `red-card` went unruled. `last-place` is ruled against every manager level on the gameweek's lowest score rather than whichever of them the cohort's arrival order reached first; because rulings are frozen at capture and the ledger is append-only, gameweeks recorded before that was true are swept on every run and the recorded ruling appended against the managers they left out, with no flag and no request (`league_history_tied_last_place_repaired`) — and where the row data cannot settle who was level, since the gameweek does not record whether it was ruled on gross or net points, the manager is named by `league_history_tied_last_place_ambiguous` rather than fined on a guess. Season totals print under `Season Fines` on console and as a `# Season Fines` report section, but only at the two milestone gameweeks (GW19 and the finale) - every other week those surfaces stay a this-week view and `league-fines` answers the season question on demand. The `## Season Fines` prompt section and `metadata.season_fines` are ungated: the editorial gets the running totals every week as optional colour (never a required beat), and a scripted consumer sees no calendar-driven appearance and disappearance. Bottom-half gameweeks and green-arrow droughts no longer surface as streaks at all (their run only drives the season count). Streak conditions also keep reset-surviving season occurrence counts ("their sixth gameweek win of the season"), each surfaced on its own rule (see [Command Reference](../docs/command-reference.md#league-recap) for the per-count table): wins and last places on multiples of 3 plus any second-half first, captain blanks and waiver hauls/backfires on multiples of 5, hits on multiples of 3, weeks on top on multiples of 5, bottom-half gameweeks on multiples of 10 in the second half only, and green-arrow droughts on an unbroken run of 5 or 10 (gameweeks spent leading the league never count towards a drought — the leader could not have climbed). Some counts carry same-week peers alongside the milestone — past a fixed floor for counts that stay small, or within a relative window for counts that climb all season. A firing count reaches the report's `## Season Counts` subsection and the editorial's League History section; at the two milestone gameweeks both surfaces carry every nonzero count as the season set-piece. Every line states its span and any not-judged gameweeks beside the number, and every nonzero count is emitted in `metadata.notes_pack.season_count_entries` regardless. A recap of the most recently finished gameweek keeps working once the next gameweek has started: the league table describes that later gameweek from its deadline onward, so the run takes the replay path (figures from each manager's own gameweek history, positions and cumulative totals derived rather than read off the table) and raises a `league_standings_moved_on` warning, instead of reconciling two different gameweeks' numbers against each other and exiting 1 with the ledger row unwritten. `--summarise` with no usable synthesis key no longer aborts the run: the recap, report and ledger capture all proceed, and a `synthesis_provider_unavailable` warning says the editorial was skipped; an editorial the provider cut off at its token ceiling is regenerated once, told it was cut off (a refused editorial is never regenerated, even with a wrong claim in it), and one the provider stopped early on that is still the one kept is written out as it stands with the reason on stderr, a `synthesis_stopped_early` warning in JSON, and a line in the report's warning callout above the editorial (issue #372). A gameweek that cannot be resolved at all exits 1 in both output formats - it used to exit 1 under `--format json` with a generic `"Could not resolve a gameweek to recap."` and 0 in table mode - and the resolver's own reason (`Gameweek 7 is not yet finished`) now reaches the error envelope and the stderr prose alike. On the league's opening gameweek only (GW1, or its own `start_event`), each classic manager's FPL-wide prior-season record — seasons played, last season's points/rank/top-% finish, best season — is fetched from the manager-history endpoint (one request per manager, that gameweek only, and only with `--save`/`--output` or `--summarise`; a league created after GW1 shares the fetch with its league-start offset) and rendered as a `# Prior Seasons` report section and a locked `## Prior Seasons` prompt section with an explicit count; tenure is phrased FPL-wide because entry and league IDs are reissued each season, and a manager with no record or a failed fetch is named as such rather than omitted. Nothing of it reaches the ledger or the JSON rows; draft has no such endpoint and no such section | Yes | General | No | via-agent |
|
|
51
51
|
| `league-fines` | Show who has been fined this season and how much of it was actually ruled, folded out of the league history ledger. Reads local files only - no network - so it works for any season still on disk (`--season 2025-26`). Nothing is re-ruled: a fine is counted exactly as its gameweek recorded it — including one `league-recap` appended against a manager who tied for last and was left out at capture. Counts, never money (`penalty` is free text; settlement and escalation are not modelled). Every gameweek that could not be ruled is named beneath the table - never captured, unreadable, captured before rulings were recorded, captured with no rules configured, or captured at a fidelity that could not rule a given rule. `--format json` emits one entry per recorded manager, fined or not, each with `counts` (zero included), `total`, `fined_gameweeks`, `ruled_gameweeks`, `unruled_gameweeks`, `first_recorded_gameweek`, `last_recorded_gameweek` and `is_fully_ruled`; `metadata` carries `season`, `fpl_format`, `league_id`, `gameweek`, `start_gameweek`, `rule_types`, `total_fines` and `qualifiers`. Exits 1 with the shared error envelope when no league id is configured for the format | Yes | General | No | direct-api |
|
|
52
52
|
| `captain` | Analyse and rank captain options for next gameweek. Your squad is the default, so an absent `classic_entry_id` exits 1 with an `error` envelope naming `--global` rather than returning the global list as though it were your options; `metadata.my_squad_mode` says which list the payload holds, and `metadata.warnings` carries `captain_global_fallback` when there is no squad to rank yet (before the first deadline) or `captain_no_next_gameweek` when there is no next gameweek at all (finished season, or the next one unpublished). An entry ID that does not resolve is a failure, not a fallback, worded exactly as `squad` words it (`get_own_squad_picks`, `agents/common.py`). JSON candidates include `adjusted_npxg_per_90` and `raw_npxg_per_90` when fixture adjustment is active. Consistency tiebreaker (CV-xGI percentile) phased in GW6-10. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the GW1-9 shrinkage rather than pulled toward the position mean | Yes | Classic | Yes | via-agent |
|
|
53
53
|
| `differentials` | Find differential picks - high potential, low ownership. Inverted consistency bonus (volatile players score higher, phased in GW6-10). Before GW10 the score's quality baseline carries the prior blend (last season's pedigree weighted by confidence) rather than position-mean shrinkage, so a quiet-starting elite is not ranked below a one-game wonder; the ownership, matchup and consistency bonuses stay pure observation; JSON `metadata.warnings` carries the early-season notice while the blend is live (`early_season_prior_informed`, or `early_season_small_sample` when history could not be loaded) plus `understat_team_unmatched` for any club Understat carried no players for, table mode prints them to stderr. Players ruled out of the gameweek (0% chance, or no minutes from GW6) are held out of the blend rather than handed last season's standing. 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 |
|
|
@@ -83,7 +83,7 @@ with `2>/dev/null` without losing the table, whichever command it runs.
|
|
|
83
83
|
|
|
84
84
|
## Skills
|
|
85
85
|
|
|
86
|
-
Agent playbooks in `.agents/skills/`. Each has a `SKILL.md` entry point. Claude Code discovers them via the `.claude/skills/` symlink.
|
|
86
|
+
Agent playbooks in `.agents/skills/`. Each has a `SKILL.md` entry point. Claude Code discovers them via the `.claude/skills/` symlink. The four product skills (gw-prep, squad-builder, update-gw-prep, preview-ingest) pin a model and an effort level in their frontmatter and in every sub-agent parameter block; the levels and the reasons for them are in [README.md](README.md#model-and-effort).
|
|
87
87
|
|
|
88
88
|
Skills write their own outputs (`gw{N}-recommendations.md`, `gw{N}-squad-builder.md`, `season-start-squad.md`) into the same `<output dir>/<season>/` directory, for the same reason.
|
|
89
89
|
|
|
@@ -130,4 +130,4 @@ Python classes in `fpl_cli/agents/` that implement `async run(context) -> AgentR
|
|
|
130
130
|
- `validate_draft_waivers.py` (`.agents/skills/gw-prep/scripts/`) — cross-checks the Draft waiver table against the live waiver pool and squad grid (gw-prep Phase D1). JSON stdout; read-only; exits 0 whatever the validation finds — the one non-zero exit is the startup guard below.
|
|
131
131
|
- `normalise_entities.py` (`.agents/skills/gw-prep/scripts/`) — decodes HTML-escaped text in a report assembled from sub-agent output and reports the entity references it deliberately leaves alone. JSON stdout; rewrites its `--file` in place; shared by gw-prep Phase D, squad-builder Phase E and update-gw-prep Phase D, all of which warn rather than block on its findings. Contract and warn posture live once, in `.agents/skills/gw-prep/references/entity-normalisation.md`.
|
|
132
132
|
- Every script under `.agents/skills/gw-prep/scripts/` imports the `fpl_cli` package, so it runs on the interpreter fpl-cli is installed on — every SKILL.md that calls one (gw-prep, squad-builder, update-gw-prep) writes that interpreter as `[YOUR_PYTHON]`, and squad-builder/update-gw-prep reach gw-prep's `scripts/` cross-skill via `[YOUR_SKILLS_DIR]` since `${CLAUDE_SKILL_DIR}` resolves to the *calling* skill's own directory (issue #221). A standalone `fpl` on `PATH` (uv tool, pipx) provides the command but not the package; `_bootstrap.py` catches that case ahead of the fpl_cli imports and exits 1 with `{"error": true, "messages": [...]}` on stdout, which the phases parsing that stdout surface as a warning instead of a traceback
|
|
133
|
-
- The extractor and the waiver validator locate markdown sections via `fpl_cli.utils.markdown` (`HeadingMatcher`, `find_section`, `section_body`, `leaf_body`, `fence_flags`), tolerating LLM heading drift (qualifiers, case, leading annotations, opt-in aliases) without matching a different heading that shares a prefix. Also used by `fpl_cli/prompts/review.py` for the GW Narrative section boundary.
|
|
133
|
+
- The extractor and the waiver validator locate markdown sections via `fpl_cli.utils.markdown` (`HeadingMatcher`, `find_section`, `section_body`, `leaf_body`, `fence_flags`), tolerating LLM heading drift (qualifiers, case, leading annotations, opt-in aliases) without matching a different heading that shares a prefix. Also used by `fpl_cli/prompts/review.py` for the GW Narrative section boundary. Both scripts read the tables inside those sections with `iter_tables`, the reader `fpl review --compare-recs` uses, so a `| - | - |` placeholder row never ends a table early. The waiver validator also cleans its cells with `split_name_team`, so a `(LIV, MID)` suffix comes off a name as `(LIV)` does, skips the same placeholder cells as the review (`No claim`, `-`), and reports rows under Waiver Recommendations that no table took in (`waiver-rows-unread`, via `pipe_rows`) rather than passing them unchecked. It still matches the template's column names exactly, where the review takes aliases: it checks gw-prep's fresh output, not older files.
|
|
@@ -6,6 +6,7 @@ description: >
|
|
|
6
6
|
each league format. Use when the user asks to prepare for the next gameweek,
|
|
7
7
|
get transfer/waiver recommendations, or review their squad.
|
|
8
8
|
model: opus
|
|
9
|
+
effort: high
|
|
9
10
|
compatibility:
|
|
10
11
|
claude-code: full (parallel Classic + Draft sub-agents via Agent tool)
|
|
11
12
|
codex: partial (sequential execution - no sub-agent spawning)
|
|
@@ -38,6 +39,8 @@ Get it wrong and the scripts say so: each one exits 1 with `{"error": true, "mes
|
|
|
38
39
|
|
|
39
40
|
**In all cases** the final output format is identical: Classic section followed by Draft section (or whichever formats are active).
|
|
40
41
|
|
|
42
|
+
**Model and effort:** the frontmatter pins the orchestrator's model and effort, and each Phase C block pins its own pair. Classic gets more effort than draft because it weighs transfers, hits, chips and captaincy against each other, while draft has no budget, chips or captains to trade off. These are judgement calls, not measurements; the levels, the reasoning and how far each override reaches are in `.agents/README.md` (Model and effort).
|
|
43
|
+
|
|
41
44
|
---
|
|
42
45
|
|
|
43
46
|
## Phase A: Context Detection
|
|
@@ -358,8 +361,13 @@ Each sub-agent receives the JSON output from Phase B commands as context.
|
|
|
358
361
|
|
|
359
362
|
### C1 -- Classic League Analysis (skip if format is "draft")
|
|
360
363
|
|
|
361
|
-
|
|
362
|
-
|
|
364
|
+
```
|
|
365
|
+
Agent tool parameters:
|
|
366
|
+
subagent_type: general-purpose
|
|
367
|
+
model: opus
|
|
368
|
+
effort: high
|
|
369
|
+
description: "GW prep - classic analysis"
|
|
370
|
+
```
|
|
363
371
|
|
|
364
372
|
Branch on `squad_builder_result` (set in Phase A3; unset on transfer weeks):
|
|
365
373
|
|
|
@@ -589,8 +597,13 @@ Proceed immediately (non-interactive).
|
|
|
589
597
|
|
|
590
598
|
### C2 -- Draft League Analysis (skip if format is "classic")
|
|
591
599
|
|
|
592
|
-
|
|
593
|
-
|
|
600
|
+
```
|
|
601
|
+
Agent tool parameters:
|
|
602
|
+
subagent_type: general-purpose
|
|
603
|
+
model: sonnet
|
|
604
|
+
effort: medium
|
|
605
|
+
description: "GW prep - draft analysis"
|
|
606
|
+
```
|
|
594
607
|
|
|
595
608
|
**Prompt structure:**
|
|
596
609
|
|
|
@@ -770,6 +783,10 @@ _Runs after Phase D file write, before Phase E. Not embed-gated._
|
|
|
770
783
|
> Edit the file to fix the flagged row(s), then re-run `/gw-prep` to continue.
|
|
771
784
|
|
|
772
785
|
**Stop the pipeline.** Do not proceed to Phase E. Do not mark the run as complete.
|
|
786
|
+
- Else, if a warning has `type == "waiver-rows-unread"` → emit this in-chat warning, followed by the pool-miss one below if any `waiver-not-in-pool` flags are present too:
|
|
787
|
+
> ⚠️ Phase D1: {rows} row(s) under Waiver Recommendations in `gw{N}-recommendations.md` were not read as waiver rows, so they were not checked for cross-position swaps. A table needs a `|---|` row straight under its header, no blank line inside it, and nothing stacked beneath it. Fix the table and re-run `/gw-prep` before submitting waivers.
|
|
788
|
+
|
|
789
|
+
Proceed to Phase E.
|
|
773
790
|
- Else (only warnings and/or `waiver-not-in-pool` flags) → emit an in-chat warning:
|
|
774
791
|
> ⚠️ Phase D1: {N} waiver pool miss(es) in `gw{N}-recommendations.md` — see managed block in the Draft section. Claims may be normaliser false positives; verify before submitting waivers.
|
|
775
792
|
|
|
@@ -195,6 +195,8 @@ The **Data quality** sub-section carries `data_caveat` from gw-prep Phase B1 --
|
|
|
195
195
|
|
|
196
196
|
Every recommendation in this file that leans on fixture difficulty rests on those ratings, so this belongs with the recommendations rather than only in the terminal scrollback the run scrolled past.
|
|
197
197
|
|
|
198
|
+
**Read back by `fpl review --compare-recs`.** The review grades what was done against this file, through the column headers of Transfer Recommendations (`Out`, `In`) and Waiver Recommendations (`Drop`, `Claim`), the `**Recommended captain:**` line, and on embed-mode runs the Classic Squad's `#### Starting XI` and `#### Bench` tables. Write them exactly as above, with the club code in brackets after each player (`Tzolis (ARS)`) and the initial left on (`B.Fernandes`, not `Fernandes`): a renamed column leaves every move in that table ungraded, and a bare surname cannot be told apart from a same-club namesake. When there is nothing to recommend, say so in words ("Roll the free transfer", "No waiver claims this week") rather than leaving the table's rows blank: a blank table reads the same as one the review could not parse. `tests/test_recommendations_parser.py` parses this template, so change it and `fpl_cli/parsers/recommendations.py` together.
|
|
199
|
+
|
|
198
200
|
---
|
|
199
201
|
|
|
200
202
|
## Fallback Banners (Wildcard/Free Hit re-derivation)
|
|
@@ -31,6 +31,7 @@ from fpl_cli.utils.markdown import (
|
|
|
31
31
|
fence_flags,
|
|
32
32
|
find_section,
|
|
33
33
|
has_heading,
|
|
34
|
+
iter_tables,
|
|
34
35
|
leaf_body,
|
|
35
36
|
parse_heading,
|
|
36
37
|
)
|
|
@@ -325,9 +326,6 @@ def _validate_classic_squad_block(block: str) -> ValidationResult:
|
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
|
|
328
|
-
_TABLE_SEPARATOR_RE = re.compile(r"^\|[-:| ]+\|")
|
|
329
|
-
|
|
330
|
-
|
|
331
329
|
def _lines_in_section(block: str, heading: str | HeadingMatcher) -> list[str]:
|
|
332
330
|
"""Return the body lines of `heading`'s section within `block`.
|
|
333
331
|
|
|
@@ -340,21 +338,7 @@ def _lines_in_section(block: str, heading: str | HeadingMatcher) -> list[str]:
|
|
|
340
338
|
|
|
341
339
|
def _count_table_rows_between(block: str, start_heading: str | HeadingMatcher) -> int:
|
|
342
340
|
"""Count markdown table data rows between start_heading and the next heading of same depth."""
|
|
343
|
-
|
|
344
|
-
row_count = 0
|
|
345
|
-
|
|
346
|
-
for line in _lines_in_section(block, start_heading):
|
|
347
|
-
if line.startswith("|"):
|
|
348
|
-
if _TABLE_SEPARATOR_RE.match(line):
|
|
349
|
-
pass
|
|
350
|
-
elif not in_table:
|
|
351
|
-
in_table = True
|
|
352
|
-
else:
|
|
353
|
-
row_count += 1
|
|
354
|
-
elif in_table:
|
|
355
|
-
in_table = False
|
|
356
|
-
|
|
357
|
-
return row_count
|
|
341
|
+
return sum(len(table.rows) for table in iter_tables(_lines_in_section(block, start_heading)))
|
|
358
342
|
|
|
359
343
|
|
|
360
344
|
def _parse_budget_total(block: str) -> float | None:
|
|
@@ -376,26 +360,13 @@ def _tally_teams_from_squad_tables(block: str) -> dict[str, int]:
|
|
|
376
360
|
"""Tally team counts from the Team column in Starting XI and Bench tables."""
|
|
377
361
|
tally: dict[str, int] = {}
|
|
378
362
|
for section_heading in (_HEADING_STARTING_XI, _HEADING_BENCH):
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
headers = [h.strip().lower() for h in line.strip("|").split("|")]
|
|
387
|
-
team_col = next(
|
|
388
|
-
(i for i, h in enumerate(headers) if h == "team"), None
|
|
389
|
-
)
|
|
390
|
-
in_table = True
|
|
391
|
-
else:
|
|
392
|
-
if team_col is not None:
|
|
393
|
-
parts = [p.strip() for p in line.strip("|").split("|")]
|
|
394
|
-
if len(parts) > team_col and parts[team_col]:
|
|
395
|
-
team = parts[team_col]
|
|
396
|
-
tally[team] = tally.get(team, 0) + 1
|
|
397
|
-
elif in_table:
|
|
398
|
-
in_table = False
|
|
363
|
+
for table in iter_tables(_lines_in_section(block, section_heading)):
|
|
364
|
+
team_col = next((i for i, h in enumerate(table.headers) if h.lower() == "team"), None)
|
|
365
|
+
if team_col is None:
|
|
366
|
+
continue
|
|
367
|
+
for row in table.rows:
|
|
368
|
+
if team_col < len(row) and row[team_col]:
|
|
369
|
+
tally[row[team_col]] = tally.get(row[team_col], 0) + 1
|
|
399
370
|
return tally
|
|
400
371
|
|
|
401
372
|
|
|
@@ -407,18 +378,17 @@ def _parse_team_exposure(block: str) -> dict[str, int]:
|
|
|
407
378
|
"""
|
|
408
379
|
exposure: dict[str, int] = {}
|
|
409
380
|
|
|
410
|
-
for
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
pass
|
|
381
|
+
for table in iter_tables(_lines_in_section(block, _HEADING_TEAM_EXPOSURE)):
|
|
382
|
+
for row in table.rows:
|
|
383
|
+
if len(row) < 2:
|
|
384
|
+
continue
|
|
385
|
+
team = row[0].strip("*").strip()
|
|
386
|
+
try:
|
|
387
|
+
count = int(row[1].strip("*"))
|
|
388
|
+
except ValueError:
|
|
389
|
+
continue
|
|
390
|
+
if team and team.lower() != "total":
|
|
391
|
+
exposure[team] = count
|
|
422
392
|
|
|
423
393
|
if not exposure:
|
|
424
394
|
return _tally_teams_from_squad_tables(block)
|
|
@@ -35,7 +35,16 @@ from typing import NotRequired, TypedDict, cast
|
|
|
35
35
|
# envelope instead of a ModuleNotFoundError traceback.
|
|
36
36
|
import _bootstrap # noqa: F401 — import guard, see module docstring
|
|
37
37
|
|
|
38
|
-
from fpl_cli.
|
|
38
|
+
from fpl_cli.parsers.recommendations import PLACEHOLDER_CELLS
|
|
39
|
+
from fpl_cli.utils.markdown import (
|
|
40
|
+
HeadingMatcher,
|
|
41
|
+
find_section,
|
|
42
|
+
has_heading,
|
|
43
|
+
iter_tables,
|
|
44
|
+
leaf_body,
|
|
45
|
+
pipe_rows,
|
|
46
|
+
split_name_team,
|
|
47
|
+
)
|
|
39
48
|
|
|
40
49
|
# ---------------------------------------------------------------------------
|
|
41
50
|
# Types
|
|
@@ -54,6 +63,7 @@ class Flag(TypedDict, total=False):
|
|
|
54
63
|
class Warning(TypedDict, total=False):
|
|
55
64
|
type: str
|
|
56
65
|
name: str
|
|
66
|
+
rows: int
|
|
57
67
|
|
|
58
68
|
|
|
59
69
|
class ValidatorResult(TypedDict):
|
|
@@ -81,18 +91,14 @@ class SquadEntry(TypedDict):
|
|
|
81
91
|
|
|
82
92
|
_APOSTROPHE_RE = re.compile(r"[‘’ʼʹ`]")
|
|
83
93
|
_HYPHEN_RE = re.compile(r"[–—‒‐‑]")
|
|
84
|
-
_TEAM_SUFFIX_RE = re.compile(r"\s*\([A-Z]{2,4}\)\s*$")
|
|
85
|
-
_MARKDOWN_WRAP_RE = re.compile(r"^[\*_`]+|[\*_`]+$")
|
|
86
94
|
_WHITESPACE_RE = re.compile(r"\s+")
|
|
87
95
|
|
|
88
96
|
|
|
89
97
|
def normalise(name: str) -> str:
|
|
90
98
|
"""Return a canonical key for name matching."""
|
|
91
|
-
|
|
92
|
-
#
|
|
93
|
-
s =
|
|
94
|
-
# strip trailing team suffix like "(CHE)" or "(TOT)"
|
|
95
|
-
s = _TEAM_SUFFIX_RE.sub("", s).strip()
|
|
99
|
+
# strip markdown wrapping and every trailing bracket: "(CHE)", "(LIV, MID)",
|
|
100
|
+
# "(if fit)" -- the same cell cleaning `fpl review --compare-recs` reads with
|
|
101
|
+
s, _ = split_name_team(name)
|
|
96
102
|
# NFKD decompose then drop combining marks
|
|
97
103
|
s = unicodedata.normalize("NFKD", s)
|
|
98
104
|
s = "".join(c for c in s if not unicodedata.category(c).startswith("M"))
|
|
@@ -109,12 +115,12 @@ def normalise(name: str) -> str:
|
|
|
109
115
|
return s
|
|
110
116
|
|
|
111
117
|
|
|
112
|
-
_PLACEHOLDER_VALUES = frozenset({"-", "—", "–", "", "hold", "(depth)", "tbd", "n/a"})
|
|
113
|
-
|
|
114
|
-
|
|
115
118
|
def _is_placeholder(raw: str) -> bool:
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
"""A cell standing in for "no move here", by the cells `fpl review
|
|
120
|
+
--compare-recs` skips: "-", "hold", "No claim". A cell that is nothing but
|
|
121
|
+
a bracket ("(depth)") normalises to "", since normalise takes every
|
|
122
|
+
trailing bracket off a name."""
|
|
123
|
+
return normalise(raw) in PLACEHOLDER_CELLS
|
|
118
124
|
|
|
119
125
|
|
|
120
126
|
# ---------------------------------------------------------------------------
|
|
@@ -245,44 +251,19 @@ def parse_waiver_table(
|
|
|
245
251
|
"""Parse the first markdown table in the section.
|
|
246
252
|
|
|
247
253
|
Returns (headers, rows) where rows are dicts keyed by header name,
|
|
248
|
-
or None if no table found.
|
|
254
|
+
or None if no table found. The table is read by the same rules as
|
|
255
|
+
`fpl review --compare-recs` reads it (`iter_tables`): a placeholder row
|
|
256
|
+
of bare dashes is skipped rather than ending the table above it, and a
|
|
257
|
+
table stacked straight under this one is its own table, not more rows.
|
|
249
258
|
"""
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
lines_iter = list(section_lines)
|
|
254
|
-
for idx, line in enumerate(lines_iter):
|
|
255
|
-
stripped = line.strip()
|
|
256
|
-
if not stripped.startswith("|"):
|
|
257
|
-
# End table at first non-pipe line after rows started (e.g. blank line
|
|
258
|
-
# before the next ### subsection's table).
|
|
259
|
-
if rows:
|
|
260
|
-
break
|
|
261
|
-
continue
|
|
262
|
-
if headers is None:
|
|
263
|
-
# first pipe-delimited line is the header row
|
|
264
|
-
parts = [p.strip() for p in stripped.strip("|").split("|")]
|
|
265
|
-
headers = parts
|
|
266
|
-
continue
|
|
267
|
-
# skip separator row
|
|
268
|
-
if re.match(r"^\|[-| ]+\|", stripped):
|
|
269
|
-
continue
|
|
270
|
-
# Detect a new header row stacked directly after the current table:
|
|
271
|
-
# current line is pipe-delimited and the next line is a separator. Break
|
|
272
|
-
# before consuming it as a data row.
|
|
273
|
-
if rows and idx + 1 < len(lines_iter):
|
|
274
|
-
next_stripped = lines_iter[idx + 1].strip()
|
|
275
|
-
if re.match(r"^\|[-| ]+\|", next_stripped):
|
|
276
|
-
break
|
|
277
|
-
parts = [p.strip() for p in stripped.strip("|").split("|")]
|
|
278
|
-
row: dict[str, str] = {}
|
|
279
|
-
for i, h in enumerate(headers):
|
|
280
|
-
row[h] = parts[i] if i < len(parts) else ""
|
|
281
|
-
rows.append(row)
|
|
282
|
-
|
|
283
|
-
if headers is None:
|
|
259
|
+
table = next(iter_tables(section_lines), None)
|
|
260
|
+
if table is None:
|
|
284
261
|
return None
|
|
285
|
-
|
|
262
|
+
rows = [
|
|
263
|
+
{header: row[i] if i < len(row) else "" for i, header in enumerate(table.headers)}
|
|
264
|
+
for row in table.rows
|
|
265
|
+
]
|
|
266
|
+
return table.headers, rows
|
|
286
267
|
|
|
287
268
|
|
|
288
269
|
# ---------------------------------------------------------------------------
|
|
@@ -449,12 +430,29 @@ def _run(recommendations_file: str, waivers_json: str, squad_grid_json: str) ->
|
|
|
449
430
|
|
|
450
431
|
# Parse waiver table
|
|
451
432
|
table = parse_waiver_table(waiver_lines)
|
|
433
|
+
|
|
434
|
+
# Rows under the heading that the waiver table did not take in -- a header
|
|
435
|
+
# with no `|---|` row under it, rows a blank line cut off, a table stacked
|
|
436
|
+
# beneath -- were never checked, so say so rather than pass a clean result.
|
|
437
|
+
# Only under the heading itself: the whole-section fallback holds the
|
|
438
|
+
# Starting XI and Bench tables too.
|
|
439
|
+
unread = 0
|
|
440
|
+
if has_heading(section_lines, _HEADING_WAIVERS):
|
|
441
|
+
read = 0 if table is None else 1 + len(table[1])
|
|
442
|
+
unread = len(pipe_rows(waiver_lines)) - read
|
|
443
|
+
if unread > 0:
|
|
444
|
+
warnings.append(Warning(type="waiver-rows-unread", rows=unread))
|
|
445
|
+
|
|
452
446
|
if table is None:
|
|
453
|
-
|
|
447
|
+
if unread <= 0:
|
|
448
|
+
warnings.append(Warning(type="waiver-table-not-found"))
|
|
454
449
|
result = {"ok": True, "flags": [], "warnings": warnings}
|
|
455
450
|
json.dump(result, sys.stdout)
|
|
456
451
|
return
|
|
457
452
|
|
|
453
|
+
# Exact column names, where `fpl review --compare-recs` takes aliases and
|
|
454
|
+
# any case: it also reads older and post-waiver files under other headers,
|
|
455
|
+
# while this checks gw-prep's own fresh output, written to the template's.
|
|
458
456
|
headers, rows = table
|
|
459
457
|
if "Drop" not in headers or "Claim" not in headers:
|
|
460
458
|
warnings.append(Warning(type="drop-claim-columns-not-found"))
|
|
@@ -7,6 +7,7 @@ description: >
|
|
|
7
7
|
"build my previews folder", or hands over a team preview article and wants it
|
|
8
8
|
usable by squad-builder and gw-prep.
|
|
9
9
|
model: opus
|
|
10
|
+
effort: medium
|
|
10
11
|
compatibility:
|
|
11
12
|
claude-code: full (parallel per-team ingestion via Agent tool)
|
|
12
13
|
codex: full (single sequential pass)
|
|
@@ -54,6 +55,16 @@ expires on a schedule the loader already knows (`fpl intel --show-decay`).
|
|
|
54
55
|
team in a single parallel Agent tool block, each running Phases B–D for its own
|
|
55
56
|
team. They write to different files and do not interact.
|
|
56
57
|
|
|
58
|
+
```
|
|
59
|
+
Agent tool parameters (per agent):
|
|
60
|
+
subagent_type: general-purpose
|
|
61
|
+
model: opus
|
|
62
|
+
effort: medium
|
|
63
|
+
description: "Preview ingest - {TEAM}"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Model and effort:** the frontmatter pins the orchestrator's model and effort and the block above pins each per-team agent's, both at the effort of an extraction step rather than a trade-off step: the schema fixes what to look for, so the work is finding it in the prose, not weighing options. A judgement call, not a measurement; the levels, the reasoning and how far each override reaches are in `.agents/README.md` (Model and effort).
|
|
67
|
+
|
|
57
68
|
**Other agents:** run Phases B–D sequentially per team.
|
|
58
69
|
|
|
59
70
|
## Phase B: Extract
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: squad-builder
|
|
3
3
|
model: opus
|
|
4
|
+
effort: high
|
|
4
5
|
description: >-
|
|
5
6
|
Build an optimal 15-player FPL squad from scratch. Handles mid-season wildcard,
|
|
6
7
|
free hit, and season-start squad selection for both Classic and Draft formats.
|
|
@@ -31,6 +32,8 @@ Build-from-scratch squad optimisation. Five modes, one pipeline.
|
|
|
31
32
|
|
|
32
33
|
**Codex / Cursor / Copilot / other agents:** Run all phases sequentially in the same context. Phase B commands can be parallelised if the tool supports it. Phase B2 shortlisting and Phase C position research can be done inline per position. Use the Phase D assembly prompt as a direct instruction rather than dispatching a sub-agent.
|
|
33
34
|
|
|
35
|
+
**Model and effort:** the frontmatter pins the orchestrator's model and effort, and the Phase C and Phase D blocks pin their own. Assembly gets more effort than position research because it enforces every cross-position constraint and settles the trade-offs the position agents only surface, whereas research gathers and ranks within one position. These are judgement calls, not measurements; the levels, the reasoning and how far each override reaches are in `.agents/README.md` (Model and effort).
|
|
36
|
+
|
|
34
37
|
---
|
|
35
38
|
|
|
36
39
|
## Phase A: Context Detection
|
|
@@ -250,6 +253,7 @@ Launch 4 position-research agents in a **single parallel Agent tool block**:
|
|
|
250
253
|
Agent tool parameters (per agent):
|
|
251
254
|
subagent_type: general-purpose
|
|
252
255
|
model: opus
|
|
256
|
+
effort: medium
|
|
253
257
|
description: "Squad builder - {POSITION} research"
|
|
254
258
|
```
|
|
255
259
|
|
|
@@ -377,6 +381,7 @@ Launch after all Phase C agents return (or fail with fallback):
|
|
|
377
381
|
Agent tool parameters:
|
|
378
382
|
subagent_type: general-purpose
|
|
379
383
|
model: opus
|
|
384
|
+
effort: high
|
|
380
385
|
description: "Squad builder - {mode} assembly"
|
|
381
386
|
```
|
|
382
387
|
|
|
@@ -7,6 +7,7 @@ description: >
|
|
|
7
7
|
file. Use when the user says "update gw prep", "second pass", "add gw
|
|
8
8
|
update", or wants to revise recommendations with new information.
|
|
9
9
|
model: opus
|
|
10
|
+
effort: high
|
|
10
11
|
compatibility:
|
|
11
12
|
claude-code: full (single sub-agent via Agent tool)
|
|
12
13
|
codex: full (single sequential pass)
|
|
@@ -110,6 +111,18 @@ Read the scoring and formatting rules:
|
|
|
110
111
|
|
|
111
112
|
Dispatch a single sub-agent with the following brief.
|
|
112
113
|
|
|
114
|
+
```
|
|
115
|
+
Agent tool parameters:
|
|
116
|
+
subagent_type: general-purpose
|
|
117
|
+
model: opus
|
|
118
|
+
effort: high
|
|
119
|
+
description: "Update GW prep - gameweek {N}"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Model and effort:** the frontmatter pins the orchestrator's model and effort and the block above pins the sub-agent's, both at the effort of a trade-off step rather than an extraction step: the update revises the classic and draft recommendations against new information, so it inherits the classic analysis's trade-offs (transfers, hits, chips, captaincy). A judgement call, not a measurement; the levels, the reasoning and how far each override reaches are in `.agents/README.md` (Model and effort).
|
|
123
|
+
|
|
124
|
+
**Other agents:** apply the brief inline in the same context.
|
|
125
|
+
|
|
113
126
|
#### Sub-agent brief
|
|
114
127
|
|
|
115
128
|
You are updating gameweek recommendations with new information. Your job is to
|
|
@@ -36,7 +36,7 @@ For a complete inventory of CLI commands, analysis agents, and skills with JSON
|
|
|
36
36
|
- Help text: describe what the user sees, never reference internal components ("agent", "client")
|
|
37
37
|
- Inner async function: always name `_run`
|
|
38
38
|
- `--format json` commands emit the shared envelope via `emit_json()` / `emit_json_error()` (`cli/_json.py`), never a bare `json.dumps`. Both envelopes go to stdout and every human-readable line to stderr — success is `{command, metadata, data}` and exit 0, failure is `{command, error}` and exit 1, so a consumer parses one stream either way
|
|
39
|
-
- Table mode splits the same way: the table on stdout, and every other line — warnings, progress notices, and the reason for a nonzero exit — on stderr, so `2>/dev/null` means the same thing on every command. Report a failure with `emit_failure()` (or `handle_agent_failure()` for an agent's), and where a call site prints its own prose before `raise SystemExit(1)`, print it on `error_console`, never on `console`
|
|
39
|
+
- Table mode splits the same way: the table on stdout, and every other line — warnings, progress notices, and the reason for a nonzero exit — on stderr, so `2>/dev/null` means the same thing on every command. Report a failure with `emit_failure()` (or `handle_agent_failure()` for an agent's), and where a call site prints its own prose before `raise SystemExit(1)`, print it on `error_console`, never on `console`. `tests/test_cli_notice_streams.py` enforces this on the `fpl review` modules by reading their source; add a module to its `SCANNED_MODULES` once its hits are triaged
|
|
40
40
|
- A failure raised too deep to know the command name or the format goes through a boundary: `api_failure_boundary(command, output_format)` around the `asyncio.run()` for an unreachable upstream, and the `@config_failure_boundary` decorator on the command callback for a `settings.yaml` block a parser rejected. A config parser raises `ConfigError` (`cli/_context.py`) and nothing wider, so the boundary catches the defect and a bug in our own code still surfaces as a traceback — a bare `ValueError` escapes click and, under `--format json`, exits 1 with zero bytes on stdout (#170)
|
|
41
41
|
- **Format awareness:** New commands classified in `CLASSIC_ONLY`/`DRAFT_ONLY` frozensets in `_context.py` (omit for General). Shared commands use `@click.pass_context` and `fmt = get_format(ctx)` to gate irrelevant sections (see `league.py` for pattern).
|
|
42
42
|
- **Context access:** A command reads its settings with `get_settings(ctx)` and its format with `get_format(ctx)` — never by unwrapping `ctx.obj` and never by calling `load_settings()` a second time; `main()` already merged them onto the context. A ruff `TID251` rule enforces both, banning the `CLIContext` and `load_settings` imports in `fpl_cli/cli/` outside `__init__.py` and `doctor.py`. `load_settings()` is for callers with no context (agents, services) and for `fpl doctor`, which runs on an empty `CLIContext` so each check re-reads by design. A helper too deep to hold a context takes the value it needs as an argument (see `transfer_eval.py`'s `_render_table`)
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [2.7.4] - 2026-10-09
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- league-recap checks no longer misread part of one name as another (#380)
|
|
9
|
+
- fpl league's best/worst GW performers now match fpl review (#385)
|
|
10
|
+
- fpl review sends its notices to stderr instead of stdout (#386)
|
|
11
|
+
|
|
12
|
+
## [2.7.3] - 2026-10-09
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- default synthesis model moves to claude-sonnet-5-5 (#362)
|
|
17
|
+
- stop streak notes calling a fully recorded gameweek "not recorded" (#363)
|
|
18
|
+
- show correct ranks and scores in the gameweek review report (#365)
|
|
19
|
+
- review no longer drops the transfers summary on a net-zero total (#366)
|
|
20
|
+
- draft review no longer misreports rendering errors as a failed fetch (#369)
|
|
21
|
+
- stop gw-prep, squad-builder and FPL Mate ruling out a legal 5-2-3 (#370)
|
|
22
|
+
- flag a recap editorial that misnames who lost a contested waiver (#368)
|
|
23
|
+
- draft review no longer crashes on bracketed player or manager names (#373)
|
|
24
|
+
- classic review no longer crashes on bracketed names (#378)
|
|
25
|
+
- flag recap editorials that misplace a transfer net or invent a tie (#374)
|
|
26
|
+
|
|
4
27
|
## [2.7.2] - 2026-09-16
|
|
5
28
|
|
|
6
29
|
### Bug Fixes
|