Anja Gutierrez
macOS · native Swift

Woolly

A desktop pet that derives the deadline nobody's calendar contains.

SwiftmacOS nativeEventKitICS parsingOllamalaunchd
1Native binary
5Deadline kinds
0Cloud dependencies
100%Local inference

What it is

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 deadline that isn't in the calendar

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.

The code

Pulled straight from the production codebase — unedited except for trimming.

main.swift swift
Synthesizing an event that has no source record. For each non-extra-credit discussion, find the first Sunday strictly after the post is due and emit a derived deadline. 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)
        }
    }

Screens

From the running application.

Woolly desktop pet showing calendar deadlines
Woolly on the desktop, surfacing what's due.

Want to see more of this one?

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