-
v0.71.0
Stablereleased this
2026-10-09 12:18:25 -05:00 | 11 commits to main since this releaseAdded
- Alerts on
create_eventandupdate_event.alertsis 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)"
is1440. list_eventsreports 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,
alertsreplaces the plain alerts and nothing else.[]removes them;
leavingalertsout 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_eventaccepts an update carrying onlyalerts. Its "nothing to change" guard
listed every other field, so without this an alerts-only update would have been refused.alertsis checked strictly. FastMCP's argument checking turns"30"and30.0into
30 andtrueinto 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 withcreatedor
updatedfalse 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 thescoperules contradicting
it. The same stale note onEventis corrected.
Reconnect the calendar connector after deploying, or Claude keeps the old tool list
withoutalerts.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Alerts on
-
v0.70.0
Stablereleased this
2026-10-07 19:16:25 -05:00 | 12 commits to main since this releaseFixed
list_announcementsreturned only the last fourteen days, and said nothing about
it. It called/api/v1/announcementswith nostart_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_dateas 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
sinceonlist_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_feedbackset
in 0.55.0: asinceread 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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.69.0
Stablereleased this
2026-10-06 21:00:50 -05:00 | 13 commits to main since this releaseAdded
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 asBearer; the results
token goes to quiz-api bare, becauseBearerthere 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 begradedorpending(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, itscan_see_results_*flags and its quiz-api status. A hidden result
comes backresults_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 istype_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 sayscorrect_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
authoritativeone, never the latest by
assumption;attemptreads any other.percentis on 0-100, because the service's
percentageis a fraction, and scores are rounded off the service's float noise.
Changed
get_submission_feedbacknamesget_quiz_resultson every graded New Quiz. The row
carriesopens_withbeside thecourse_idandassignment_idit 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_assignmentsandget_assignmentmark a New Quiznew_quiz: true, because
external_toolalone 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.mdis deleted again: C8 was its only item. Its four outstanding live
checks moved toCLAUDE.mdfirst, which now lists seven.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.68.0 Stable
released this
2026-10-05 00:08:47 -05:00 | 16 commits to main since this releaseChanged
- The matchup table's
LIVErow is nowEXPECTED, 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. AsEXPECTEDunder 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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- The matchup table's
-
v0.67.3 Stable
released this
2026-10-04 23:02:46 -05:00 | 17 commits to main since this releaseFixed
-
A real Canvas course id is out of the tree, and
test_no_pii.pynow refuses one.
_course_of's docstring incanvas/client.pyused a course id read off the account as
its example of acontext_code, and had since C2 on 2026-08-24, copied from that
phase's measurement. It iscourse_101now, 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
f502822up to this release. A rewrite
would repoint 45 of 115 tags, this one included. Whether to rewrite is an open decision
indocs/canvas-plan.md, with the numbers.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v0.67.2 Stable
released this
2026-10-04 20:31:47 -05:00 | 19 commits to main since this releaseFixed
- 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.8to112, whose last digit then sat under the tenths above it, and LIVE kept
118.6only 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 to999.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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Every number in the matchup table lines up on its decimal point. The number columns
-
v0.67.1 Stable
released this
2026-10-04 18:20:42 -05:00 | 20 commits to main since this releaseFixed
- 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 shapesJoey's CandRichard'. 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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- A team name in the table's header is cut at a word. This league sends no manager
-
v0.67.0 Stable
released this
2026-10-04 18:08:40 -05:00 | 21 commits to main since this releaseThe 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
LIVErow underTOTAL: ESPN's live projected total for each side, the same
figure as the box score'sprojected_score(ESPN'stotalProjectedPointsLive, 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 like22.25on22.2. The bot's header line rounds the same
figures in JavaScript and said22.3, so a table and the line above it could differ by
a tenth. - The
table.pydocstring'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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- A
-
v0.66.0 Stable
released this
2026-09-30 00:51:44 -05:00 | 22 commits to main since this releaseA 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 carriestable, 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
-
tableon everyget_box_scorematchup, built inespnff/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.0is104).
Rows pair the two teams slot by slot, with
Emptywhere one side has fewer. A bye has
no table (null). -
manageron 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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
-
v0.65.0 Stable
released this
2026-09-29 20:31:50 -05:00 | 25 commits to main since this releaseThe 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 ofESPN_WRITESsends 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 underlive.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_idand byexpected_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_amountoncreate_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: trueon every rehearsal, withdry_run: truebeside it, so a consumer
that reads onlydry_runnever offers a rehearsal as a real change. Measured changes
now sayuntested: false.- The request shapes, read from ESPN's web bundle and not measured. A pickup is a
claim with typeFREEAGENT(addPlayers). A cancel is aWAIVERtransaction with no
items,executionType: CANCELandrelatedTransactionIdset 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.pyis now "only what was measured is sent".MEASUREDnames
the three changes that send (lineup, claim, reorder), and a test pins it._sends
checks it for every change, and_sendrefuses 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
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads