The capture contract
The contract is the one way captures reach an inbox. Flywheel’s statement defines it, a crate in flywheel-next implements it, and millpond and Flywheel both take it. One conformance pack proves every host and every adapter part against it: lines in, records expected.
The lines
An adapter writes JSON Lines to standard output. Each object has
exactly one of three keys: capture, cursor or done.
{"capture": {"key": "release/mise/2026.10.10", "source": "release", "event_at": "2026-10-10T14:02:11Z", "title": "mise 2026.10.10", "url": "https://github.com/jdx/mise/releases/tag/v2026.10.10", "raw": ["mise-2026.10.10/notes.md", "mise-2026.10.10/release.json"], "excerpt": {"text": "…the notes, whole…", "kind": "ask", "asserted_by": "jdx/mise", "assertion": "mise 2026.10.10 is released; the repository pins 2026.9.18.", "subjects": ["release", "mise"], "argues_with": ["packages/mise/default.nix"], "position": "release notes"}}}
{"cursor": "2026-10-17T04:30:05Z"}
{"done": {"listed": 14, "failed": [{"what": "github:jdx/mise", "why": "HTTP 502"}]}}
A capture
A capture line describes one source event:
| Field | Required | Meaning |
|---|---|---|
key | yes | The source event’s identity. The same event gets the same key from every host and every run (111). |
source | yes | The source’s name: release, meeting, folder, deploy. |
event_at | yes | When the event happened, in RFC 3339. |
title | no | One line a person reads in a list. |
url | no | Where the event lives upstream. |
raw | no | The files the adapter wrote under the raw directory, as relative paths. |
excerpt | no | Present only when the capture is its own excerpt. It holds the signal’s fields. |
The key
The key is what makes a capture idempotent. millpond looks every key
up before it writes, so a source event listed twice, by two hosts or by
two runs over the same window, lands once (111). A / separates the
key’s segments, as in release/mise/2026.10.10,
meeting/wispr/<meeting id> and
folder/<folder name>/<sha256 of the file>.
A key never starts with /, never has a .. segment, and never starts
with captures or moves. A key becomes part of a folder’s name when
the capture comes to rest, and these rules keep that folder inside the
inbox and clear of the inbox’s own folders.
The event date
event_at is when the event happened: a release’s publication, a
meeting’s start, the date a dropped file’s name begins with. It is
never when the adapter noticed the event, because weight counts by
event date (118). A backlog imported in one afternoon still weighs as
the months it spans.
The excerpt
An adapter includes an excerpt only when the capture is its own excerpt: when the event’s own words are the signal, with no judgment needed to find it. A release is its own excerpt. Its notes are the excerpt, and its signal asks for a bump. A meeting is not, so a reader session turns its transcript into signals later.
The excerpt holds the signal’s fields (113):
text, the excerpt verbatim and whole;kind, one of constraint, ask, question, commitment and reaction;asserted_by, who said it;assertion, what it asserts, in a sentence;subjects, its subject tags;argues_with, what it argues with, such as the declarations a bump moves;position, where in the source the text sits.
The adapter never cuts the text. millpond cuts it when it writes the signal, so every adapter’s excerpt meets the same limit the same way.
The cursor
A cursor line carries a value only the adapter understands. millpond
keeps the last cursor of a run that ended in done and hands it back
on the next run. An adapter works without one: listing a window again
is safe, because millpond dedupes by key.
The end of a run
A done line ends a complete run with what it listed and the items
that failed, each with what failed and why. One failed item never stops
the others.
A run that exits without done, or with a status other than 0, keeps
every capture already taken, since captures are idempotent. Its cursor
does not advance, and millpond records the run as failed, so the next
run lists the same window again and lands only what the failed run did
not.
Lines and raw files
The lines describe each event, and the raw files hold it.
- A line is small. It holds what millpond checks and what enters git as the capture and its signal.
- A raw file is the material as the source served it, whole: a transcript, the full notes, a log. It can run to megabytes. It stays on the host and never enters git (111).
Once millpond banks the raw files, three readers use them:
- a reader session, for which a meeting’s transcript is the whole input;
- millpond’s check that each excerpt a reader writes appears verbatim in its capture’s raw material;
- a person following the capture’s pointer past the cut.