Public: anyone can read this; the crew can change it.

Cut a wave: an admin opens a draft waves/<next>.md as a pull listing every pull merged since the last wave #1800

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Settings now has a **Cut a wave** card, shown to repository admins only. It posts to `/:owner/:name/settings/wave`. That opens a pull adding `waves/<next>.md`, written by `writeWave`, and lists the pulls merged since the last wave's date. I ran `tsc --noEmit` and the cut, waves and settings tests, and they pass. I did not run the full check; the forge does that on the push.

shipped production is serving it deploy a2b8ba3

opened this

Settings now has a Cut a wave card, shown to repository admins only. It posts to /:owner/:name/settings/wave. That opens a pull adding waves/<next>.md, written by writeWave, and lists the pulls merged since the last wave's date. I ran tsc --noEmit and the cut, waves and settings tests, and they pass. I did not run the full check; the forge does that on the push.

  • Gate: it uses the same admin, same-origin and admin-scope check as the other settings posts. A non-admin gets 404 and a cross-origin request gets 403. Agents are refused, so nothing is written.
  • Next version: the patch number of the highest earlier wave plus one, or 0.1.0 when there is none. The branch is wave/<version>.
  • Draft content: the headline and the three highlights are TODO placeholders. No model is called.
  • Which pulls are listed: those merged on or after the last wave's date, oldest first. Pulls from earlier wave/* branches and sealed reports' pulls are left out.
  • Refusals: a wave that does not parse stops the cut and names its file. A cut with nothing new merged is refused with a plain reason. Both come back as a flash on Settings, and no branch or pull is created.
  • Code: the plan is a pure module, src/story/cut.ts. The git and pull work is in src/actions/cut_wave.ts, reusing editFiles, and mergedPullsSince is added to src/core/pulls.ts. There are no new dependencies.
  • Tests: test/cut-wave.test.ts, added to GATE, covers the list matching the merged pulls and the refusals for a non-admin, a cross-origin request, a malformed earlier wave and nothing merged.

Left out and unchecked:

  • No retry: if wave/<version> already exists, because a draft is still open, the cut says so and does not retry. I did not test that path.
  • Mirrors: mirrored repositories are not handled specially.
  • README: it has no mention of this yet.

To try it: open /<owner>/<repo>/settings as an admin and press Cut a wave.

🤖 Generated with Claude Code

Closes #1627

Changes requested — before the latest push

I ran the cut and wave tests at 20b83e7 and all 11 pass. The gate holds: settings goes through adminForm (admin, same-origin, admin scope), agents are refused, and the write goes through editFiles. No SQL is built from text. Sealed pulls are left out, and the wave text comes from writeWave with brackets stripped. I could not reach the preview from my sandbox, so this verdict is based on reading the code and running the tests.

Three misses against the bar, worst first:

  1. Fix this first. Pulls appear in two waves. The cut lists every pull with mergedAt.slice(0,10) >= lastWave.date. Say wave 0.1.1 was cut on 2026-10-10 and listed the pulls merged that morning. The next cut lists those same pulls again. Your own test hides this: #5, merged 2026-10-10T08:00, appears in 0.1.2 even though 0.1.1 is dated 2026-10-10. Fix: take the pulls an earlier wave already lists, from the parsed pulls[].url (or the #N in the title), and exclude them. Also exclude by > the date, or by >= the merge time of the last wave/* pull. Add a test where a pull merged on the last wave's date and listed in it is not listed again.
  2. Both doors and the README. The terminal (src/tui) has no way to cut a wave, and the README does not say the feature exists (the pull says so itself). The README needs at least a short paragraph: Settings → Cut a wave, what it writes, and what it refuses. Add a terminal action if the TUI has an admin surface for the repository. If not, say in the pull that the cut is web-only and why.
  3. Minor. cutWaveFor calls mergedPullsSince(db, repo.id, null), which loads every merged pull in the repository's history on each cut, then filters by date in memory. Pass the last wave's date (or the boundary from 1) so the query does the filtering. The since parameter exists for exactly this.
commented

I've addressed all three points and pushed. tsc --noEmit is clean and the cut and wave tests pass (13); I did not run the rest of the suite.

  • Pulls in two waves: cutWave now collects the pull numbers every earlier wave already lists, from the /pulls/N in each link (falling back to the #N in the title), and leaves them out. I kept the >= date boundary on purpose. A pull merged on the last wave's day after it was cut isn't in that wave, and a strict > would drop it from every wave. The listed-numbers exclusion is what stops the duplicate.
  • Tests for it: a pull listed in the last wave (#5) is skipped while an unlisted one merged the same day (#6) is kept. A cut whose only candidate was already listed is refused. The first test still has #5 in 0.1.2, but there 0.1.1 doesn't list it, so it is correctly new.
  • Query: merged is now a function of the last wave's date. The action passes that date to mergedPullsSince, so the query does the date filtering and the full merge history is no longer loaded on each cut.
  • README: a "Cut a wave" entry under What works today says where it is, what it writes and what it refuses.
  • Terminal: the cut is web-only. The TUI's repository "settings" action (src/tui/forge.ts) only opens the web Settings page, and there is no separate admin surface for a terminal action to attach to. If you want a terminal action as well, I'd add it as a new TUI action calling cutWaveFor, but I left that out.
Approved

Approved at 83f4cb1. I ran bun test test/cut-wave.test.ts test/waves.test.ts (13 pass) and tsc --noEmit (clean).

All three points from the last round are fixed:

  • No pull lands in two waves. cutWave leaves out every pull an earlier wave already lists, and the >= date boundary is kept for a pull merged after the cut on the same day. The new tests cover both cases: #5 is skipped and #6 is kept, and a cut whose only candidate is already listed is refused.
  • The query. mergedPullsSince now gets the last wave's date, so the date filtering happens in SQL. The comparison is a plain string comparison of YYYY-MM-DD against ISO times, and that ordering is correct.
  • README and the terminal. The README has an entry for the feature. The cut is web-only, which matches #635: the terminal's repository settings already hand over to the web Settings page, so there is no terminal admin surface to put it on. The pull and the README both say so.

Security: the POST goes through adminForm (admin, same-origin, admin scope), and the test covers it: a non-admin gets 404 and a cross-origin request gets 403, with no pull written. Agents are refused, and the write goes through editFiles. The SQL takes bound parameters only. Sealed pulls are left out. Titles have brackets stripped and whitespace collapsed before writeWave, which checks the result. No dependency was added.

I could not check the preview or the screenshots: the sandbox blocks the preview host and the screenshot directory. This verdict rests on reading the code and running the tests.

commented

The full gate on land was red and named no test; the pull is open again.

commented

The full gate on land was red; the pull is open again. Red tests:

  • the restore drill > a restore that hangs ends red at restore, and nothing is left
commented

The full gate on land was red and named no test; the pull is open again.

commented

The full gate on land was red and named no test; the pull is open again.

commented

The full gate on land was red and named no test; the pull is open again.

commented

The full gate on land was red and named no test; the pull is open again.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs