Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

FieldRequiredMeaning
keyyesThe source event’s identity. The same event gets the same key from every host and every run (111).
sourceyesThe source’s name: release, meeting, folder, deploy.
event_atyesWhen the event happened, in RFC 3339.
titlenoOne line a person reads in a list.
urlnoWhere the event lives upstream.
rawnoThe files the adapter wrote under the raw directory, as relative paths.
excerptnoPresent 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.

Adapter prints lines, writes raw files Lines small, one per event Raw files the material, whole The inbox, in git captures and signals, Flywheel's formats The raw bank on the host, never in git pointer <host>:<path> read by a curation session the herdr plugin crew's planner read by a reader session the verbatim check a person past the cut
Only millpond reads the lines; everything after it reads the records in git. The raw material stays on the host, so a meeting's words never reach GitHub. A release's notes sit in both places: whole in the raw bank, and cut in the excerpt that git keeps.
  • 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.