aboutsummaryrefslogtreecommitdiff
path: root/docs/specs
diff options
context:
space:
mode:
Diffstat (limited to 'docs/specs')
-rw-r--r--docs/specs/2026-07-06-fancy-music-player-ui-spec.org203
-rw-r--r--docs/specs/2026-07-06-fancy-music-player-ui.prototype.html207
-rw-r--r--docs/specs/2026-07-06-radio-browser-lookup-spec.org202
-rw-r--r--docs/specs/2026-07-06-radio-browser-lookup.prototype.html190
-rw-r--r--docs/specs/2026-07-10-org-workflow-doctor-spec.org260
-rw-r--r--docs/specs/2026-07-17-org-agenda-fullscreen-frame-spec.org404
-rw-r--r--docs/specs/ai-kb-spec.org14
-rw-r--r--docs/specs/ai-vterm-spec.org (renamed from docs/specs/ai-vterm-spec-superseded.org)12
-rw-r--r--docs/specs/cache-helper-design-spec.org (renamed from docs/specs/cache-helper-design-spec-implemented.org)14
-rw-r--r--docs/specs/company-to-corfu-migration-spec.org12
-rw-r--r--docs/specs/coverage-spec.org (renamed from docs/specs/coverage-spec-implemented.org)12
-rw-r--r--docs/specs/debug-profiling-spec.org12
-rw-r--r--docs/specs/dev-setup-project-spec.org12
-rw-r--r--docs/specs/dupre-clear-theme-spec.org12
-rw-r--r--docs/specs/face-font-diagnostic-popup-spec.org (renamed from docs/specs/face-font-diagnostic-popup-spec-implemented.org)13
-rw-r--r--docs/specs/flycheck-modeline-customization-spec.org (renamed from docs/specs/flycheck-modeline-customization-spec-implemented.org)12
-rw-r--r--docs/specs/gloss-spec-doing.org320
-rw-r--r--docs/specs/google-keep-emacs-integration-spec.org9
-rw-r--r--docs/specs/init-load-graph-spec.org (renamed from docs/specs/init-load-graph-spec-doing.org)54
-rw-r--r--docs/specs/keybinding-console-safety-spec.org (renamed from docs/specs/keybinding-console-safety-spec-doing.org)19
-rw-r--r--docs/specs/messenger-unification-spec.org14
-rw-r--r--docs/specs/music-config-without-emms-spec.org12
-rw-r--r--docs/specs/org-faces-spec.org (renamed from docs/specs/org-faces-spec-implemented.org)13
-rw-r--r--docs/specs/signal-client-spec.org (renamed from docs/specs/signal-client-spec-doing.org)13
-rw-r--r--docs/specs/theme-studio-completion-preview-spec.org11
-rw-r--r--docs/specs/theme-studio-nerd-icons-colors-spec.org9
-rw-r--r--docs/specs/theme-studio-package-faces-spec.org (renamed from docs/specs/theme-studio-package-faces-spec-doing.org)14
-rw-r--r--docs/specs/theme-studio-palette-generator-spec.org (renamed from docs/specs/theme-studio-palette-generator-spec-doing.org)15
-rw-r--r--docs/specs/theme-studio-perceptual-color-metrics-spec.org (renamed from docs/specs/theme-studio-perceptual-color-metrics-spec-implemented.org)14
-rw-r--r--docs/specs/theme-studio-preview-locate-spec.org13
-rw-r--r--docs/specs/theme-studio-seeding-engine-spec.org (renamed from docs/specs/theme-studio-seeding-engine-spec-doing.org)21
-rw-r--r--docs/specs/theme-studio-semantic-theme-architecture-spec.org19
-rw-r--r--docs/specs/theme-studio-structured-output-spec.org13
-rw-r--r--docs/specs/utility-consolidation-spec.org (renamed from docs/specs/utility-consolidation-spec-doing.org)20
-rw-r--r--docs/specs/vterm-to-ghostel-migration-spec.org (renamed from docs/specs/vterm-to-ghostel-migration-spec-implemented.org)14
35 files changed, 1742 insertions, 466 deletions
diff --git a/docs/specs/2026-07-06-fancy-music-player-ui-spec.org b/docs/specs/2026-07-06-fancy-music-player-ui-spec.org
new file mode 100644
index 00000000..832a9dfa
--- /dev/null
+++ b/docs/specs/2026-07-06-fancy-music-player-ui-spec.org
@@ -0,0 +1,203 @@
+#+TITLE: Fancy music-player UI (hi-fi / vinyl) — Spec
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-06
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Fancy music-player UI
+:PROPERTIES:
+:ID: af4f2688-ce7d-43f5-82e5-595a603e2593
+:END:
+- 2026-07-09 Thu @ 17:50:32 -0500 — Craig ran the Phase-3 visual VERIFY and found no issues: hero cover art, serif title, amber accents, advancing bar, on-air marker, and the plain-text fallback all pass. All three phases built and pushed. DOING -> IMPLEMENTED.
+- 2026-07-06 Mon @ 15:14:06 -0500 — spec-response Phase 6: decomposed into 3 build tasks + flip-to-IMPLEMENTED under the todo.org parent (stamped :SPEC_ID: af4f2688), Phase-3 visual VERIFY filed. READY -> DOING.
+- 2026-07-06 Mon @ 15:14:06 -0500 — spec-response: all 7 findings dispositioned (5 accept, 2 modify, 0 reject), Review findings [7/7]. Both blockers resolved and folded in. Decisions [5/5], no blocking open → Ready. DRAFT -> READY.
+- 2026-07-06 Mon @ 15:14:06 -0500 — spec-review: Not ready. Decisions [5/5] but Review findings [0/7], two :blocking: (progress-bar data source undefined; row rendering overloads cj/music--track-description). Stays DRAFT until dispositioned.
+- 2026-07-06 Mon @ 15:03:57 -0500 — Craig accepted all five recommended decisions; Decisions [5/5]. Still DRAFT, ready for spec-review.
+- 2026-07-06 Mon @ 14:38:15 -0500 — drafted.
+
+* Metadata
+| Status | implemented |
+|----------+-------------------------------------------------------------------|
+| Owner | Craig Jennings |
+|----------+-------------------------------------------------------------------|
+| Reviewer | Craig Jennings |
+|----------+-------------------------------------------------------------------|
+| Related | [[file:2026-07-06-fancy-music-player-ui.prototype.html][prototype (fancy direction)]]; modules/music-config.el |
+|----------+-------------------------------------------------------------------|
+
+* Summary
+
+Turn the EMMS playlist buffer from a raw-URL text list into a hi-fi "fancy" reading surface: station and track names instead of stream URLs, cover art, a now-playing hero with a serif title and a progress bar, and a warm amber palette, all still inside the Emacs buffer. It degrades cleanly to a plain text version in a terminal frame, where images can't render.
+
+* Problem / Context
+
+The music player is an EMMS playlist buffer with a custom multi-line header (Playlist / Current / Mode / Keys / Radio) drawn by cj/music--header-text as an overlay. The track list shows whatever EMMS stores as the track name, which for internet radio is the raw stream URL (https://ice6.somafm.com/groovesalad-256-mp3). It reads as a debug buffer, not a music player. Craig chose the "fancy" direction from the prototype: cover art, serif titles, a now-playing hero, warm amber.
+
+Two facts shape the design. First, Emacs renders images and variable-pitch faces in a GUI frame but not in a TTY, so the fancy look needs a text fallback. Second, cover art is reachable: radio-browser stations carry a favicon (logo) URL, retrievable at search time from the station plist or later via /json/stations/byuuid/<uuid> (verified live 2026-07-06); local files carry embedded album art. Stations with neither get a generated placeholder.
+
+* Goals and Non-Goals
+
+** Goals
+- Show human names, not URLs: station name for streams (from the .m3u #EXTINF or a radio-browser lookup), artist and title for local files.
+- A now-playing hero: cover art, a serif title, a subtitle (station/album), a progress bar, and a time or "on air" line.
+- A track list with small cover thumbnails, serif names, and right-aligned meta; the current row accented.
+- A warm amber palette, theme-owned (the dupre theme).
+- Degrade to a plain text version (names + a dim glyph + a thin bar) in a TTY or when the fancy view is off.
+
+** Non-Goals
+- No separate EMMS browser pane, library manager, or tag editor.
+- No mouse-first chrome, animated equalizers, or transport buttons you click (keys stay the interface).
+- No change to the radio lookup, playback engine, or keybindings (the n/t/m radio row and the transport keys are untouched).
+- No streaming-service integrations.
+
+** Scope tiers
+- v1: name resolution + the text base (TTY-safe), cover-art fetch + cache, and the fancy GUI render layered on top.
+- Out of scope: album/library browsing, playlists-of-playlists UI, per-track ratings.
+- vNext (log to todo.org): a scrubbable/seekable progress bar, homepage/link affordances in the hero, animated now-playing transitions, per-station color theming from the cover art.
+
+* Design
+
+At a caller's altitude: Craig opens the music player as today (same keys). Instead of URLs he sees named rows with a small logo each; the top of the buffer is a now-playing panel with the current station's cover, its name in a serif face, and a progress bar. In a terminal frame or with the fancy view disabled, the same buffer shows the plain text version (names, a dim glyph, a thin bar) with no images.
+
+At the implementer's altitude, three layers, each shippable on its own:
+
+1. Name resolution (pure + text, TTY-safe). A pure cj/music--display-name maps a track to its name only: for a url track, the #EXTINF label from its .m3u if present, else the station name resolved from radio-browser, else a tidied host; for a file track, artist and title from EMMS track info. This helper is shared — the header's Current line calls it directly, and so does the row renderer — so the two never drift and the glyph/meta never leak into the header. Row rendering is a separate seam: emms-track-description-function composes cj/music--display-name + a dim nerd-icon glyph (broadcast for streams, note for files) + right-aligned meta. The glyph is guarded on nerd-icons being available with a Nerd Font present (music-config.el gains the require/guard — it doesn't reference nerd-icons today), degrading to a plain marker or none otherwise. The meta is right-aligned with a display :align-to spec so it stays aligned across window resizes, not a pad-to-column computed once at insert time. The now-playing line under Current gets a thin faces-drawn bar plus a time / "on air" string. This is the plain text base and the fallback; it has no images.
+
+2. Cover art (image infra). cj/music-art--for-track returns a cached local image path for a track: a station favicon (from the station plist at creation time, or a byuuid lookup for an existing .m3u carrying #RADIOBROWSERUUID), embedded album art for a file, or the vinyl placeholder when neither exists. An empty favicon field counts as no art, and a fetched response is cached only after it validates as a displayable image (a create-image / image-type-available-p probe); a non-image or unsupported-format response (an .ico Emacs can't show, an HTML error page served 200) falls to the placeholder, a distinct outcome from a failed fetch. The placeholder is a single shipped static SVG asset under assets/ that scales cleanly at hero and thumbnail sizes on any DPI — not procedurally generated per station (per-station tinting is vNext); on an Emacs without svg support it degrades to no image. Fetches are asynchronous and written to an on-disk cache keyed by station UUID or file, so a redisplay never blocks on the network and art survives restarts.
+
+3. Fancy render (GUI). When the frame is graphical and the fancy view is on, cj/music--header-text and the row renderer insert the cover images (create-image / insert-image), remap the title to a serif variable-pitch face, and paint the amber palette and the segmented progress bar. A small timer advances the bar during playback. When the frame is a TTY or the fancy view is off, the render falls through to layer 1's text.
+
+Pure pieces (name resolution, the m3u-label extraction, the bar-fill computation, the placeholder-vs-art decision) carry the tests; the image insertion, the fetch, and the timer are exercised live.
+
+* Alternatives Considered
+
+** Layer the fancy view on top of a text base (chosen)
+- Good, because the text base ships value immediately (names beat URLs for everyone), is the honest TTY fallback, and de-risks the image work.
+- Bad, because two render paths to keep in sync.
+- Neutral, because EMMS already separates track-name from display.
+
+** Fancy-only, no text fallback
+- Good, because one render path.
+- Bad, because it breaks in a TTY and forces the image/art work before any value lands.
+
+** A separate dedicated buffer/major-mode instead of the EMMS playlist overlay
+- Good, because full control of layout.
+- Bad, because it abandons EMMS's playlist machinery (marking, reorder, the existing keymap) and doubles maintenance.
+- Neutral, deferred: the overlay approach reuses everything and is enough for v1.
+
+* Decisions [5/5]
+
+** DONE Cover art for streams — where the favicon comes from
+- Owner / by-when: Craig / before Phase 2
+- Context: a radio-browser search result carries a favicon URL, so a newly-created station can capture it. An existing .m3u only has the stream URL and (on 15 of 73) a #RADIOBROWSERUUID; the other 58 have no UUID. A byuuid lookup gets the favicon for the ones that have a UUID.
+- Decision: We will (a) write a #RADIOBROWSERFAVICON line into search-created stations at creation (the radio-browser result carries the favicon) so they need no lookup, (b) for an existing station carrying #RADIOBROWSERUUID, fetch the favicon by UUID once and cache it, and (c) for a station with neither — including manually-created stations (the m row and cj/music-create-radio-station), which have no favicon and no UUID — use the vinyl placeholder.
+- Consequences: easier — most stations get real art with at most one lookup; harder — a small .m3u format addition (an extra comment line) and a byuuid path for legacy files. Manually-entered stations always show the placeholder, which is acceptable.
+
+** DONE Image cache location and refresh
+- Owner / by-when: Craig / before Phase 2
+- Context: fetched favicons and extracted album art need to persist so redisplay is instant and offline-safe.
+- Decision: We will cache under data/music-art/ (gitignored runtime state), keyed by station UUID or a file hash, fetched once and reused; a manual command clears the cache, and there is no automatic TTL.
+- Consequences: easier — instant, offline, no invalidation logic; harder — a stale logo persists until the user clears the cache (acceptable for logos).
+
+** DONE Serif title face
+- Owner / by-when: Craig / before Phase 3
+- Context: the fancy titles want a serif variable-pitch face, theme-owned.
+- Decision: We will add a defcustom for the title family defaulting to the shared Reading font profile's serif (currently "Merriweather") and a theme-owned face the dupre theme colors.
+- Consequences: easier — one knob, consistent with the nov-reading typography choice; harder — another face to register in theme-studio if we want it tunable there (deferred).
+
+** DONE Progress bar rendering and cadence
+- Owner / by-when: Craig / before Phase 3
+- Context: the bar can be drawn with block-character faces or a small SVG; it advances during playback via a timer.
+- Decision: We will draw the bar with faces (block characters, accent-colored fill) rather than SVG for v1, and redraw on a ~1s timer only while a track is playing, only when the player buffer is visible. The bar's data is per track type: a stream has no duration, so it renders indeterminate (the existing "on air" line, no fill); a local file renders elapsed/total, where total is info-playing-time (already read at music-config.el:807) and elapsed comes from an mpv IPC get_property round-trip on percent-pos — cj/music--mpv-command (currently send-only) is extended to read its reply, or a small cj/music--mpv-get-property helper is added. v1 does not re-enable EMMS's own playing-time timer (deliberately disabled at music-config.el:941); mpv is the single position source.
+- Consequences: easier — no SVG dependency, works the moment faces do, and mpv already runs the socket the seek commands use; harder — a character-cell bar is coarser than an SVG one (fine for v1; SVG is a vNext upgrade), and the file bar needs a reply-reading round-trip the seek path never needed. mpv's get_property over the JSON IPC socket is a documented, stable command.
+
+** DONE Fancy view toggle and TTY fallback
+- Owner / by-when: Craig / before Phase 3
+- Context: images need a GUI frame; a TTY (or a user who wants plain) needs the text version.
+- Decision: We will gate the fancy render on (display-graphic-p) AND a defcustom (default on), falling through to the layer-1 text render otherwise, decided per-redisplay so a frame on a TTY and a frame on a GUI can differ live.
+- Consequences: easier — never broken in a TTY, user can opt out; harder — both render paths stay maintained (already a chosen tradeoff).
+
+* Review findings [7/7]
+
+** DONE Progress-bar data source is undefined :blocking:
+Decision 4 picks the bar's rendering (block-char faces) and cadence (~1s timer), but never its input: where elapsed and total come from per track type. Two facts from the code make this a gap the implementer would have to invent. First, a radio stream has no duration — the bar is indeterminate, and the header already shows "on air" for it. Second, for a local file the elapsed position is not available today: emms-playing-time-display-mode is disabled (music-config.el:941), and cj/music--mpv-command (music-config.el:168) is send-only — it writes to the mpv IPC socket and discards the reply, so nothing reads back time-pos. The total duration is reachable (info-playing-time, already used at music-config.el:807), but the moving position is not.
+MODIFY — folded into Decision 4. Accepted the gap; narrowed the resolution: v1 uses mpv's JSON IPC get_property on percent-pos (simplest — 0..100 directly) as the single position source for local files, with cj/music--mpv-command extended to read its reply; a stream renders indeterminate ("on air", no fill). Deliberately did NOT re-enable EMMS's own playing-time timer (it was disabled on purpose), keeping mpv the one source rather than reviving parallel machinery.
+
+** DONE Row rendering overloads cj/music--track-description :blocking:
+The Design named the row renderer as cj/music--track-description, but that function is also the source of the header's now-playing line (music-config.el:843) and is EMMS's emms-track-description-function (music-config.el:952), so EMMS calls it for the mode line and elsewhere. Overloading it with glyph + meta + image leaks those into the header and every other consumer.
+ACCEPT — folded into Design layer 1 and Phase 1. A pure cj/music--display-name (name only) is now the shared seam the header's Current line and the row renderer both call; only the row renderer (emms-track-description-function) adds the glyph, :align-to meta, and Phase-3 image, so the header stays clean.
+
+** DONE "#RADIOBROWSERFAVICON at creation" only covers search-created stations
+Decision 1(a) wrote a favicon line into "new stations at creation," but only the radio-browser search path (cj/music-radio--station-m3u, music-config.el:1078) has a favicon in hand. The manual creators — the m row and cj/music-create-radio-station (music-config.el:1014) — take just a name and URL and have none.
+ACCEPT — folded into Decision 1. Scoped clause (a) to search-created stations and stated that manually-created stations (no favicon, no UUID) show the vinyl placeholder.
+
+** DONE Fetched favicon may be empty, non-image, or an unsupported format
+radio-browser's favicon field is frequently the empty string, and when present can be an .ico, .svg, .gif, an HTML error page served 200, or an oversized image. Placeholder-on-fetch-failure covers a failed request but not a successful fetch that isn't a usable image.
+ACCEPT — folded into Design layer 2, Phase 2, and acceptance. Empty favicon counts as no art; a fetched response is cached only after it validates as a displayable image (create-image / image-type-available-p probe), else placeholder — a distinct outcome from a failed fetch. The decision helper's tests cover empty and non-image inputs.
+
+** DONE Right-aligned meta goes stale on window resize
+emms-track-description-function runs at track-insert time, not on redisplay, so a meta column computed from window width at insert is wrong after the playlist window resizes (F10 docks it right or bottom at varying widths — music-config.el:659).
+ACCEPT — folded into Design layer 1 and Phase 1. The meta is right-aligned with a display :align-to spec, recomputed by redisplay, rather than a pad-to-column measured once.
+
+** DONE Vinyl placeholder generation mechanism unnamed
+The spec called the placeholder "generated" without saying how — SVG (needs svg support) or a shipped static asset.
+MODIFY — folded into Design layer 2 and Phase 2. Named it and narrowed scope: v1 ships a single static vinyl-placeholder SVG under assets/ (scales at hero and thumbnail sizes, degrades to no image where svg is unavailable), NOT procedurally generated per station. Per-station tinted generation is explicitly vNext — that's the "generated" ambition the original word implied, deferred.
+
+** DONE nerd-icons is not required in music-config.el
+The Reuse dimension claimed the row glyphs reuse nerd-icons "already used by dashboard/dirvish," but music-config.el neither requires nor references it today (it loads only transitively).
+ACCEPT — folded into Design layer 1 and Phase 1. Phase 1 adds the require/availability guard; the glyph needs a Nerd Font in the GUI and degrades to a plain marker or none otherwise, so a row never renders a tofu box.
+
+* Implementation phases
+
+** Phase 1 — Name resolution + text base (TTY-safe, no images)
+Extract a pure cj/music--display-name (url -> #EXTINF label / radio-browser name / tidy host; file -> artist and title) and point the header's Current line at it. Add the row renderer on emms-track-description-function: display-name + dim glyph (nerd-icons require/guard, TTY fallback) + :align-to right-aligned meta. Add the thin now-playing bar + time/on-air line and the header spacing + rule. Unit-test the pure name/label/bar-fill helpers. Ships the "names not URLs" win on its own and is the fallback for later phases.
+
+** Phase 2 — Cover art fetch + cache (image infra)
+cj/music-art--for-track returning a cached local image path: favicon capture at search-station creation (a #RADIOBROWSERFAVICON line), byuuid favicon fetch for legacy UUID stations, embedded album art for files, and the shipped vinyl-placeholder SVG when neither resolves. Validate a fetched response is a displayable image before caching (empty / non-image / unsupported-format -> placeholder). Async fetch into data/music-art/. Unit-test the art-vs-placeholder decision (including empty and non-image inputs) and the cache-key logic; the fetch is a smoke test.
+
+** Phase 3 — Fancy GUI render
+The now-playing hero (cover image + serif title + subtitle + segmented bar) and the thumbnailed serif list with the amber palette, gated on (display-graphic-p) + the toggle, degrading to Phase 1's text. The ~1s playback timer. Live verification (images, faces, the bar advancing) since it can't run headless.
+
+* Acceptance criteria
+- [ ] A radio row shows the station name, not the stream URL; a local track shows artist and title.
+- [ ] Each row shows a dim glyph (broadcast for streams, note for files) and right-aligned meta.
+- [ ] The header's Current line and the playlist rows both derive their name from the shared cj/music--display-name; the header line carries no row glyph or meta.
+- [ ] In a GUI frame with the fancy view on, the now-playing hero shows the current station's cover art, a serif title, and a progress bar. For a local file the bar advances from mpv's reported position; for a stream it shows "on air" with no fill.
+- [ ] A station with no favicon or embedded art shows the vinyl placeholder, not a broken image; a fetched favicon that is empty, non-image, or an unsupported format also yields the placeholder and is not cached as art.
+- [ ] In a TTY frame, or with the fancy view off, the same buffer renders the text version (names + glyph + thin bar) with no images and no errors.
+- [ ] Art is fetched once and cached under data/music-art/; a second open is instant and works offline.
+- [ ] The name/label, bar-fill, and art-decision helpers have Normal/Boundary/Error tests.
+
+* Readiness dimensions
+- Data model & ownership: display names are derived (never stored); .m3u files gain an optional #RADIOBROWSERFAVICON comment (Craig-owned, written at creation); cached art under data/music-art/ is generated and disposable.
+- Errors, empty states & failure: a fetch failure or missing art yields the placeholder, never a broken image; a TTY yields text; an empty playlist yields the header with no rows.
+- Security & privacy: fetching a station's favicon URL hits a third-party host the user chose; fetch with a timeout and cache locally; no credentials; nothing sensitive logged.
+- Observability: the player messages when it can't fetch art (once, quietly); a clear-art-cache command exists.
+- Performance & scale: art fetch is async and cached, never blocking redisplay; the bar timer runs ~1s only while playing and only when the buffer is visible; the list is small (a playlist), so per-row images are cheap.
+- Reuse & lost opportunities: reuses the EMMS playlist buffer + keymap, the existing header overlay (cj/music--header-text / update-header), cj/music--track-description, nerd-icons (already used by dashboard/dirvish), the dupre theme, and the nov-reading serif choice. Images via built-in create-image/insert-image. Nothing new is invented that Emacs already provides.
+- Architecture fit & weak points: layers on music-config.el's existing header/render path; the seam is the header overlay + a per-row renderer. Weak point: keeping the text and fancy render paths in sync — mitigated by making the text render the base and the fancy render an image/face overlay on the same rows.
+- Config surface: cj/music-fancy-ui (default on), the serif title face + family, the amber palette (theme-owned), the art cache dir, the bar redraw interval. All with defaults and doc.
+- Documentation plan: a note in the module commentary and the keybinding/header list; no separate doc.
+- Dev tooling: existing make test / test-file; no new tooling.
+- Rollout, compatibility & rollback: additive; cj/music-fancy-ui off restores the plain text render; the #RADIOBROWSERFAVICON line is an ignorable comment in older readers; deleting data/music-art/ is a safe reset.
+- External APIs & deps: radio-browser favicon field and /json/stations/byuuid/<uuid> VERIFIED live 2026-07-06. Local album-art extraction depends on an image being embedded (or a folder cover.jpg) — the fallback is the placeholder.
+
+* Risks, Rabbit Holes, and Drawbacks
+- Local album-art extraction is the likeliest rabbit hole (embedded art via a tag reader vs a sibling cover.jpg). v1 can start with sibling-cover-file + placeholder and defer embedded-tag extraction if it balloons.
+- Image sizing across frame DPIs: pick a fixed pixel size for the hero and thumbnails, scaled by the frame, and don't chase per-monitor perfection.
+- Two render paths drifting: the text base must stay the source of truth for row content; the fancy path only adds images/faces, never different text.
+
+* References / Appendix
+- Prototype (open in a browser): [[file:2026-07-06-fancy-music-player-ui.prototype.html][2026-07-06-fancy-music-player-ui.prototype.html]] — three directions (minimal / fancy / modern); this spec builds the fancy one, with minimal as the text fallback.
+- radio-browser favicon + byuuid verified live 2026-07-06 against de1.api.radio-browser.info.
+
+* Review and iteration history
+** 2026-07-06 Mon @ 15:14:06 -0500 — Claude Code (emacs-d) — responder
+- What: dispositioned all 7 review findings — 5 accept, 2 modify, 0 reject; Review findings [7/7]. Modifies: (1) the progress-bar data source narrowed to mpv percent-pos over the existing IPC socket for files + indeterminate "on air" for streams, deliberately not reviving EMMS's disabled playing-time timer; (6) the placeholder named as a single shipped static vinyl SVG under assets/, with per-station tinted generation pushed to vNext. Accepts folded into Design layers 1-2, Decisions 1/3/4, Phases 1-2, and acceptance criteria: the pure cj/music--display-name split so the header never gets row glyph/meta, :align-to meta, fetched-image validation, favicon-at-creation scoped to search stations, the nerd-icons guard, and Decision 3's serif pinned to Merriweather. Flipped DRAFT -> READY (keyword + history + Metadata mirror).
+- Why: two findings were true blockers that would have forced the implementer to invent behavior; the rest tightened scope and named mechanisms. The modifies avoid re-introducing machinery that was turned off on purpose (the EMMS timer) and defer the expensive half of "generated" art.
+- Artifacts: Review findings [7/7] in this spec; scope-expansion check — mpv get_property percent-pos is a documented IPC command on an already-open socket, the SVG asset uses built-in svg on the GUI-only path, so neither adds an unverified dependency needing a new finding.
+
+** 2026-07-06 Mon @ 15:14:06 -0500 — Claude Code (emacs-d) — reviewer
+- What: first review pass. Read the render path in modules/music-config.el (cj/music--track-description, cj/music--header-text/update-header, the emms use-package config, the radio client) before critiquing. Rubric: Not ready — recorded Review findings [0/7], two :blocking:. The three implementation phases are present and cleanly decomposable (each reaches a clean stopping point; no broken intermediate state). Confirmed the concrete default for Decision 3's serif is cj/nov-reading-font-family = "Merriweather" (nov-reading.el:197), and that data/ is gitignored (so data/music-art/ under Decision 2 is covered).
+- Why: two gaps would force the implementer to invent product behavior. The progress bar has a chosen rendering and cadence but no data source — a stream has no duration, and for a local file the elapsed position isn't wired (playing-time display disabled, the mpv IPC helper is send-only). And the named row-render seam, cj/music--track-description, is shared by the header's Current line and EMMS internals, so overloading it with glyph/meta/image corrupts them. Five non-blocking findings tighten the favicon-at-creation scope, fetched-image validation, resize-safe meta alignment, the placeholder mechanism, and the nerd-icons dependency.
+- Artifacts: Review findings [0/7] in this spec; source checks at music-config.el:168/807/843/941/952/1014/1078 and nov-reading.el:197.
diff --git a/docs/specs/2026-07-06-fancy-music-player-ui.prototype.html b/docs/specs/2026-07-06-fancy-music-player-ui.prototype.html
new file mode 100644
index 00000000..8b7ddd21
--- /dev/null
+++ b/docs/specs/2026-07-06-fancy-music-player-ui.prototype.html
@@ -0,0 +1,207 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Music player UI — three directions</title>
+<style>
+ :root {
+ --page-bg: #f3f0ea; --page-fg: #29261f; --page-dim: #6b6459; --card-line: #ded8cc; --accent: #9a6b2f;
+ }
+ @media (prefers-color-scheme: dark) {
+ :root { --page-bg: #131210; --page-fg: #d6cfc0; --page-dim: #8a8272; --card-line: #2b271f; --accent: #d8a24f; }
+ }
+ :root[data-theme="light"] { --page-bg: #f3f0ea; --page-fg: #29261f; --page-dim: #6b6459; --card-line: #ded8cc; --accent: #9a6b2f; }
+ :root[data-theme="dark"] { --page-bg: #131210; --page-fg: #d6cfc0; --page-dim: #8a8272; --card-line: #2b271f; --accent: #d8a24f; }
+
+ * { box-sizing: border-box; }
+ body { margin: 0; background: var(--page-bg); color: var(--page-fg);
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; line-height: 1.55;
+ padding: 3rem 1.25rem 5rem; }
+ .wrap { max-width: 64rem; margin: 0 auto; }
+ .eyebrow { text-transform: uppercase; letter-spacing: .14em; font-size: .72rem; color: var(--accent); font-weight: 600; margin: 0 0 .5rem; }
+ h1 { font-size: 1.85rem; margin: 0 0 .6rem; text-wrap: balance; font-weight: 650; }
+ header p { margin: .3rem 0; max-width: 64ch; color: var(--page-dim); }
+ header p strong { color: var(--page-fg); font-weight: 600; }
+
+ .dir { margin: 2.6rem 0 0; }
+ .dir > .head { display: flex; align-items: baseline; gap: .8rem; flex-wrap: wrap; margin-bottom: .7rem; }
+ .dir .n { font-variant-numeric: tabular-nums; color: var(--accent); font-weight: 700; }
+ .dir h2 { font-size: 1.15rem; margin: 0; font-weight: 640; }
+ .dir .sub { color: var(--page-dim); font-size: .92rem; }
+ .feas { margin: .55rem 0 0; font-size: .86rem; color: var(--page-dim); }
+ .feas b { color: var(--page-fg); font-weight: 600; }
+ .cost { display: inline-block; font-size: .72rem; letter-spacing: .04em; text-transform: uppercase;
+ border: 1px solid var(--card-line); border-radius: 999px; padding: .06rem .55rem; margin-right: .4rem; color: var(--page-dim); }
+
+ /* shared editor frame */
+ .frame { border-radius: 9px; overflow: hidden; box-shadow: 0 16px 40px -22px rgba(0,0,0,.65); border: 1px solid #0000; }
+ .mono { font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; }
+
+ /* ---------- CURRENT (reference) ---------- */
+ .cur-ref { background: #15140f; color: #b9b1a1; border-color: #322d25; font-size: 12.5px; }
+ .cur-ref .buf { padding: .6rem 0; }
+ .cur-ref .r { white-space: pre; padding: .05rem 1rem; }
+ .cur-ref .lbl { color: #8a8170; }
+ .cur-ref .play { color: #e6c98a; font-weight: 700; }
+
+ /* ---------- 1 · MINIMAL ---------- */
+ .min { background: #15140f; color: #cfc8b8; border-color: #322d25; font-size: 13px; }
+ .min .buf { padding: .7rem 0 .8rem; }
+ .min .r { white-space: pre; padding: .07rem 1.1rem; }
+ .min .lbl { color: #9a917f; }
+ .min .dim { color: #6f685b; }
+ .min .on { color: #e6c98a; } .min .off { color: #4a453b; }
+ .min .keys { color: #6f685b; } .min .keys .k { color: #9a917f; }
+ .min .rule { color: #4a453b; padding: .12rem 1.1rem .28rem; white-space: pre; }
+ .min .bar-full { color: #8faf7f; } .min .bar-empty { color: #43403a; } .min .time { color: #6f685b; }
+ .min .track { display: grid; grid-template-columns: 1.4ch 1fr auto; gap: .7ch; align-items: baseline; padding: .13rem 1.1rem; }
+ .min .g-radio { color: #8fb0c4; } .min .g-note { color: #a7b58f; }
+ .min .track .name { color: #cfc8b8; } .min .track .meta { color: #6f685b; white-space: pre; }
+ .min .track.cur { background: #262119; } .min .track.cur .name { color: #e6c98a; font-weight: 700; }
+
+ /* ---------- 2 · FANCY (hi-fi / vinyl) ---------- */
+ .fan { background: radial-gradient(120% 90% at 12% 0%, #241a10 0%, #17110a 55%, #120d07 100%); color: #e8d9bd; border-color: #3c2c17; }
+ .fan .top { display: flex; gap: 1rem; padding: 1.1rem 1.2rem 1rem; align-items: center; border-bottom: 1px solid #3a2b18; }
+ .fan .cover { width: 78px; height: 78px; border-radius: 6px; flex: none; position: relative;
+ background: radial-gradient(circle at 50% 50%, #3a2c18 0 22%, #1a130b 23% 26%, #2a2012 27% 46%, #17110a 47% 50%, #2a2012 51%);
+ box-shadow: inset 0 0 0 1px #4a3820, 0 6px 16px -8px #000; }
+ .fan .cover::after { content: ""; position: absolute; inset: 45% 45% auto auto; width: 8px; height: 8px; border-radius: 50%;
+ background: #e6c98a; box-shadow: 0 0 0 3px #17110a; top: 46%; left: 46%; }
+ .fan .np-title { font-family: "Iowan Old Style", "Palatino Linotype", Georgia, serif; font-size: 1.5rem; font-weight: 600; color: #f2e4c6; line-height: 1.15; }
+ .fan .np-sub { color: #b99a63; font-size: .9rem; margin-top: .15rem; letter-spacing: .02em; }
+ .fan .seg { display: flex; gap: 3px; margin-top: .7rem; }
+ .fan .seg i { height: 6px; flex: 1; border-radius: 2px; background: #3a2c18; }
+ .fan .seg i.on { background: linear-gradient(#f0cf8a, #d8a24f); }
+ .fan .tline { display: flex; justify-content: space-between; color: #a98c58; font-size: .74rem; margin-top: .3rem; font-variant-numeric: tabular-nums; }
+ .fan .list { padding: .55rem .4rem .7rem; }
+ .fan .t { display: grid; grid-template-columns: 30px 1fr auto; gap: .7rem; align-items: center; padding: .34rem .8rem; border-radius: 7px; }
+ .fan .t .th { width: 26px; height: 26px; border-radius: 4px; background: #241a10; box-shadow: inset 0 0 0 1px #4a3820; display: grid; place-items: center; color: #c79a54; font-size: 12px; }
+ .fan .t .nm { font-family: "Iowan Old Style", Georgia, serif; font-size: 1.02rem; color: #ecdcbc; }
+ .fan .t .mt { color: #9c8154; font-size: .8rem; font-variant-numeric: tabular-nums; }
+ .fan .t.cur { background: linear-gradient(90deg, #2a1f10, #201810); box-shadow: inset 2px 0 0 #e6c98a; }
+ .fan .t.cur .nm { color: #f7e7c4; }
+
+ /* ---------- 3 · MODERN (streaming card) ---------- */
+ .mod { background: #0e1013; color: #cdd3da; border-color: #1c2027; }
+ .mod .top { display: flex; gap: .95rem; padding: 1.05rem 1.1rem; align-items: center; }
+ .mod .cover { width: 66px; height: 66px; border-radius: 12px; flex: none;
+ background: linear-gradient(145deg, #2a6f5a, #1b3b6b); box-shadow: 0 8px 20px -10px #000; display: grid; place-items: center; color: #bfeede; font-size: 20px; }
+ .mod .np-title { font-size: 1.28rem; font-weight: 680; color: #f2f5f8; letter-spacing: -.01em; }
+ .mod .np-sub { color: #8b94a0; font-size: .88rem; margin-top: .1rem; }
+ .mod .prog { display: flex; align-items: center; gap: .6rem; padding: 0 1.1rem .2rem; }
+ .mod .track-bar { position: relative; height: 4px; border-radius: 4px; background: #232830; flex: 1; }
+ .mod .track-bar > i { position: absolute; left: 0; top: 0; bottom: 0; width: 42%; border-radius: 4px; background: #5fd0a8; }
+ .mod .track-bar > b { position: absolute; left: 42%; top: 50%; width: 11px; height: 11px; border-radius: 50%; background: #eafaf3; transform: translate(-50%, -50%); box-shadow: 0 0 0 3px rgba(95,208,168,.25); }
+ .mod .tm { color: #7f8894; font-size: .72rem; font-variant-numeric: tabular-nums; }
+ .mod .ctl { display: flex; gap: .5rem; padding: .55rem 1.1rem .1rem; }
+ .mod .ctl .pill { border: 1px solid #262c34; background: #171b21; border-radius: 999px; padding: .2rem .7rem; font-size: .82rem; color: #aeb6c0; }
+ .mod .ctl .pill.main { background: #5fd0a8; color: #08130e; border-color: #5fd0a8; font-weight: 700; }
+ .mod .list { padding: .5rem .45rem .7rem; }
+ .mod .t { display: grid; grid-template-columns: 34px 1fr auto; gap: .7rem; align-items: center; padding: .4rem .7rem; border-radius: 10px; }
+ .mod .t .th { width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; font-size: 13px; color: #dfe6ee; }
+ .mod .t .th.radio { background: linear-gradient(145deg, #2a6f5a, #1b3b6b); } .mod .t .th.note { background: linear-gradient(145deg, #3a3f4a, #23272e); }
+ .mod .t .nm { color: #dfe4ea; font-weight: 550; } .mod .t .mt { color: #7f8894; font-size: .8rem; font-variant-numeric: tabular-nums; }
+ .mod .t.cur { background: #151b22; } .mod .t.cur .nm { color: #eafaf3; }
+ .mod .t.cur .eq { color: #5fd0a8; }
+
+ .foot { margin-top: 2.4rem; padding-top: 1.2rem; border-top: 1px solid var(--card-line); color: var(--page-dim); font-size: .9rem; }
+ .foot b { color: var(--page-fg); }
+</style>
+</head>
+<body>
+<div class="wrap">
+ <header>
+ <p class="eyebrow">EMMS playlist buffer · three directions</p>
+ <h1>Music player: pick a look</h1>
+ <p>Same playlist, three treatments. All are achievable inside an Emacs buffer, but they cost different amounts. Today it shows raw stream URLs (below); each direction fixes that and goes further. Mockups, not live renders.</p>
+ <div class="frame cur-ref mono" style="margin-top:1rem; max-width:34rem">
+ <div class="buf">
+ <div class="r"><span class="lbl">Current :</span> https://ice6.somafm.com/groovesalad-256-mp3</div>
+ <div class="r play">https://ice6.somafm.com/groovesalad-256-mp3</div>
+ <div class="r">https://ice1.somafm.com/groovesalad-256-mp3</div>
+ <div class="r">https://ice2.somafm.com/groovesalad-256-mp3</div>
+ </div>
+ </div>
+ </header>
+
+ <!-- 1 · MINIMAL -->
+ <section class="dir">
+ <div class="head"><span class="n">1</span><h2>Minimal</h2><span class="sub">refined terminal — names, one dim glyph, thin now-playing bar</span></div>
+ <div class="frame min mono">
+ <div class="buf">
+ <div class="r"><span class="lbl">Playlist</span> <span class="lbl">:</span> Evening mix <span class="dim">(5)</span></div>
+ <div class="r"><span class="lbl">Current </span> <span class="lbl">:</span> <span style="color:#8faf7f">▶ </span>Groove Salad <span class="dim">· SomaFM</span></div>
+ <div class="r"><span class="dim"> </span><span class="bar-full">━━━━━━━━━━━━</span><span class="bar-empty">────────────────</span> <span class="time">live · 256k</span></div>
+ <div class="r keys"><span class="k">Keys </span> <span class="k">:</span> a:add c:clear L:load S:stop &lt;&gt;:skip</div>
+ <div class="r keys"><span class="k">Radio </span> <span class="k">:</span> n:by name t:by tag m:enter manually</div>
+ <div class="rule"> ────────────────────────────────────────</div>
+ <div class="track cur"><span class="g-radio">◉</span><span class="name">Groove Salad</span><span class="meta">SomaFM · 256k</span></div>
+ <div class="track"><span class="g-radio">◉</span><span class="name">Drone Zone</span><span class="meta">SomaFM · 256k</span></div>
+ <div class="track"><span class="g-note">♪</span><span class="name">Miles Davis — So What</span><span class="meta">9:22</span></div>
+ <div class="track"><span class="g-note">♪</span><span class="name">Bill Evans — Peace Piece</span><span class="meta">6:41</span></div>
+ <div class="track"><span class="g-radio">◉</span><span class="name">Jazz Radio Blues</span><span class="meta">FR · 128k</span></div>
+ </div>
+ </div>
+ <p class="feas"><span class="cost">low lift</span><b>Faces + text only.</b> Names come from the .m3u #EXTINF and local tags; the glyph is a nerd-icon; the bar is one timer-driven line. No images. Sits naturally in the buffer you already have.</p>
+ </section>
+
+ <!-- 2 · FANCY -->
+ <section class="dir">
+ <div class="head"><span class="n">2</span><h2>Fancy</h2><span class="sub">hi-fi / vinyl — cover art, serif titles, warm amber, a real now-playing hero</span></div>
+ <div class="frame fan">
+ <div class="top">
+ <div class="cover"></div>
+ <div>
+ <div class="np-title">Groove Salad</div>
+ <div class="np-sub">SOMA FM · AMBIENT / DOWNTEMPO · 256K</div>
+ <div class="seg"><i class="on"></i><i class="on"></i><i class="on"></i><i class="on"></i><i class="on"></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>
+ <div class="tline"><span>on air</span><span>live stream</span></div>
+ </div>
+ </div>
+ <div class="list">
+ <div class="t cur"><span class="th">◉</span><span class="nm">Groove Salad</span><span class="mt">SomaFM</span></div>
+ <div class="t"><span class="th">◉</span><span class="nm">Drone Zone</span><span class="mt">SomaFM</span></div>
+ <div class="t"><span class="th">♪</span><span class="nm">Miles Davis — So What</span><span class="mt">9:22</span></div>
+ <div class="t"><span class="th">♪</span><span class="nm">Bill Evans — Peace Piece</span><span class="mt">6:41</span></div>
+ <div class="t"><span class="th">◉</span><span class="nm">Jazz Radio Blues</span><span class="mt">FR</span></div>
+ </div>
+ </div>
+ <p class="feas"><span class="cost">medium lift</span><b>Cover art + variable-pitch serif.</b> Emacs shows images (station favicons, embedded album art) and a serif face for titles via `display` and face remaps. The vinyl cover and segmented bar are drawn with faces/SVG. Needs an art fetch-and-cache layer; the warmth is a theme overlay.</p>
+ </section>
+
+ <!-- 3 · MODERN -->
+ <section class="dir">
+ <div class="head"><span class="n">3</span><h2>Modern</h2><span class="sub">streaming-app card — rounded art, sans title, a slim seek bar with a handle, pill controls</span></div>
+ <div class="frame mod">
+ <div class="top">
+ <div class="cover">♪</div>
+ <div>
+ <div class="np-title">Groove Salad</div>
+ <div class="np-sub">SomaFM · ambient · 256 kbps</div>
+ </div>
+ </div>
+ <div class="prog"><span class="tm">LIVE</span><span class="track-bar"><i></i><b></b></span><span class="tm">∞</span></div>
+ <div class="ctl"><span class="pill">shuffle</span><span class="pill">⏮</span><span class="pill main">⏸ pause</span><span class="pill">⏭</span><span class="pill">repeat</span></div>
+ <div class="list">
+ <div class="t cur"><span class="th radio">◉</span><span class="nm">Groove Salad <span class="eq">▎▍▎</span></span><span class="mt">SomaFM</span></div>
+ <div class="t"><span class="th radio">◉</span><span class="nm">Drone Zone</span><span class="mt">SomaFM</span></div>
+ <div class="t"><span class="th note">♪</span><span class="nm">Miles Davis — So What</span><span class="mt">9:22</span></div>
+ <div class="t"><span class="th note">♪</span><span class="nm">Bill Evans — Peace Piece</span><span class="mt">6:41</span></div>
+ <div class="t"><span class="th radio">◉</span><span class="nm">Jazz Radio Blues</span><span class="mt">FR</span></div>
+ </div>
+ </div>
+ <p class="feas"><span class="cost">higher lift</span><b>Pushes hardest against the buffer model.</b> Rounded art, pill controls, the seek-bar dot handle, and rounded rows are easy in a browser but need SVG-rendered widgets or `svg-lib`-style images in Emacs, redrawn on playback. Doable in GUI Emacs, but it's the most code and the least "text buffer." A cool accent (mint) instead of the theme's amber to read contemporary.</p>
+ </section>
+
+ <div class="foot">
+ <p>My read: <b>Minimal</b> is the honest sweet spot for an Emacs player you live in, and it kills the primitive feeling on its own. <b>Fancy</b> is worth it if you want the now-playing moment to feel like hi-fi and you're happy to add cover art. <b>Modern</b> is the most striking but fights Emacs the most, so it's the biggest build for a look that a browser does more naturally. All three keep the n/t/m radio row and the same keys.</p>
+ <p style="margin:.7rem 0 0">Left out of every direction to stay tasteful: animated equalizers everywhere, a separate browser pane, mouse-first chrome. Glyphs shown here (◉ ♪) stand in for nerd-icons.</p>
+ </div>
+</div>
+<script>
+ (function () { var r = document.documentElement;
+ try { var t = localStorage.getItem('theme'); if (t === 'dark' || t === 'light') r.setAttribute('data-theme', t); } catch (e) {} })();
+</script>
+</body>
+</html>
diff --git a/docs/specs/2026-07-06-radio-browser-lookup-spec.org b/docs/specs/2026-07-06-radio-browser-lookup-spec.org
new file mode 100644
index 00000000..c65de850
--- /dev/null
+++ b/docs/specs/2026-07-06-radio-browser-lookup-spec.org
@@ -0,0 +1,202 @@
+#+TITLE: Radio-browser station lookup + playlist creator — Spec
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-06
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Radio-browser lookup
+:PROPERTIES:
+:ID: 4839b2c8-0552-4029-9e0f-4bf69b9a4dcd
+:END:
+- 2026-07-09 Thu @ 17:50:32 -0500 — Craig ran the queue-first VERIFY (n / t / m picks, save on v, file-playlist save unchanged, empty and offline searches) and found no issues. DOING -> IMPLEMENTED.
+- 2026-07-08 Wed @ 10:16:25 -0500 — model revision (Craig): queue-first, save-on-request. The lookup (and the manual m creator) no longer writes .m3u files at pick time; picked stations become url tracks in the queue (name/uuid/favicon as track properties) and play immediately. Saving is the normal playlist save (moved to w — the earlier S binding was shadowed by emms-stop): an all-stream queue saves into the MPD playlist dir, the plain station name pre-fills the prompt (the "-Radio" filename suffix is retired), and a custom .m3u emitter writes the station metadata back out so load round-trips names and cover art. First pick names a multi-station save. Still DOING.
+- 2026-07-06 Mon @ 14:04:53 -0500 — build UX changes (Craig, during Phase 2 verify): pulled tag search from vNext into v1; the feature is a Radio row in the playlist buffer (n: by name, t: by tag, m: enter manually) rather than one command on S; single mode moved off t to s and emms-stop off s to S; created filenames carry a "-Radio" suffix. Still DOING.
+- 2026-07-06 Mon @ 13:08:32 -0500 — spec-response Phase 6: decomposed into build tasks in todo.org (parent stamped :SPEC_ID:); READY -> DOING. Build under way.
+- 2026-07-06 Mon @ 13:01:55 -0500 — spec-response: all 7 findings dispositioned (6 accept/modify, 1 resolved via new Decision 5); decisions [5/5], findings [7/7]. No blocking finding remains; readiness rubric re-run on the expanded spec passes. DRAFT -> READY. Awaiting Craig's go to decompose into build tasks (spec-response Phase 6, flips READY -> DOING).
+- 2026-07-06 Mon @ 10:48:20 -0500 — spec-review: Not ready. 7 findings recorded (1 blocking: completing-read-multiple splits on commas in station names). Stays DRAFT pending disposition via spec-response.
+- 2026-07-06 Mon @ 10:12:00 -0500 — all four decisions resolved (Craig): url.el; multi-select one-file-per-station; save into the MPD playlist dir; create-and-play. Ready for spec-review.
+- 2026-07-06 Mon @ 10:01:27 -0500 — drafted.
+
+* Metadata
+| Status | implemented |
+|----------+-------------------------------------------------------------------|
+| Owner | Craig Jennings |
+|----------+-------------------------------------------------------------------|
+| Reviewer | Craig Jennings |
+|----------+-------------------------------------------------------------------|
+| Related | [[file:../../todo.org][todo.org: Music — create playlists from a radio.info lookup]] |
+|----------+-------------------------------------------------------------------|
+
+* Summary
+
+Add an in-Emacs command that searches the radio-browser.info directory for internet-radio stations and turns a selection into an M3U the music player can load and play. It closes the loop opened by the multi-directory sourcing work: the player already reads and plays radio .m3u from both ~/music/ and the MPD playlist directory, but every station still has to be found in a browser and hand-entered through cj/music-create-radio-station (name + URL typed by hand). This feature makes discovery native.
+
+* Problem / Context
+
+Craig listens to internet radio through the EMMS + mpv-subprocess player in music-config.el. A radio station is a one-line .m3u: an #EXTINF label and a stream URL. Today those come from two places, neither good. The 73 existing stations were built up over time in an MPD client and relocated into dotfiles; adding a new one means opening radio-browser.info in a browser, copying the stream URL, running cj/music-create-radio-station, and pasting name and URL by hand. There is no way to search, compare, or audition stations without leaving Emacs.
+
+radio-browser.info is a free community directory with an open JSON API: search by name, tag, country, language; each station carries a stable UUID, a resolved stream URL, codec, bitrate, tags, and popularity counts. That is exactly the metadata a good picker needs, and the existing radio .m3u already use radio-browser's UUID header on 15 of 73 files. The problem is purely that nothing in the config talks to the API.
+
+The verified API shape (checked live 2026-07-06):
+- Server list: GET https://all.api.radio-browser.info/json/servers -> [{ip, name}, ...]. A concrete host like de1.api.radio-browser.info answers directly.
+- Search: GET https://<server>/json/stations/search?name=<q>&limit=<n>&hidebroken=true -> array of station objects.
+- Station fields used: stationuuid, name, url_resolved, codec, bitrate, tags (comma string), countrycode, votes, clickcount.
+
+* Goals and Non-Goals
+
+** Goals
+- Search radio-browser.info from inside Emacs and pick from the results.
+- Turn the selection into a playable M3U in the format the existing player already reads (#EXTM3U / #EXTINF:1,<name> / <url>), carrying the #RADIOBROWSERUUID header for provenance.
+- Reuse the existing writer and the multi-directory sourcing so a created station is immediately loadable and playable.
+- Be a good radio-browser client: a descriptive User-Agent and server rotation.
+
+** Non-Goals
+- No replacement of cj/music-create-radio-station; the manual name+URL path stays for stations not in the directory.
+- No station editing, tagging, or curation UI beyond create.
+- No favorites/rating sync back to radio-browser (vote/click counting is vNext).
+- No dependence on MPD's stored-playlist mechanism; this writes .m3u files, consistent with the sourcing design.
+- No audio format work; mpv already plays whatever the stream serves.
+
+** Scope tiers
+- v1: search by name and by tag, a result picker, and playlist creation from the selection, wired to the existing writer + sourcing. Surfaced as a Radio row in the playlist buffer (n: by name, t: by tag, m: enter manually).
+- Out of scope: transient dashboards, station browsing by curated category, in-buffer station management.
+- vNext (log to todo.org): click/vote counting (POST /json/url/<uuid>), country faceted search, choosing among a station's codec/bitrate variants, an audition-before-save preview, a homepage/favicon-rich annotation. (Tag search moved into v1 2026-07-06.)
+
+* Design
+
+At a caller's altitude: Craig runs a search command (bound in the playlist keymap next to R), types a query, and gets a completion list of stations annotated with codec, bitrate, country, ♥votes, and top tags (Decision 5, Variant B). He picks one or more. The command writes an M3U and reports where it landed; the station is then visible in the load list and plays through mpv like any other radio .m3u.
+
+At the implementer's altitude, four pieces:
+
+1. A thin API client. cj/music-radio--server returns a pinned default host (cj/music-radio-server). On a connection failure or timeout against it, the client fetches /json/servers once and retries against the first reachable host from that list, capped at one such retry so it never loops. cj/music-radio--search runs the GET, parses JSON with the built-in json-parse-string, and returns a list of plists with only the fields the picker and writer need. All network calls send a descriptive User-Agent and carry a timeout. A failed or empty response returns a clear user-error, never a stack trace.
+
+2. A pure result-to-candidate layer. cj/music-radio--format-candidate turns a station plist into the completion string plus an annotation, and the reverse map recovers the chosen station. Keeping this pure keeps the picker testable without the network.
+
+3. A pure M3U emitter. cj/music-radio--station-m3u takes a station plist and returns the exact file text: #EXTM3U, an optional #RADIOBROWSERUUID line, #EXTINF:1,<name>, and the stream URL. It prefers url_resolved and falls back to url; a station with neither is not emitted (see the writer below). This mirrors the existing radio files byte-for-byte, so old and new stations are indistinguishable to the player.
+
+4. The interactive command. cj/music-radio-search reads the query, calls the client, and drives a repeated single-select picker: completing-read is called in a loop, each pick removed from the pool and a "[done]" sentinel finishing selection, so several stations are chosen in one search. This avoids completing-read-multiple, whose comma separator would mis-split a station name that contains a comma; each pick maps straight from its display string to the station object, never through a delimiter. For each selected station the command hands the station to the writer, which builds the filename from cj/music--safe-filename (disambiguating a name that collides with an already-written pick this run by appending a short stationuuid fragment) and writes cj/music-radio--station-m3u into cj/music-radio-save-dir (default the MPD playlist directory, alongside the existing radio set). A station with no usable stream URL is skipped and named in the closing message. The command then enqueues the created stations and starts playback through mpv, so a search ends in radio audio (see Decision 4 for the interrupt-vs-append behavior). The feature reuses cj/music--safe-filename and the existing radio M3U format; it does not call cj/music-create-radio-station, which stays as-is on its own write target.
+
+The network client and the interactive command are the only impure pieces. The candidate formatting and the M3U emission are pure and carry the test weight, following the module's existing internal/interactive split.
+
+* Alternatives Considered
+
+** HTTP via built-in url.el
+- Good, because no new dependency; ships with Emacs; url-retrieve-synchronously with a let-bound timeout is enough for a one-shot search.
+- Bad, because url.el's error handling and header ergonomics are clunky; async needs a callback dance.
+- Neutral, because JSON parsing is json-parse-string either way.
+
+** HTTP via plz.el (or request.el)
+- Good, because a clean synchronous-or-async API, straightforward headers, better error surfacing.
+- Bad, because a new package dependency for one feature; another thing to keep installed and byte-clean.
+- Neutral, because both are actively maintained.
+
+** Shell out to curl
+- Good, because trivial and already used ad hoc; robust header/timeout handling.
+- Bad, because process management and quoting in Elisp, and a hard curl dependency at runtime; less portable than staying in-process.
+- Neutral, because output still parses with json-parse-string.
+
+** Playlist shape: one .m3u per station vs one multi-station .m3u
+- One-per-station is Good, because it matches all 73 existing files and MPD's per-station model; each station is independently loadable.
+- One multi-station .m3u is Good for a genre queue you skip through, but Bad because it diverges from the existing shape and complicates naming.
+- This is the load-bearing product question; see Decision 2.
+
+* Decisions [5/5]
+
+** DONE HTTP client choice
+- Context: one JSON GET (plus an occasional server-list GET). No streaming, no auth. url.el ships with Emacs; plz is cleaner but a new dep.
+- Decision: We will use built-in url.el (url-retrieve-synchronously with a bound timeout) and json-parse-string, adding no dependency.
+- Consequences: easier install and byte-compile story, no new package to track; harder error ergonomics and any future async, which we accept for a one-shot search.
+
+** DONE Playlist shape and selection
+- Context: "create playlists from a lookup" can mean one file per station (matches the existing 73) or one file holding several stations (a genre queue). Selection can be single or multi.
+- Decision: We will support multi-select and write one .m3u per selected station (matching the existing shape), so a search that returns several good stations creates several stations in one pass rather than a combined file.
+- Consequences: easier consistency with the existing library and independent loadability; harder to express "a single genre playlist of five streams" (deferred to vNext if wanted).
+
+** DONE Save destination for created stations
+- Context: the multi-directory sourcing reads both ~/music/ and ~/.local/share/mpd/playlists/. The radio home is the MPD playlist directory, where the existing 73 stations live as dotfiles-tracked relative symlinks. Craig wants new stations to land with the rest of the radio set, not in ~/music/.
+- Decision: We will write new stations into ~/.local/share/mpd/playlists/ (Craig's call), so a created station sits alongside the existing radio playlists and both MPD and the multi-directory sourcing see it immediately. A defcustom cj/music-radio-save-dir (default that directory) keeps it configurable.
+- Consequences: easier — new stations are co-located with the radio set and instantly playable through either client; harder — a newly written file is a real file in a directory otherwise made of dotfiles-tracked symlinks, so it is not version-controlled until Craig stows it into ~/.dotfiles. The config does not automate that promotion.
+
+** DONE Play-on-create behavior
+- Context: after creating a station Craig may want it to start playing, or just exist for later. Craig's call: play it.
+- Decision: We will create the station and immediately enqueue and play the selection through EMMS (mpv), so a search ends in audio. With several picks the created stations enqueue in order and the first starts playing right away, interrupting whatever was playing (the natural reading of "always play" — the search ends in the radio, not behind the current queue). A prefix argument suppressing playback (create-only) is a vNext nicety, not v1.
+- Consequences: easier — search-to-sound in one command, no separate load step; harder — creating always interrupts current playback, so a "just save it for later" flow means creating and then stopping. Acceptable given radio is play-oriented, and confirmable against a real listen.
+
+** DONE Candidate annotation format
+- Context: the marginalia annotation on each station line can carry codec/bitrate/country plus either popularity or genre. The prototype (docs/specs/2026-07-06-radio-browser-lookup.prototype.html) shows both.
+- Decision: Variant B (Craig's call): codec, bitrate, country, ♥votes, and top tags. Drop the play count — Craig isn't interested in it, and the tags help tell same-named stations apart by what they play.
+- Consequences: the station line runs a little wider to fit the tags, and cj/music-radio--format-candidate emits the tag snippet (first few tags) rather than the play count.
+
+* Review findings [7/7]
+** DONE Multi-select splits on commas in station names :blocking:
+Accepted. Design piece 4 now specifies a repeated single-select loop (completing-read with a "[done]" sentinel, each pick removed from the pool) instead of completing-read-multiple, so a comma in a station name can never mis-split the selection — each pick maps straight from its display string to the station object. This keeps Decision 2's "several picks in one search" without the delimiter hazard.
+** DONE Empty or missing url_resolved writes a broken station
+Accepted. The emitter (piece 3) prefers url_resolved and falls back to url; the writer (piece 4) skips a station with neither and names it in the closing message.
+** DONE Two picks with the same name collide on the filename
+Accepted. Piece 4's writer disambiguates a name that collides with an already-written pick this run by appending a short stationuuid fragment.
+** DONE "Reuses the existing writer" is imprecise
+Accepted (modified). Rather than refactor cj/music-create-radio-station (scope creep + test churn on working code), piece 4 now states the feature reuses cj/music--safe-filename and the existing radio M3U format but ships its own emitter/writer, and cj/music-create-radio-station stays as-is on its own write target. Clarifies the imprecision without expanding scope.
+** DONE Server-selection fallback underspecified
+Accepted. Piece 1 now pins a default host (cj/music-radio-server) and, on a connection failure or timeout, fetches /json/servers once and retries against the first reachable host, capped at one retry.
+** DONE create-and-play behavior when music is already playing
+Accepted (modified). Decision 4's body now states create-and-play interrupts current playback (plays the first created station immediately), the natural reading of "always play." Proposed default surfaced to Craig for veto; a create-only prefix arg is logged as vNext.
+** DONE Annotation format A vs B unresolved
+Resolved via new Decision 5: Craig picked Variant B (codec/bitrate/country/♥votes/tags, drop play count). Folded into the caller-altitude design description and Decision 5.
+
+* Implementation phases
+
+** Phase 1 — API client + pure emitter (no UI)
+Add cj/music-radio--server, cj/music-radio--search (returns station plists), cj/music-radio--format-candidate, and cj/music-radio--station-m3u. Unit-test the pure pieces (candidate formatting, M3U emission) against fixture station plists; test the client with a stubbed url-retrieve or a recorded JSON fixture, never a live call. Leaves the tree working: helpers exist, nothing bound yet.
+
+** Phase 2 — interactive command + binding
+Add cj/music-radio-search (query -> search -> completing-read multi-select -> writer), reusing cj/music--safe-filename and the save-dir defcustom. Bind it in the playlist keymap next to R. Handle empty results, network failure, and cancel with clear user-errors. Manual live verification (the network + picker + play can't be driven headless) filed as a VERIFY.
+
+* Acceptance criteria
+- [ ] A search for a known station name returns annotated candidates in a completion prompt.
+- [ ] Selecting a station writes a .m3u whose bytes match the existing radio format (#EXTM3U / #EXTINF:1,<name> / <url_resolved>), with a #RADIOBROWSERUUID line.
+- [ ] Creating a station writes it into cj/music-radio-save-dir, then enqueues and starts it playing through mpv; it also appears in the player's load list (multi-directory sourcing).
+- [ ] An empty result set and a network failure each produce a clear message, not a stack trace.
+- [ ] Selecting several stations in one search creates one file per station, and a station name containing a comma is selected correctly (no mis-split).
+- [ ] A station with no usable stream URL is skipped and named in the closing message, not written as a broken file.
+- [ ] The pure emitter and candidate formatter have Normal/Boundary/Error tests that run without the network.
+
+* Readiness dimensions
+- Data model & ownership: a station plist (uuid, name, url, codec, bitrate, tags, country, votes, clickcount) derived from the API; the .m3u file is generated and owned by Craig once written. No local cache in v1.
+- Errors, empty states & failure: named user-errors — no server reachable, empty results, cancelled selection, write failure (naming the file). No silent data loss; overwrite reuses the existing cj/confirm-strong prompt from create-radio-station.
+- Security & privacy: no credentials. The only outbound data is the search query and a User-Agent to a public API. No sensitive data logged.
+- Observability: the command messages the server used, the result count, and each file written. Search is one short synchronous call; if it ever feels slow, a "Searching radio-browser…" message covers it.
+- Performance & scale: result sets bounded by an explicit limit (default ~30). One GET per search. No scaling concern.
+- Reuse & lost opportunities: reuses cj/music--safe-filename, the overwrite-confirm pattern, cj/music-m3u-root, and the whole multi-directory sourcing + mpv play path. json-parse-string and url.el are built in. Nothing new is invented that the platform already provides.
+- Architecture fit & weak points: integrates at music-config.el alongside cj/music-create-radio-station; the writer is the shared seam. Weak point: radio-browser server availability — mitigated by the server-list fallback and a timeout.
+- Config surface: cj/music-radio-save-dir (default ~/.local/share/mpd/playlists/, the radio home), cj/music-radio-server (default pinned host, with the /json/servers fallback), cj/music-radio-search-limit (default 30), cj/music-radio-user-agent (descriptive default). All with defaults and doc.
+- Documentation plan: a line in the module commentary and the keybinding list; no separate doc needed.
+- Dev tooling: existing make test / test-file targets cover the new unit tests; no new tooling.
+- Rollout, compatibility & rollback: additive — a new command and helpers, no change to existing behavior or files. Removing it is deleting the code; created .m3u files land in the MPD playlist directory as ordinary files (untracked until Craig stows them) and stay.
+- External APIs & deps: radio-browser /json/servers and /json/stations/search VERIFIED live 2026-07-06 (shape recorded in Problem/Context). The vote/click endpoint (vNext) is a research prerequisite if that feature is pursued.
+
+* Risks, Rabbit Holes, and Drawbacks
+- radio-browser etiquette: the project asks clients to identify via User-Agent and to rotate servers rather than hammer one. v1 honors both; skipping click-counting is polite-neutral (it slightly under-reports popularity but adds no load).
+- url.el error handling is the likeliest rabbit hole. Keep the client tiny: one GET, parse, or a single user-error. Do not build a general HTTP layer.
+- Stream URL choice: a station can list several codec/bitrate variants under one name. v1 takes url_resolved as-is; picking among a station's variants is a vNext refinement, not a v1 problem.
+
+* References / Appendix
+- UI prototype (open in a browser): [[file:2026-07-06-radio-browser-lookup.prototype.html][2026-07-06-radio-browser-lookup.prototype.html]] — faithful vertico + marginalia mockup of the four minibuffer screens (query, station list, multi-select, created+playing) with real jazz-search data. Shows the two candidate-annotation variants the design chooses between (A: codec/bitrate/country/votes/plays; B: codec/bitrate/country/votes/tags).
+- API shape verified live 2026-07-06 against de1.api.radio-browser.info (recorded in Problem / Context).
+
+* Review and iteration history
+** 2026-07-06 Mon @ 13:01:55 -0500 — Claude (for Craig) — responder
+- What: dispositioned all 7 review findings. Six accepted or accepted-with-modification and folded into the Design, Decisions, config, and acceptance sections; the seventh (annotation format) resolved by adding Decision 5, which Craig answered Variant B (tags, drop play count). Re-ran the readiness rubric on the expanded spec and flipped DRAFT -> READY.
+- Why: the blocking CRM comma-split needed a selection-mechanism decision (now a loop-based single-select), and the empty-URL, filename-collision, writer-precision, and server-fallback gaps each needed a concrete rule before an implementer could build without inventing behavior.
+- Artifacts: * Review findings [7/7]; * Decisions [5/5]; the loop-select, skip-empty-URL, filename-dedup, and server-fallback rules in the Design; Decision 5 (Variant B).
+** 2026-07-06 Mon @ 10:48:20 -0500 — Claude (for Craig) — reviewer
+- What: first review pass. Recorded 7 findings (1 blocking: CRM comma-splitting breaks multi-select for station names containing commas); rubric Not ready pending disposition. Confirmed the two Implementation phases decompose cleanly.
+- Why: verify implementation-readiness before build. The multi-select mechanism, empty-URL and duplicate-name robustness, the shared-writer seam, server fallback, and the create-and-play semantics were the real gaps an implementer would hit.
+- Artifacts: * Review findings [0/7]; grounded in music-config.el (writer, m3u reader, multi-dir sourcing, cj/music-create-radio-station) and the live spike; API verified live.
+** 2026-07-06 Mon @ 10:12:00 -0500 — Claude (for Craig) — author
+- What: resolved all four open decisions from Craig's answers and folded them into the design, config, rollout, and acceptance sections.
+- Why: HTTP client, playlist shape, save destination, and play-on-create were the real product choices gating a build.
+- Artifacts: Decisions section now [4/4]; save dir is the MPD playlist directory; create-and-play is the default.
+** 2026-07-06 Mon @ 10:01:27 -0500 — Claude (for Craig) — author
+- What: initial draft.
+- Why: the music player can source and play radio .m3u but has no native way to discover stations; radio-browser's API supplies exactly the needed metadata.
+- Artifacts: todo.org "Music — create playlists from a radio.info lookup"; API shape verified live against de1.api.radio-browser.info.
diff --git a/docs/specs/2026-07-06-radio-browser-lookup.prototype.html b/docs/specs/2026-07-06-radio-browser-lookup.prototype.html
new file mode 100644
index 00000000..69d3c435
--- /dev/null
+++ b/docs/specs/2026-07-06-radio-browser-lookup.prototype.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Radio-browser lookup — UI prototype</title>
+<style>
+ :root {
+ --page-bg: #f3f0ea; --page-fg: #2b2822; --page-dim: #6b6459;
+ --card-line: #d9d3c7; --accent: #9a6b2f;
+ /* Emacs frame (always dark — it depicts a dark editor) */
+ --e-bg: #15140f; --e-fg: #cfc8b8; --e-dim: #857c6c;
+ --e-sel-bg: #2a2620; --e-sel-fg: #e6c98a; --e-prompt: #8fb0c4;
+ --e-mark: #d98f7a; --e-play: #8faf7f; --e-modeline: #201d17;
+ --e-rule: #322d25;
+ }
+ @media (prefers-color-scheme: dark) {
+ :root { --page-bg: #14130f; --page-fg: #d4cdbf; --page-dim: #8a8272;
+ --card-line: #2c281f; --accent: #d8a24f; }
+ }
+ :root[data-theme="light"] { --page-bg: #f3f0ea; --page-fg: #2b2822; --page-dim: #6b6459; --card-line: #d9d3c7; --accent: #9a6b2f; }
+ :root[data-theme="dark"] { --page-bg: #14130f; --page-fg: #d4cdbf; --page-dim: #8a8272; --card-line: #2c281f; --accent: #d8a24f; }
+
+ * { box-sizing: border-box; }
+ body {
+ margin: 0; background: var(--page-bg); color: var(--page-fg);
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
+ line-height: 1.55; padding: 3rem 1.25rem 5rem;
+ }
+ .wrap { max-width: 60rem; margin: 0 auto; }
+ header { margin-bottom: 2.5rem; }
+ .eyebrow { text-transform: uppercase; letter-spacing: .14em; font-size: .72rem;
+ color: var(--accent); font-weight: 600; margin: 0 0 .5rem; }
+ h1 { font-size: 1.9rem; margin: 0 0 .6rem; text-wrap: balance; font-weight: 650; }
+ header p { margin: .3rem 0; max-width: 62ch; color: var(--page-dim); }
+ header p strong { color: var(--page-fg); font-weight: 600; }
+
+ .step { margin: 2.4rem 0 .8rem; }
+ .step .n { color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums; }
+ .step h2 { display: inline; font-size: 1.15rem; font-weight: 620; }
+ .step + .note { margin: 0 0 1rem; color: var(--page-dim); font-size: .92rem; max-width: 64ch; }
+
+ /* Emacs frame */
+ .frame {
+ background: var(--e-bg); color: var(--e-fg);
+ border: 1px solid var(--e-rule); border-radius: 8px; overflow: hidden;
+ box-shadow: 0 12px 34px -20px rgba(0,0,0,.6);
+ font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
+ font-size: 13.5px;
+ }
+ .buffer { padding: .55rem 0 0; }
+ .buf-line { padding: .05rem 1rem; white-space: pre; color: var(--e-dim); }
+ .buf-line .txt { color: var(--e-fg); }
+ .modeline {
+ background: var(--e-modeline); color: var(--e-dim);
+ padding: .2rem 1rem; margin-top: .55rem;
+ border-top: 1px solid var(--e-rule); border-bottom: 1px solid var(--e-rule);
+ display: flex; gap: 1.2rem; font-size: 12.5px;
+ }
+ .modeline .lead { color: var(--e-fg); }
+ .mini { padding: .45rem 1rem .6rem; display: flex; align-items: baseline; }
+ .prompt { color: var(--e-prompt); }
+ .input { color: var(--e-fg); }
+ .caret { display: inline-block; width: .55ch; height: 1.15em; background: var(--e-fg);
+ translate: 0 .18em; margin-left: 1px; }
+
+ /* vertico completion list */
+ .vhead { padding: .45rem 1rem .3rem; color: var(--e-prompt); border-top: 1px solid var(--e-rule); }
+ .vhead .count { color: var(--e-dim); float: right; font-variant-numeric: tabular-nums; }
+ .cand { display: grid; grid-template-columns: 1.4ch 1fr auto; gap: .5ch;
+ padding: .12rem 1rem; align-items: baseline; }
+ .cand .mark { color: var(--e-mark); }
+ .cand .name { color: var(--e-fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+ .cand .ann { color: var(--e-dim); white-space: pre; font-variant-numeric: tabular-nums; text-align: right; }
+ .cand.sel { background: var(--e-sel-bg); }
+ .cand.sel .name { color: var(--e-sel-fg); }
+ .cand.marked .name { color: var(--e-fg); }
+ .cand.marked .mark { color: var(--e-mark); }
+ .tag { color: #7f94a6; }
+
+ .playrow .np { color: var(--e-play); }
+ .legend { margin-top: 1.4rem; padding-top: 1.2rem; border-top: 1px solid var(--card-line);
+ color: var(--page-dim); font-size: .9rem; }
+ .legend code { background: color-mix(in srgb, var(--page-fg) 8%, transparent);
+ padding: .05rem .35rem; border-radius: 4px; font-family: ui-monospace, monospace; font-size: .85em; }
+ .pick { display: flex; gap: .6rem; flex-wrap: wrap; margin: .6rem 0 0; }
+ .pill { border: 1px solid var(--card-line); border-radius: 999px; padding: .15rem .7rem;
+ font-size: .82rem; color: var(--page-dim); }
+ .pill b { color: var(--accent); }
+</style>
+</head>
+<body>
+<div class="wrap">
+ <header>
+ <p class="eyebrow">Emacs · vertico + marginalia · prototype</p>
+ <h1>Radio-browser station lookup — how the screens read</h1>
+ <p>One command, <strong>cj/music-radio-search</strong>, taking you from a query to audio. These are faithful mockups of the four minibuffer screens, drawn with a real live search for <strong>jazz</strong> (results sorted by popularity). Everything is monospace because it all lives in the minibuffer.</p>
+ <p>The one real design choice is the station line: what metadata rides alongside the name, and in what order. Step 2 shows two variants — pick one.</p>
+ </header>
+
+ <!-- STEP 1 -->
+ <div class="step"><span class="n">1</span> &nbsp;<h2>The query</h2></div>
+ <p class="note">Bound next to <code style="font-family:ui-monospace,monospace">R</code> in the playlist keymap. You type a name or keyword; Enter fires the search.</p>
+ <div class="frame">
+ <div class="buffer">
+ <div class="buf-line">;; <span class="txt">*EMMS-Playlist*</span> — 3 tracks, playing “Kind of Blue / So What”</div>
+ </div>
+ <div class="modeline"><span class="lead">*EMMS-Playlist*</span><span>Radio</span><span>▶ playing</span></div>
+ <div class="mini"><span class="prompt">Radio search:&nbsp;</span><span class="input">jazz</span><span class="caret"></span></div>
+ </div>
+
+ <!-- STEP 2A -->
+ <div class="step"><span class="n">2</span> &nbsp;<h2>The station list</h2> &nbsp;<span style="color:var(--page-dim);font-size:.9rem">— Variant A: codec · bitrate · country · ♥votes ▶plays</span></div>
+ <p class="note">vertico lists the matches; marginalia annotates each on the right, dimmed. The current row carries the gold highlight. Codec and bitrate tell you the quality; ♥ is radio-browser votes, ▶ is play count — both proxies for “is this station any good.”</p>
+ <div class="frame">
+ <div class="vhead">Stations for “jazz” (TAB to mark, RET to create+play): <span class="count">7/842</span></div>
+ <div class="cand sel"><span class="mark"></span><span class="name">Adroit Jazz Underground</span><span class="ann">MP3 320k US ♥174208 ▶148</span></div>
+ <div class="cand"><span class="mark"></span><span class="name">101 SMOOTH JAZZ</span><span class="ann">MP3 128k US ♥86929 ▶389</span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Adroit Jazz Underground HD Opus</span><span class="ann">OGG 192k US ♥67573 ▶51</span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Jazz Radio Blues</span><span class="ann">MP3 128k FR ♥62960 ▶113</span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Jazz Radio</span><span class="ann">MP3 192k FR ♥49272 ▶108</span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Jazz Radio Classic Jazz</span><span class="ann">MP3 128k FR ♥27269 ▶52</span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Radio Swiss Jazz</span><span class="ann">MP3 128k CH ♥26626 ▶58</span></div>
+ <div class="mini"><span class="prompt">Stations for “jazz”…:&nbsp;</span><span class="input"></span><span class="caret"></span></div>
+ </div>
+
+ <!-- STEP 2B -->
+ <div class="step"><span class="n">2</span> &nbsp;<h2>The station list</h2> &nbsp;<span style="color:var(--page-dim);font-size:.9rem">— Variant B: adds a tag snippet, drops play count</span></div>
+ <p class="note">Same list, but the annotation trades ▶plays for the station’s top tags — more help telling two same-named stations apart by what they actually play, at the cost of a wider line.</p>
+ <div class="frame">
+ <div class="vhead">Stations for “jazz” (TAB to mark, RET to create+play): <span class="count">7/842</span></div>
+ <div class="cand sel"><span class="mark"></span><span class="name">Adroit Jazz Underground</span><span class="ann">MP3 320k US ♥174208 · <span class="tag">bebop, hard bop, cool</span></span></div>
+ <div class="cand"><span class="mark"></span><span class="name">101 SMOOTH JAZZ</span><span class="ann">MP3 128k US ♥86929 · <span class="tag">smooth jazz, easy</span></span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Adroit Jazz Underground HD Opus</span><span class="ann">OGG 192k US ♥67573 · <span class="tag">avant-garde, opus</span></span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Jazz Radio Blues</span><span class="ann">MP3 128k FR ♥62960 · <span class="tag">blues, jazz</span></span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Jazz Radio</span><span class="ann">MP3 192k FR ♥49272 · <span class="tag">jazz, soul</span></span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Jazz Radio Classic Jazz</span><span class="ann">MP3 128k FR ♥27269 · <span class="tag">classical, jazz</span></span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Radio Swiss Jazz</span><span class="ann">MP3 128k CH ♥26626 · <span class="tag">public radio</span></span></div>
+ <div class="mini"><span class="prompt">Stations for “jazz”…:&nbsp;</span><span class="input"></span><span class="caret"></span></div>
+ </div>
+
+ <!-- STEP 3 -->
+ <div class="step"><span class="n">3</span> &nbsp;<h2>Marking several</h2></div>
+ <p class="note">It’s <code style="font-family:ui-monospace,monospace">completing-read-multiple</code>: TAB marks a row (red bullet, name stays lit), and one search can create several stations at once. Here three are marked; Enter creates all three.</p>
+ <div class="frame">
+ <div class="vhead">Stations for “jazz” (TAB to mark, RET to create+play): <span class="count">7/842</span></div>
+ <div class="cand marked"><span class="mark">●</span><span class="name">Adroit Jazz Underground</span><span class="ann">MP3 320k US ♥174208 ▶148</span></div>
+ <div class="cand"><span class="mark"></span><span class="name">101 SMOOTH JAZZ</span><span class="ann">MP3 128k US ♥86929 ▶389</span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Adroit Jazz Underground HD Opus</span><span class="ann">OGG 192k US ♥67573 ▶51</span></div>
+ <div class="cand marked"><span class="mark">●</span><span class="name">Jazz Radio Blues</span><span class="ann">MP3 128k FR ♥62960 ▶113</span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Jazz Radio</span><span class="ann">MP3 192k FR ♥49272 ▶108</span></div>
+ <div class="cand"><span class="mark"></span><span class="name">Jazz Radio Classic Jazz</span><span class="ann">MP3 128k FR ♥27269 ▶52</span></div>
+ <div class="cand sel marked"><span class="mark">●</span><span class="name">Radio Swiss Jazz</span><span class="ann">MP3 128k CH ♥26626 ▶58</span></div>
+ <div class="mini"><span class="prompt">Stations for “jazz”…:&nbsp;</span><span class="input">Adroit Jazz Underground,Jazz Radio Blues,Radio Swiss Jazz</span><span class="caret"></span></div>
+ </div>
+
+ <!-- STEP 4 -->
+ <div class="step"><span class="n">4</span> &nbsp;<h2>Created and playing</h2></div>
+ <p class="note">Each pick is written as an .m3u into the MPD playlist directory (Decision 3), then enqueued and started through mpv (Decision 4) — the search ends in sound. The echo area confirms; the three land in the playlist, the first now playing.</p>
+ <div class="frame">
+ <div class="buffer">
+ <div class="buf-line playrow"><span class="np">▶ </span><span class="txt">Adroit Jazz Underground</span></div>
+ <div class="buf-line">&nbsp;&nbsp;<span class="txt">Jazz Radio Blues</span></div>
+ <div class="buf-line">&nbsp;&nbsp;<span class="txt">Radio Swiss Jazz</span></div>
+ </div>
+ <div class="modeline"><span class="lead">*EMMS-Playlist*</span><span>Radio</span><span class="playrow"><span class="np">▶ playing</span></span></div>
+ <div class="mini"><span class="prompt" style="color:var(--e-play)">Created + playing 3 stations&nbsp;</span><span style="color:var(--e-dim)">→ ~/.local/share/mpd/playlists/</span></div>
+ </div>
+
+ <div class="legend">
+ <div>Two things to react to:</div>
+ <div class="pick">
+ <span class="pill"><b>A</b> — quality + popularity (codec · bitrate · country · ♥ ▶)</span>
+ <span class="pill"><b>B</b> — quality + tags (codec · bitrate · country · ♥ · tags)</span>
+ </div>
+ <p style="margin:.9rem 0 0">Everything else (the flow, multi-select, create-and-play into the MPD dir) matches the resolved spec. Data is a live jazz search; ♥ = radio-browser votes, ▶ = play count. This is a static mockup — no live search runs in the page.</p>
+ </div>
+</div>
+
+<script>
+ (function () {
+ var root = document.documentElement;
+ try {
+ var t = localStorage.getItem('theme');
+ if (t === 'dark' || t === 'light') root.setAttribute('data-theme', t);
+ } catch (e) {}
+ })();
+</script>
+</body>
+</html>
diff --git a/docs/specs/2026-07-10-org-workflow-doctor-spec.org b/docs/specs/2026-07-10-org-workflow-doctor-spec.org
new file mode 100644
index 00000000..b7f5db6c
--- /dev/null
+++ b/docs/specs/2026-07-10-org-workflow-doctor-spec.org
@@ -0,0 +1,260 @@
+#+TITLE: Org workflow doctor — Spec
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-10
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* CANCELLED Org workflow doctor
+:PROPERTIES:
+:ID: c0e06025-a3b8-4238-a9a0-07f9e55913f4
+:END:
+- 2026-07-10 Fri @ 05:58:00 -0500 — CANCELLED. The feature has no job. Craig asked what the doctor buys when it refuses to install anything, and the answer is nothing that isn't already there. Every external binary is already guarded at its point of use, with a clear message: hugo-config guards hugo and the file-manager opener, org-webclipper guards pandoc, org-export-config guards zathura, and ox-pandoc guards itself upstream (ox-pandoc.el:1533, 1970). Package availability surfaces at load or first use. Path checks were the only genuinely new capability, and a startup warning about a missing org-dir contradicts this spec's own goal of keeping startup quiet, while an on-demand check nobody remembers to run is worth roughly nothing. DRAFT -> CANCELLED.
+- 2026-07-10 Fri @ 00:29:10 -0500 — drafted.
+
+* Postmortem
+
+The spec cleared spec-create's Phase 0 bar narrowly, and that was the signal to stop
+and ask what the feature bought over the mechanisms already in the tree. It wasn't
+taken.
+
+The =Reuse & lost opportunities= dimension exists to catch exactly this. It was
+filled in, it named =cj/executable-find-or-warn=, and it dismissed the helper in one
+clause for warning as a side effect. Warning as a side effect is the feature. The
+dimension was answered without being used.
+
+Three claims in the surrounding audit came from grepping for a helper's *name* rather
+than reading for the *behavior*, and all three were wrong: the org modules were said
+not to guard their binaries when they guard them at the point of use, which is the
+better place. Reading corrected the claim each time.
+
+The one real finding this line of work produced landed elsewhere and stands: ledger
+buffers were never linted, fixed in 55b85754.
+
+* Metadata
+| Status | cancelled |
+|----------+----------------------------------------------------------|
+| Owner | Craig Jennings |
+|----------+----------------------------------------------------------|
+| Reviewer | Craig Jennings |
+|----------+----------------------------------------------------------|
+| Related | [[file:../../todo.org][todo.org]] — "Add an Org workflow health check command" |
+|----------+----------------------------------------------------------|
+
+* Summary
+
+A single on-demand command, =cj/org-workflow-doctor=, that checks whether the Org
+workflow's prerequisites are actually present: the files and directories it reads
+and writes, the external programs it shells out to, and the optional packages it
+defers loading. It reports what it found and never changes anything.
+
+Today a missing prerequisite surfaces at command time, in whatever shape the first
+module to trip over it happens to produce. The doctor moves that discovery to a
+moment the user chose.
+
+* Problem / Context
+
+#+begin_quote
+*Correction, 2026-07-10.* The premise below is false and the body is left intact as
+the record of the mistake. Every external binary *is* checked, at its point of use:
+=hugo-config= guards hugo and the file-manager opener, =org-webclipper= guards
+pandoc, =org-export-config= guards zathura, and =ox-pandoc= guards itself upstream.
+The claim came from grepping for =cj/executable-find-or-warn= by name instead of
+reading the modules for the behavior. See the Postmortem above.
+#+end_quote
+
+The Org workflow spans many modules, and each depends on some mix of a personal
+path (=org-dir=, =roam-dir=), an external binary (pandoc, hugo), and an optional
+package that loads lazily (=org-noter=, =org-web-tools=). None of those
+dependencies is checked anywhere.
+
+When one is missing, the failure appears wherever the first module happens to hit
+it. A missing =contacts-file= surfaces as a capture template erroring mid-capture.
+An absent pandoc surfaces as a shell command returning nothing useful. The user
+learns about a broken prerequisite at the least convenient moment, and the message
+rarely names the prerequisite.
+
+Nothing about this is hard. It just isn't anywhere, and the checks are scattered
+across modules that each know only their own corner.
+
+* Goals and Non-Goals
+
+** Goals
+- One command reports the health of every Org workflow prerequisite.
+- The check never mutates user data. It reads, it does not create or repair.
+- The result is structured data, so it is unit-testable and can be rendered to
+ either the echo area or a buffer without recomputing.
+- Startup stays quiet. Nothing runs unless asked.
+
+** Non-Goals
+- It will not fix anything. No creating a missing directory, no installing a
+ package, no offering to. A doctor that repairs is a different, riskier command.
+- It will not check every package in the config, only the Org workflow's.
+- It will not run on a timer, a hook, or at startup.
+- It will not be a general config linter. The scope is the Org workflow.
+
+** Scope tiers
+- v1: the seven paths, the two external binaries, the six optional packages, a
+ structured result, and a rendering to a buffer.
+- Out of scope: repair actions, non-Org prerequisites, scheduled runs.
+- vNext: a =--fix= variant that offers to create missing directories after
+ confirmation; checking that =org-agenda-files= entries all resolve.
+
+* Design
+
+** For the caller
+
+=M-x cj/org-workflow-doctor= opens a report buffer listing every prerequisite with
+its status. A prerequisite is =ok=, =missing=, or =skipped= (checked something the
+user hasn't configured). With a prefix argument the command reports a one-line
+summary to the echo area instead, for a quick "is anything broken?" glance.
+
+Nothing on disk changes. Running it twice produces the same report.
+
+** For the implementer
+
+The command splits in two, per the interactive-versus-internal rule.
+
+=cj/org-workflow--check= is pure with respect to user data: it probes the
+environment and returns a list of plists, one per prerequisite, each carrying
+=:name=, =:kind= (=path= / =executable= / =package=), =:status= and =:detail=. It
+takes no arguments and prompts for nothing, so a test can call it directly against
+a temp =user-emacs-directory= and assert on the structure.
+
+=cj/org-workflow-doctor= is the thin interactive wrapper: call the internal,
+render the result, done.
+
+*** The probe each kind uses
+
+Paths are checked with =file-exists-p= against the variable's value, and reported
+=skipped= when the variable is unbound or nil rather than =missing= — an unset
+=cj/hugo-content-org-dir= means the user doesn't publish with Hugo, which is not a
+fault.
+
+Executables are checked with =executable-find=.
+
+Packages are the one place a naive implementation gets it wrong, and this is the
+decision the spec exists to record. The obvious probe is =featurep=, and it is
+incorrect here: this config defers loading, so =featurep= returns nil for a package
+that is installed and perfectly healthy. Probed on 2026-07-10, =org-noter= and
+=org-web-tools= both report =(featurep) => nil= while =(locate-library) => t=. A
+doctor built on =featurep= would report a false failure for precisely the packages
+the config is designed to load lazily, which is worse than no doctor: it teaches
+the user to ignore it.
+
+=locate-library= answers the question actually being asked, which is "can this load
+when something needs it?" rather than "has it loaded already?".
+
+* Alternatives considered
+
+*** Probe packages with =featurep=
+- Good, because: it is the first thing that comes to mind and costs nothing.
+- Bad, because: it reports false failures for every lazily-loaded package, which is
+ most of them. Verified, not theorised.
+- Neutral, because: it would be correct in a config that loads everything eagerly.
+
+*** Check prerequisites at startup instead of on demand
+- Good, because: the user learns about a broken prerequisite before they need it.
+- Bad, because: it costs startup time on every launch to answer a question asked a
+ few times a year, and it puts warnings in front of a user who didn't ask.
+- Neutral, because: a doctor command can be run from a startup hook later if the
+ cost turns out to be trivial.
+
+*** Have each module check its own prerequisites
+- Good, because: the check lives next to the thing that needs it.
+- Bad, because: this is the status quo, and the problem is that the checks don't
+ compose into an answer to "is my Org workflow healthy?".
+- Neutral, because: the doctor doesn't prevent a module from also checking.
+
+* Decisions [2/2]
+
+** DONE Probe optional packages with =locate-library=, not =featurep=
+Context: the config loads Org packages lazily, so a healthy package is routinely
+unloaded. Probed 2026-07-10: =org-noter= and =org-web-tools= are both
+=featurep=-nil and =locate-library=-non-nil.
+
+Decision: we will probe package availability with =locate-library=.
+
+Consequences: the doctor answers "can this load?", which is the question that
+matters, and it stops reporting false failures for deferred packages. Harder: the
+doctor cannot distinguish "installed but broken on load" from "installed and fine",
+because it deliberately does not load anything. That is the right trade for a
+read-only check, and a load error surfaces at use time anyway.
+
+** DONE An unset optional path reports =skipped=, not =missing=
+Context: not every user of this config publishes with Hugo or uses reveal.js. An
+unbound or nil =cj/hugo-content-org-dir= is a configuration choice, not a fault.
+
+Decision: we will report =skipped= when a path variable is unbound or nil, and
+=missing= only when it holds a value that does not resolve on disk.
+
+Consequences: the report stays honest, so a clean report means something. Harder:
+the status vocabulary grows a third value, and the renderer has to distinguish
+three states rather than two.
+
+* Implementation phases
+
+1. *The internal, with tests.* =cj/org-workflow--check= plus its three probe
+ helpers (path, executable, package). Tests drive real state: a temp directory
+ that exists and one that doesn't, an executable that resolves and a nonsense
+ name, a library that locates and one that doesn't. Tree is working; nothing is
+ bound to a key yet.
+2. *The renderer and the command.* =cj/org-workflow-doctor=, the report buffer, and
+ the prefix-argument echo-area summary. Tests cover the rendering of a synthetic
+ result list, not the environment.
+
+* Acceptance criteria
+
+- =cj/org-workflow--check= returns one plist per prerequisite, each with =:name=,
+ =:kind=, =:status= and =:detail=.
+- A path variable holding a resolving directory reports =ok=; one holding a
+ nonexistent path reports =missing=; one unbound or nil reports =skipped=.
+- =org-noter= and =org-web-tools= report =ok= on this machine despite being
+ unloaded. This is the regression the spec exists to prevent.
+- Running the command twice leaves the filesystem byte-identical.
+- The command is absent from every hook and timer.
+
+* Readiness dimensions
+
+- *Data model & ownership* — the result is generated, ephemeral, and owned by the
+ command. Nothing persists.
+- *Errors, empty states & failure* — a probe that throws is caught per-prerequisite
+ and reported as =missing= with the error text as =:detail=, so one bad probe
+ cannot abort the report.
+- *Security & privacy* — the report prints paths from the user's config. It stays
+ in a local buffer and is never written to disk or transmitted.
+- *Observability* — the report is the observability.
+- *Performance & scale* — fifteen probes, all local filesystem stats. No concern.
+- *Reuse & lost opportunities* — =executable-find= and =locate-library= are the
+ platform's answers; nothing is reimplemented. =cj/executable-find-or-warn=
+ (=system-lib.el=) exists but warns as a side effect, which a read-only check must
+ not do, so the doctor calls =executable-find= directly.
+- *Architecture fit* — a new module, =modules/org-workflow-doctor.el=, requiring
+ nothing but the variables it probes. It must not require the Org modules, or
+ probing them would load them and defeat the lazy-loading it is checking.
+- *Config surface* — none in v1. The prerequisite list is a defconst.
+- *Documentation plan* — module commentary, plus the keybinding if one is added.
+- *Dev tooling* — the existing =make test= covers it. No new target.
+- *Rollout, compatibility & rollback* — additive, read-only, deletable. N/A.
+- *External APIs & deps* — none. Both binaries were verified present on 2026-07-10
+ (=/usr/bin/pandoc=, =/usr/bin/hugo=), and their absence is the case under test.
+
+* Risks, rabbit holes, and drawbacks
+
+The rabbit hole is scope. "Check the Org workflow's prerequisites" slides easily
+into "lint the whole config", and from there into "offer to fix what it finds". The
+non-goals exist to hold that line. If the doctor is useful, a =--fix= variant is a
+separate spec with a separate risk profile, because a command that creates
+directories is no longer read-only.
+
+The smaller risk is the prerequisite list going stale as modules change. A doctor
+that checks the wrong things is worse than none, since it reports health that isn't
+real. The defconst lives next to the probes so it is at least easy to find.
+
+* Review and iteration history
+
+** 2026-07-10 Fri @ 00:29:10 -0500 — Craig Jennings — Author
+What: drafted the spec.
+Why: the task is feature-level, so the speedrun's per-item disposition rule
+delivers a spec rather than an implementation.
+Artifacts: this file. Probed the live daemon for the seven paths, both binaries,
+and the six packages; the =featurep= finding drove the first decision.
diff --git a/docs/specs/2026-07-17-org-agenda-fullscreen-frame-spec.org b/docs/specs/2026-07-17-org-agenda-fullscreen-frame-spec.org
new file mode 100644
index 00000000..567da53b
--- /dev/null
+++ b/docs/specs/2026-07-17-org-agenda-fullscreen-frame-spec.org
@@ -0,0 +1,404 @@
+#+TITLE: org-agenda fullscreen frame — Spec
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-17
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED org-agenda fullscreen frame
+:PROPERTIES:
+:ID: 7705c94b-9bb7-47d8-9828-e9584172c54f
+:END:
+- 2026-07-20 Mon @ 15:33 -0500 — post-implementation correction: dropped =(fullscreen . fullboth)= from the spawned frame. Craig's "fullscreen" meant a normal frame at its full tiled position, not a compositor-wide fullscreen; the frame is now a plain =make-frame= that a tiling WM (Hyprland) places side by side with the working frame. The engage-routing and focus logic are unchanged and now matter more (open a task in the adjacent working pane). The spec's "fullscreen" wording and filename are retained as historical; read them as "dedicated tiled frame". Covered by =test-org-agenda-frame-parameters-normal-tiled-frame=.
+- 2026-07-20 Mon @ 14:10 -0500 — IMPLEMENTED. Built both phases in =modules/org-agenda-frame.el= (58 ERT tests, full suite green, byte-compile clean, full init loads clean, live-reloaded into the daemon with all wiring confirmed). Phase 1: frame lookup/predicate/working-frame routing, the =F= today-anchored 7-day view + registration, the default-deny =cj/agenda-frame-mode= (allowlist + two message classes + menu removal + finalize re-enable), transactional spawn/raise/delete/toggle, engage routing, sticky/close lifecycle. Phase 2: the 5-min wall-clock =org-agenda-redo= timer with the window/focus contract, duplicate-timer prevention, deterministic point restoration, the frame-owned last-good snapshot with cloned markers + failure overlay + report-once latch, and the public =cj/agenda-frame-toggle= on =S-<f8>= with the force-rescan moved to =C-M-<f8>=. The compositor fullscreen/focus and real-redo behaviors are a residual manual check (VERIFY filed under Manual testing).
+- 2026-07-20 Mon @ 13:30 -0500 — DOING. Decomposed into build tasks (Phase 1, Phase 2, flip-to-IMPLEMENTED) under the fullscreen-frame PROJECT in todo.org; =:SPEC_ID:= stamped. Building Phase 1.
+- 2026-07-20 Mon @ 13:26 -0500 — READY (Craig accepted for build). Folded Codex's marker-clone finding (all 28 findings, 13 decisions resolved). Craig's call: build v1 now; the Hyprland-window variant is vNext once the in-Emacs frame proves out. Decomposing into build tasks next.
+- 2026-07-20 Mon @ 13:07 -0500 — spec-review (Codex): still DRAFT. The two prior responses close their stated behavior gaps, but Org invalidates the old agenda's marker objects during redo; the specified shallow property-bearing snapshot therefore restores dead =org-marker=/=org-hd-marker= links after a failed rebuild. One blocking marker-cloning/ownership finding added.
+- 2026-07-20 Mon @ 12:59 -0500 — spec-response: both fifth-pass findings dispositioned. Enumerated the full default-deny allowlist + honest keys/mouse enforcement boundary (menu removed, M-x out of contract); made failed-redo a complete retryable state (frame-owned snapshot with Org properties, explicit policy re-enable on the error path, overlay header, rebuild from =org-redo-cmd=). Findings [27/27]; Decisions [13/13]. Stays DRAFT pending Codex re-review.
+- 2026-07-20 Mon @ 12:47 -0500 — spec-review (Codex): still DRAFT. The structural response closes the prior eight findings, but two blockers remain: the default-deny minor-mode map neither defines its actual allowlist nor covers menu/M-x invocation, and failed-redo rollback does not restore a complete policy-enabled, retryable agenda state.
+- 2026-07-20 Mon @ 12:29 -0500 — renamed the file =…-dock-mode-spec.org= → =…-org-agenda-fullscreen-frame-spec.org= to match the design; inbound links in todo.org and .ai/notes.org updated. The ":ID:" is unchanged.
+- 2026-07-20 Mon @ 12:26 -0500 — spec-response: all 8 fourth-pass findings dispositioned. Root fix: replaced keymap enumeration with a default-deny =cj/agenda-frame-mode= policy (re-enabled by a finalize hook so redo can't strip it), frame-owned timer/failure state, sticky-kill-on-close, failed-redo snapshot restore, non-interactive Phase 1, synced ERT. Decisions now 13; findings [25/25]. Stays DRAFT pending Codex re-review.
+- 2026-07-20 Mon @ 12:11 -0500 — spec-review (Codex): still DRAFT. Detail-complete re-review found eight blocking gaps beneath the resolved values: Phase 1 remains callable through M-x; mutation, display-opening, view-change, and manual-redo command classes are incomplete; Org redo discards dedicated buffer-local policy/state; sticky reopen can show stale content; failed-redo display state is undefined; and the ERT surface does not cover the expanded contracts.
+- 2026-07-20 Mon @ 12:02 -0500 — spec-response: all 7 third-pass findings dispositioned (accepted; 4 with a chosen value). Key =F=, current-window, cached build on spawn, read-only command policy, source-opening routing, retry-report-once refresh, unbound-Phase-1 then binding-in-Phase-2, deterministic point tie-break. Decisions now 12; findings [17/17]. Stays DRAFT pending Codex re-review.
+- 2026-07-20 Mon @ 11:29 -0500 — spec-review (Codex): still DRAFT. A stricter second implementation-readiness pass found seven blocking choices still deferred to implementation: agenda command identity/window setup, initial source-list preparation, the permitted command surface, non-RET/TAB display routing, timer-failure recovery, phase exposure, and refresh point tie-breaking.
+- 2026-07-20 Mon @ 10:22 -0500 — spec-response: all 10 findings dispositioned (accepted; 2 with a chosen behavior). Design, Decisions (now 10), phases (now 2), acceptance, and the test surface updated. Stays DRAFT pending a re-review to confirm the blockers are closed.
+- 2026-07-20 Mon @ 10:12 -0500 — spec-review (Codex): demoted READY → DRAFT. Independent code-grounded re-review found six blocking gaps in date anchoring, exit/buffer lifecycle, working-frame fallback, phase safety, refresh context, and automated test coverage; three non-blocking implementation details also need definition.
+- 2026-07-20 Mon @ 09:53 -0500 — spec-review: READY. All 8 decisions resolved; code read confirmed the F8 bindings and display rule. One non-blocking finding recorded (the frame view needs its own custom-command entry). Ready to decompose the 3 phases.
+- 2026-07-20 Mon @ 09:45 -0500 — redesigned: a dedicated fullscreen frame replaces the right-side side-window dock. All eight design decisions resolved (see Decisions). The design is a fullscreen agenda frame, not a side dock; the file was later renamed to match.
+- 2026-07-17 Fri @ 19:34:07 -0500 — drafted.
+
+* Metadata
+| Status | implemented |
+|----------+------------------------------------------------|
+| Owner | Craig Jennings |
+|----------+------------------------------------------------|
+| Reviewer | Codex |
+|----------+------------------------------------------------|
+| Related | [[file:../../todo.org][fullscreen-frame task (Emacs Open Work)]] |
+|----------+------------------------------------------------|
+
+* Summary
+
+A dedicated fullscreen Emacs frame that shows the agenda. One key spawns (or raises) a frame of the running daemon, fullscreened, displaying the next seven days of schedule and tasks, refreshing itself every few minutes. Inside that frame focus stays on the agenda. The point is a standing, always-current agenda surface you can throw on its own workspace or monitor, one keystroke away, without the agenda ever stealing the frame you work in.
+
+* Problem / Context
+
+The normal agenda (=<f8>=, =cj/main-agenda-display=) opens below-selected at 75% of the frame (=cj/org-agenda-window-height=, applied through =cj/--org-agenda-display-rule=). That's a modal, take-over-the-frame view: you summon it, read it, dismiss it, and go back to work. There's no way to keep the schedule and task list glanceable while working, and no live refresh, so an agenda left open drifts stale (the now-line and freshly-synced calendar events don't move).
+
+Craig wants a second, non-modal surface: the agenda living in its own fullscreen frame, current and ready to engage, so it can sit on a separate workspace or monitor while the working frames stay untouched. This is a distinct mode from the existing full-view agenda, not a replacement for it.
+
+* Goals and Non-Goals
+
+** Goals
+- A dedicated fullscreen frame of the running daemon showing the seven-day schedule and task list, wholly separate from the working frames.
+- Live: the frame refreshes on a few-minute cadence so the now-line and synced events stay current. It shares the daemon's state, so calendar-sync results and buffer edits are already reflected.
+- One key to spawn, raise, and close it.
+- Focus stays on the agenda inside that frame; engaging a task opens the file in the working frame, not over the agenda.
+
+** Non-Goals
+- Not a replacement for the existing =<f8>= full-view agenda — that stays as is.
+- Not a separate OS process. It's a frame of the running daemon (chosen for live shared state over an isolated second Emacs). It therefore dies with the daemon, which is acceptable.
+- Not a capture/scratch surface in v1. Whether an agenda surface should double as a scratchpad is a separate exploration (see the [#D] task); v1 is read + engage only.
+- Not a startup auto-open. The frame is spawned on demand by its key; there is no auto-open defcustom.
+- Not a redesign of agenda content or faces — it reuses org-agenda's rendering.
+
+** Scope tiers
+- v1: a spawn/raise/close command bound to =S-<f8>=; a fullscreen frame; a seven-day agenda view filling it; focus held on the agenda; jump-to-task routed to the working frame; a frame-scoped refresh timer; the force-refresh keybinding move.
+- Out of scope: the scratchpad surface; a separate-process agenda; persisting frame state across daemon restarts.
+- vNext: scratchpad exploration (its own task); richer view tuning once the mode is in daily use; if the fullscreen frame proves out, reimplement as a Hyprland-managed window with its own keybinding — an external launcher replacing the in-Emacs =S-<f8>= spawn (Craig, 2026-07-20). v1 stays the in-Emacs frame.
+
+* Design
+
+The agenda lives in its own frame of the running daemon, created with =make-frame= carrying a marker parameter (=(cj/agenda-frame . t)=) so the toggle can find, raise, or delete it. (The frame was originally spawned =(fullscreen . fullboth)=; that was dropped 2026-07-20 so a tiling WM places it side by side with the working frame rather than covering the whole output — see the status history.) Sharing the daemon means the frame sees the same live state as every working frame: calendar-sync writes, unsaved buffer edits, and the current now-line are all already there, so "live" needs only a periodic redo, not a reload.
+
+For the user, the interface is one key. =S-<f8>= toggles the frame: spawn it fullscreen if none exists, raise and select it if it exists but isn't focused, delete it if it's the selected frame. The current occupant of =S-<f8>= (=cj/org-agenda-refresh-files=, the manual force-rescan shipped in 17ae3e2a) moves to =C-M-<f8>=, keeping the whole force-refresh idea in the F8 family.
+
+Inside the agenda frame, focus stays on the agenda. The frame holds a single window showing the seven-day agenda buffer, so point rests there by construction. Engaging a task (=RET=/=TAB= on an agenda line) opens the target file in the working frame and raises it; the agenda frame is never chosen as the display target, so it keeps showing the agenda. In the working frame (the one that launched it, and every other frame) there are no such restrictions — normal behavior throughout.
+
+For the implementer, these pieces compose:
+
+1. A toggle command that spawns, raises, or deletes the marked frame. Spawn is transactional: create the fullscreen frame, run the cached non-forced =(cj/build-org-agenda-list)= (the same prep =cj/main-agenda-display= does at :380-391, so a frame spawned early after daemon startup isn't the base-files-only agenda), build and display the dedicated agenda buffer, install the frame-local routing, and start the timer. If any step fails after =make-frame= succeeds — including a build failure — cancel the timer, delete the partial frame and buffer, leave the original working frame selected, and report =Agenda frame: <operation> failed: <cause>=. Delete removes the frame and stops the timer. The frame carries the =cj/agenda-frame= marker so the command locates it among the daemon's frames.
+
+2. A dedicated seven-day agenda view, defined as its own =org-agenda-custom-commands= key =F= (the existing top-level key at =org-agenda-config.el:344= is =d=, so =F= is collision-free), with =org-agenda-sticky= and =(org-agenda-window-setup 'current-window)= bound in that command's *local* settings. =org-agenda-sticky= local gives Org a distinct =*Org Agenda(F)*= buffer without touching the ordinary =<f8>= behavior; =current-window= keeps the view in the frame's sole window (Org 9.7.11 defaults =org-agenda-window-setup= to =reorganize-frame=, which would split the new frame). The span is anchored to today, not the week: =(org-agenda-span 7)= plus =(org-agenda-start-day "0d")= and =(org-agenda-start-on-weekday nil)= (=org-agenda-list= otherwise anchors any seven-day span to Monday in Org 9.7.11; the daily command uses the same pair at :347-349). No separate TODO block — the span surfaces scheduled work; unscheduled-priority surfacing is a vNext question.
+
+3. A read-only command policy by *default-deny, not enumeration*. The dedicated buffer runs a minor mode =cj/agenda-frame-mode= whose keymap binds =[t]= (the catch-all) to a denial handler, so every *key or mouse* command not on an explicit allowlist is intercepted by one fallthrough — the whole keymap is covered, and a future Org binding is denied by default. The enforcement boundary is keys and mouse, stated honestly: a minor-mode map can't intercept an Agenda-menu item or a direct =M-x=, so the mode also *removes the Agenda menu-bar* in the dedicated buffer (no menu path to a mutation), and direct =M-x org-agenda-…= is explicitly *out of contract* — typing the command name is a deliberate bypass of a read-only surface, like editing a read-only buffer under =inhibit-read-only=. The complete allowlist is: (a) navigation — =org-agenda-next-line=/=-previous-line=, =org-agenda-next-item=/=-previous-item=, the arrow keys, =C-n=/=C-p=, =C-v=/=M-v= scroll, =M-<=/=M->=, and =C-g=; (b) engage/open — =RET=/=TAB= (=org-agenda-switch-to=/=org-agenda-goto=), mouse-2 (=org-agenda-goto-mouse=), =C-c C-o= (=org-agenda-open-link=), each routed to the working frame — the MRU live non-agenda frame at engagement time, or a new normal (non-fullscreen) frame when none exists — never into the agenda frame; and (c) the frame's own controls — =S-<f8>= toggle, =C-M-<f8>= force-rescan, =q=/=Q=/=x= close, and =r= (remapped to the safe-redo wrapper, below). Everything else is denied with a one-line message: source mutations (TODO-state, tags, priority, effort, schedule/deadline, refile, archive, kill, bulk, and any other) and buffer-opening/preview/follow commands that would split or replace the frame (=SPC=/=DEL=, mouse-3, =C-c C-x b=, clock-goto, follow mode) show =Agenda frame is read-only — press RET to edit in your working frame=; and view-changing commands that would break the today-anchored span (=d=/=w=/=y=, =f=/=b=, =j=, =g=) show =Agenda frame is fixed to the 7-day view=. The one exception is =r= (manual redo): it's allowlisted but remapped to the frame's own safe-redo wrapper (the manual version of the timer tick), so a manual refresh takes the same failure-latched, focus-safe path. The dedicated view also binds =org-agenda-start-with-follow-mode= nil locally, so a non-nil global default can't turn follow mode on at buffer creation. Because =org-agenda-redo= rebuilds through =org-agenda-mode=, which runs =kill-all-local-variables=, the mode is re-enabled after every build by =org-agenda-finalize-hook= gated on the frame's =cj/agenda-frame= marker (a frame parameter, which survives the buffer reset) — so the refresh never strips the policy. =q=/=Q=/=x= delete the marked frame and cancel its timer, and closing the frame (by any path) also kills the =*Org Agenda(F)*= sticky buffer, so the next spawn regenerates fresh rather than reusing stale sticky content; killing the dedicated buffer deletes the frame too, so no orphan fullscreen frame survives.
+
+4. A frame-scoped refresh timer: =org-agenda-redo= on the dedicated buffer every 5 minutes, wall-clock aligned, started on spawn and cancelled on delete/kill, with a live-frame/live-buffer guard and duplicate-timer prevention. The callback runs with the dedicated agenda window selected for the redo's dynamic extent, restores the prior selected frame and window afterward, and never calls an input-focus function — so a tick while Craig works in another frame neither errors on an out-of-range =window-start= nor steals focus. Point restoration is deterministic: restore the same-=org-marker= occurrence closest to the old agenda line (one marker can appear twice, e.g. a scheduled and a deadline line); if the marker is gone, clamp the old line number into the rebuilt buffer's range; on a header line move to the first agenda item; an empty view leaves point at buffer start. On a failed redo the timer keeps running and retries on the next scheduled tick — a transient failure shouldn't kill live refresh — and the failure is reported once per consecutive-failure run (naming the next action: =C-M-<f8>= force-rescan or re-spawn), with the next successful tick clearing the failure state. Because =org-agenda-redo= erases the buffer before it rebuilds and =org-agenda-mode= would strip the policy, the callback keeps a *frame-owned last-good snapshot* — the buffer text with its =org-redo-cmd=/=org-lprops= properties, point, window-start, and any active filter/identity — captured from the last successful build, undecorated. Markers get special handling: =org-agenda-reset-markers= nulls the old =org-marker=/=org-hd-marker= objects during a rebuild, so the snapshot must not keep them by reference — it *clones* each agenda marker into a snapshot-owned live marker (=copy-marker= into the unchanged source buffer) and reinstalls the clones on restore, so =RET=/=TAB= still resolve to the right source line; the clones are released when a successful build replaces the snapshot or the frame closes, so repeated failures don't leak markers. On a redo error it restores that snapshot verbatim, *explicitly re-enables =cj/agenda-frame-mode=* (the finalize hook runs only on success, so the error path must reinstate the policy or the frame would be left unrestricted), and shows the "refresh failed (=C-M-<f8>= to force-rescan)" line as an *overlay*, not inserted text — so the Org properties aren't corrupted and consecutive failures don't accumulate headers. The next tick then redoes from the preserved =org-redo-cmd= (a real rebuild, not a no-op off an error header); a success removes the overlay and replaces the snapshot. So the frame is never blank, never unrestricted, and always retryable — =RET= and navigation keep working on the restored snapshot. A thin wrapper suppresses =org-agenda-redo='s routine "Rebuilding..." chatter but lets that one failure message through. =redo= re-reads file contents, not the file list — a new project's todo.org still needs the manual force-rescan.
+
+** Not a prototype-pipeline UI
+
+=ui-prototyping.md='s research → five-prototype → iterate process governs bespoke visual surfaces (panels, multi-control widgets, SVG faceplates). This frame is org-agenda's existing text rendering shown fullscreen; the layout question ("where does it go?") is answered in a sentence (its own fullscreen frame). So the prototype pipeline is intentionally skipped. The open questions here were behavioral (frame vs process, view span, focus, jump routing, auto-open), not visual-layout, and they're settled in the Decisions below.
+
+* Alternatives Considered
+
+** Dedicated fullscreen frame of the daemon (chosen)
+- Good, because it shares the daemon's live state: calendar-sync, open buffers, and edits are already reflected, so "live" costs only a periodic redo.
+- Good, because it's genuinely separate from the working frames — it can sit on its own workspace or monitor and never steals working space.
+- Neutral, because it dies with the daemon (no cross-restart persistence). Acceptable for v1.
+
+** A separate Emacs process
+- Good, because it's fully isolated and survives a daemon restart.
+- Bad, because it doesn't share the daemon's in-memory state — it reads the org files from disk and only reflects what's been saved. The "always current, shares live edits" goal argues against it, so it was rejected in favor of a frame.
+
+** A right-side side-window dock (the prior draft's design)
+- Good, because it lives in the working frame and is protected from =delete-other-windows=.
+- Bad, because it isn't what Craig wants: it shares the working frame rather than standing alone, it's bounded to a dock width that crowds the working area, and it can't move to its own workspace or monitor. Superseded by the fullscreen frame.
+
+** A normal split window
+- Good, because it's the least new machinery.
+- Bad, because it isn't protected, has no clean "this is the agenda surface" identity, and takes over the working frame. This is what the current 0.75 rule already gives and what the frame needs to be different from.
+
+** Global "refresh any visible agenda" timer
+- Good, because it would also refresh the =<f8>= full view when left open.
+- Bad, because it has no clean lifecycle — it has to poll for visible agenda buffers and decide when to stop. The frame-scoped timer starts and stops with the frame, which is simpler and matches the feature's boundary.
+
+* Decisions [13/13]
+
+** DONE Dedicated fullscreen frame as the display mechanism
+- Context: the surface must stand wholly apart from the working frames, be placeable on its own workspace or monitor, and stay live.
+- Decision: We display the agenda in a dedicated frame of the running daemon, created with =make-frame= + =(fullscreen . fullboth)= and a =cj/agenda-frame= marker parameter. A frame (not a separate process) so it shares the daemon's live state.
+- Consequences: easier — live state, no reload, full isolation from working frames. Harder — it dies with the daemon; frame lookup and lifecycle must be explicit.
+
+** DONE Launch/toggle on S-<f8>; force-refresh moves to C-M-<f8>
+- Context: =S-<f8>= currently runs =cj/org-agenda-refresh-files= (shipped 17ae3e2a); Craig wants =S-<f8>= to spawn/raise/close the agenda frame.
+- Decision: =S-<f8>= toggles the agenda frame (spawn if none, raise+select if unfocused, delete if selected). =cj/org-agenda-refresh-files= rebinds to =C-M-<f8>=, keeping both in the F8 family.
+- Consequences: easier — one gesture, next to the other agenda keys. Harder — one existing binding moves; the docstring/family comment needs updating.
+
+** DONE View: the seven-day agenda span alone
+- Context: "the day's work visible" is a composite; how many days, and whether to append a TODO block.
+- Decision: the frame shows a seven-day agenda span (=org-agenda-span= 7) as one =org-agenda-custom-commands= entry, with no separate prioritized TODO block. The span already surfaces scheduled work.
+- Consequences: easier — one custom-command entry, no block composition to tune. Harder — unscheduled priorities aren't surfaced; that's a vNext tuning question if it turns out to matter.
+
+** DONE Focus stays on the agenda inside the frame
+- Context: opening or working the agenda frame should keep point on the agenda, not scatter into other buffers; the launching frame stays unrestricted.
+- Decision: the agenda frame holds a single window on the agenda buffer and point rests there; no focus restrictions apply to any other frame.
+- Consequences: easier — the frame is unambiguous to use. Harder — anything that would open a buffer in the frame must be routed elsewhere (see jump-to-task).
+
+** DONE Jump-to-task opens the file in the working frame
+- Context: engaging a task from a fullscreen agenda must not replace the agenda with the target file.
+- Decision: =RET=/=TAB= on an agenda line opens the target file in the working frame (the most-recently-selected non-agenda frame) and raises it; the agenda frame keeps showing the agenda.
+- Consequences: easier — the agenda frame is genuinely "ready to engage" and stays pure. Harder — needs deliberate =org-agenda-window-setup= / =display-buffer= handling so the agenda frame is never the jump target (the likeliest rabbit hole).
+
+** DONE Frame-scoped refresh via org-agenda-redo, wall-clock aligned
+- Context: the frame must stay current (now-line, synced events) without re-scanning the file list or churning a timer when it's closed.
+- Decision: run =org-agenda-redo= on the agenda buffer every 5 minutes aligned to the :00/:05/:10 mark, started on spawn and cancelled on delete/kill. Not a file-list rebuild — that's the manual force-rescan.
+- Consequences: easier — cheap, self-scoped, picks up content changes. Harder — a brand-new project's todo.org won't appear until a manual force-rescan or the 24h cache TTL lapses.
+
+** DONE No startup auto-open
+- Context: the frame could spawn automatically on daemon start.
+- Decision: no auto-open. The frame is spawned on demand by =S-<f8>=; there is no auto-open defcustom.
+- Consequences: easier — no startup coupling, no small-frame-at-startup edge case, one fewer knob. Harder — none; a standing frame is one keystroke away.
+
+** DONE Not a separate process — a frame of the daemon
+- Context: "its own Emacs" could mean an isolated process or a frame of the running daemon.
+- Decision: a frame of the daemon, for live shared state (calendar-sync, edits, now-line). A separate process would only reflect saved-to-disk state.
+- Consequences: easier — always current, zero duplicated config load. Harder — no cross-restart persistence; the frame is gone after a daemon restart and re-spawned by its key.
+
+** DONE Frame-local exit and buffer-kill semantics
+- Context: =S-<f8>= is the intended close, but Org's =q=/=Q=/=x= and a buffer-kill can otherwise leave the sole-window fullscreen frame alive on an unrelated buffer.
+- Decision: =q=/=Q=/=x= in the agenda frame delete the marked frame and cancel its timer; killing the dedicated agenda buffer deletes the frame too. Ordinary agenda buffers in other frames are unaffected.
+- Consequences: easier — the frame can never strand on a non-agenda buffer. Harder — needs frame-local key remaps and a buffer-kill hook scoped to the dedicated buffer.
+
+** DONE Working-frame target and no-frame fallback
+- Context: "the most-recently-selected non-agenda frame" is the intent; the launch frame may be gone, several may exist, or the agenda frame may be the only live frame.
+- Decision: engage targets the MRU live non-agenda frame at engagement time; when none exists, create a normal (non-fullscreen) frame and open there. The engage action never falls back into the agenda frame.
+- Consequences: easier — a deterministic target in every frame state. Harder — the deleted-launch, multiple-frame, and no-frame cases each need explicit handling and tests.
+
+** DONE Read-only policy by default-deny, not enumeration
+- Context: enumerating =org-agenda-mode-map= command-by-command can't converge (a review always finds a missed key), and =org-agenda-redo='s =kill-all-local-variables= would strip a buffer-local policy on every refresh.
+- Decision: a minor mode =cj/agenda-frame-mode= with a =[t]= catch-all denial handler shadows =org-agenda-mode-map= — only an enumerated allowlist (navigation, the engage/open keys routed to the working frame, and the frame's own controls =S-<f8>=/=C-M-<f8>=/=q=/=Q=/=x=/=r=) is permitted; every other key/mouse command shows a read-only or fixed-view message. The enforcement boundary is keys and mouse: the mode removes the Agenda menu-bar in the buffer, and direct =M-x= is out of contract. The mode is re-enabled after each build by =org-agenda-finalize-hook= gated on the frame's =cj/agenda-frame= marker (and by the failed-redo error path), so redo can't strip it.
+- Consequences: easier — one rule covers the whole keymap and survives refresh, with nothing to enumerate or keep in sync. Harder — the allowlist and the two message classes (read-only vs fixed-view) need explicit definition and tests.
+
+** DONE Sticky buffer and failed-redo display lifecycle
+- Context: closing the frame left the command-local sticky =*Org Agenda(F)*= buffer alive (a reopen would reuse stale content), and =org-agenda-redo= erases the buffer before rebuilding (a failed redo would show blank/partial).
+- Decision: closing the frame by any path also kills the =*Org Agenda(F)*= buffer, so the next spawn regenerates fresh. The refresh keeps a frame-owned last-good snapshot (buffer text with =org-redo-cmd=/=org-lprops=, point, window-start, filter/identity — undecorated; agenda markers *cloned* via =copy-marker= since =org-agenda-reset-markers= nulls the originals on rebuild); a failed redo restores it verbatim, re-enables the policy explicitly, and shows the failure as an overlay; the next tick rebuilds from the preserved =org-redo-cmd=. The clones are released on the next success or on close. The frame is never blank, unrestricted, or non-retryable.
+- Consequences: easier — reopen is always current and a failed refresh degrades to stale-but-readable. Harder — the close paths must kill the sticky buffer, and the callback carries a pre-redo snapshot.
+
+** DONE Refresh-failure recovery: retry, report once
+- Context: a failed =org-agenda-redo= tick needs a defined recovery, not "cancel or retry".
+- Decision: keep the timer running and retry on the next scheduled tick; report the failure once per consecutive-failure run, naming the next action (=C-M-<f8>= force-rescan or re-spawn); the next successful tick clears the failure state.
+- Consequences: easier — a transient failure doesn't kill live refresh, and the user isn't spammed. Harder — needs a small failure-state latch across ticks.
+
+* Review findings [28/28]
+
+** DONE Frame view needs its own custom-command entry (avoid the span-8 collision)
+=modules/org-agenda-config.el:344-348= already sets =org-agenda-custom-commands= with a =(org-agenda-span 8)= entry. The frame view must be its own custom-command key rather than reusing or editing the existing span-8 entry, and =org-agenda-sticky= is bound in that command's local settings, not globally, so Org derives the dedicated =*Org Agenda(KEY)*= buffer without changing the ordinary =<f8>= behavior.
+Response (accept): folded into Design piece 2.
+
+** DONE Seven-day view is week-anchored, not next-seven-days
+=org-agenda-span= 7 alone anchors to Monday in Org 9.7.11 (=org-agenda-start-on-weekday= defaults to 1). The frame command adds =(org-agenda-start-day "0d")= and =(org-agenda-start-on-weekday nil)= (the same pair the daily command uses at :347-349) so the range is today..today+6.
+Response (accept): folded into Design piece 2; acceptance criterion added (midweek starts today, ends six days later).
+
+** DONE Agenda exit paths can abandon the dedicated frame
+=q=/=Q=/=x= and a buffer-kill could otherwise leave the sole-window fullscreen frame alive on an unrelated buffer.
+Response (accept): added a Decision (frame-local exit + buffer-kill semantics), folded into Design piece 3, and added two acceptance criteria.
+
+** DONE Working-frame target and fallback are undefined
+The launch frame may be gone, several working frames may exist, or the agenda frame may be the only live frame.
+Response (accept, with a chosen behavior): target the MRU live non-agenda frame at engagement time; when none exists, create a normal (non-fullscreen) frame and open there (chose Codex's create-a-frame option over a user-facing refusal, so engage always succeeds). Added a Decision, folded into Design piece 3, and added the three frame-state acceptance cases.
+
+** DONE Phase 1 exposed a frame that violates v1 invariants
+The former Phase 1 exposed a fullscreen frame before the sticky buffer and jump routing existed, which is a broken intermediate state.
+Response (accept, with a chosen restructure): merged the former Phase 1+2 into a single Phase 1 that ships the dedicated sticky view plus full frame-local navigation/exit routing, so the first user-reachable state is fully isolated. Phases are now 2, not 3.
+
+** DONE Refresh callback lacks a window-and-focus contract
+=org-agenda-redo= reads selected-window state (=window-start=), so a bare =with-current-buffer= tick from another frame can miscalculate or error.
+Response (accept): folded into Design piece 4 (select the agenda window for the redo's extent, restore the prior frame/window, no input-focus calls) with an acceptance criterion.
+
+** DONE Frame and timer test surface is understated
+The repo already tests these boundaries (=tests/test-dirvish-config-popup.el= mocks frame lookup/focus/delete; =tests/test-ai-term--project-color.el= drives timer callbacks + a dead-buffer case), so "not cleanly unit-testable" was wrong.
+Response (accept): rewrote the Dev-tooling readiness dimension to require ERT coverage across frame lookup, spawn/raise/close, keybindings, sticky-buffer identity, the today-anchored settings, RET/TAB/exit routing, the MRU + no-frame fallback, timer alignment, duplicate-timer prevention, every cancellation path, dead frame/buffer guards, and silent-success/visible-failure behavior, with a live-daemon checklist only for compositor fullscreen/focus.
+
+** DONE Refresh point-preservation semantics are ambiguous
+=org-agenda-redo= preserves a line number, not the same item when lines shift.
+Response (accept): folded into Design piece 4 (restore the same =org-marker= when it exists, else clamp to the nearest valid line); Phase 2 carries it.
+
+** DONE Silent timer conflicts with org-agenda-redo messages
+=org-agenda-redo= emits "Rebuilding..." chatter on every run.
+Response (accept): folded into Design piece 4 (a wrapper suppresses routine success chatter, surfaces a failure once, and cancels/retries the stale timer); Observability updated; acceptance criterion added.
+
+** DONE Spawn failure cleanup and error message are undefined
+A failure after =make-frame= could leave an orphan frame and partial state.
+Response (accept): made spawn transactional in Design piece 1 (on failure: cancel the timer, delete the partial frame/buffer, restore the working frame, report =Agenda frame: <operation> failed: <cause>=); Errors dimension and an acceptance criterion added.
+
+** DONE Dedicated view identity and window setup remain placeholders
+The custom-command key and =org-agenda-window-setup= value are observable behavior, and the Org 9.7.11 default (=reorganize-frame=) would split the frame.
+Response (accept): chose key =F= (existing top-level key is =d=) and =(org-agenda-window-setup 'current-window)=, both in the command's local settings; folded into Design piece 2 with an acceptance criterion.
+
+** DONE Initial agenda-file preparation is undefined
+Spawning early after startup could show the base-files-only agenda, since the project-file list is built by an idle timer and =cj/main-agenda-display= calls =cj/build-org-agenda-list= first (:380-391).
+Response (accept): spawn runs the cached non-forced =(cj/build-org-agenda-list)= before rendering, and a build failure is part of the transactional spawn (Design piece 1); early-start acceptance criterion added.
+
+** DONE Read-and-engage command surface conflicts with Org's mutation keys
+=org-agenda-mode-map= exposes source-mutating keys the "read + engage only" non-goal doesn't want.
+Response (accept, resolved from the existing non-goal): source-mutating keys (TODO-state, schedule/deadline, refile, archive, kill, bulk) are remapped to a read-only message; engage (=RET=) reaches the file to edit it. Added a Decision (read-only command policy), folded into Design piece 3, acceptance criterion added.
+
+** DONE Source-opening routes beyond RET and TAB are unaccounted for
+=SPC=/=DEL= preview, mouse-2, =C-c C-o=, and follow mode can also show source or split the frame.
+Response (accept): defined the full class — =RET=/=TAB=/mouse-2/=C-c C-o= route to the working frame; =SPC=/=DEL= and follow mode are disabled in the dedicated buffer. Folded into Design piece 3 (and the read-only-policy Decision); invariant acceptance criterion added.
+
+** DONE Refresh failure recovery still contains an unresolved branch
+"Cancels or safely retries" is two different user-visible outcomes.
+Response (accept, chose retry): the timer keeps running and retries next tick; the failure is reported once per consecutive-failure run (naming =C-M-<f8>= / re-spawn), and the next success clears the state. Added a Decision (refresh-failure recovery), folded into Design piece 4, acceptance criterion added.
+
+** DONE Phase 1 exposes a non-live version of a live feature
+The former Phase 1 bound =S-<f8>= before the timer (the "live" contract) existed, shipping a static intermediate.
+Response (accept, chose defer-the-binding): Phase 1 now builds the command unbound (reachable only from ERT/=M-x=); Phase 2 adds the timer and only then binds =S-<f8>= and moves the force-rescan. No user-reachable intermediate is non-live. Phases and an acceptance criterion updated.
+
+** DONE Refresh point restoration lacks duplicate and fallback rules
+A source marker can occur twice, and "nearest" was undefined.
+Response (accept): restore the same-marker occurrence closest to the old agenda line; if the marker is gone, clamp the old line number into range; header line → first item; empty view → buffer start. Folded into Design piece 4, acceptance criterion added.
+
+** DONE Phase 1 is still user-reachable through M-x
+Phase 1's body made the toggle an interactive command, so =M-x= could still open the non-live intermediate.
+Response (accept): Phase 1 now builds only non-interactive helpers (=cj/--agenda-frame-*=); no =interactive= command exists until Phase 2, so it's reachable only from ERT. Phases + acceptance criterion updated.
+
+** DONE Read-only mutation policy is not exhaustive
+Enumerating blocked mutations misses many keys (tags, priority, effort, clock, capture, =C-c C-c=, the menu).
+Response (accept, resolved structurally): replaced the denylist with a *default-deny* policy — the =cj/agenda-frame-mode= keymap shadows =org-agenda-mode-map= and permits only a small allowlist; every unlisted command (all mutations, present and future) is denied with the read-only message. Nothing to enumerate. Design piece 3 + the Decision rewritten.
+
+** DONE Frame-isolation policy omits buffer-opening commands and initial follow state
+mouse-3, =C-c C-x b=, clock-goto, calendar, and non-nil =org-agenda-start-with-follow-mode= could still split/replace the frame.
+Response (accept): the same default-deny allowlist covers every buffer-opening command (denied unless allowlisted); the dedicated view binds =org-agenda-start-with-follow-mode= nil locally. Folded into Design piece 3.
+
+** DONE View-changing and manual-redo commands have no dedicated-frame policy
+=d=/=w=/=y=, =f=/=b=, =j=, =r=, =g= had no policy against the fixed today-anchored view.
+Response (accept, chose fixed view): the seven-day view is fixed — view-changers are denied with a =fixed to the 7-day view= message; =r= is remapped to the frame's safe-redo wrapper (manual version of the timer tick) so a manual refresh uses the failure-latched path; =g= is denied (it targets other buffers). Design piece 3 + acceptance criterion.
+
+** DONE Org redo discards dedicated buffer-local policy and state
+=org-agenda-redo= → =org-agenda-mode= runs =kill-all-local-variables=, stripping a buffer-local map and any buffer-local timer/failure state.
+Response (accept): =cj/agenda-frame-mode= is re-enabled after every build by =org-agenda-finalize-hook= gated on the frame's =cj/agenda-frame= marker (a frame parameter, which survives the reset), and the timer/failure state is frame-owned, not buffer-local — so redo can't strip either. Design piece 3 (policy) + Decision; acceptance criterion for policy-survives-redo.
+
+** DONE Sticky close and reopen freshness is undefined
+Closing left the command-local sticky =*Org Agenda(F)*= buffer alive, so a reopen reused stale content.
+Response (accept, chose kill-on-close): every close path also kills the sticky buffer, so the next spawn regenerates fresh. Added to Design piece 3 and the buffer-lifecycle Decision; close→change→reopen acceptance criterion.
+
+** DONE Failed-redo display state is undefined
+=org-agenda-redo= erases before rebuilding, so a failure could leave the frame blank/partial.
+Response (accept, chose last-known-good): the callback snapshots the last-good buffer before redoing and, on error, restores it with a "refresh failed" header; the frame is never blank/half-built, and the next success replaces it. Design piece 4 + the buffer-lifecycle Decision; acceptance criterion.
+
+** DONE Required ERT surface was not synchronized with the new contracts
+The Dev-tooling dimension still listed the older surface.
+Response (accept): expanded it to require ERT for the cached-build early-start, the default-deny policy (denied + allowed keys, both message classes), the policy surviving a redo, sticky kill-on-close / fresh reopen, the failed-redo snapshot restore, the deterministic marker cases, and the Phase-1 no-interactive-entry boundary; only compositor fullscreen/focus stays on the live-daemon checklist.
+
+** DONE Default-deny does not yet define or enforce the actual interactive surface
+The allowlist wasn't concrete (lifecycle keys, =C-g=, scrolling absent), and a minor-mode map can't intercept the Agenda menu or a direct =M-x=, so "every command is intercepted" overclaimed.
+Response (accept): enumerated the complete allowlist in Design piece 3 (navigation with concrete commands + =C-g=, engage/open routed to the working frame, and the frame's own controls =S-<f8>=/=C-M-<f8>=/=q=/=Q=/=x=/=r=), and chose the honest enforcement boundary — keys/mouse via the =[t]= catch-all, the Agenda menu-bar removed in the buffer, and direct =M-x= explicitly out of contract. Decision + acceptance updated.
+
+** DONE Failed-redo rollback is not yet a complete retryable agenda state
+The error path didn't re-enable the policy (finalize runs only on success), and the snapshot didn't promise Org properties / point/window/filter / an undecorated copy — so =RET= and the next rebuild weren't guaranteed.
+Response (accept): defined a frame-owned last-good snapshot (text + =org-redo-cmd=/=org-lprops=/markers + point/window/filter, undecorated); the error path restores it verbatim, explicitly re-enables =cj/agenda-frame-mode=, and shows the failure as an *overlay* (no header accumulation, no property corruption); the next tick rebuilds from the preserved =org-redo-cmd=. Design piece 4 + the buffer-lifecycle Decision; acceptance for failure-before-finalize, failure→failure, and failure→retry updated.
+
+** DONE Failed-redo snapshots retain markers only by shallow reference
+=org-agenda-reset-markers= nulls the old =org-marker=/=org-hd-marker= objects during rebuild, so a text-property copy holds dead markers and =RET=/=TAB= break after a failed restore.
+Response (accept): the snapshot *clones* each agenda marker into a snapshot-owned live marker (=copy-marker= into the unchanged source buffer) and reinstalls the clones on restore, so engage/navigation resolve to the right source line; the clones are released on the next successful build or on frame close, so repeated failures don't leak markers. Design piece 4 + the buffer-lifecycle Decision; ERT covers fail-after-reset → restored-marker targets → repeat → success → clones released.
+
+* Implementation phases
+
+** Phase 1 — Frame, view, and routing (private helpers, not user-reachable)
+Build the mechanism as *non-interactive* helpers (=cj/--agenda-frame-spawn/-raise/-delete/-toggle=, =cj/agenda-frame-mode=): transactional spawn (cached =(cj/build-org-agenda-list)=, fullscreen =make-frame= + =fullboth= + the =cj/agenda-frame= marker, the dedicated =F= today-anchored seven-day view with =(org-agenda-window-setup 'current-window)= + local sticky), the default-deny read-only policy + source-opening routing to the working frame (MRU live non-agenda frame, or a new normal frame when none exists), and =q=/=Q=/=x=/buffer-kill close that also kills the sticky buffer. No =interactive= command exists yet, so the mechanism is reachable only from ERT — not =M-x=, not a key. Do NOT bind =S-<f8>= and do NOT move the force-rescan. Clean stop: the frame, view, and policy are correct and tested; nothing user-visible changed.
+
+** Phase 2 — Refresh timer + public command
+Add the frame-scoped 5-minute wall-clock =org-agenda-redo= timer (window/focus contract, duplicate-timer prevention, dead frame/buffer guard, deterministic point restoration, retry-and-report-once failure recovery with the pre-redo snapshot restore, message-suppressing wrapper). Then wrap =cj/--agenda-frame-toggle= in the public interactive =cj/agenda-frame-toggle=, bind it to =S-<f8>=, and move =cj/org-agenda-refresh-files= to =C-M-<f8>=. The public gesture appears only when the feature is complete and live — the first user-reachable state is the finished feature.
+
+* Acceptance criteria
+- [ ] =S-<f8>= spawns a fullscreen agenda frame; pressing it from that frame closes it; pressing it from a working frame raises it.
+- [ ] =C-M-<f8>= runs the force-rescan (=cj/org-agenda-refresh-files=); =S-<f8>= no longer does.
+- [ ] The agenda frame shows a seven-day span and keeps focus on the agenda.
+- [ ] Opening a file from an agenda line (=RET=) shows the file in the working frame and raises it; the agenda frame remains on the agenda.
+- [ ] A midweek invocation starts today and ends six days later (not Monday-anchored).
+- [ ] =q=, =Q=, and =x= in the agenda frame close the frame and cancel its timer; ordinary =<f8>= agenda buffers are unaffected.
+- [ ] Killing the dedicated agenda buffer deletes the frame (no orphan fullscreen frame).
+- [ ] =RET= with the launch frame deleted, with multiple working frames, and with no working frame each opens the file outside the agenda frame (creating a normal frame in the last case).
+- [ ] The frame refreshes on the wall-clock 5-minute mark while open, and no timer runs after it's closed.
+- [ ] A refresh tick while another frame is active neither errors nor changes focus.
+- [ ] The refresh timer is silent on success and surfaces a failure once with an actionable message.
+- [ ] A spawn failure after =make-frame= leaves no orphan frame or timer and reports the failure.
+- [ ] Deleting the frame (or killing its buffer) cancels the timer without erroring.
+- [ ] The view uses key =F= with =current-window= — the frame stays a single window (no split).
+- [ ] A frame spawned right after daemon startup shows the full project agenda (the cached build ran), not the base-files-only view.
+- [ ] A source-mutating key (TODO-state, schedule, refile, archive, kill) in the frame shows the read-only message and edits nothing; =SPC=/=DEL= and follow mode don't split the frame; mouse-2 and =C-c C-o= open in the working frame.
+- [ ] A failed refresh keeps the timer, reports once per consecutive-failure run, and the next successful tick clears the failure state.
+- [ ] Point restoration: a duplicate source marker restores the occurrence nearest the old line; a missing marker clamps the old line into range; header-line and empty-view cases are handled.
+- [ ] A denied command in the frame (a mutation, a view-changer like =w=/=d=, a splitting preview) shows the right read-only/fixed-view message and does not act; an allowlisted command (navigation, =RET= engage) works.
+- [ ] After a refresh tick (=org-agenda-redo=), a denied command is still blocked — the policy survives =kill-all-local-variables=.
+- [ ] Closing the frame kills the =*Org Agenda(F)*= sticky buffer; the next spawn shows fresh content, not the stale sticky buffer.
+- [ ] A redo error restores the last-good buffer with a "refresh failed" header; the frame is never blank or half-built.
+- [ ] The Agenda menu-bar is absent in the frame; every allowlisted key (=C-g=, scroll, navigation, the lifecycle keys) works and every other key shows the message.
+- [ ] After a failed redo the policy is still active (a denied key is still blocked) and =RET=/navigation still work on the restored snapshot with point/filter preserved.
+- [ ] Failure → failure shows one overlay and one message (no header/overlay accumulation); failure → success removes the overlay and rebuilds from =org-redo-cmd=.
+- [ ] After a failed redo the restored snapshot's cloned markers point at the right source lines (=RET=/=TAB= resolve correctly); on the next success or on close the clones are released (no marker leak across repeated failures).
+- [ ] Phase 1 exposes no interactive command — the mechanism is reachable only from ERT (no =M-x=, no key); =S-<f8>= and the =C-M-<f8>= move appear only in Phase 2.
+- [ ] The normal =<f8>= full-view agenda is unchanged.
+
+* Readiness dimensions
+- Data model & ownership: N/A — the frame reads existing agenda files; it authors nothing. Refresh is display-only.
+- Errors, empty states & failure: an empty agenda renders as org's normal empty agenda in the frame. A frame/buffer killed out from under the timer must cancel the timer rather than error on the next tick — a live-frame/live-buffer guard in the callback. A spawn that fails after =make-frame= is transactional: cancel the timer, delete the partial frame/buffer, restore the working frame, and report =Agenda frame: <operation> failed: <cause>=.
+- Security & privacy: N/A — no credentials, no new data surface.
+- Observability: the frame is its own visible state; the refresh wrapper is quiet on success and surfaces a failure once with an actionable message.
+- Performance & scale: =redo= is cheap and already the manual-refresh path; a 5-minute cadence on one buffer is negligible. The file-list scan (the expensive path) is deliberately not on the tick.
+- Reuse & lost opportunities: reuses =make-frame=, =org-agenda-custom-commands=, =org-agenda-sticky=, =org-agenda-redo=, and the F8 family. No new rendering.
+- Architecture fit & weak points: integrates at =make-frame= + a frame parameter, the agenda display call, and the F8 keymap. Weak point: jump-to-task display routing across frames; timer lifecycle tied to frame deletion is the other watch point.
+- Config surface: the seven-day custom-command view is the one implicit knob. No width, no auto-open flag (both removed by the redesign).
+- Documentation plan: an entry in the keybinding/agenda notes; docstrings on the toggle command. No user-facing README beyond that.
+- Dev tooling: existing =make test= / byte-compile / live-reload. ERT covers frame lookup, spawn/raise/close, the =S-<f8>=/=C-M-<f8>= rebind, distinct sticky-buffer identity, the today-anchored seven-day settings, the cached-build early-start, the =RET=/=TAB=/mouse/=C-c C-o= engage routing and the MRU + no-frame fallback, the default-deny policy (the full allowlist works including =C-g=/scroll/lifecycle keys, representative denied keys show the right message, the Agenda menu-bar is absent), the policy surviving an =org-agenda-redo= (re-enabled by the finalize hook) and being re-enabled on the failed-redo error path, sticky-buffer kill on close and a fresh reopen, the failed-redo snapshot restore (policy active, =RET=/nav work, point/filter preserved, one overlay across consecutive failures, rebuild-from-=org-redo-cmd= on retry), timer alignment, duplicate-timer prevention, every cancellation path, dead frame/buffer guards, and silent-success/visible-failure behavior — the repo already tests these boundaries (=tests/test-dirvish-config-popup.el= mocks frame lookup/focus/delete; =tests/test-ai-term--project-color.el= drives timer callbacks + a dead-buffer case). One live-daemon checklist covers compositor fullscreen/focus; manual verification supplements, not replaces, the ERT surface.
+- Rollout, compatibility & rollback: additive. The one compatibility touch is the =S-<f8>= rebind (force-refresh moves to =C-M-<f8>=). Rollback is removing the command and the binding; nothing persisted changes.
+- External APIs & deps: N/A — all built-in Emacs/org.
+
+* Risks, Rabbit Holes, and Drawbacks
+- Jump-to-task display routing across frames is the likeliest rabbit hole: getting =RET= to reliably open in the working frame (not the agenda frame) across =org-agenda-window-setup= values and single-frame states can take fiddling. Dodge: pin the agenda buffer's window-setup and target the file's =display-buffer= at a non-agenda frame explicitly.
+- Frame lifecycle: closing or killing the frame must cancel the timer; a killed frame must not error on tick. Dodge: guard the callback and hang cancellation off =delete-frame-functions= / buffer-kill.
+- Daemon-only lifetime: the frame is gone after a daemon restart. Accepted (re-spawn by key); noted so it isn't mistaken for a bug.
+
+* Review and iteration history
+** 2026-07-20 Mon @ 13:26:53 -0500 — Claude Code (emacs-d) — responder
+- What: dispositioned Codex's sixth-pass marker finding — the snapshot now clones agenda markers (=copy-marker=) instead of holding them by reference, since =org-agenda-reset-markers= nulls the originals on rebuild; restore reinstalls the clones (so =RET=/=TAB= resolve), and they're released on the next success or on close. Added the vNext note (Hyprland-managed window with its own keybind, once the in-Emacs frame proves out). All 28 findings and 13 decisions resolved. Craig accepted the spec for build, so flipped =DRAFT= → =READY=.
+- Why: the marker fix is a real correctness point (a text-property copy holds dead markers). With it folded, the spec is behavior-complete; Craig's call is to build v1 rather than run further review rounds.
+- Artifacts: all Review findings DONE [28/28]; Decisions [13/13]; status =READY=, mirror =ready=. Six review/response rounds total; the default-deny structural pivot (12:26) was the turning point.
+** 2026-07-20 Mon @ 13:07:58 -0500 — Codex (emacs-d) — reviewer
+- What: re-ran the full readiness gate after all 27 prior findings were dispositioned. Verdict: Not ready. Added one blocking finding: a failed-redo snapshot must clone or reconstruct live agenda markers rather than retain shallow text-property references.
+- Why: Org 9.7.11 calls =org-agenda-reset-markers= during regeneration, moving the old =org-marker=/=org-hd-marker= objects to nil. Emacs text-property snapshots retain those same objects, so the promised verbatim restore cannot support =RET=/=TAB= after a failed rebuild without an explicit marker-copy and cleanup contract.
+- Artifacts: this spec's =Review findings [27/28]=. Source checks: =org-agenda-redo=, =org-agenda-prepare=, =org-agenda-reset-markers=, =org-agenda-new-marker=, and =org-agenda-goto= in installed Emacs 30.2 / Org 9.7.11; a batch check confirmed the snapshot property and source property hold the same marker object and both become dead after reset.
+** 2026-07-20 Mon @ 12:59:00 -0500 — Claude Code (emacs-d) — responder
+- What: dispositioned both of Codex's fifth-pass findings. (1) Enumerated the complete default-deny allowlist (navigation commands + =C-g= + scroll, engage/open routed out, and the frame's own controls =S-<f8>=/=C-M-<f8>=/=q=/=Q=/=x=/=r=) and set the honest enforcement boundary — keys/mouse via the =[t]= catch-all, the Agenda menu-bar removed in the buffer, direct =M-x= out of contract (dropped the overclaim that "every command" is intercepted). (2) Made the failed-redo rollback a complete retryable state: a frame-owned last-good snapshot carrying Org properties (=org-redo-cmd=/=org-lprops=/markers), point/window/filter, undecorated; the error path restores it and *explicitly re-enables the policy* (finalize only runs on success), shows the failure as an *overlay* (no accumulation, no property corruption), and the next tick rebuilds from the preserved =org-redo-cmd=.
+- Why: Codex accepted the default-deny structure but flagged the two absolute claims it hadn't yet backed — the map can't cover the menu/M-x, and a text-only snapshot doesn't guarantee a re-enabled, retryable agenda. Both are now bounded by an explicit contract.
+- Artifacts: all Review findings DONE [27/27]; Decisions [13/13] (the read-only and buffer-lifecycle Decisions were tightened, not added). Status stays DRAFT pending Codex's re-review.
+** 2026-07-20 Mon @ 12:47:44 -0500 — Codex (emacs-d) — reviewer
+- What: verified the renamed spec and all 25 resolved findings against installed Org's key dispatch, finalize, redo, and text-property behavior. Verdict: Not ready. Added two blocking findings: the default-deny map's exact/enforced interaction boundary and the failed-redo snapshot's policy-enabled, metadata-preserving retry contract.
+- Why: the new structural design closes the earlier enumeration and sticky-lifecycle problems, but its remaining absolute claims exceed what a minor-mode map and a text-only rollback guarantee. Both gaps would make the implementer choose observable behavior and could leave the frame unrestricted or permanently stale after a failed refresh.
+- Artifacts: this spec's =Review findings [25/27]=. Source checks: =org-agenda-mode-map=, =org-agenda-mode=, =org-agenda-finalize=, =org-agenda-redo=, and the =org-redo-cmd=/=org-lprops= text-property flow in installed Emacs 30.2 / Org 9.7.11. Filename/location precondition passes after the tracked rename.
+** 2026-07-20 Mon @ 12:26:29 -0500 — Claude Code (emacs-d) — responder
+- What: dispositioned all 8 of Codex's fourth-pass findings — accepted every one. The key change is structural: replaced the command-by-command enumeration (which the prior two rounds proved can't converge against a ~100-entry keymap) with a *default-deny* policy — a =cj/agenda-frame-mode= minor mode shadows =org-agenda-mode-map= and permits only a small allowlist, so all mutations / buffer-openers / view-changers are denied in one rule (closes findings 2, 3, 4). Re-enabled via =org-agenda-finalize-hook= keyed on the frame marker so =org-agenda-redo='s =kill-all-local-variables= can't strip it, with timer/failure state frame-owned (finding 5). Close kills the sticky buffer (finding 6); failed redo restores a pre-redo snapshot (finding 7); Phase 1 is non-interactive helpers only (finding 1); the ERT surface is synced (finding 8). Fixed the 7-day view, remapped =r= to the safe wrapper, bound follow-mode nil locally. Rewrote the read-only Decision to default-deny and added one (buffer/failed-redo lifecycle) — 13 Decisions.
+- Why: the fourth review showed the earlier responses were enumerating a keymap instead of stating a policy — the root cause of the round-over-round regress. Default-deny covers the whole map in one rule and terminates.
+- Artifacts: all Review findings DONE [25/25]; Decisions [13/13]. Status stays DRAFT pending Codex's re-review.
+** 2026-07-20 Mon @ 12:11:31 -0500 — Codex (emacs-d) — reviewer
+- What: verified all 17 prior findings against the current repo and installed Org, then traced every agenda command/lifecycle path through regeneration and sticky reuse. Verdict: Not ready. Added eight blocking findings covering the still-interactive Phase 1, incomplete mutation/display/view/manual-redo policies, loss of dedicated local state on redo, stale sticky reopen, failed-redo display state, and the unsynchronized ERT matrix.
+- Why: the chosen values from the prior response are viable, but implementation would still require behavior and state-ownership decisions. In particular, Org's full keymap is broader than the enumerated commands, =org-agenda-mode= resets the local map during every redo, and command-local sticky agendas reuse a closed buffer without regenerating it.
+- Artifacts: this spec's =Review findings [17/25]=. Source checks: =org-agenda-mode-map=, =org-agenda-mode=, =org-agenda-prepare=, =org-agenda-use-sticky-p=, =org-agenda-redo=, =org-agenda-goto-calendar=, =org-agenda-tree-to-indirect-buffer=, and =org-agenda-clock-goto= in installed Emacs 30.2 / Org 9.7.11; current hooks and display rules in =modules/org-agenda-config.el=.
+** 2026-07-20 Mon @ 12:02:00 -0500 — Claude Code (emacs-d) — responder
+- What: dispositioned all 7 of Codex's third-pass findings — accepted every one; four carried a chosen behavior. View identity: key =F= + =(org-agenda-window-setup 'current-window)=. Command policy: source-mutating keys read-only-remapped, all source-opening keys routed to the working frame, preview/follow disabled. Refresh failure: retry + report-once-per-run. Phase safety: Phase 1 builds the command unbound, Phase 2 adds the timer and the =S-<f8>= binding. Also pinned the cached-build-on-spawn and the point-restoration tie-break. Added three Decisions (read-only policy, refresh-failure recovery, plus the earlier two = now 12), and expanded acceptance criteria.
+- Why: Craig's strict gate — no behavior decision deferred to implementation. Codex's third pass named seven still-open keymap/lifecycle choices against the installed Org 9.7.11; each is now pinned with a concrete value.
+- Artifacts: all Review findings DONE [17/17]; Decisions [12/12]. The read-only mutation policy was resolved from the existing "read + engage only" non-goal, not a fresh product call — flip it if in-agenda mutation was actually wanted. Status stays DRAFT pending Codex's re-review.
+** 2026-07-20 Mon @ 11:29:33 -0500 — Codex (emacs-d) — reviewer
+- What: re-ran the authoritative code-grounded review after all 10 earlier findings were dispositioned. Verdict: Not ready. Added seven blocking findings covering the exact dedicated-command identity/window setup, initial agenda-file preparation, read-versus-mutate command policy, source-opening commands beyond =RET=/=TAB=, refresh-failure recovery, safe phase exposure, and deterministic point restoration.
+- Why: Craig set the stricter gate that no behavior decision may be deferred to implementation. The revised spec closes the earlier findings, but still contains explicit alternatives and conflicts with the installed Org keymap and this repo's deferred agenda-file initialization.
+- Artifacts: this spec's =Review findings [10/17]=. Source checks: =modules/org-agenda-config.el:186-220,344-391,431-433= and =org-agenda-mode-map=, =org-agenda-prepare-window=, =org-agenda-goto=, =org-agenda-open-link=, and =org-agenda-show-and-scroll-up= in installed Emacs 30.2 / Org 9.7.11.
+** 2026-07-20 Mon @ 10:22:00 -0500 — Claude Code (emacs-d) — responder
+- What: dispositioned all 10 of Codex's findings — accepted every one, two with a chosen behavior. Working-frame fallback: the MRU live non-agenda frame, creating a normal frame when none exists (Codex's create-a-frame option over a refusal). Phase safety: merged the former Phase 1+2 into one isolated Phase 1 so no broken intermediate state ships. Folded the resolutions into Design (transactional spawn, today-anchored sticky seven-day view, frame-local navigation/exit routing, refresh window/focus contract + message wrapper + org-marker restore), added two Decisions (exit semantics, working-frame fallback), cut to 2 phases, and expanded the acceptance criteria and the Dev-tooling test surface.
+- Why: Codex's code-grounded re-review found six blocking gaps the first (my) review missed — week anchoring, exit lifecycle, working-frame fallback, phase safety, refresh window context, and the understated test surface. Each needed the spec to specify behavior it had left implicit.
+- Artifacts: all Review findings DONE [10/10]; Decisions [10/10]. Status stays DRAFT pending a re-review to confirm the responses close the blockers.
+** 2026-07-20 Mon @ 10:12:30 -0500 — Codex (emacs-d) — reviewer
+- What: independently re-ran the full spec-review against =modules/org-agenda-config.el=, its ERT coverage, relevant frame/timer precedent, and the installed Emacs 30.2 / Org 9.7.11 source. Verdict: Not ready. Added six blocking and three non-blocking findings; retained the prior non-blocking custom-command finding.
+- Why: the first review checked the local F8 premises but did not trace Org's seven-day anchoring, sticky exit behavior, cross-frame jump boundaries, selected-window dependency during redo, phase-by-phase safety, or the repository's existing frame/timer test patterns. Those gaps would force implementation decisions or ship broken intermediate behavior.
+- Artifacts: this spec's =Review findings [0/10]=; lifecycle demoted =READY= → =DRAFT= and Metadata mirrored to =draft=. Source checks: =org-agenda-list=, =org-agenda-quit=, =org-agenda-redo=, =org-agenda-switch-to=, and =org-agenda-goto= in Org 9.7.11.
+** 2026-07-20 Mon @ 09:53:02 -0500 — Claude Code (emacs-d) — reviewer
+- What: spec-review of the redesigned spec. Verdict READY. Read =modules/org-agenda-config.el= first — confirmed =cj/org-agenda-refresh-files= is bound to =S-<f8>= (:232), the =cj/--org-agenda-display-rule= / =cj/org-agenda-window-height= 0.75 rule (:27-37), and =<f8>= = =cj/main-agenda-display= (:392), so the rebind and override premises hold. All 8 decisions resolved (cookie complete); phases decompose cleanly into 3 sessions; no blocking finding. One non-blocking finding recorded: the frame's seven-day view needs its own custom-command entry (the code already has a span-8 entry).
+- Why: gate the DRAFT → READY transition before decomposing the build. The reworked design has no unverified API assumptions (all built-in Emacs/org) and no data/security surface.
+- Artifacts: this spec's Review findings [0/1]; flipped status heading DRAFT → READY and the Metadata mirror.
+** 2026-07-20 Mon @ 09:45:00 -0500 — Craig Jennings — redesign
+- What: replaced the right-side side-window dock with a dedicated fullscreen frame of the daemon. Resolved all eight decisions: fullscreen frame (not side window, not separate process), =S-<f8>= toggle, seven-day span, focus-on-agenda, jump-to-task opens in the working frame, frame-scoped 5-min redo, no startup auto-open.
+- Why: Craig wants a standing, fullscreen agenda surface placeable on its own workspace/monitor, sharing the daemon's live state, rather than a dock bounded to the working frame.
+- Artifacts: this spec; the dock-mode task in todo.org (Emacs Open Work).
+** 2026-07-17 Fri @ 19:34:07 -0500 — Craig Jennings — author
+- What: initial draft (right-side side-window dock).
+- Why: dock mode had four-plus open design questions and real trade-offs; settling them on paper before code.
+- Artifacts: dock-mode task in todo.org (Emacs Open Work); supersedes the folded "auto-refresh every 5 min" roam item.
diff --git a/docs/specs/ai-kb-spec.org b/docs/specs/ai-kb-spec.org
index fbd35ca5..6d973b94 100644
--- a/docs/specs/ai-kb-spec.org
+++ b/docs/specs/ai-kb-spec.org
@@ -1,11 +1,15 @@
-:PROPERTIES:
-:ID: 03742426-35ce-41c5-aed7-d4e248e91833
-:STATUS: not-started
-:END:
#+TITLE: Design: AI Knowledge Base (ai-kb)
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-24
#+OPTIONS: toc:nil num:nil
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Design: AI Knowledge Base (ai-kb)
+:PROPERTIES:
+:ID: 03742426-35ce-41c5-aed7-d4e248e91833
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
@@ -15,7 +19,7 @@ In scope: Step 1 (store + contract/CLI + global rule + provisioning) and Step 2
* Scope decision: memory store, not (yet) an LLM Wiki
-ai-kb v1 is a *global, durable, cross-project memory store* for AI coding agents (Claude Code today; agent-neutral by contract): org-roam nodes holding lessons, principles, Craig's preferences, reusable procedures, and durable observations. It is the concrete first slice of the broader "org-roam as agent memory" vision in [[file:agentic-knowledgebase.org][agentic-knowledgebase.org]].
+ai-kb v1 is a *global, durable, cross-project memory store* for AI coding agents (Claude Code today; agent-neutral by contract): org-roam nodes holding lessons, principles, Craig's preferences, reusable procedures, and durable observations. It is the concrete first slice of the broader "org-roam as agent memory" vision in [[file:../design/agentic-knowledgebase.org][agentic-knowledgebase.org]].
It is *not* a Karpathy-style LLM Wiki in v1. That pattern — immutable =raw/= sources, compiled =wiki/= synthesis pages, =schema.org=, source hashes, and full ingest/query/lint pipelines — is a larger product whose value is *grounding compiled knowledge in re-checkable sources*. v1 adopts the one piece that pays off immediately: a =raw/= capture for *external* sources (see [[*Grounding external sources][Grounding external sources]]). The rest of that machinery is the documented evolution path (see [[*vNext][vNext]]); v1's structure is chosen so it can grow that way without a rewrite.
diff --git a/docs/specs/ai-vterm-spec-superseded.org b/docs/specs/ai-vterm-spec.org
index 0b6bfb86..7015a862 100644
--- a/docs/specs/ai-vterm-spec-superseded.org
+++ b/docs/specs/ai-vterm-spec.org
@@ -1,11 +1,15 @@
-:PROPERTIES:
-:ID: 3abd0270-e87c-42b7-9b3a-ef60300db99d
-:STATUS: superseded
-:END:
#+TITLE: Design: ai-vterm — in-Emacs Claude launcher
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-07
#+OPTIONS: toc:nil num:nil
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* SUPERSEDED Design: ai-vterm — in-Emacs Claude launcher
+:PROPERTIES:
+:ID: 3abd0270-e87c-42b7-9b3a-ef60300db99d
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword SUPERSEDED from existing :STATUS: superseded + -superseded filename (Craig's prior determination)
* Status
diff --git a/docs/specs/cache-helper-design-spec-implemented.org b/docs/specs/cache-helper-design-spec.org
index 27c818dc..5bfb661b 100644
--- a/docs/specs/cache-helper-design-spec-implemented.org
+++ b/docs/specs/cache-helper-design-spec.org
@@ -1,14 +1,18 @@
-:PROPERTIES:
-:ID: 647c5101-21c2-47bb-aaa7-72c757f45fb7
-:STATUS: implemented
-:END:
#+TITLE: Cache Helper Design Addendum
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-10
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Cache Helper Design Addendum
+:PROPERTIES:
+:ID: 647c5101-21c2-47bb-aaa7-72c757f45fb7
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Status
-Phase 5 design addendum to [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec-doing.org]]. Specifies the cache API to extract before any code moves.
+Phase 5 design addendum to [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec.org]]. Specifies the cache API to extract before any code moves.
* Problem
diff --git a/docs/specs/company-to-corfu-migration-spec.org b/docs/specs/company-to-corfu-migration-spec.org
index a7b059a3..ef094937 100644
--- a/docs/specs/company-to-corfu-migration-spec.org
+++ b/docs/specs/company-to-corfu-migration-spec.org
@@ -1,11 +1,15 @@
-:PROPERTIES:
-:ID: 68733ba2-37a7-4a7b-bfaa-b845d82ff1e7
-:STATUS: not-started
-:END:
#+TITLE: Design: Migrate from Company to Corfu (with prescient integration)
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-15
#+OPTIONS: toc:nil num:nil
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Design: Migrate from Company to Corfu (with prescient integration)
+:PROPERTIES:
+:ID: 68733ba2-37a7-4a7b-bfaa-b845d82ff1e7
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
diff --git a/docs/specs/coverage-spec-implemented.org b/docs/specs/coverage-spec.org
index 65734fb3..e2ac4b3c 100644
--- a/docs/specs/coverage-spec-implemented.org
+++ b/docs/specs/coverage-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 7d7f4486-fad7-4f0a-bd9a-775bd4cd8f7e
-:STATUS: implemented
-:END:
#+TITLE: Design: Coverage Reporting
#+AUTHOR: Craig Jennings
#+DATE: 2026-04-22
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Design: Coverage Reporting
+:PROPERTIES:
+:ID: 7d7f4486-fad7-4f0a-bd9a-775bd4cd8f7e
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Status
diff --git a/docs/specs/debug-profiling-spec.org b/docs/specs/debug-profiling-spec.org
index 5961071b..3492d3a2 100644
--- a/docs/specs/debug-profiling-spec.org
+++ b/docs/specs/debug-profiling-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: c713b431-ae14-498d-aba9-b84d52f981b6
-:STATUS: not-started
-:END:
#+TITLE: Design: debug-profiling.el module
#+AUTHOR: Craig Jennings
#+DATE: 2026-04-26
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Design: debug-profiling.el module
+:PROPERTIES:
+:ID: c713b431-ae14-498d-aba9-b84d52f981b6
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
diff --git a/docs/specs/dev-setup-project-spec.org b/docs/specs/dev-setup-project-spec.org
index 5d64f368..058784a5 100644
--- a/docs/specs/dev-setup-project-spec.org
+++ b/docs/specs/dev-setup-project-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 596fce5d-1bab-46e7-8567-d4a2e0923091
-:STATUS: not-started
-:END:
#+TITLE: Design: cj/dev-setup-project
#+AUTHOR: Craig Jennings
#+DATE: 2026-04-22
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Design: cj/dev-setup-project
+:PROPERTIES:
+:ID: 596fce5d-1bab-46e7-8567-d4a2e0923091
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
diff --git a/docs/specs/dupre-clear-theme-spec.org b/docs/specs/dupre-clear-theme-spec.org
index 578eb240..8027ee2a 100644
--- a/docs/specs/dupre-clear-theme-spec.org
+++ b/docs/specs/dupre-clear-theme-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 20df7f50-4759-47ba-9782-8dd25a2e173e
-:STATUS: not-started
-:END:
#+TITLE: dupre-clear — a contrast-first AAA sibling theme
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-07
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT dupre-clear — a contrast-first AAA sibling theme
+:PROPERTIES:
+:ID: 20df7f50-4759-47ba-9782-8dd25a2e173e
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
diff --git a/docs/specs/face-font-diagnostic-popup-spec-implemented.org b/docs/specs/face-font-diagnostic-popup-spec.org
index 3e8fadcd..aae355f9 100644
--- a/docs/specs/face-font-diagnostic-popup-spec-implemented.org
+++ b/docs/specs/face-font-diagnostic-popup-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: 98f065cf-8bd5-46a0-ac24-da94d66855ad
-:STATUS: implemented
-:END:
#+TITLE: Face and Font Diagnostic Popup — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-14
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Face and Font Diagnostic Popup — Spec
+:PROPERTIES:
+:ID: 98f065cf-8bd5-46a0-ac24-da94d66855ad
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Metadata
diff --git a/docs/specs/flycheck-modeline-customization-spec-implemented.org b/docs/specs/flycheck-modeline-customization-spec.org
index 59567be6..2a58b447 100644
--- a/docs/specs/flycheck-modeline-customization-spec-implemented.org
+++ b/docs/specs/flycheck-modeline-customization-spec.org
@@ -1,11 +1,15 @@
-:PROPERTIES:
-:ID: 76979608-956e-474f-90a8-8d0c958101a0
-:STATUS: implemented
-:END:
#+TITLE: Design: Flycheck modeline customization
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-15
#+OPTIONS: toc:nil num:nil
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Design: Flycheck modeline customization
+:PROPERTIES:
+:ID: 76979608-956e-474f-90a8-8d0c958101a0
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Status
diff --git a/docs/specs/gloss-spec-doing.org b/docs/specs/gloss-spec-doing.org
deleted file mode 100644
index 320b83eb..00000000
--- a/docs/specs/gloss-spec-doing.org
+++ /dev/null
@@ -1,320 +0,0 @@
-:PROPERTIES:
-:ID: 295f9969-ccef-4df9-945b-9e08d8069daf
-:STATUS: doing
-:END:
-#+TITLE: Design — gloss (Glossary Lookup with Online-Sourced Selection)
-#+DATE: 2026-04-28
-#+STATUS: Draft
-
-* Problem
-
-A personal glossary inside Emacs, modelled on the existing =quick-sdcv= UX (=C-h d=) but for self-curated terms rather than packaged dictionaries. =C-h g= prompts for a term (defaulting to word-at-point), looks it up in a single git-tracked org file, and shows the definition in a side buffer that =q= dismisses. On a local miss, the package fetches candidate definitions from an online source, lets the user pick one, and saves it with provenance. The same org file feeds =org-drill= for spaced-repetition study.
-
-The pain point: domain jargon — government acronyms, technical terms, philosophy vocabulary, project-specific names — doesn't live in any general dictionary, so existing tools like =quick-sdcv= can't help. A personal glossary that grows by use (encounter term → save it once → it's permanently looked-up-able and study-card-able) closes that gap.
-
-* Non-Goals
-
-The following are explicitly out of scope for v1. Each is a defensible v2+ topic on its own.
-
-- *Multi-language support.* English only. Wiktionary returns French/Latin/etc. — v1 ignores everything but the =en= key.
-- *Synonyms, cross-references, related terms.* Even when the upstream source returns them, v1 stores only the picked definition.
-- *Audio pronunciation.* Not fetched, not played.
-- *Etymology, usage notes, parsed examples.* Discarded during HTML strip.
-- *Multiple glossaries / domain separation.* One file, one glossary.
-- *Backup or sync infrastructure.* Delegated to git on whatever path =gloss-file= points at.
-- *Org-drill scheduling control.* The exporter prepares entries; =org-drill= itself runs unmodified.
-
-In scope (kept after triage): edit-in-place via =C-h g e=, which jumps to the source file at the entry's heading.
-
-* Approaches Considered
-
-Six approaches evaluated during brainstorm. Three conventional, three tail samples for diversity.
-
-** Recommended: Layered multi-module package
-
-Five =.el= files, each owning one concern: =gloss-core= (data), =gloss-fetch= (network), =gloss-display= (UI), =gloss-drill= (drill export), =gloss= (orchestration entry point). Each layer mocks at its own natural boundary; no layer mocks another layer's internals.
-
-*Why this over the alternatives.* The codebase already prefers layering — =coverage-core= + =coverage-elisp= split, Hugo pure-helpers + interactive wrappers, LSP file-watch defvar + function. The four concerns (data, fetch, display, drill) have genuinely different test boundaries (file I/O, HTTP, mode UI, =org-element=). Mixing them in one file would force overmocking, which the project's testing rules flag as a smell. The package is also public-style — clear module boundaries reward cold readers.
-
-*What's traded away.* About 30 minutes more structural setup at the start, in exchange for boilerplate that may never pay off if the package stays personal forever. Cheap trade against the testing and reading wins.
-
-** Rejected: Single-file quick-sdcv-clone
-
-One =.el= file (~400 lines) covering all four concerns. Simplest path, lowest dependency footprint, but everything (data, HTTP, mode definition, drill) cohabits a single namespace. Test isolation gets awkward; refactor cost grows when one piece needs replacing.
-
-** Rejected: Backend-pluggable registry
-
-A =glossary-backend= protocol covering both local-org and online sources, with =lookup= / =save= / =list= operations. Local and online become interchangeable backends. Real future-proofing, but for v1 with two backends and probably never a third, the protocol is overkill — YAGNI risk. The forward-compat shape we did adopt (the =gloss-fetch-sources= registry, see Architecture) gets the same benefit at a fraction of the design weight, scoped only to where source variety is real.
-
-** Rejected: quick-sdcv + generated StarDict
-
-Round-trip the org file through StarDict format on save; reuse =quick-sdcv='s UI verbatim. Reuses 100% of an existing UI but loses provenance metadata in the round-trip, fights drill (which reads org, not StarDict), and forces a binary intermediate format for what should be a plain-text data store.
-
-** Rejected: Org-roam node per term
-
-Each entry is its own =org-roam= node. Free fuzzy/exact title search, free backlinks. But it's a heavy dependency for an otherwise self-contained package, file-explodes (1000 terms = 1000 files), and contradicts the locked single-file storage decision.
-
-** Rejected: Lazy-reactive minor mode
-
-Passive recognition — =gloss-mode= scans buffer text for known terms, underlines them, hover/click reveals definitions. Different and arguably more-natural mental model, but it reframes the brief (active =C-h g= lookup is what was asked for) and doesn't naturally support online fallback or auto-add. Probably belongs as a v3 feature on top of the layered architecture, not as the architecture itself.
-
-* Design
-
-** Architecture
-
-Five =.el= files:
-
-#+begin_example
-gloss-core.el data layer — org file I/O + in-memory cache
-gloss-fetch.el network layer — Wiktionary REST + HTML strip
-gloss-display.el UI layer — side buffer + picker
-gloss-drill.el drill export — :drill: tag + twosided property
-gloss.el entry point — defcustoms, prefix keymap, user commands
-#+end_example
-
-*Public API by layer.*
-
-=gloss-core=: =gloss-core-lookup TERM=, =gloss-core-save TERM DEFINITION SOURCE=, =gloss-core-list=, =gloss-core-find-buffer-position TERM=.
-
-=gloss-fetch=: =gloss-fetch-definitions TERM= → =(:ok DEFS) | (:empty :no-defs SOURCES :failed SOURCES)=. Internally a registry: =gloss-fetch--sources= alist (source-symbol → fetcher function), walked in order per the user-facing =gloss-fetch-sources= defcustom.
-
-=gloss-display=: =gloss-display-show-entry TERM BODY=, =gloss-display-pick-definition TERM DEFINITIONS=. Defines =gloss-mode= (derived from =special-mode=, =q= quits).
-
-=gloss-drill=: =gloss-drill-export-all=, =gloss-drill-untag-all=. Operates on the org file via =org-element=.
-
-=gloss=: =defcustom gloss-file= (path), =gloss-prefix-map= for =C-h g=, user commands =gloss-lookup=, =gloss-add=, =gloss-edit=, =gloss-fetch-online=, =gloss-drill-export=.
-
-** Data Flow
-
-*Shapes.*
-
-A definition (in flight from fetch through display to save) is a plist:
-
-#+begin_src emacs-lisp
-(:source wiktionary :text "Reference to something earlier in the discourse...")
-#+end_src
-
-An entry (saved in cache and on disk) is a plist:
-
-#+begin_src emacs-lisp
-(:term "anaphora"
- :body "Reference to something earlier in the discourse..."
- :source wiktionary
- :added "2026-04-28"
- :marker #<marker at 1247 in gloss.org>)
-#+end_src
-
-The cache is a hash table, term-string → entry-plist. The org file is the source of truth; the cache is a read-side index.
-
-*Lookup flow (=C-h g=).*
-
-1. Read input — word-at-point if available, else minibuffer prompt.
-2. =gloss-core-lookup TERM=. Cache loaded if cold.
-3. Hit → =gloss-display-show-entry=. Done.
-4. Miss → silent fall-through to =gloss-fetch-definitions TERM=.
-5. Orchestrate on result:
- - 0 definitions or all-failures → side buffer message (see Error Handling).
- - 1 definition → auto-save via =gloss-core-save=, then =gloss-display-show-entry=.
- - >1 definitions → =gloss-display-pick-definition= → user picks → =gloss-core-save= → =gloss-display-show-entry=.
-
-*Add flow (=C-h g a=).*
-
-=gloss-add= prompts for term and body (small temp buffer for multi-line body, =C-c C-c= accepts). =gloss-core-save TERM BODY 'manual=. Then =gloss-display-show-entry=.
-
-*Edit flow (=C-h g e=).*
-
-=gloss-edit= resolves the term to a buffer position via =gloss-core-find-buffer-position=. Opens the org file at that heading in the *source* buffer (not the side buffer). User edits inline. On save, the buffer-local =after-save-hook= refreshes the cache for that single term.
-
-*Drill export (=C-h g D=).*
-
-=gloss-drill-export-all= walks the org file via =org-element=, ensures every term heading has =:drill:= tag and =:DRILL_CARD_TYPE: twosided= property. =M-x org-drill= runs the session — gloss does not wrap or invoke =org-drill= itself.
-
-** Persistence
-
-*File shape.* Single org file at =gloss-file= (default: =(expand-file-name "gloss.org" (or org-directory user-emacs-directory))=). One =* term= heading per entry, alphabetical order maintained on insert. Each entry has a =:PROPERTIES:= drawer with =:SOURCE:= and =:ADDED:=. Body is plain text immediately under the heading.
-
-#+begin_example
-#+TITLE: Glossary
-#+STARTUP: showall
-
-* anaphora
-:PROPERTIES:
-:SOURCE: wiktionary
-:ADDED: 2026-04-28
-:END:
-Reference to something earlier in the discourse...
-
-* SBIR
-:PROPERTIES:
-:SOURCE: wiktionary
-:ADDED: 2026-04-28
-:END:
-Initialism of Small Business Innovation Research...
-#+end_example
-
-After =gloss-drill-export-all=, the heading line gains a =:drill:= tag and the properties drawer gains =:DRILL_CARD_TYPE: twosided=.
-
-*Cache lifecycle.* Hash table loaded lazily on first lookup of the session. Populated by reading =gloss-file= once and parsing with =org-element-parse-buffer=. Subsequent lookups hit the cache directly.
-
-*Cache invalidation.* Four triggers, in order of cost:
-
-1. =gloss-core-save= mutates the cache directly when it writes.
-2. *mtime check on every lookup.* =file-attributes= the file before each =gloss-core-lookup= returns; if mtime > cached-mtime, reload before answering. Sub-millisecond cost; catches every out-of-band edit (other Emacs session, =git pull=, hand-edit, =sed=).
-3. =gloss-edit='s buffer-local =after-save-hook= updates the single edited term immediately; overlaps with #2 but doesn't wait for the next lookup.
-4. Manual =gloss-reload= command — nuclear option for paranoia.
-
-=file-notify-add-watch= rejected: platform-specific backend, async callback complicates the model, mtime path is already sub-millisecond.
-
-*Write strategy.* Append-on-add via direct buffer editing (=find-file-noselect=, insert at the alphabetically-correct heading position, save, kill the buffer if not previously open). No journal, no temp file — org-mode's =auto-save-mode= and the user's git tracking provide durability. Single-user, single-Emacs assumed; concurrent access isn't a concern.
-
-*Alphabetical order.* Maintained on insert via case-insensitive string compare. Cheap; the file stays diff-clean (only the inserted block changes).
-
-** Error Handling
-
-*Per-source status taxonomy.* Five internal values; three user-facing rollups.
-
-#+begin_src emacs-lisp
-;; Internal per-source result:
-(:source SYM :status STATUS :reason STRING)
-
-;; STATUS values:
-;; :ok :defs (def1 def2 ...) — success
-;; :no-defs — server reached, term not there (HTTP 404 or empty 200)
-;; :unreachable — network problem (DNS, refused, timeout)
-;; :server-error — HTTP 5xx, malformed JSON, schema mismatch, HTTP 4xx other than 404/429
-;; :rate-limited — HTTP 429
-#+end_src
-
-*=:reason= strings* carry the technical detail (=timeout (5s)=, =HTTP 503=, =malformed JSON: ...=) and land in =*gloss-debug*=. They are never user-facing.
-
-*User-facing rollup.* =gloss-fetch-definitions= aggregates per-source results into:
-
-#+begin_src emacs-lisp
-(:ok DEFS) ;; any source returned >=1 def
-(:empty :no-defs (...) :failed (...)) ;; everything else
-#+end_src
-
-=:failed= unions =:unreachable=, =:server-error=, =:rate-limited=.
-
-| Result shape | Message |
-|-------------------------------------------+--------------------------------------------------------------------|
-| Every source =:no-defs=, none failed | "No definition for X in Wiktionary." |
-| Every source failed, none =:no-defs= | "Couldn't reach Wiktionary." |
-| Mix of =:no-defs= and failures | "No definition in Wiktionary; couldn't reach DictionaryAPI." |
-| Any =:ok= with defs | Silent on others — picker shows what came back |
-
-When v2 starts surfacing =:rate-limited= regularly, the rollup wording will gain a third visible category. v1 with no-key Wiktionary doesn't need it.
-
-*libxml as a precondition, not a per-source failure.* First time =gloss-fetch-definitions= runs, probe =(libxml-parse-html-region 1 1)= on a temp buffer. If unavailable, online fetching is disabled package-wide for the session with a one-shot =user-error=: "Online fetch requires Emacs built with libxml2; manual add still works." Subsequent online attempts in the session short-circuit to that message.
-
-*Partial-success on per-sense HTML failures.* If libxml is available but fails on a specific sense's content, drop that sense and return the rest. Source status stays =:ok= with N-1 entries; the dropped sense logs to =*gloss-debug*=. A single bad sense doesn't poison the whole source.
-
-*Storage failures.* First call creates =gloss-file= and any missing parent directory with a =#+TITLE: Glossary= header. Permission denied raises =user-error= naming the path. Corrupt org file (=org-element-parse-buffer= raises) preserves the existing cache and surfaces "glossary file corrupt at line N; cache not refreshed" — operations fall back to the stale cache until the user fixes the file and runs =gloss-reload=. Term collision (saving an existing term) prompts: replace, append-with-separator, or cancel.
-
-*Drill.* =org-drill= checked via =featurep= before export runs. If absent: =user-error= with install hint.
-
-*User cancellations.* =C-g= during the picker → no save, side buffer shows the local-miss state. Empty term input from =gloss-add= → re-prompt once, then abort silently. Cancelled at the term-collision prompt → no write.
-
-** Testing
-
-Per-function test files; three categories (Normal/Boundary/Error) per function. TDD by default. Real production code via =require=, never inlined.
-
-*=gloss-core=.* Temp files + real =org-element-parse-buffer=. No mocking — exercises the actual file I/O and parser.
-
-#+begin_example
-test-gloss-core--lookup.el
-test-gloss-core--save.el
-test-gloss-core--invalidate-on-mtime.el
-test-gloss-core--corrupt-file-preserves-cache.el
-test-gloss-core--alphabetical-insert.el
-test-gloss-core--first-call-creates-file.el
-#+end_example
-
-*=gloss-fetch=.* =cl-letf= mock on =url-retrieve-synchronously=, injecting canned response buffers. Captured Wiktionary fixtures in =tests/fixtures/wiktionary-*.json= — real responses for SBIR, anaphora, API, frozen once, replayed forever.
-
-#+begin_example
-test-gloss-fetch--definitions-200-returns-ok.el
-test-gloss-fetch--definitions-404-returns-no-defs.el
-test-gloss-fetch--definitions-500-returns-server-error.el
-test-gloss-fetch--definitions-timeout-returns-unreachable.el
-test-gloss-fetch--strip-html.el
-test-gloss-fetch--multi-source-walks-registry.el
-test-gloss-fetch--libxml-probe.el
-#+end_example
-
-*=gloss-display=.* The candidate-formatting helper =gloss-display--format-candidate PLIST → "[wiktionary] text..."= is pure → full N/B/E coverage. =gloss-display-show-entry= and =gloss-mode= get one smoke test each (Emacs already tests =switch-to-buffer= and major-mode definition).
-
-#+begin_example
-test-gloss-display--format-candidate.el
-test-gloss-display--show-entry-smoke.el
-#+end_example
-
-*=gloss-drill=.* Temp file + real =org-element=. Tests assert tag/property changes on entries.
-
-#+begin_example
-test-gloss-drill--export-all-tags-untagged.el
-test-gloss-drill--export-all-skips-already-tagged.el
-test-gloss-drill--export-all-no-orgdrill-installed.el
-test-gloss-drill--untag-all.el
-#+end_example
-
-*=gloss=.* The orchestration policy =gloss--orchestrate-fetch-result RESULT → SYMBOL= is a pure pattern-matcher. Tested with shaped inputs covering every result variant.
-
-#+begin_example
-test-gloss--orchestrate-fetch-result.el
-#+end_example
-
-*Integration tests.* Three small ones, each with a docstring naming participants per project convention.
-
-#+begin_example
-test-integration-gloss-lookup-flow-local-hit.el
-test-integration-gloss-lookup-flow-online-fall-through.el
-test-integration-gloss-lookup-flow-online-failure.el
-#+end_example
-
-*Coverage targets.* 90%+ on =gloss-core=, =gloss-fetch=, =gloss-drill=, and pure helpers in =gloss-display= / =gloss=. 70%+ on display mode-glue. Overall ≥80%.
-
-** Observability
-
-*=*gloss-debug*= log buffer.* Off until =gloss-debug= defcustom is non-nil, or session-only =gloss-toggle-debug= flips it. One timestamped, layer-prefixed line per significant event.
-
-#+begin_example
-2026-04-28 11:14:02 [fetch:wiktionary] GET /API → 200, 12 senses
-2026-04-28 11:14:02 [fetch:wiktionary] sense 7 HTML parse failed, dropping
-2026-04-28 11:14:02 [core] cache hit for "anaphora"
-2026-04-28 11:14:09 [core] mtime change detected, reloading cache (47 terms)
-2026-04-28 11:14:11 [save] "API" → wiktionary, 11 alts not saved
-#+end_example
-
-Per-source statuses from Error Handling land here verbatim. No personal data beyond user-supplied terms.
-
-*=*Messages*= for user-facing events.* Saves, picker-shown, "no definition found" messages — short single-line =message= calls, persisted in =*Messages*= via Emacs idiom. Strict separation: =*Messages*= for things the user did or asked for; =*gloss-debug*= for everything else.
-
-*Inspection commands.*
-
-- =gloss-list-terms= — completing-read over every term in the cache. Pick one to jump to it.
-- =gloss-stats= — small buffer summarizing total terms, breakdown by =:source=, count of drill-tagged entries, file size, cache mtime.
-
-No metrics export, no telemetry, no profiling hooks — v3 territory if the package ever needs them.
-
-* Open Questions (will become ADRs)
-
-Each was decided during the brainstorm. Listed for traceability; each becomes an ADR in the gloss repo's =docs/decisions/=.
-
-- [ ] *ADR-1: storage path default* → =(expand-file-name "gloss.org" (or org-directory user-emacs-directory))=. Rationale: respects the user's existing =org-directory= convention; falls back gracefully.
-- [ ] *ADR-2: auto-fetch on local miss* → silent fall-through with graceful network-failure path. Rationale: y/n prompt is yes 99% of the time and an annoyance the other 1%; the offline case is better handled by detecting the failure than by pre-asking permission.
-- [ ] *ADR-3: drill direction* → =:DRILL_CARD_TYPE: twosided=. Rationale: tests both recognition and recall over time without doubling the deck.
-- [ ] *ADR-4: HTML strip strategy* → =libxml-parse-html-region= (plain text only, no italic/bold preservation). Rationale: more robust than regex on edge cases; libxml2 is standard on Linux/Mac; ~30 lines.
-
-* Next Steps
-
-1. *Scaffold the repo.* =~/code/gloss= with the claude-template structure: =.ai/= and =todo.org= and =inbox/= gitignored, =Makefile= for tests/lint/compile, =README.org= placeholder, =LICENSE=, package skeleton (=gloss.el= with package-header autoload entry).
-2. *Set up remotes.* Bare repo on cjennings.net at =/var/cjennings/git/gloss.git/= with the existing post-receive hook pattern that mirrors to =github.com/cjennings/gloss=.
-3. *Decompose into todo.org tasks.* One TODO per layer, in implementation order: core → fetch → display → drill → entry-point → integration tests → README. Each task carries its acceptance criteria from this design.
-4. *Implement v1 layer by layer*, TDD per project rules. Run =/start-work= once per task.
-5. *First-week shakedown.* Use the package on real terms for a week. File issues against any rough edges as v1.1 tasks.
-6. *Record the four ADRs* in =docs/decisions/= once the repo exists.
-
-* Status
-
-Draft. Pending: repo scaffold, ADR records, implementation.
diff --git a/docs/specs/google-keep-emacs-integration-spec.org b/docs/specs/google-keep-emacs-integration-spec.org
index 376522ab..96fd83e5 100644
--- a/docs/specs/google-keep-emacs-integration-spec.org
+++ b/docs/specs/google-keep-emacs-integration-spec.org
@@ -1,7 +1,14 @@
#+TITLE: Google Keep <-> Emacs integration — Spec
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-06-24
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING Google Keep <-> Emacs integration — Spec
+:PROPERTIES:
+:ID: 4c796fb9-1d3e-42a9-9b76-eb286eee8732
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from Metadata Status: v1 implemented, v2 next -> work ongoing
* Metadata
| Status | v1 implemented (Phases 1-3); live setup pending; v2 next |
diff --git a/docs/specs/init-load-graph-spec-doing.org b/docs/specs/init-load-graph-spec.org
index 05dd9e0a..33ed0d34 100644
--- a/docs/specs/init-load-graph-spec-doing.org
+++ b/docs/specs/init-load-graph-spec.org
@@ -1,10 +1,15 @@
-:PROPERTIES:
-:ID: e1fd137e-e164-42f4-a658-f4d32fbe3228
-:STATUS: doing
-:END:
#+TITLE: Design: Untangle the init.el Load Graph
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-04
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING Design: Untangle the init.el Load Graph
+:PROPERTIES:
+:ID: e1fd137e-e164-42f4-a658-f4d32fbe3228
+:END:
+- 2026-07-10 Fri @ 22:50:03 -0500 — reconciled the programming target to shipped decisions: generic LSP policy consolidated under =prog-general= (not =prog-lsp=, which was folded in and deleted, commit dfdb3580), and tree-sitter auto-install gated to ='prompt=. Both were owned items of this spec; recording the outcome, keyword stays DOING for the remaining load-graph work.
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Status
@@ -178,7 +183,7 @@ Foundation modules should be able to load in batch mode without package,
network, timer, or UI-package side effects.
Adding a new Layer 1 module requires a coordinated update to the
-=system-lib.el= dependency budget in [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec-doing.org]].
+=system-lib.el= dependency budget in [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec.org]].
Topic libraries introduced by the utility project join Layer 1 only when their
first consumer is foundation-eager. Otherwise they are Layer 2 and loaded by an
@@ -308,7 +313,7 @@ Category key:
| =video-audio-recording= | O/D/S | command-loaded | External process/device probing only on command. |
| =transcription-config= | O/D/P | command-loaded | Auth/process workflow. |
| =weather-config= | O/D/P | command-loaded | Optional command. |
-| =prog-general= | C/P/S | eager or hooks | Projectile, treesit policy, LSP ownership concerns. |
+| =prog-general= | C/P/S | eager or hooks | Projectile, treesit 'prompt, sole LSP policy owner. |
| =test-runner= | C/L | eager command entry | Test keymap and project-scoped state. |
| =vc-config= | C/P | eager command entry | Magit/git keymap; clone command hardening separate. |
| =flycheck-config= | C/P | hooks | General linting. |
@@ -316,7 +321,6 @@ Category key:
| =prog-c= | D/P | mode-loaded | C hooks and compile command. |
| =prog-go= | D/P | mode-loaded | Go hooks/LSP. |
| =prog-lisp= | D/P | mode-loaded | Lisp package config. |
-| =prog-lsp= | C/P | package policy owner | Should consolidate generic LSP policy. |
| =prog-shell= | D/P/S | mode-loaded | after-save executable hook should be opt-in or scoped. |
| =prog-python= | D/P | mode-loaded | Python hooks/LSP. |
| =prog-webdev= | D/P | mode-loaded | Webdev modes/LSP. |
@@ -395,7 +399,7 @@ Worked example:
;; Runtime requires: user-constants, seq, subr-x.
;; Direct test load: yes (batch-safe; private config is optional).
;;
-;; See also: docs/specs/init-load-graph-spec-doing.org, tests/test-calendar-sync.el.
+;; See also: docs/specs/init-load-graph-spec.org, tests/test-calendar-sync.el.
;;
;;; Code:
#+end_src
@@ -452,7 +456,7 @@ Inventory rules:
- Every module required by =init.el= must be represented before Phase 2 starts.
- Discoveries during later phases update the inventory.
- This inventory is independent from the helper inventory owned by
- [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec-doing.org]].
+ [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec.org]].
Exit criteria:
@@ -599,15 +603,20 @@ Programming target:
- Keep generic programming defaults and F-key command entry points available.
- Load language-specific modules by major mode.
-- Consolidate generic LSP policy under =prog-lsp=.
- - Move to =prog-lsp=: global LSP toggles such as =lsp-idle-delay=,
- =lsp-log-io=, =lsp-enable-folding=, =lsp-enable-snippet=,
- =lsp-headerline-breadcrumb-enable=, and file-watch ignore lists.
- - Keep per-language: server client settings such as
- =lsp-clients-clangd-args= and =lsp-pyright-*=, plus language-mode hook
- wiring.
-- Tree-sitter grammar auto-install is always on; the project policy is global
- allow. =treesit-auto-install= is =t= without per-language conditionals.
+- Generic LSP policy is consolidated under =prog-general= (done 2026-07-10, commit
+ dfdb3580). The original plan named =prog-lsp= as owner, but that module was
+ required by nothing and never loaded, so its config was dead; folding it into the
+ module that actually loads (=prog-general=) and deleting it was the working fix.
+ - In =prog-general=: global LSP toggles such as =lsp-idle-delay=, =lsp-log-io=,
+ =lsp-enable-folding=, =lsp-enable-snippet=, the quiet-UI toggles, and the
+ file-watch ignore list.
+ - Kept per-language: server client settings such as =lsp-clients-clangd-args= and
+ =lsp-pyright-*=, plus the =lsp-deferred= language-mode hook wiring.
+ - Remaining: several language modules call =lsp-deferred= from both a local setup
+ function and a package hook; collapse each to one hook path per language.
+- Tree-sitter grammar auto-install is gated to ='prompt= (done). Batch/test runs never
+ auto-install, and =cj/install-treesit-grammars= is the explicit bootstrap for a new
+ machine.
Org target:
@@ -621,9 +630,10 @@ Org target:
=cj-cache.el= extraction is owned by utility-consolidation Phase 5 and may
follow.
-The =prog-lsp= consolidation and tree-sitter policy decisions are owned by this
-load-graph project. Utility consolidation owns reusable helper extraction, not
-programming policy.
+The LSP consolidation and tree-sitter policy decisions are owned by this
+load-graph project. Both landed 2026-07-10 (LSP under =prog-general=, tree-sitter at
+='prompt=). Utility consolidation owns reusable helper extraction, not programming
+policy.
Exit criteria:
@@ -647,7 +657,7 @@ rollback shapes.
This sibling project can run beside Phase 2. When explicit-dependency work finds
a generic duplicated helper, the sibling project owns the extraction commit when
the helper is in scope for that project. See
-[[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec-doing.org]] for candidate
+[[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec.org]] for candidate
helpers, naming rules, dependency budgets, migration phases, and test policy.
* Testing Strategy
diff --git a/docs/specs/keybinding-console-safety-spec-doing.org b/docs/specs/keybinding-console-safety-spec.org
index 4a1dec81..5fd7d52c 100644
--- a/docs/specs/keybinding-console-safety-spec-doing.org
+++ b/docs/specs/keybinding-console-safety-spec.org
@@ -1,13 +1,18 @@
-:PROPERTIES:
-:ID: 540bf06b-16b8-46c6-b459-c40d1b9c795d
-:STATUS: doing
-:END:
#+TITLE: Keymap Consolidation — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-12
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* READY Keymap Consolidation — Spec
+:PROPERTIES:
+:ID: 540bf06b-16b8-46c6-b459-c40d1b9c795d
+:END:
+- 2026-07-21 Tue @ 07:07 -0500 — DOING → READY: the DOING was a legacy :STATUS: retrofit with no build tasks ever decomposed, and the primary work has not started (the key-translation layer is still live in keyboard-compat.el, M-S- bindings global). READY is the honest state: design settled per Path 2, awaiting decomposition. Flipped in the 2026-07-21 board review with Craig.
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Metadata
-| Status | doing |
+| Status | ready |
|----------+--------------------------------------------------------------------|
| Owner | Craig Jennings |
|----------+--------------------------------------------------------------------|
@@ -568,7 +573,7 @@ source module.
- M-S-o — cj/kill-other-window — kill the other window's buffer and close it — undead-buffers.el
- M-S-m — cj/kill-all-other-buffers-and-windows — close all other windows, kill their buffers — undead-buffers.el
- M-S-y — yank-media — paste an image/media object from the clipboard — keybindings.el
-- M-S-f — fontaine-set-preset — switch the font preset — font-config.el
+- M-S-f — cj/fontaine-select-profile — switch the workflow font profile — font-config.el
- M-S-w — wttrin — show the weather report — weather-config.el
- M-S-e — eww — open the EWW web browser — eww-config.el
- M-S-l — cj/switch-themes — select/cycle the theme — ui-theme.el
@@ -894,7 +899,7 @@ translation block being retired). =C-l= appears only minibuffer-local in
- Why: a touched key family broke in GUI and is dead in console; the fix path is
cross-cutting (18 keys, a translation layer to retire, a console-safety
architecture) with real trade-offs, so it clears the spec bar.
-- Artifacts: docs/specs/keybinding-console-safety-spec-doing.org; supersedes the
+- Artifacts: docs/specs/keybinding-console-safety-spec.org; supersedes the
pre-template draft docs/design/keybinding-console-safety.org.
** 2026-06-12 Fri @ 18:30:30 -0500 — Craig Jennings — review response
- What: processed Craig's four review comments. Recorded his first-choice
diff --git a/docs/specs/messenger-unification-spec.org b/docs/specs/messenger-unification-spec.org
index 92985f59..7847fd04 100644
--- a/docs/specs/messenger-unification-spec.org
+++ b/docs/specs/messenger-unification-spec.org
@@ -1,11 +1,15 @@
-:PROPERTIES:
-:ID: 4bfc2011-8ffc-4765-8886-91df12141171
-:STATUS: not-started
-:END:
#+TITLE: Messenger Unification — Shared Window Placement and Key Conventions
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-06-11
-#+STATUS: Draft — decisions 1-9 settled (Craig, 2026-06-11/12); held open for further ideas before Ready
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Messenger Unification — Shared Window Placement and Key Conventions
+:PROPERTIES:
+:ID: 4bfc2011-8ffc-4765-8886-91df12141171
+:END:
+- 2026-07-14 Tue @ 01:10:00 -0500 — premise shift: the signel client was retired to archive/ (agents drive Signal via signal-cli), so the registry example, the "signel remains the running reference" language, and the backend list all need a rewrite before this leaves DRAFT — the live in-Emacs backends are now telega and Slack, with smoke (~/code/smoke) still the future native adopter. Also landed since the 06-11 survey: slack-config gained signel-shape notification hardening (c69f2f56) and telega notifications are on (5bc5ef7a), so the shared cj/messenger-notify extraction has two live call sites ready.
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started (held open for more ideas)
* Problem
diff --git a/docs/specs/music-config-without-emms-spec.org b/docs/specs/music-config-without-emms-spec.org
index 32fd6736..c63706e5 100644
--- a/docs/specs/music-config-without-emms-spec.org
+++ b/docs/specs/music-config-without-emms-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 423bc355-18d3-4e39-9e7a-f768b865d95b
-:STATUS: not-started
-:END:
#+TITLE: Design: music-config Without EMMS
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-15
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Design: music-config Without EMMS
+:PROPERTIES:
+:ID: 423bc355-18d3-4e39-9e7a-f768b865d95b
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
diff --git a/docs/specs/org-faces-spec-implemented.org b/docs/specs/org-faces-spec.org
index c8855906..94fe7bb4 100644
--- a/docs/specs/org-faces-spec-implemented.org
+++ b/docs/specs/org-faces-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: 35578114-8c29-43af-97a2-fdfea01a802e
-:STATUS: implemented
-:END:
#+TITLE: Org Header-Row Faces — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-15
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Org Header-Row Faces — Spec
+:PROPERTIES:
+:ID: 35578114-8c29-43af-97a2-fdfea01a802e
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Metadata
| Status | implemented |
diff --git a/docs/specs/signal-client-spec-doing.org b/docs/specs/signal-client-spec.org
index beee0acf..56c9ea1a 100644
--- a/docs/specs/signal-client-spec-doing.org
+++ b/docs/specs/signal-client-spec.org
@@ -1,10 +1,15 @@
+#+TITLE: Design: Signal client in Emacs (forked signel)
+#+DATE: 2026-05-26
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Design: Signal client in Emacs (forked signel)
:PROPERTIES:
:ID: 0cabd6ee-c458-47b5-a8af-3ee054b25821
-:STATUS: doing
:END:
-#+TITLE: Design: Signal client in Emacs (forked signel)
-#+DATE: 2026-05-26
-#+STATUS: Draft
+- 2026-07-14 Tue @ 01:10:00 -0500 — RETIRED from the config (Craig's call, ~an hour after the IMPLEMENTED flip): agents drive Signal via signal-cli / signal-mcp, so the interactive in-Emacs client earns no keep. signal-config.el and its seven test files moved to archive/ (see archive/README.org); the C-; M prefix unregistered; the ~/code/signel fork repo untouched. Keyword stays IMPLEMENTED as the honest record of what was built.
+- 2026-07-14 Tue @ 01:01:32 -0500 — IMPLEMENTED (Craig's call): v1 is shipped and in daily use — forked signel engine, contact picker with cached contacts (lifecycle hardened 703b4841 / 1296cc45), notifications with sound gating and script-with-fallback delivery, bottom-30% window rule. Next-generation client work belongs to the smoke project (~/code/smoke, its own architecture spec), which the messenger-unification spec designates as the ground-up replacement; signal-config.el stays the running reference until smoke reaches parity.
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Problem
I want a Signal chat client inside Emacs: link it as a secondary device to my phone, pick a contact from my contact list, hold a text 1:1 conversation (read and send), and get a desktop notification on incoming messages, with an optional sound. Signal has no official API, so this is built on =signal-cli=, the mature headless CLI, driven over JSON-RPC.
diff --git a/docs/specs/theme-studio-completion-preview-spec.org b/docs/specs/theme-studio-completion-preview-spec.org
index 588f35a9..7d0c2608 100644
--- a/docs/specs/theme-studio-completion-preview-spec.org
+++ b/docs/specs/theme-studio-completion-preview-spec.org
@@ -1,7 +1,14 @@
#+TITLE: Theme Studio Minibuffer-Completion Preview — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-23
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Theme Studio Minibuffer-Completion Preview — Spec
+:PROPERTIES:
+:ID: 2462f067-4c8d-4c33-a5be-54c0abc2eb1d
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from Metadata Status: Not ready, review found blockers
* Metadata
| Status | Not ready — first Codex review found implementation-readiness blockers (2026-06-23) |
@@ -195,7 +202,7 @@ Add the caption naming minibuffer-prompt + highlight as living in UI Faces. When
- Manual: open theme-studio in Chrome on the owner's inventory and confirm the Vertico section + baseline render, orderless/marginalia toggle, and vertico-current shows no background.
* References / Appendix
-- Reuse: [[file:theme-studio-preview-locate-spec.org][theme-studio-preview-locate-spec.org]] (hover/click locate), [[file:theme-studio-package-faces-spec-doing.org][theme-studio-package-faces-spec-doing.org]].
+- Reuse: [[file:theme-studio-preview-locate-spec.org][theme-studio-preview-locate-spec.org]] (hover/click locate), [[file:theme-studio-package-faces-spec.org][theme-studio-package-faces-spec.org]].
- Spike: /tmp completion-face-preview.el (verified render; not committed — informs this spec, not grown into it).
- Live face values captured 2026-06-23 (WIP theme): minibuffer-prompt #899bb1/#100f0f bold; orderless-match-face-0..3 #cbd0d6 / #c99990 / #c5d4ae / #bea9dc bold italic; vertico-current inherits highlight (#eddba7 bold, no background).
diff --git a/docs/specs/theme-studio-nerd-icons-colors-spec.org b/docs/specs/theme-studio-nerd-icons-colors-spec.org
index c0f07b6d..94a5d178 100644
--- a/docs/specs/theme-studio-nerd-icons-colors-spec.org
+++ b/docs/specs/theme-studio-nerd-icons-colors-spec.org
@@ -1,7 +1,14 @@
#+TITLE: Theme-driven nerd-icons colors + theme-studio filetype legend — Spec
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-06-23
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* READY Theme-driven nerd-icons colors + theme-studio filetype legend — Spec
+:PROPERTIES:
+:ID: 6df4e8a3-1fca-452a-9416-3fa0647b8dff
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword READY from Metadata Status: Ready pending Craig's go
* Metadata
| Status | Ready pending Craig's go — Codex review rounds 1-3 incorporated |
diff --git a/docs/specs/theme-studio-package-faces-spec-doing.org b/docs/specs/theme-studio-package-faces-spec.org
index 566f34db..6e431dd5 100644
--- a/docs/specs/theme-studio-package-faces-spec-doing.org
+++ b/docs/specs/theme-studio-package-faces-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 8f37a1fd-cfd3-4b25-92e5-772468092bdc
-:STATUS: doing
-:END:
#+TITLE: theme-studio — package faces (tier 3), starting with org-mode
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-07
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING theme-studio — package faces (tier 3), starting with org-mode
+:PROPERTIES:
+:ID: 8f37a1fd-cfd3-4b25-92e5-772468092bdc
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Status
@@ -548,7 +552,7 @@ generalized face-control helper, package style kept inside the package object,
- *Why:* The direction is coherent and the first-round decisions are folded in,
but v1 now depends on behavior that is not yet implementable from the current
static generator without a defined inventory and state/export contract.
-- *Artifacts:* [[file:theme-studio-package-faces-spec-review.org][theme-studio-package-faces-spec-review.org]]
+- *Artifacts:* =theme-studio-package-faces-spec-review.org=
** 2026-06-07 Sun @ 18:28:02 -0500 — Claude Code (emacs-d) — responder
- *What:* Ran spec-response against the Codex review. Added Implementation
diff --git a/docs/specs/theme-studio-palette-generator-spec-doing.org b/docs/specs/theme-studio-palette-generator-spec.org
index b98e1078..ab84894c 100644
--- a/docs/specs/theme-studio-palette-generator-spec-doing.org
+++ b/docs/specs/theme-studio-palette-generator-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: 2df157b8-c7c1-47a9-b080-d9586c6f424c
-:STATUS: doing
-:END:
#+TITLE: Theme Studio Palette Generator -- Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-14
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING Theme Studio Palette Generator -- Spec
+:PROPERTIES:
+:ID: 2df157b8-c7c1-47a9-b080-d9586c6f424c
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Metadata
| Status | doing |
@@ -275,7 +278,7 @@ Use the existing Theme Studio test stack:
- Manual Chrome pass on at least one dark palette and one light palette.
* References / Appendix
-- [[file:design/theme-studio-color-harmony.org][theme-studio color harmony explainer]]
+- [[file:../design/theme-studio-color-harmony.org][theme-studio color harmony explainer]]
- [[id:15db8ae3-fc14-49f3-9ed5-d5ff59790904][perceptual color metrics spec]]
- [[file:theme-studio-palette-ramps-spec.org][palette ramps and contrast safety spec]]
- [[file:theme-studio-palette-columns-spec.org][palette columns spec]]
diff --git a/docs/specs/theme-studio-perceptual-color-metrics-spec-implemented.org b/docs/specs/theme-studio-perceptual-color-metrics-spec.org
index 57a4c70b..f84bc5bb 100644
--- a/docs/specs/theme-studio-perceptual-color-metrics-spec-implemented.org
+++ b/docs/specs/theme-studio-perceptual-color-metrics-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 15db8ae3-fc14-49f3-9ed5-d5ff59790904
-:STATUS: implemented
-:END:
#+TITLE: theme-studio — perceptual color metrics (OKLCH, APCA, ΔE)
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-08
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED theme-studio — perceptual color metrics (OKLCH, APCA, ΔE)
+:PROPERTIES:
+:ID: 15db8ae3-fc14-49f3-9ed5-d5ff59790904
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Status
@@ -506,7 +510,7 @@ Modified or rejected recommendations only; everything else in the Codex review
values or make that second chromatic fixture optional.
- *Why:* The implementation is otherwise ready-shaped, but APCA math and numeric
fixtures need a single authoritative source before coding starts.
-- *Artifacts:* [[file:theme-studio-perceptual-color-metrics-spec-review.org][theme-studio-perceptual-color-metrics-spec-review.org]]
+- *Artifacts:* =theme-studio-perceptual-color-metrics-spec-review.org=
** 2026-06-08 Mon @ 13:19:15 -0500 — Claude Code — responder
- *What changed:* Processed Codex's second pass. Accepted all three findings, no
diff --git a/docs/specs/theme-studio-preview-locate-spec.org b/docs/specs/theme-studio-preview-locate-spec.org
index dee27e8c..2f07d9dd 100644
--- a/docs/specs/theme-studio-preview-locate-spec.org
+++ b/docs/specs/theme-studio-preview-locate-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: fbcf0e20-1328-42b4-aa36-3401509e7816
-:STATUS: ready-pending-go
-:END:
#+TITLE: Theme Studio Preview Element Locate — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-15
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* READY Theme Studio Preview Element Locate — Spec
+:PROPERTIES:
+:ID: fbcf0e20-1328-42b4-aa36-3401509e7816
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword READY from existing :STATUS: ready-pending-go
* Metadata
| Status | Ready pending Craig's go — four reviews incorporated (Codex, 2026-06-23) |
diff --git a/docs/specs/theme-studio-seeding-engine-spec-doing.org b/docs/specs/theme-studio-seeding-engine-spec.org
index baf9f5b0..25fab26c 100644
--- a/docs/specs/theme-studio-seeding-engine-spec-doing.org
+++ b/docs/specs/theme-studio-seeding-engine-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: b70b37f2-37df-4c8e-ac2f-1f20d12e33dd
-:STATUS: doing
-:END:
#+TITLE: theme-studio — seeding engine (role table to guide-correct defaults)
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-08
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING theme-studio — seeding engine (role table to guide-correct defaults)
+:PROPERTIES:
+:ID: b70b37f2-37df-4c8e-ac2f-1f20d12e33dd
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Status
@@ -317,7 +321,7 @@ response resolved; everything else was woven into the body as written.
budget this engine executes.
- =scripts/theme-studio/generate.py= — =CATS=, =UI_FACES=/=UIMAP=, =APPS= /
=seedPkgmap=, =exportObj= (the target shape).
-- =docs/specs/theme-studio-perceptual-color-metrics-spec-implemented.org= — the
+- =docs/specs/theme-studio-perceptual-color-metrics-spec.org= — the
=colormath.js= core that v1 OKLCH shade generation uses.
* Review and iteration history
@@ -352,3 +356,10 @@ response resolved; everything else was woven into the body as written.
and reseed behavior, package preservation rules, artifact round-trip tests,
and vNext boundaries.
- *Artifacts:* No review file written; no blocking findings.
+
+** 2026-07-04 Sat @ 18:33:29 -0500 — Claude Code — implementation finding (Phase 2 blocked)
+- *What changed or was recommended:* Phase 1 landed clean (commit f407959b). Starting Phase 2, tracing the actual generator surfaced that a load-bearing premise of this spec is false against the code. The spec's Package scope says the ~20 non-org bespoke packages "keep their curated =APPS= seed colors" as the state the engine preserves and reseed resets to. They don't: =apply_default_face_seeds= (=app_inventory.py:139=) unconditionally overwrites *every* bespoke package face with =defaults.seed(face, False)= — the Emacs-default snapshot color — whenever =emacs-default-faces.json= is present (it is). So in the built page the curated dupre =SEED= dicts in =face_data.py= (=ORG_SEED=, =MAGIT_SEED=, …) are shadowed and never live: org faces open empty, and magit/elfeed/mu4e/the rest open on light-theme snapshot hexes (e.g. =magit-section-heading= #8b6508 on #f2f2f2), not their dupre curated colors.
+- *Why it blocks Phase 2:* Two acceptance criteria then contradict under the real data. "Non-org packages keep their curated =APPS= defaults" and "a Chrome eyeball confirms a coherent dupre" cannot both hold — seeding syntax/UI/org to dark dupre while the package tiers stay on light snapshot hexes reads incoherent. A second wrinkle: =seed()= (JS) emits only org among packages by design, but the non-org dupre colors live in =face_data.py= (Python, name-resolved), so the =dupre-revised.json= emitter cannot be pure-Node — it needs a Python contribution or a different source for the non-org package colors.
+- *The fork (Craig to decide before the spec is revised):* (1) reseed the non-org packages from the curated dupre =SEED= dicts in =face_data.py=, resolved against a dupre palette — coherent, honors "curated," but the seeded build must stop =apply_default_face_seeds= from shadowing them and the emitter needs a Python side; or (2) keep the Emacs-default snapshot hexes — literally "keep current defaults," but the theme reads incoherent and the coherence gate fails.
+- *Disposition:* Craig chose to pause Phase 2 and revise the spec first (its "curated seeds are live" premise needs correcting). Spec kept =DOING=; Phase-2 and test-surface build tasks marked =:blocked:= in =todo.org= with a =VERIFY= capturing the direction decision. Phase 1 (=seed()= + =#seedtest=) stands.
+- *Artifacts:* the =todo.org= seeding-engine subtree (Phase-2 subtask + VERIFY).
diff --git a/docs/specs/theme-studio-semantic-theme-architecture-spec.org b/docs/specs/theme-studio-semantic-theme-architecture-spec.org
index 01ef1902..cc46d336 100644
--- a/docs/specs/theme-studio-semantic-theme-architecture-spec.org
+++ b/docs/specs/theme-studio-semantic-theme-architecture-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: fe980b12-451a-4d8b-a550-d99f9ec49f45
-:STATUS: not-started
-:END:
#+TITLE: Theme Studio Semantic Theme Architecture -- Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-14
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Theme Studio Semantic Theme Architecture -- Spec
+:PROPERTIES:
+:ID: fe980b12-451a-4d8b-a550-d99f9ec49f45
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Metadata
| Status | not-started |
@@ -255,9 +258,9 @@ Rollout should keep the current flat output path as the default and add a separa
* References / Appendix
- Modus Themes source: [[https://github.com/protesilaos/modus-themes][github.com/protesilaos/modus-themes]]
-- Current converter: [[file:../scripts/theme-studio/build-theme.el][scripts/theme-studio/build-theme.el]]
-- Current Theme Studio README: [[file:../scripts/theme-studio/README.md][scripts/theme-studio/README.md]]
-- Package-face model spec: [[id:8f37a1fd-cfd3-4b25-92e5-772468092bdc][theme-studio-package-faces-spec-doing.org]]
+- Current converter: [[file:../../scripts/theme-studio/build-theme.el][scripts/theme-studio/build-theme.el]]
+- Current Theme Studio README: [[file:../../scripts/theme-studio/README.md][scripts/theme-studio/README.md]]
+- Package-face model spec: [[id:8f37a1fd-cfd3-4b25-92e5-772468092bdc][theme-studio-package-faces-spec.org]]
* Review and iteration history
** 2026-06-14 Sunday @ 14:37:00 -0500 -- Craig -- author
diff --git a/docs/specs/theme-studio-structured-output-spec.org b/docs/specs/theme-studio-structured-output-spec.org
index ad189b7e..10aea1a8 100644
--- a/docs/specs/theme-studio-structured-output-spec.org
+++ b/docs/specs/theme-studio-structured-output-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: eaac7707-ed05-43df-9e51-b17c1d672531
-:STATUS: not-started
-:END:
#+TITLE: Theme-Studio Structured Theme Output — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-15
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Theme-Studio Structured Theme Output — Spec
+:PROPERTIES:
+:ID: eaac7707-ed05-43df-9e51-b17c1d672531
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Metadata
| Status | not-started |
diff --git a/docs/specs/utility-consolidation-spec-doing.org b/docs/specs/utility-consolidation-spec.org
index b0a5fe2b..871295d7 100644
--- a/docs/specs/utility-consolidation-spec-doing.org
+++ b/docs/specs/utility-consolidation-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: fc2e3926-b4a1-4b45-92eb-20841e13f655
-:STATUS: doing
-:END:
#+TITLE: Design: Consolidate Shared Utility Helpers
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-04
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING Design: Consolidate Shared Utility Helpers
+:PROPERTIES:
+:ID: fc2e3926-b4a1-4b45-92eb-20841e13f655
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Status
@@ -295,7 +299,7 @@ Worked =system-lib.el= header:
;; Private helpers rename without alias when all call sites change in the
;; same commit.
;;
-;; See also: docs/specs/utility-consolidation-spec-doing.org for design rationale.
+;; See also: docs/specs/utility-consolidation-spec.org for design rationale.
;;
;;; Code:
#+end_src
@@ -332,7 +336,7 @@ Load shape:
- =cj-cache.el= follows the first real cache consumer's layer, likely Layer 2 if
modeline/agenda/refile remain eager or near-eager.
- Coordinate every new topic library with
- [[id:e1fd137e-e164-42f4-a658-f4d32fbe3228][init-load-graph-spec-doing.org]] before migrating its first consumer.
+ [[id:e1fd137e-e164-42f4-a658-f4d32fbe3228][init-load-graph-spec.org]] before migrating its first consumer.
* Naming Rules
@@ -785,7 +789,7 @@ Recommendation:
design addendum proves the API can drive the alignment.
- Then decide whether modeline's buffer-local cache can use the same library or
should remain specialized.
-- Phase 5 step 1 produces =docs/specs/cache-helper-design-spec-implemented.org=. Until that
+- Phase 5 step 1 produces =docs/specs/cache-helper-design-spec.org=. Until that
file exists, =cj-cache.el= must not be created. The addendum is the
prerequisite for any cache extraction commit.
@@ -906,7 +910,7 @@ Inventory artifact:
- Treat the inventory as living documentation. Cleared high-priority candidates
may move to Phase 2 before the whole inventory is complete.
- This inventory is independent from the module-shape inventory maintained by
- [[id:e1fd137e-e164-42f4-a658-f4d32fbe3228][init-load-graph-spec-doing.org]]. The two projects may walk the same files, but they
+ [[id:e1fd137e-e164-42f4-a658-f4d32fbe3228][init-load-graph-spec.org]]. The two projects may walk the same files, but they
record different facts in separate artifacts.
For each helper record:
diff --git a/docs/specs/vterm-to-ghostel-migration-spec-implemented.org b/docs/specs/vterm-to-ghostel-migration-spec.org
index 1be4fe22..f3d39d22 100644
--- a/docs/specs/vterm-to-ghostel-migration-spec-implemented.org
+++ b/docs/specs/vterm-to-ghostel-migration-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: b54c94a0-d762-4b41-afd7-cf5593ce6675
-:STATUS: implemented
-:END:
#+TITLE: Migration: vterm → ghostel (single terminal engine)
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-04
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Migration: vterm → ghostel (single terminal engine)
+:PROPERTIES:
+:ID: b54c94a0-d762-4b41-afd7-cf5593ce6675
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Status
@@ -175,7 +179,7 @@ Audited file set.
** Docs (active references only — historical notes stay)
- =todo.org= current task link (already updated to this -spec path).
-- =docs/design/module-inventory.org=, =docs/specs/init-load-graph-spec-doing.org= —
+- =docs/design/module-inventory.org=, =docs/specs/init-load-graph-spec.org= —
update active =vterm-config= / =ai-vterm= references to the new names.
** Tests (~35 files)