A music player for GNOME and Android

Two native apps.One Rust core.

Built with AI agents. What gets merged is decided by the gates, not by the agent.

The core carries no interface. That is not an architectural preference, it is the reason the second platform had a price tag instead of a rewrite — and the reason a third one would have a price tag too.

Scroll
Available · Q4Five weeks, one developer, agents under gate control.The same method, your codebase ↓
0:00−—:—

The seek bar, shaped by reprise-viewWhat the colours mean ↓

Idea to alpha

5weeks

Counted from the first commit — a design document, not a line of product code — to a running alpha on all four frontends. The weeks are the record in timeline.md, and the number beside it is how many rows it has.

  1. 11–17 Jul

    CORE

    The idea, the workspace split into `reprise-core` and a Linux platform layer, and the UX rulebook that has governed every change since.

  2. 18–24 Jul

    SURFACES

    One frontend became four: `reprise-cli`, `reprise-mcp` and `reprise-stems` joined the GNOME app.

  3. 25–31 Jul

    DEPTH

    No new surface. The single-owner runtime, its versioned protocol and its client went in underneath the ones that already existed.

  4. 1–7 Aug

    ANDROID

    The shared presentation layer, then the FFI bridge and the Android app on top of it — the library running on a phone.

  5. 8–14 Aug

    SIGNATURE

    The GNOME conformance rulebook, and the showroom itself: a prerendered page that reads its own numbers out of the tree.

CH.01

One core, four frontends.

412'958
lines of Rust and Kotlin387'403 Rust · 25'555 Kotlin
45.1 %
of them are tests173'510 Rust · 12'704 Kotlin
1 → 4
one core, four frontendsGNOME · Android · CLI · MCP
27
gates before every mergeall of them, every time

A GNOME desktop app in GTK4 and an Android app in Kotlin with Media3 sit on the same Rust core. So do a CLI and an MCP server — four frontends over one verified application layer, not four codebases that happen to share a name.

Core and edges

Four surfaces. Dependency only points inward.

machine-enforced ↗
  1. 01GNOMEGTK4 · libadwaitanative desktop
  2. 02AndroidKotlin · Composenative mobile
  3. 03CLIRust · terminalheadless surface
  4. 04MCPRust · JSON-RPCagent surface
The dependency arrows only point inward. GNOME and Android keep their native toolkit and interaction model; CLI and MCP reuse the same application layer without pretending to be screens. The build rejects GTK, libadwaita, GStreamer and D-Bus in the core.

Where the lines sit

  • Rust, product · 208'647the core and everything built on it
  • Rust, tests · 166'433inline `#[cfg(test)]` items and the files they pull in
  • Rust, Android bridge · 12'323crates/reprise-android-ffi
  • Kotlin · 25'555the whole Android frontend, product and tests

CH.02

Nobody judges their own writing.

An agent will tell you its work is finished. So will a green test. Neither counts here.

One incident · 2026-08-14

A test was measuring an app that never ships.

A queue test failed on header heights. The headers were fine. The fixture never installs the app stylesheet, so it was measuring widgets the app never renders.

what the test measured

Fixture, no stylesheet

what ships

The app, with its stylesheet

Section header heights, drawn at 3×. The unstyled fixture measured 20 px for Now Playing as a bare label and 34 px for Play Next with a button. The app stylesheet makes both uniform at the 36 px floor.

“A geometry assertion against unstyled widgets passes while the shipped button is a different size.”

the doc comment on app_css_for_test() — written before the incident, naming the trap that produced it

Since then no pull request may claim Fixes #444 until three mutations turn the suite red. If one leaves it green, the claim does not go in.

Fail closed

There is no partial merge.

SourceAI agentone change
Gates · 27
Bounds
Install
Reachable
Traceable
Green
Toolchain
Mutations3 must turn red
Mergedmainall green

27 checks green · ready to merge

27 checks from check-merge-readiness.sh. Hover one to see what it is; click one to fail it. A red check does not stop the report. It stops the merge.

What the checks refuse

Six ways a change can stop short of the branch.

04Boundaries

The core cannot grow a UI framework.

05Distribution

It installs as a desktop app, not as a demo.

03Reachable

Every action works without a mouse.

03Traceable

A rule without a test fails the build.

07Green means green

Tests, lints, formatting, documented API.

05Toolchain hygiene

The branch, the shell scripts, the worktrees.

A rule ID leads to a test, the test to a commit, the commit to the decision. None of that makes an agent trustworthy. It makes trust unnecessary.

CH.03

Two frameworks. One visual signature.

The two apps look different on purpose. GNOME conventions on the desktop, Material on the phone. Making them match would not show craft, it would show missing platform UX. What is shared is the signature — and that is the harder half: two rendering stacks, GSK against Skia, two layout systems, two languages, the same visualisation and the same physics. Not a shared component. A shared specification.

The seek bar is the case in point. The decision: show the structure of the track instead of an empty gutter. The implementation: a portable visuals layer that neither frontend owns. The result: physics that were measured afterwards rather than asserted.

The spectral seek bar, live

Height is the level. Colour is the frequency.

0:00centroid 0.00level 0.00−—:—

Move across the measured track to inspect its values. The bars are shaped by the same functions the apps use — bars.rs, waveform.rs and spectral_colour.rs — with only the band values standing in for live PCM.

Height — the body

Every bar is the RMS of its slice, mapped through the track's own p10–p95 window and smoothed against flicker. A compressed master still shows verse against chorus instead of one loud wall.

Colour — the frequency

The tint is the spectral centroid: coral is low and weighty, teal high and airy.

#FF6F5E · lowhigh · #4FDBD4

Two platforms. Every view, tab and dialogue.

Click any plate to enlarge

CH.04

Two frontends with no screen at all.

A core with no interface is only a claim until something without a screen uses it. The CLI and the MCP server run as separate processes against the same database as the desktop app, and a change-log notifier shows their edits live in a running GTK window without a restart. That is the boundary being load-bearing rather than documented.

reprise-cli · frontend 03

reprise-cli library summary

reprise-cli search "portishead"

reprise-cli playlist create "Focus"

reprise-cli scan ~/Music

reprise-cli instrumental create 481

reprise-cli jobs status --batch b-2f9c

reprise-cli events tail --since 0

reprise-cli concerts list --all --json

Every command takes --json for machine consumption and --db for a scratch library, so automation never has to touch the real one. Deleting a playlist refuses to run without --yes.

reprise-mcp · frontend 04read is safe, writes are opt-in
  • library:readsearch tracks, artists, albums, playlistson
  • playback:controltransport, volume, seek, queueon
  • playlist:createcreate a manual playlistoff
  • playlist:managerename, append tracksoff
  • sources:managepodcasts, YouTube, radiooff
  • device:syncconfigure and run phone syncoff

Tools over stdio, each behind one of six capability flags read live from the library. A revocation takes effect on the next call. Responses never carry filesystem paths, cache locations or credentials — and the source resources omit stored URLs, because those can hold access tokens.

CH.05

Measured afterwards. Price attached.

The title index was rebuilt for one reason: the track list was too slow over a large library. What it bought was measured after the fact rather than asserted before it — and what it cost sits in the same table, not in the small print.

The ledgerWhat the title index cost, and what it bought
Measured over 100'000 tracks, before and after the index rebuild. Quoted from the record, which carries the commit, the date and the method behind every row.
WhatBeforeAfterDelta
Title window over 100'000 tracks53'605 µs1'333 µs−97.51 %
Playback ID projection8'125 µs298 µs−96.33 %
Main-thread CPU while idle110 ms/s64 ms/s−41.8 %
Tag reads on a warm start4190−100 %

The price sits next to it, not in the small print: the title index costs 2'379'776 extra database bytes, up 9.85 %. The track list stays pinned by test to eight cached SQL windows and 1'600 retained rows — unchanged between 10'000 and 100'000 tracks.