Skip to content

millpond is designed, not built. Its design book holds the design whole.

How it works

An event reaches millpond as a capture. millpond keeps captures in an inbox, a git repository or a branch in one. Each capture becomes signals: one when the capture carries its own excerpt, or as many as a reader session finds in it. Curation gives every signal one move and joins signals into items. millpond sends an item to one worktree, where an agent does the work, or an item that asks a question to a sounding, which ends on a conclusion you accept.

An adapter knows one source. It lists the source’s events, keys each one, fetches the raw material and writes it to a directory millpond hands it. Then it prints one JSON line per event on standard output:

{"capture": {"key": "release/mise/2026.10.10", "source": "release", "event_at": "2026-10-10T14:02:11Z", "raw": ["mise-2026.10.10/notes.md"], "excerpt": {"text": "…the notes, whole…", "kind": "ask"}}}
{"cursor": "2026-10-17T04:30:05Z"}
{"done": {"listed": 14, "failed": [{"what": "github:jdx/mise", "why": "HTTP 502"}]}}

The key is the event’s identity, the same from every host and every run. The cursor is the adapter’s own bookmark, which millpond hands back on the next run. The done line ends a complete run with its counts and its failures. An adapter keeps no list of what it reported, since millpond dedupes by key.

millpond run <part> starts an adapter on a schedule and keeps its cursor and status. millpond land takes the same lines on standard input from anything that pipes them in: a deployer reporting a failed step, a script, a person. millpond tick runs every part whose schedule is due, so one timer on a host serves every part there.

Lines go into git, raw files stay on the hostOne run of eddy’s wispr part prints one capture line on standard output and writes the meeting’s transcript, 2.4 MB, into its raw directory. millpond banks the transcript in the raw bank on this host, writes the capture into the inbox’s git checkout with a pointer of the form host:path, and commits it. The push carries the commit, 0.6 KB, to GitHub. The transcript is never pushed, so the meeting’s words never reach GitHub. ONE RUN OF ONE ADAPTER THIS HOST eddy · its wispr part millpond run eddy-wispr listing since its cursor… done · 1 capture --raw-dir run-41/ a cursor line RUN STATE · NOT IN GIT cursor …16T09:00Z …16T15:00Z last run ok · 0 taken ok · 1 taken for millpond run, per part a line, on stdout a file, in --raw-dir THE INBOX · A GIT CHECKOUT THE RAW BANK · NEVER IN GIT {"capture":{"key":… transcript.txt2.4 MB, as serveda reader reads it here key meeting/wispr/7c1e04 event_at 2026-10-16T15:00Z raw host-a:raw/7c1e04/… the pointer capture meeting/wispr/7c1e04 GITHUB the remote push the capture 0.6 KB, its pointer never pushed the meeting’s words never reach GitHub
Lines go into git, raw files stay on the hostOne run of eddy’s wispr part prints one capture line on standard output and writes the meeting’s transcript, 2.4 MB, into its raw directory. millpond banks the transcript in the raw bank on this host, writes the capture into the inbox’s git checkout with a pointer of the form host:path, and commits it. The push carries the commit, 0.6 KB, to GitHub. The transcript is never pushed, so the meeting’s words never reach GitHub. ONE RUN OF ONE ADAPTER THIS HOST eddy · its wispr part millpond run eddy-wispr listing since its cursor… done · 1 capture a line a file THE INBOX · GIT THE RAW BANK never in git {"capture":{"key":… transcript.txt2.4 MB, as served key meeting/wispr/7c1e04 event_at 2026-10-16 15:00Z raw host-a:raw/7c1e04 the pointer a commit GITHUB the inbox’s remote push meeting/wispr/7c1e04 0.6 KB, its pointer never pushed the meeting’s words never reach GitHub
One run of eddy's meeting part. The line becomes a commit; the transcript stays in the raw bank, behind a pointer the capture carries.

A line holds what millpond checks and what enters git: the capture and, for a capture that is its own excerpt, its signal. A raw file holds the event as the source served it, whole: a transcript, the full release notes, a log. It can run to megabytes. millpond banks it on the host, outside git, and the capture points at it as <host>:<path>.

Three readers follow that pointer:

  • a reader session, for which a meeting’s transcript is the whole input;
  • millpond’s check that every excerpt a reader writes appears verbatim in the raw material;
  • a person reading past the cut.
Every line takes the same seven stepsOne run hands millpond four lines. Line A, a release that carries its own excerpt, passes the check, the key lookup, raw banking and the capture write, and step 5 writes its one signal, cutting the excerpt at a line under 8 KiB and closing it with what the raw record holds past the cut. Line B has a key with a .. segment and is refused at the check, named in the run. Line C has a key the inbox already holds and is passed over at the lookup. Line D, a meeting with no excerpt, is taken and marked unread, which charges a reader. Step 7 records the run: two taken, one passed over, one refused, the run ended with done, so its cursor advances, and the doctor reads the status from millpond status --json. ONE RUN · FOUR LINES · THE SAME STEPS A release/mise/2026.10.10 carries its excerpt taken · 1 signal, cut at a line B release/../notes a key with a .. segment refused · named in the run C release/jq/1.8.1 a key the inbox holds passed over · counted D meeting/wispr/7c1e04 no excerpt taken · unread, a reader is charged 1 check the line 2 look up the key 3 bank raw files 4 write the capture 5 write its signal 6 mark it unread LINE A’S SIGNAL · ITS EXCERPT, CUT AT STEP 5 - fix: `mise use` keeps the lockfile when a tool is pinned [the raw record holds 41 more lines, 6.2 KB, past this cut under 8 KiB] 7 record the run taken 2 passed over 1 refused 1 ended with done cursor advances line B, a .. in its key the doctor reads millpond status --json
Every line takes the same seven stepsOne run hands millpond four lines. Line A, a release that carries its own excerpt, passes the check, the key lookup, raw banking and the capture write, and step 5 writes its one signal, cutting the excerpt at a line under 8 KiB and closing it with what the raw record holds past the cut. Line B has a key with a .. segment and is refused at the check, named in the run. Line C has a key the inbox already holds and is passed over at the lookup. Line D, a meeting with no excerpt, is taken and marked unread, which charges a reader. Step 7 records the run: two taken, one passed over, one refused, the run ended with done, so its cursor advances, and the doctor reads the status from millpond status --json. ONE RUN · FOUR LINES · THE SAME STEPS A release/mise/2026.10.10 carries its excerpt taken B release/../notes with a .. segment refused C release/jq/1.8.1 the inbox holds passed over D meeting/wispr/7c1e04 no excerpt taken 1 check the line 2 look up the key 3 bank raw files 4 write the capture 5 write its signal 6 mark it unread A B C D A’S SIGNAL, CUT AT STEP 5 - fix: `mise use` keeps the lock [41 more lines, 6.2 KB, in raw] cut at a line under 8 KiB 7 record the run taken 2 passed over 1 refused 1 ended with done cursor advances line B, a .. in its key the doctor reads millpond status --json
Four lines from one run. Each takes the steps in order, and a line that stops never stops the others.

For every line, millpond:

  1. checks it, and refuses a line with a bad key, a missing field or an excerpt with no text, naming it in the run;
  2. looks up the key in every capture the inbox holds, and passes over a key it already holds;
  3. banks the raw files on the host, under the capture’s name;
  4. writes the capture, with its pointer into the raw bank;
  5. writes the one signal of a capture that carries an excerpt. It cuts the text at a line under the inbox’s limit, 8 KiB unless the settings say otherwise, and closes it with what the raw record holds past the cut;
  6. marks the capture unread when it carries no excerpt, which charges a reader;
  7. records the run: its counts, its failures, its cursor and the status a fleet’s doctor reads from millpond status --json.

A run that exits without done keeps every capture it already handed over, since a capture lands once whatever happens. Its cursor stays where it was, and millpond records the run as failed.

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, a Claude session, turns its transcript into signals through millpond’s read tool. millpond refuses a signal whose excerpt does not appear verbatim in the capture’s raw material. The reader’s instructions are the inbox’s own data: what makes a signal, and how to name a speaker the transcript leaves unnamed.

A backlog of hundreds, worked fifty at a timeThe inbox holds 412 unmoved signals. A Claude session lists the oldest fifty and proposes a move for each: answered, join into an item, or drop, tallying drop 18, answered 6 and join 26 into 5 items. The operator changes one row, the stale CI image, from drop to join into item 1, and accepts. One commit writes all fifty moves, five proposed items form, bump mise among them, and 362 unmoved signals remain. The next batch starts where this one ended. AN INBOX’S BACKLOG, WORKED IN BATCHES 412 362 unmoved signals releases 96 · meetings 251 · files 65 releases 85 · meetings 222 · files 55 BATCH 1 · THE OLDEST 50 a session lists them a Claude session proposes you change one row you accept one commit writes all 50 agesource signalproposed move 41 d release jq 1.8.1 is out answered 40 d meeting move the backup window join→ item 2 38 d file an invoice, no ask drop 37 d meeting the CI image is stale drop join→ item 1 35 d release mise 2026.10.10 is out join→ item 1 33 d meeting who owns the runbook? answered 30 d file a newsletter drop and 43 more rows, each with its move drop 18 · answered 6 · join 26 into 5 items drop 17 · answered 6 · join 27 into 5 items batch 2 starts where batch 1 ended accept THE SESSION’S CALLS signals(unmoved, oldest, 50) capture(…) for the 4 unclear moves(the batch of 50) ONE WRITE moves · batch 1 · 50 signals, one commit ITEMS, PROPOSED bump mise 3 signals move the backup window 4 signals fix the docs links 6 signals rotate the deploy key 2 signals answer the vendor’s audit 12 signals each item can go to a worktree of its own
A backlog of hundreds, worked fifty at a timeThe inbox holds 412 unmoved signals. A Claude session lists the oldest fifty and proposes a move for each: answered, join into an item, or drop, tallying drop 18, answered 6 and join 26 into 5 items. The operator changes one row, the stale CI image, from drop to join into item 1, and accepts. One commit writes all fifty moves, five proposed items form, bump mise among them, and 362 unmoved signals remain. The next batch starts where this one ended. AN INBOX’S BACKLOG, WORKED IN BATCHES 412 362 unmoved signals releases 96 · meetings 251 · files 65 releases 85 · meetings 222 · files 55 BATCH 1 · THE OLDEST 50 a session lists them a Claude session proposes you change one row you accept one commit writes all 50 41 d · release jq 1.8.1 is out answered 40 d · meeting move the backup window join→ item 2 38 d · file an invoice, no ask drop 37 d · meeting the CI image is stale drop join→ item 1 35 d · release mise 2026.10.10 is out join→ item 1 33 d · meeting who owns the runbook? answered 30 d · file a newsletter drop and 43 more rows, each with its move drop 18 · answered 6 · join 26 into 5 items drop 17 · answered 6 · join 27 into 5 items batch 2 starts where batch 1 ended accept ONE WRITE moves · batch 1 · 50 signals, one commit ITEMS, PROPOSED bump mise 3 signals move the backup window 4 signals fix the docs links 6 signals rotate the deploy key 2 signals answer the vendor’s audit 12 signals each item can go to a worktree of its own
Nobody picks through hundreds of signals by hand. A session proposes, you change what you disagree with, and one commit writes the batch.

The curate skill starts a Claude session that asks millpond’s signals tool for the oldest fifty unmoved signals, reads the raw material of the unclear ones, and proposes one move for each. You accept the batch or change rows, and the session writes it with one moves call, one commit. The next batch starts where that one ended.

Move What follows
drop millpond records the reason.
answered The move names the claim or record that settled it.
challenge The signal weighs against a claim. Only an inbox beside Flywheel’s claims has one.
join The signal makes an item, or grows a proposed one, with any number of others.
attach The signal joins an item already sent.
route The signal becomes a chore, an item of one, or an ask millpond records.

An unmoved signal stays in the inbox until a move settles it, and its weight counts by the date of its event.

An item is the unit of work: the signals curation joined, as five of a meeting’s ten, or two bumps that move together. millpond keeps its brief and every place it was sent. send takes it to one of two places.

Where an item goes, and what comes back to itItem 4 asks a question: take mise 3.0, a major release? millpond sends it to a sounding, one session in the worktree sounding/mise-3, with question.md written from the item’s signals and the release notes. The session tries 3.0, offers a finding beyond its question back to the inbox as the capture sounding/mise-3/1, and concludes: take 3.0 once CI has it, proposing a bump. You accept, the conclusion becomes item 4’s record, and the proposed bump becomes item 5. millpond sends item 5 to one worktree, where an agent works from BRIEF.md and submits; sluice tests main plus the branch as merged and lands the merge 4be81a0, and millpond writes the landing on item 5. The sounding’s own code never lands. THE INBOX A SOUNDING · ONE QUESTION ITS CONCLUSION · YOU DECIDE ONE WORKTREE SLUICE item 4 · asks a question take mise 3.0, a major? sources: 2 signals, the 3.0 notes sends none yet sounding/mise-3 record: waiting on a conclusion record: its accepted conclusion send sounding/mise-3 its code never lands question.mdwritten by millpond one session, one question › reads the notes, the signals › tries 3.0 in this worktree › sounding offer: a finding › sounding conclude sounding/mise-3/1 a capture conclusion.md answer take 3.0 once CI has it proposes bump mise to 3.0 stays open the lockfile’s format accept go on drop accept keeps the conclusion as item 4’s record item 5 · from item 4 bump mise to 3.0 brief sends none yet item/5-bump-mise landing: waiting on sluice landed 4be81a0 on main send item/5-bump-mise BRIEF.md written by millpond from main removed, landed an agent, in a herdr pane › reads BRIEF.md › bumps packages/mise to 3.0 › wt submit main the agent’s commit the queue of main main + item/5-bump-mise testing as merged… passed 4be81a0 main sluice moves main only to a merge that passed millpond reads sluice’s record of the branch and writes the landing on item 5
Where an item goes, and what comes back to itItem 4 asks a question: take mise 3.0, a major release? millpond sends it to a sounding, one session in the worktree sounding/mise-3, with question.md written from the item’s signals and the release notes. The session tries 3.0, offers a finding beyond its question back to the inbox as the capture sounding/mise-3/1, and concludes: take 3.0 once CI has it, proposing a bump. You accept, the conclusion becomes item 4’s record, and the proposed bump becomes item 5. millpond sends item 5 to one worktree, where an agent works from BRIEF.md and submits; sluice tests main plus the branch as merged and lands the merge 4be81a0, and millpond writes the landing on item 5. The sounding’s own code never lands. THE INBOX A SOUNDING · ONE QUESTION ITS CONCLUSION · YOU DECIDE ONE WORKTREE SLUICE item 4 · asks a question take mise 3.0, a major? sources: 2 signals, the 3.0 notes sends none yet sounding/mise-3 record: waiting on a conclusion record: its accepted conclusion send sounding/mise-3 its code never lands question.mdwritten by millpond one session, one question › reads the notes, the signals › tries 3.0 in this worktree › sounding offer: a finding › sounding conclude sounding/mise-3/1 a capture conclusion.md answer take 3.0 once CI has it proposes bump mise to 3.0 stays open the lockfile’s format accept go on drop item 5 · from item 4 bump mise to 3.0 brief sends none yet item/5-bump-mise landing: waiting on sluice landed 4be81a0 on main send item/5-bump-mise BRIEF.md written by millpond from main removed, landed an agent, in a herdr pane › reads BRIEF.md › bumps packages/mise to 3.0 › wt submit main the queue of main main + item/5-bump-mise testing as merged… passed 4be81a0 main
A question goes to a sounding, and its accepted conclusion becomes the item's record. The work the conclusion proposes becomes a new item, which goes to one worktree, and sluice's landing comes back to it.

One worktree. send makes a worktree of the repository the send names, on a branch named for the item, and writes the brief there. Then it starts an agent on the brief, in a herdr pane or as a command it prints. The agent submits its work through sluice, and millpond writes the landing against the item from sluice’s record of that branch.

A sounding. An item that asks a question ends in an answer, not a change: whether to take a major release, or how to settle what a meeting left open. send opens a sounding with the item’s signals and the captures they came from as its sources. One session works the question in a worktree of its own, and ends on a conclusion: the answer, its evidence, the work it proposes and what stays open. Code the session writes there to test an answer never lands from it.

You decide the conclusion. Accept, and millpond keeps it as the item’s record and makes an item of each piece of work it proposes; curation answers a later signal on the same subject by naming it. Go on, and the sounding starts another round. Drop, and it records why. A finding the sounding makes beyond its question comes back to millpond as a capture of its own, keyed sounding/<name>/<n>, and takes the same path as every other capture.

An inbox that crew keeps sends no item itself. Its settings hand items to crew’s planner, which orders them across teams and turns each into a chore or a unit. Planning across items is crew’s, and Flywheel’s later, and never millpond’s.