A desktop pet that derives the deadline nobody's calendar contains.
A pixel-art sheep that wanders the desktop, chats through a local Ollama model, searches a knowledge graph, and — the actual reason it exists — watches a Canvas calendar feed and warns about what's coming.
Native Swift, no Electron, no cloud service. It auto-launches through launchd and reads the calendar feed from a local file, which doubles as the kill switch: delete the file, the nudges stop.
The Canvas .ics feed publishes the date an initial discussion post is due. It does not publish the date the peer replies are due — that lives in the syllabus as a rule (“by the following Sunday”), not as an event. So the deadline that carries real weight, and that can't be made up after the fact, appears in no calendar at all.
The fix is to treat the feed as raw input rather than truth, and derive the missing events from the syllabus rule. It's the same instinct as the compliance engine in the fleet project: encode the domain rule once, in code, instead of expecting a human to re-apply it correctly every week.
An earlier version routed this through the system's calendar layer, which silently filtered out subscription calendars — so the feed parsed to nothing and the pet cheerfully reported no deadlines. Reading the .ics directly was the correct fix.
Pulled straight from the production codebase — unedited except for trimming.
nextDate(after:matching:) with weekday: 1 handles the month and year rollovers that hand-rolled date math gets wrong, and startOfDay normalizes so a due-time of 23:59 doesn't push the reply a week out. The title is rebuilt from the discussion's own stem, so the derived item is recognizable next to the real one. /// For every discussion, synthesize the peer-reply deadline: the first Sunday
/// strictly after the initial post is due. This date is in NO calendar.
private static func derivedPeerReplies(from items: [CourseworkItem], cal: Calendar) -> [CourseworkItem] {
items.filter { $0.kind == .discussion && !$0.isExtraCredit }.compactMap { d in
guard let sunday = cal.nextDate(after: d.due,
matching: DateComponents(weekday: 1),
matchingPolicy: .nextTime) else { return nil }
let stem = d.title
.replacingOccurrences(of: "Participate:", with: "")
.replacingOccurrences(of: " ", with: " ")
.trimmingCharacters(in: .whitespaces)
return CourseworkItem(due: cal.startOfDay(for: sunday),
title: "Peer replies — \(stem)",
kind: .peerReply,
isExtraCredit: false)
}
}
From the running application.

The repository is private, but I'm glad to walk through the codebase live — architecture, tradeoffs, the parts that went wrong first.