• v0.71.0 a03b02fb13

    v0.71.0
    All checks were successful
    CI / gate (push) Successful in 2m39s
    CI / image (push) Successful in 28s
    Stable

    joeyr released this 2026-10-09 12:18:25 -05:00 | 11 commits to main since this release

    Added

    • Alerts on create_event and update_event. alerts is a list of whole minutes
      before the start: [30] is half an hour before, [1440, 30] a day and half an hour
      before, [0] at the start. At most two, matching the iPhone's Alert and Second Alert.
      Until now an event booked through the connector could not carry a reminder at all, and
      each one had to be added by hand on a phone afterwards.
    • An all-day event's only alert is 09:00 on the day, written as [0]. Any other value
      on an all-day event is refused. The count is anchored at 09:00 rather than midnight so
      the Calendar app's own all-day choices read back as round numbers: "1 day before (9 AM)"
      is 1440.
    • list_events reports each event's alerts, earliest first, and only when there are
      some. Every calendar on the account reports them, writable or not; a subscribed calendar
      never does, because Apple usually strips a feed's alerts on the device and reporting the
      file's would describe reminders that never ring. The subscriptions tenant follows the
      same rule.
    • Write responses always state the alerts, as [] when there are none, so clearing them
      is confirmed by the response rather than by a missing field.

    Changed

    • On an update, alerts replaces the plain alerts and nothing else. [] removes them;
      leaving alerts out keeps every alarm as it was, which is what edits did already.
      Travel-time and location alerts are display alarms too, so the rule is not "keep anything
      that is not a display alarm": what is replaced is an alarm a fixed number of minutes from
      the start, shown or sounded. Travel time, location, absolute-time, email and
      counted-from-the-end alarms are never reported and never removed.
    • scope: "all" and "following" give occurrences moved by hand the new alerts. Every
      other field deliberately leaves a moved occurrence alone, and still does. Alerts are the
      exception because "add a 30 minute alert every week" means every week, including the one
      that moved to Wednesday.
    • update_event accepts an update carrying only alerts. Its "nothing to change" guard
      listed every other field, so without this an alerts-only update would have been refused.
    • alerts is checked strictly. FastMCP's argument checking turns "30" and 30.0 into
      30 and true into 1 before a tool runs, so a boolean would have set a one-minute alert
      and reported success. The parameter is advertised as an array of integers and validated
      as raw values, and anything that is not a real integer is refused with created or
      updated false and the value named.
    • update_event's description no longer says repeating events cannot be changed. That
      has been wrong since 0.9.0 and sat one paragraph above the scope rules contradicting
      it. The same stale note on Event is corrected.

    Reconnect the calendar connector after deploying, or Claude keeps the old tool list
    without alerts.

    Downloads
  • v0.70.0 2177f2f011

    v0.70.0
    All checks were successful
    CI / gate (push) Successful in 1m59s
    CI / image (push) Successful in 21s
    Stable

    joeyr released this 2026-10-07 19:16:25 -05:00 | 12 commits to main since this release

    Fixed

    • list_announcements returned only the last fourteen days, and said nothing about
      it.
      It called /api/v1/announcements with no start_date, and Canvas documents an
      omitted one as fourteen days ago. So "recent" in the description was not a choice; it
      was a fortnight nobody had picked, and a course whose last announcement was three
      weeks old answered exactly like a course with none. Found while filing a term's
      announcements into notes, when everything from its first five weeks was out of reach.
    • Both ends of the window now go on every request. Canvas also documents an omitted
      end_date as twenty-eight days after the start, so sending a start alone would have
      moved the window rather than opened it. The default start is a floor before Canvas
      existed (ANNOUNCEMENTS_FLOOR) and the end is a day past now, so the default read is
      every announcement in the active courses.

    Added

    • since on list_announcements, an ISO date or datetime, for "anything new this
      week" without the whole term. A bare date, or a time with no offset, is read in the
      tenant's timezone, because in UTC "since the first" starts the evening before. A value
      that does not parse is refused before any request, naming the argument, rather than
      reaching Canvas as a 400 that names nothing.
    • Announcements come back newest first, sorted here rather than trusted from the
      endpoint, whose order nothing has measured.
    • An empty answer says which kind it is, by the rule get_submission_feedback set
      in 0.55.0: a since read that finds nothing says it was bounded and says nothing about
      earlier weeks; only an unbounded read may say the courses have no announcements.

    Changed

    • The tool description no longer says "recent" and states that the default is the whole
      term. Reconnect the Canvas connector after deploying, or Claude keeps the old one.
    Downloads
  • v0.69.0 e8ac28442e

    v0.69.0
    All checks were successful
    CI / gate (push) Successful in 2m11s
    CI / image (push) Successful in 18s
    Stable

    joeyr released this 2026-10-06 21:00:50 -05:00 | 13 commits to main since this release

    Added

    • get_quiz_results: a graded New Quiz, question by question — every question, every
      option, which one the user picked, which one was correct, the points on each question and
      any feedback. It is C8, opened because a person asked to see their answers on a graded
      homework and the tenant could give only the score. The Canvas tenant is at thirteen tools.
    • No published API returns this, so it reads what the results page reads. Nine
      requests across three hosts: on Canvas the assignment, the course's account, the
      assignment's launch page (the first HTML this tenant parses) and a token mint; on
      quiz-lti the LTI launch and each attempt's results key; on quiz-api the attempt, its
      questions and its scored answers. Every request shape was read out of the page's code or
      the owner's network panel and driven end to end before a line was written. A step that
      answers in any other shape is refused with the step named (failed_step), never
      returned as a partial answer.
    • Two POSTs, the browser's own, and no others. The mint on Canvas and the launch on
      quiz-lti sit at constant paths and carry nothing from a tool argument; a test walks the
      package's source and fails on a third non-GET, and another checks a full call's traffic.
      Neither changes coursework.
    • Five credentials, each to one host. The session cookie and CSRF token stay on
      Canvas; the minted token and the access token go to quiz-lti as Bearer; the results
      token goes to quiz-api bare, because Bearer there is a 401. Quiz hosts are named by
      responses, so each must be HTTPS under the Canvas host's parent domain before anything
      is sent. No token is logged, cached past one call, or returned.
    • Finished attempts only. Canvas must call the quiz graded before any quiz host is
      contacted. The launch's own session must be graded or pending (the next attempt, not
      started); anything else may be an open attempt and refuses the whole call, earlier
      attempts included, because they hold the same questions. Each attempt is then gated on
      its own session, its can_see_results_* flags and its quiz-api status. A hidden result
      comes back results_hidden, not empty.
    • Five question types rendered, as measured: single choice, multiple answer,
      true-false, formula (with the variables that attempt drew) and matching (as prompt pairs
      and the answer bank). Anything else is type_not_rendered. The correct answer is the
      option that scores — on a miss the unpicked option at 1 — and a formula's
      correct_answer; where none is sent the question says correct_answer_not_sent, and a
      wrong multiple-answer or matching question, never yet seen, is read the same way under
      correct_answer_unverified. The owner decided on 2026-10-06 to show correct answers.
    • The attempt that counts is the service's authoritative one, never the latest by
      assumption; attempt reads any other. percent is on 0-100, because the service's
      percentage is a fraction, and scores are rounded off the service's float noise.

    Changed

    • get_submission_feedback names get_quiz_results on every graded New Quiz. The row
      carries opens_with beside the course_id and assignment_id it takes, and a graded
      New Quiz now always earns a row
      , comment or not. Without that the pair could not fire:
      an autograded quiz carries no comment and no rubric, so the live pass found no row to
      name the tool on at all. A classic quiz's row says its results are only in Canvas.
    • list_assignments and get_assignment mark a New Quiz new_quiz: true, because
      external_tool alone is any LTI tool.
    • Every request the Canvas client makes goes through one _exchange, so the four
      cookie rules are written once for the GETs, the launch page and the mint.
    • docs/canvas-plan.md is deleted again: C8 was its only item. Its four outstanding live
      checks moved to CLAUDE.md first, which now lists seven.
    Downloads
  • v0.68.0 89e8c76015

    v0.68.0 Stable

    joeyr released this 2026-10-05 00:08:47 -05:00 | 16 commits to main since this release

    Changed

    • The matchup table's LIVE row is now EXPECTED, under the PTS columns. The owner
      could not tell the table's three numbers per side apart: the pre-game projection, the
      points scored and ESPN's live projection. The live one sat under the PROJ heading beside
      the pre-game total, the bot's header line called it "proj" as well, and "live" fits the
      points scored just as well. As EXPECTED under PTS it reads as what it is, the final
      score ESPN expects. A PROJ column now only ever holds the pre-game guess, and the two
      expected finals sit side by side in the middle, where the scores are compared. The label
      fills the eight-wide name column exactly, so no column moved and the table stays at 45
      characters. get_box_score's description names the new row and where it sits.
    Downloads
  • v0.67.3 ddb113cb68

    v0.67.3 Stable

    joeyr released this 2026-10-04 23:02:46 -05:00 | 17 commits to main since this release

    Fixed

    • A real Canvas course id is out of the tree, and test_no_pii.py now refuses one.
      _course_of's docstring in canvas/client.py used a course id read off the account as
      its example of a context_code, and had since C2 on 2026-08-24, copied from that
      phase's measurement. It is course_101 now, the id the fixtures already use. Every
      rule in the PII guard checks a shape a real value has and an invented one does not, and
      an integer has none of them, so the file passed throughout.

      The new rule takes the one shape left, the digit count. It reads Canvas id shapes only,
      a context code (course_<n>) and a REST path segment (/courses/<n>, and the quiz
      sessions C8 will read), and refuses four or more significant digits. Measured across
      every tracked file before it was set: each invented id had three digits at most or was
      a zero-padded placeholder, and every real id read off the account had five to seven.
      It failed on exactly one line before the fix and passes after. It never prints a
      refused id, because this repo's CI logs are public. A bare integer is out of scope,
      since ESPN and NWS fixtures are full of large ones, and the test says that is the gap
      it leaves.

      The id is still in history: 65 commits, from f502822 up to this release. A rewrite
      would repoint 45 of 115 tags, this one included. Whether to rewrite is an open decision
      in docs/canvas-plan.md, with the numbers.

    Downloads
  • v0.67.2 1f3cc6f455

    v0.67.2 Stable

    joeyr released this 2026-10-04 20:31:47 -05:00 | 19 commits to main since this release

    Fixed

    • Every number in the matchup table lines up on its decimal point. The number columns
      were four characters wide, which holds a player's points and not a team's: TOTAL rounded
      111.8 to 112, whose last digit then sat under the tenths above it, and LIVE kept
      118.6 only by spilling into the space before its column, so it started a character
      left of everything above it. Each number column is now five wide, every value keeps
      ESPN's one decimal up to 999.9, and the table is at most 45 characters rather than 41.
      The owner chose that over keeping 41 and splitting TOTAL into two rows, which would
      still have left a three-digit total in the gap. get_box_score's description says five
      characters where it said four.
    Downloads
  • v0.67.1 dd6b4d89c3

    v0.67.1 Stable

    joeyr released this 2026-10-04 18:20:42 -05:00 | 20 commits to main since this release

    Fixed

    • A team name in the table's header is cut at a word. This league sends no manager
      names, so the header labels each side by its team name, and 0.67.0's wider column cut
      two real ones mid-word, into the shapes Joey's C and Richard'. Seven columns had
      hidden it by luck: the cut landed exactly on a space and exactly before an apostrophe.
      A label now backs up to its last whole word, and drops an apostrophe left dangling at
      the end. One long word is still cut, having nowhere to back up to.
    Downloads
  • v0.67.0 c0f533b305

    v0.67.0 Stable

    joeyr released this 2026-10-04 18:08:40 -05:00 | 21 commits to main since this release

    The matchup table gains a LIVE row, a letter at each end, and one rounding rule. The
    owner read the table mid-game and took it for stale: TOTAL's PROJ adds up the PROJ column,
    so it is what the starters were projected before kickoff, and it sat beside ESPN's live
    "Proj Total" as though the two were the same number. They are not, so the live one now
    has a row of its own.

    Added

    • A LIVE row under TOTAL: ESPN's live projected total for each side, the same
      figure as the box score's projected_score (ESPN's totalProjectedPointsLive, points
      scored so far plus what is still to come). It sits under each PROJ column and keeps its
      decimal: the row has nothing beside its two numbers, so each may take up to five
      characters where a TOTAL rounds to a whole number. get_box_score's description now
      says which of the two rows is which.

    Changed

    • Surnames are cut to seven letters, not six, so the first and last columns are each
      one wider and the table is at most 41 characters rather than 39. Asked for by the
      owner. A flagged name gives up its seventh letter for the space (Longna Q).
    • Every number rounds half up from the decimal ESPN sent. The four-character rule
      already did for whole numbers, but one decimal came from float formatting, which lands
      an exact binary tie like 22.25 on 22.2. The bot's header line rounds the same
      figures in JavaScript and said 22.3, so a table and the line above it could differ by
      a tenth.
    • The table.py docstring's example is invented. The 0.66.0 example quoted real
      managers and players from the live league, which the public-repo rule forbids; the
      history still carries it.
    Downloads
  • v0.66.0 0c6c7577de

    v0.66.0 Stable

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

    A box score now carries its matchup as a table Discord can show. Discord draws no
    markdown table, but it lines up a code block. So every box score carries table, its
    starters laid out in fixed-width columns: the asker's team on the left, then projected
    points, actual points, the position, and the other team mirrored on the right. The model
    is told to show it unchanged rather than lay it out itself.

    Added

    • table on every get_box_score matchup, built in espnff/table.py. It is under
      40 characters wide to fit a phone:

      • starters only;
      • surnames cut to six letters, suffixes dropped, and a defense by its team name;
      • a status letter after a flagged name (Nacua Q), which gives up its sixth letter
        for the space;
      • positions in three letters (FLX, DST);
      • numbers at most four characters: one decimal while that fits, else the nearest whole
        number, halves rounding up (104.0 is 104).

      Rows pair the two teams slot by slot, with Empty where one side has fewer. A bye has
      no table (null).

    • manager on each box score side: the first manager's first name, or the first
      word of their display name. The table's header uses it.

    Downloads
  • v0.65.0 7d34f90d50

    v0.65.0 Stable

    joeyr released this 2026-09-29 20:31:50 -05:00 | 25 commits to main since this release

    The three changes 0.64.0 refused now exist, as rehearsals that are never sent. A
    free-agent pickup, a waiver claim with a FAAB bid, and cancelling a claim were never
    tested against ESPN. They run every check and say what they would have sent, but no
    setting of ESPN_WRITES sends them. They are built so they can be tried now and finished
    later: each needs one live probe before it sends anything. The four changes 0.64.0
    measured are untouched and still send under live.

    Added

    • create_free_agent_pickup: add a free agent and drop one of the team's own, at
      once. It refuses a player on waivers (that is a claim), a player on another roster, and
      a name that does not match its id. Rehearsal only.
    • delete_waiver_claim: cancel one of the team's pending claims, named twice, by
      claim_id and by expected_add_player_name, the exact name of the player it adds. It
      refuses a claim that is not the team's or not pending. Rehearsal only.
    • bid_amount on create_waiver_claim, for a league that bids with FAAB. It is
      refused in a league that does not, required in one that does, and checked against the
      budget left. A claim with a bid is a rehearsal; one without is sent as before.
    • untested: true on every rehearsal, with dry_run: true beside it, so a consumer
      that reads only dry_run never offers a rehearsal as a real change. Measured changes
      now say untested: false.
    • The request shapes, read from ESPN's web bundle and not measured. A pickup is a
      claim with type FREEAGENT (addPlayers). A cancel is a WAIVER transaction with no
      items, executionType: CANCEL and relatedTransactionId set to the claim
      (cancelWaiverClaim). The fake ESPN in the tests models both, marked GUESSED. Tests
      that widen the measured set drive the send and read-back paths against it.
    • What promoting one takes, in docs/operations.md: a probe each, and what to
      measure. A FAAB bid cannot be measured in this league, which does not bid.

    Changed

    • Rule 6 in writes.py is now "only what was measured is sent". MEASURED names
      the three changes that send (lineup, claim, reorder), and a test pins it. _sends
      checks it for every change, and _send refuses any POST outside it as a backstop.
    • A claim for a free agent is refused with a pointer to create_free_agent_pickup,
      where it said that pickups were unsupported.
    • The instructions and descriptions name the rehearsals as NOT LIVE YET and tell the
      model to say the change was not made. The live boot line says pickups, bids and cancels
      are rehearsed, never sent.

    Fixed

    • Nothing tested that a claim-order preview or dry run sends nothing. A mutant that
      let the reorder send under both passed the whole suite. The guard itself was right; it
      was unproven. A test now covers both, and all 19 guards touched in this release were
      mutated one at a time and caught.
    Downloads