• v0.64.0 25ab2cad4a

    v0.64.0 Stable

    joeyr released this 2026-09-29 19:27:45 -05:00 | 26 commits to main since this release

    The fantasy tenant can change its own team, behind a new ESPN_WRITES setting that
    defaults to off.
    Under off nothing changes: the same ten reads, name for name. Under
    dry_run or live it gains four tools, all acting on ESPN_TEAM_ID alone. dry_run
    runs every check and sends nothing.

    Added

    • ESPN_WRITES: off (default), dry_run or live. Anything else fails the boot.
      The boot summary's writes= line says which, and with changes on, preflight adds
      check=espn_owner, confirming the cookies manage the team.
    • update_lineup: move players between starting slots, BENCH and IR in one
      transaction. It takes effect immediately. It is refused, with nothing sent, when a
      player cannot play the slot, is locked because his game has started, or would overfill
      a slot. FLEX means the league's one flex slot and is refused where there are two.
    • create_waiver_claim: add a player on waivers and drop one of the team's own. The
      claim runs at the next waiver process, and the drop only happens if it succeeds. It
      refuses a free agent, a FAAB league, a player on another roster, and a claim identical
      to one already pending.
    • list_waiver_claims: the team's pending claims in order, each with the
      claim_id the reorder takes, plus the team's waiver priority.
    • update_waiver_claim_order: set the order, naming every pending claim once.
    • preview: true on all three changes: run every check and return what would be
      sent, without sending it.
    • Every change names each player by id AND exact name, whitespace aside and case
      kept, and refuses on a mismatch. Every change checks that the cookies own the team
      before sending. Every change is read back and reports verified.
    • change on every change response: the change in words, with ESPN's names for the
      players and no status prefix. The bot shows it beside its Confirm button.
    • Tool labels. Every read carries readOnlyHint: true. The four above carry
      _meta.owner_only: true, list_waiver_claims included, because pending claims are
      private on ESPN. The bot decides who may use a tool from these labels.
    • espn/write, a log phase of its own for every request that changes the team.

    Measured before it was built

    All live, on 2026-09-29, from gitignored probes. The checklist is in docs/operations.md.

    • A lineup change is a ROSTER transaction POSTed to
      lm-api-writes.fantasy.espn.com/.../transactions/, answered status: EXECUTED. The
      roster shows it on the very next read. Nine moves in one transaction all landed.
    • A waiver claim is a WAIVER transaction with ADD/DROP items and bidAmount: 0,
      answered status: PENDING and appended to the end of the claim order.
    • Pending claims come from mPendingTransactions under the key pendingTransactions,
      ordered by subOrder. Claims and reorders made in the ESPN app appear there too.
    • Claim order is the request ESPN's own site sends (updateTransactionOrder in its web
      bundle): every pending id with a 1-based subOrder, POSTed to
      /teams/<id>/pendingTransactions.
    • A player filter with a limit and no sort is a 400 (FILTER_LIMIT_MISSING_SORT).

    Changed

    • The server instructions and the get_free_agents description say what this deployment
      can change, per mode. Under off they still say it cannot change anything.
    • The boot summary's writes line reads the setting. It had been a constant.

    Fixed

    • A weather test failed every evening from 20:00 New York time. It counted "twelve
      days out" from the UTC date, while get_outlook counts from the coordinate's own
      date, so after UTC midnight its target was day 13 and outside the 6-10 day window it
      was testing. It now counts the way the tool does. The tool was right throughout.
    Downloads
  • v0.63.0 0ed4ad1930

    v0.63.0 Stable

    joeyr released this 2026-09-24 10:16:54 -05:00 | 27 commits to main since this release

    Every weather tool takes units and timezone, and get_model_run gains UV. An
    answer can now be used as it arrives, with no °F↔°C or UTC→local conversion left to
    the caller. Breaking for existing callers: the default is now metric, and every
    field that carried a unit in its name was renamed. Tool descriptions changed, so the
    connector needs a reconnect after the deploy.

    Added

    • units on all eleven tools: metric (C, km/h, mm, m, hPa) or imperial (F, mph,
      in, ft, mb). When a call omits it, WEATHER_DEFAULT_UNITS decides, and that defaults
      to metric. A misspelled value refuses to boot rather than quietly serving the default.
    • timezone on all eleven tools: local (the coordinate's own zone, the default),
      utc, or an IANA name. Every timestamp in the answer is rendered in that zone with an
      explicit offset, including those inside sentences. An unknown name is refused rather
      than read as UTC.
    • applied on every response: the units and zone used, and where the zone came from
      (nws_points, open_meteo_auto, caller, or fallback when no lookup answered and
      the answer is in UTC).
    • A uv block on get_model_run, with the hourly UV index and clear-sky index, a
      daily maximum per local day, and the model named. It comes from gfs_global on its
      own request. Measured across eleven models, GFS was the only one supplying UV, and
      best_match returned GFS's values without naming a model. A failed UV request costs
      the UV block only.
    • A units block wherever a response carries measured quantities without one:
      get_hourly_forecast, get_taf, list_stations, normals, and distances.
    • Local time-zone lookup outside the US. Open-Meteo's timezone=auto answers with
      the zone name alone, and the answer is cached for 30 days like /points.

    Changed

    • Field renames, so no name claims a unit: temperature_f → temperature,
      dewpoint_f → dewpoint, wind_speed_mph → wind_speed, wind_speed_high_mph →
      wind_speed_high, wind_gust_mph → wind_gust, distance_miles → distance,
      elevation_feet → elevation, model_cell_elevation_feet → model_cell_elevation
      (with model_cell_elevation_unit), searched_miles_north_south →
      searched_north_south (with searched_north_south_unit), the normals'
      normal_high_f/normal_low_f/normal_mean_f → normal_high/normal_low/
      normal_mean, median_precipitation_in → median_precipitation,
      normal_total_precipitation_in → normal_total_precipitation, a sky layer's base_ft
      → base, and the sun block's date_utc → date.
    • Five tools reported "timezone": "UTC" for a US coordinate. get_model_run,
      get_taf, get_observations, get_observation_history and get_outlook now answer in
      the coordinate's zone like the rest.
    • Offset-less start and end are read in the answer's zone, on get_observations
      and get_observation_history, and get_outlook counts days from the coordinate's
      today. All three used the server's TZ before.
    • Sunrise and sunset are filed under local days. Open-Meteo requests name the zone
      and ask for epoch seconds, because given a zone it applies one fixed offset to the whole
      request. Measured across 2025-11-02, its local time strings run an hour off after a
      DST change. A daily row's date is taken from the response's own offset, so the day
      after a change is not labelled with the previous date.
    • Metric Open-Meteo requests omit the unit parameters, which is its default, and the
      answer is checked against a metric echo table recorded on 2026-09-24. Metric
      /forecast/hourly is asked for units=si, so wind_speed_text reads in km/h.

    Removed

    • from_time_local and to_time_local on wind-shift hits, and issued_local on the
      forecast discussion.
      Every time is now in the answer's zone, so each was a second
      copy of the field beside it.

    Notes

    • Two premises of the plan were wrong, and both were caught by measuring before
      building.
      The raw grid was not losing precision: its Celsius values are exact
      conversions of whole Fahrenheit, so they convert back to whole numbers. And
      Open-Meteo's heights and visibility were already in feet under precipitation_unit=inch,
      so converting them for imperial would have converted them twice.
    • The DST change cannot be checked live until late October, because the forecast
      endpoint reaches only 16 days ahead. The suite pins it against recorded fixtures, and
      docs/operations.md carries the live check with its date.
    • WEATHER_DEFAULT_UNITS=imperial in the stack file restores the previous numbers
      without a rebuild. The field renames stay either way.
    • 33 new tests, 1895 in all. Nine new guards were proved by breaking them: the
      source skip, the daily-date offset, refusing an unknown zone, omitting metric unit
      parameters, units=si, UV isolation, the boot refusal, the one rendering pass, and
      reading the window in the answer's zone.
    Downloads
  • v0.62.1 53b194236c

    0.62.1 Stable

    joeyr released this 2026-09-22 23:13:40 -05:00 | 31 commits to main since this release

    Two fixes from the live pass on 0.62.0. No tool description changed, so a deploy is
    enough and no reconnect is needed.

    Fixed

    • get_alerts could lead with a watch while a warning of the same severity was in
      effect.
      At a point in Hudspeth County TX a Flood Watch and a Flash Flood Warning
      were both Severe with the same onset, and the watch came first because onset was
      the only tie-break. Ties now break on the issuing office's own urgency, then
      certainty, then onset: the warning was Immediate and Likely, the watch Future
      and Possible.
    • The weather boot line named three upstreams of six. It names all six now, and a
      test reads the client's host constants so a new one cannot be left off.
    Downloads
  • v0.62.0 2e3ecd1c25

    0.62.0 Stable

    joeyr released this 2026-09-22 22:50:09 -05:00 | 32 commits to main since this release

    Step 5 of the weather enhancement plan: get_observation_history, the tenant's eleventh
    tool.
    What a station actually reported over a past window, from the Iowa Environmental
    Mesonet's METAR archive, which reaches back decades and works anywhere. Every rule below
    was measured against the live archive first, and the live pass matched three KJFK routine
    reports against the NWS feed for the same minutes field for field.

    Added

    • get_observation_history(latitude, longitude, start, end, station, count). Routine
      and special reports, newest first, each with its raw METAR, sky layers, present
      weather, hourly precipitation and peak wind, in the units get_observations uses. A
      field null on every report is named in not_reported_in_window and left off the rows.
      At most seven days per call; past count the window is thinned to one report per hour
      and the response says what that hides.
    • An empty window says which of four things it is: a gap in the record, a date
      outside the span the archive holds for the station, a station the archive does not
      carry (with the next nearest named), or an identifier that does not exist.
    • A seventh upstream, mesonet.agron.iastate.edu, keyless.

    Changed

    • The client paces the archive's data endpoint to one request every 7 seconds. Its
      changelog says a one-second throttle; measured, requests 3 and 5 seconds apart answered
      429 and 6 and up answered 200. Two calls back to back now wait instead of failing.
    • get_observations points at the archive for windows it cannot read and for the raw
      METAR text. Its description changed, so the connector needs a reconnect.
    • in_window and latest_per_hour take any report with a time, so both tools thin
      the same way.

    Notes

    • Outside the US the archive's precipitation is a fill, and it is refused. Heathrow's
      1008 reports from 1 to 21 September carried 0.00 on every one, 89 of them in rain. An
      amount is read only where the report's own remarks state one.
    • A special report carries the running amount since the last routine one, so adding
      every row double-counts. The response says to add the routine reports only.
    • The plan's "strip the K or get nothing" no longer holds for the data endpoint, which
      answers KJFK as JFK. The metadata endpoint still needs JFK, so the K comes off.
    Downloads
  • v0.61.0 9d49227845

    0.61.0 Stable

    joeyr released this 2026-09-22 18:44:22 -05:00 | 33 commits to main since this release

    Step 4 of the weather enhancement plan: get_outlook, the tenant's tenth tool. For a
    date a week to a year out it returns the 1991-2020 normals and NOAA Climate Prediction
    Center outlooks for the period around it. It never returns a forecast for the day.

    Added

    • get_outlook(latitude, longitude, date). point_forecast is always null, with a
      sentence saying why. outlooks lists every CPC product covering the date separately:
      6-10 day, 8-14 day, monthly and the shortest seasonal lead. Each one has its category
      (above_normal, near_normal, below_normal, equal_chances), CPC's probability as
      "at least", its period, its issuance time and its own staleness. normals gives the
      daily and monthly 1991-2020 normals from the nearest station that carries them, named
      with its distance.
    • Two upstreams, both keyless: mapservices.weather.noaa.gov for the outlooks and
      www.ncei.noaa.gov for normals. United States only, Alaska and Hawaii included.
      Elsewhere the answer is outside_nws_coverage.

    Changed

    • The plan named IEM for normals, and the tool uses NCEI. IEM's daily climatology
      averages each station's whole record (133 years for its New York average), while
      CPC's categories are against 1991-2020. NCEI serves exactly that baseline without a
      token.
    • envelope takes a derived_note, so a composite answer can say what kind of
      composite it is. search_wind_shifts keeps its sentence.

    Notes

    • There is no Week 3-4 layer in NOAA's map services. Days 15 to 28 are answered by
      the monthly and seasonal outlooks, and the response says so.
    • The live pass removed a computed probability band. The first draft named an upper
      edge from CPC's contour levels; a 6-10 day value of 36 is not one of them.
    Downloads
  • v0.60.0 c0d9d1ac7e

    0.60.0 Stable

    joeyr released this 2026-09-22 18:23:33 -05:00 | 34 commits to main since this release

    Step 3 of the weather enhancement plan: get_observations reads a past window.
    Measured against both upstreams first. The plan assumed passing start, end and
    limit through would be the whole job, and report volume made it more than that.

    Added

    • start and end on get_observations, as dates or ISO 8601 datetimes, read in
      the server's zone when they carry no offset. end defaults to now and start to a
      day before end; what was defaulted or clamped is named in window.adjusted.
    • A window block: the span, how many reports it held, how many came back, and
      flags for thinning, cutting and a full upstream page.

    Changed

    • A window with more reports than count is thinned to the newest report per
      hour.
      A busy field reports every five minutes, so a day is about 300 reports.
      The response says what thinning hides. count defaults to 48 with a window, up to
      168, and is unchanged without one.
    • A window NWS no longer holds falls through to AWC's hourly reports, which
      reached 30 days back when measured. NWS keeps about seven, and the response says
      which feed answered.
    • A past window is not judged by the staleness ceiling, which would have raised on
      every answer about yesterday. Every row is filtered to the window instead, and the
      response says none of it is current.
    Downloads
  • v0.59.0 0bdd66a990

    0.59.0 Stable

    joeyr released this 2026-09-22 17:43:44 -05:00 | 35 commits to main since this release

    Steps 1 and 2 of the weather enhancement plan: two models side by side with
    precipitation and instability, and a grid that says what it publishes.
    Every
    behaviour below was measured against the live upstreams before it was written, and three
    of the measurements changed the design.

    Added

    • get_model_run returns both configured models in models, the high-resolution
      one and the global one, each with its own run block and run_age. The envelope
      describes the first that answered and envelope_describes names it. Outside the
      high-resolution domain that entry says available: false, outside_domain: true.
    • Sixteen more hourly fields: precipitation, rain, showers, CAPE, lifted index,
      CIN, boundary layer height, UV (both forms), cloud cover total and low/mid/high,
      visibility, weather code and freezing level. A field a model never fills is named in
      variables_not_supplied and dropped from its rows.
    • Sunrise and sunset per UTC day in sun.
    • get_forecast_grid lists what the gridpoint publishes: parameters_available
      and parameters_published_empty, read from the document. probabilityOfThunder is
      in the default set.

    Changed

    • get_model_run's response shape. model_run, run and model_cell moved into
      each entry of models. The description changed too, so the connector needs a
      reconnect, not just a deploy.
    • One request per model, not a comma list in &models=. Measured: over London a
      comma list answers 200 with HRRR silently dropped and its keys unsuffixed.
    • Precipitation probability is not requested. HRRR's ran to hour 48 when its run
      stopped at 45, and matched the GFS ensemble hour for hour, so it was never a value
      from that run. not_requested says so in the response. is_day is left out too.

    Fixed

    • Asking the grid for weather or hazards raised a TypeError. Their values are
      objects. The reader refuses a non-numeric value by type, and the tool names it in
      parameters_not_numeric with a sentence pointing at the tools that carry it.
    • A grid parameter with "uom": null was labelled "None". The key is present, so
      a .get default never applied. probabilityOfThunder is null-uom at every office
      sampled, and its percent is now named in grid.UNDECLARED_UNITS.
    Downloads
  • v0.58.3 f3bd045243

    v0.58.3 Stable

    joeyr released this 2026-09-22 14:52:01 -05:00 | 37 commits to main since this release

    The two items the live pass left open, closed. One by measurement that turned out
    not to need days of waiting, and one by decision.

    Fixed

    • The model stale line fired on about half of all normal reads. It was the update
      interval plus the current run's own availability lag, with no slack, so a next run
      slower than this one tripped it: ECMWF at 814 minutes against 794 on the live pass.
      Open-Meteo's open-data bucket keeps about eight days of runs, and each run's
      meta.json upload time gave the spread directly: the next run landed later on 48% of
      ECMWF cycles (largest slip 0.85 h), 43% of GFS (2.58 h) and 55% of HRRR (1.18 h).
      LAG_SLACK_SECONDS adds 1 h, 3 h and 1.5 h respectively, and the live case is a test.
    • get_model_run put a second coordinate in its response. model_cell carried
      Open-Meteo's cell latitude and longitude, against the rule that the requested
      coordinate is the only one in a response (live-pass check 13). It now carries
      distance_miles to the cell instead, which is how station rows answer the same
      question.
    Downloads
  • v0.58.2 fe273f272f

    v0.58.2 Stable

    joeyr released this 2026-09-22 14:43:02 -05:00 | 38 commits to main since this release

    The weather live pass ran, and every defect it found was a true field under a false
    sentence.
    Fourteen checks from docs/operations.md, run through the connector against
    0.58.1: eleven passed, one was partial for want of a front, and three responses told the
    reader something the data did not support. No green suite could have seen any of them,
    because the fields were right and the tests read fields.

    Fixed

    • A US coordinate was told NWS did not cover it. get_model_run, get_taf and an
      explicit-station get_observations never ask /points, and the shared location block
      gave every caller the outside-coverage sentence, so a KJFK call said the US grid did not
      cover JFK. _location_without_point now takes a required outside_coverage argument,
      and a tool that never asked says it did not look rather than that the answer was no.
    • search_wind_shifts said nothing in it was a forecast. It rode the station list's
      no-issuance branch: "a mapping rather than a weather product... Nothing here is a
      measurement or a forecast", on a scan over a model run and a TAF. envelope gains
      derived, whose sentence says the scan has no issuance time of its own and points at
      the per-source run and bulletin times. The fields are unchanged: issued null and
      stale explicitly null.
    • list_stations reported airports as issuing no TAF when it never looked. From a
      point on Long Island, KEWR and KTEB were listed observation-only; from KJFK the same two
      were listed with a TAF. The TAF box stops widening once it answers and the NWS list
      reaches further, so both arrived from NWS alone. A station outside a product's box now
      carries products_not_searched, and the description says a product named there is
      unknown, not absent.
    • The empty scan's note ran two sentences together: "came back from gfs_hrrr The
      terminal forecast read was...". The model clause now ends with a full stop.

    Changed

    • docs/operations.md records the pass: what passed, the partial, the three defects
      above, and what it left open. The ECMWF stale line fired at 814 minutes against 794 on a
      run that was simply not yet published, which is fourteen minutes of headroom and wants
      several days of samples before it moves. get_model_run's model_cell is a second
      coordinate in the response and keeping it is an operator decision. And the run was
      Claude Code rather than the claude.ai app, so the three wording checks still want a run
      there.
    • CLAUDE.md no longer calls the pass owed, and the station-distance section gains
      the rule that products is what a search found rather than what a station issues.
    Downloads
  • v0.58.1 e02c5e69ef

    v0.58.1 Stable

    joeyr released this 2026-09-22 00:48:51 -05:00 | 40 commits to main since this release

    0.58.0's gate went red, and the check that caught it could not have caught it before the
    commit.
    A coordinate-shaped literal — Open-Meteo's own model-cell position, quoted from a
    live measurement into a docstring — went into weather/model.py. The full suite was green
    locally and red on the first CI run, and the difference is the whole finding:
    test_no_pii.py asked git ls-files, which lists what is tracked, so all eight files
    0.58.0 added were invisible to every rule in that file until the commit that published
    them.

    A guard whose scope is "what is published" has to run on what is ABOUT to be published.
    One that waits for the commit reports the leak instead of preventing it — and reports it
    from CI rather than from the keyboard, after the value has reached a public remote.

    Fixed

    • The literal is gone from weather/model.py, replaced by a description of the value's
      shape. That is CLAUDE.md's own instruction for this case: describe the shape instead.
      Nothing read the number; it was there to record what the endpoint answers.
    • test_no_pii.py now also walks git ls-files --others --exclude-standard, so the set
      it scans is what a git add -A would put in the repo rather than what is already in it.
      --exclude-standard applies .gitignore, so every false positive that function's
      docstring lists — .env, probes/, a scrub script — stays excluded exactly as before.
      Proved by putting a coordinate in an untracked file and watching the rule go red, then
      removing it.

    Changed

    • The 48 weather tests added in 0.58.0 are async def rather than asyncio.run, which
      is what the other 107 in that file already were. pyproject.toml's own filterwarnings
      comment says fastmcp's in-memory transport leaks one half of an anyio stream and that GC
      timing decides which test the warning lands on
      , so left fatal it fails a different
      unrelated test each run. Building and tearing down an event loop per test changes exactly
      that timing, repo-wide, for no benefit — and it put those tests on a different footing
      from every other test in the file.

    No behaviour change. The nine tools, their responses and their descriptions are
    byte-identical to 0.58.0; this is the gate and the test suite.

    0.58.0 published no image, because image has needs: gate. So the running container
    is still 0.57.3 until this one goes green, and the deploy plus connector reconnect that
    0.58.0's entry describes applies to this version instead.

    Downloads