aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--todo.org1731
1 files changed, 884 insertions, 847 deletions
diff --git a/todo.org b/todo.org
index ec7be8674..11dfcb18d 100644
--- a/todo.org
+++ b/todo.org
@@ -55,263 +55,19 @@ Tags are additive. For example, a small wrong-behavior fix can be
=:bug:quick:=, and a feature that requires internal restructuring can be
=:feature:refactor:=.
* Emacs Open Work
-** TODO [#C] ai-term multi-LLM support — Claude / Codex / ollama :feature:
-Allow creating an ai-term that launches any of Claude, Codex, or a local LLM via ollama, switchable at session start. From rulesets/Craig via the roam inbox. Spec note: =inbox/PROCESSED-2026-06-23-2123-from-rulesets-ai-term-multi-llm-support-from-craig.org=.
-** TODO [#C] theme-studio: package coverage for pearl, wttrin, chime :feature:studio:
-Three projects shipped themeable faces and asked theme-studio to render accurate previews. Data lives in the PROCESSED handoff files.
-*** TODO pearl — 6 faces + overlay-driven appearance
-Six faces in the =pearl= customize group plus overlay-driven appearance a raw buffer read won't show. =inbox/PROCESSED-2026-06-23-2239-from-pearl-theme-studio-pearl-spec.org= + cover + =sample-pearl-buffer.org=.
-*** TODO emacs-wttrin — 4 new faces
-Was hardcoded "gray60"; now four customizable faces (branch =feature/themeable-faces=). =inbox/PROCESSED-2026-06-23-2253-from-emacs-wttrin-wttrin-faces-handoff.org= + rendered sample.
-*** TODO chime — 4 themeable modeline faces
-Four modeline faces shipped (081d76e). =inbox/PROCESSED-2026-06-23-2326-from-chime-chime-added-four-themeable-modeline.org=.
-** TODO [#D] Evaluate google-keep Emacs package :quick:
-From the roam inbox. Look at the google-keep Emacs package — worth adding for in-editor Keep, or does the existing google-keep MCP cover it? Triage / shortlist, not a commitment.
-** TODO [#D] Theme Studio nerd-icons vNext follow-ups :feature:
-Deferred from [[file:docs/specs/theme-studio-nerd-icons-colors-spec.org][theme-studio-nerd-icons-colors-spec.org]]: extend the legend to
-buffer-mode and command/symbol categories if the file set proves insufficient;
-add a "reset to nerd-icons native palette" button.
-** TODO [#C] VAMP — extract music-config into a standalone player :feature:refactor:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-21
-:END:
-Build VAMP ("VAMP Audio Music Player"), a standalone, publishable Emacs music player at =~/code/vamp= — derived from a maintained subset of EMMS, depending on the EMMS package not at all, with MPV and mpd behind a generalized adapter API. =.emacs.d= keeps thin glue (=vamp-config.el=: keybindings, paths, dashboard); archsetup owns OS wiring (Super+/ launcher, m3u MIME). Models the =linear-config= → =pearl= migration.
-
-Brainstorm complete 2026-06-22 — validated design at [[file:docs/design/vamp-music-player.org][docs/design/vamp-music-player.org]]. It builds on the prior EMMS-removal work ([[file:docs/specs/music-config-without-emms-spec.org][spec]] + [[file:docs/design/music-config-without-emms-review.org][2026-05-15 review]]), confirming its B1/B2/B4/S3 decisions and pivoting four things (publishable-now, two adapters + generalized API, VAMP name, desktop integration).
-
-Next: (1) revise the spec to the new direction; (2) spike the risky assumptions (mpd dumb-single-file-player contract; m3u =.desktop= on Hyprland); (3) =/start-work= against the revised spec — pure-helper extraction (review Migration Plan step 1) is the safe first phase. Priority [#C] is a placeholder pending Craig's call.
-
-
-** TODO [#C] ai-term: step between running ai-terms even when detached :feature:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-22
-:END:
-The step-to-next-agent family (s-F9 and friends) should cycle to a running ai-term even when that ai-term is currently detached, instead of skipping it. Today the step only lands on attached/visible ai-terms, so a detached-but-running agent gets passed over and there's no keyboard path back to it — re-attach/display it on landing. From the roam inbox.
-
-** TODO [#C] ai-term: multi-backend (Claude / Codex / local ollama) :feature:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-22
-:END:
-Allow creating an ai-term backed by any of Claude, Codex, or a local LLM via ollama, with the backend chosen seamlessly at the start of the session. ai-term currently assumes Claude; generalize the launch path so the agent backend is a selectable parameter and switching between them at session start is frictionless. Routed here from the rulesets roam-inbox item "multiple agent source improvements" (its bullet 3 asked to send emacs this note); the item's other bullets — naming the agent so non-Claude agents aren't called "Claude", and tightening workflow wording for Codex's more literal reading — stay with rulesets.
-
-** TODO [#C] Compare terminal themeability: EAT vs vterm vs ghostel :feature:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-22
-:END:
-Research how completely each of EAT, vterm, and ghostel can be themed — in particular how far theme studio can theme each terminal and what it leaves out. Produce a comparison document, then review it with an eye to whether ai-term should move off ghostel (current) to EAT or vterm. Connects to the chime/emacs-wttrin/pearl face-exposure theme-studio thread. From the roam inbox.
-
-** TODO [#B] Un-pin ghostel from 0.33.0 once upstream fixes #422/#423 :bug:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-20
-:END:
-ghostel is held at 0.33.0 (=ghostel-20260604.2049=, commit 5779a2adceb2) in =modules/term-config.el= to dodge the 0.35.x native-PTY crash. When dakra/ghostel ships a fix for #422 (Linux malloc/signal reentrancy) and #423 (macOS recursive lock), restore =:ensure t= (drop the pin comment) and =package-upgrade ghostel=, then re-run the open-ghostel-in-a-GUI-frame survival check. Watch the two issues for the fixing commit.
-
-archsetup automated the zig 0.15.2 pin (managed =install_zig_pin= step, sha-verified, unit-tested). If the un-pinned ghostel bumps its ghostty dependency to a newer zig, send archsetup the new version + sha256 so it bumps its =ZIG_VERSION= / =ZIG_SHA256= constants (=inbox-send archsetup=).
-
-** VERIFY [#B] calendar-sync robustness: atomic writes, curl --fail, zero-event false errors :bug:solo:next:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-20
-:END:
-Deferred, pairs with the calendar-sync recurrence VERIFY above. The mechanical parts (write to a temp file + rename, add curl --fail, guard the zero-event case) are doable, but any calendar-sync change needs verification against a real .ics feed to avoid masking a genuine empty/failed sync. Do this together with the recurrence fix once you provide a fixture / confirm the live feed.
-From the 2026-06 config audit, =modules/calendar-sync.el=:
-- =:1309= — agenda file written via =with-temp-file= directly on the target (truncate-in-place); org-agenda/chime reading mid-write sees a partial calendar, hourly. Write temp + =rename-file= (atomic same-fs). Same for =--save-state= :258.
-- =:1284= — curl runs without =--fail=: an HTTP 404/500 error page exits 0 and the HTML proceeds into conversion.
-- =:1229-1233= — =--parse-ics= returns nil for both garbage and a valid calendar with zero in-window events, so healthy near-empty calendars report "parse failed" in =calendar-sync-status=. Distinguish the cases.
-
-** VERIFY [#B] org-roam :config triggers the 15-20s refile scan synchronously at first idle :bug:solo:next:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-20
-:END:
-Needs from Craig: this is measurement-first (perf), not a blind fix — it's the same bottleneck as the "optimize org-capture target building" debug task. Run /debug with debug-profiling to measure what actually costs the 15-20s (file count? regex? agenda rebuild?), then fix from the data. I won't restructure the refile/agenda scan without a profile. Say "let's debug it" and I'll profile + fix.
-=modules/org-roam-config.el:78-79= — org-roam is =:defer 1=, so its :config calls =cj/build-org-refile-targets= at 1s idle, BEFORE the 5s background timer (=org-refile-config.el:144-151=); on a cold cache the 30k-file scan runs inline and freezes Emacs at first idle. Drop the call — org-roam is loaded long before the 5s timer fires. Likely a player in the filed org-capture 15-20s perf task (=[#B] Optimize org-capture target building performance=) — check both together. From the 2026-06 config audit.
-
-** VERIFY [#B] transcription: stderr never reaches the log, video transcripts stranded in /tmp :bug:solo:next:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-20
-:END:
-Deferred from the batch (no blocker; needs a focused pass with live verification). Plan: (1) transcription-config.el:210 — make-process :stderr with a file path creates a buffer, not a file; route stderr into the process buffer and write the captured text out in the sentinel, then drop the leaked buffer. (2) :370-374 — derive the txt/log base from the VIDEO path, not the temp mp3's /tmp path, so transcripts land alongside the source. The path-derivation half is cleanly unit-testable; the stderr half needs a real transcription run to verify, which is why I held it for a focused session rather than the batch.
-From the 2026-06 config audit, =modules/transcription-config.el=:
-- =:210= — =make-process :stderr= with a file PATH creates a BUFFER named like the path (verified by probe); the "Errored. Logs in <file>" notification points at a log without the error text, and the hidden stderr buffer leaks per transcription. Route stderr into the process buffer or write it out in the sentinel.
-- =:370-374= — video path derives txt/log from the temp mp3's /tmp path; the transcript lands in /tmp and dies on reboot, contradicting the "alongside the source" docstring. Pass the video's path as the output base.
-
-** VERIFY [#C] Remove unused system-power keybindings :refactor:quick:next:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-20
-:END:
-Needs from Craig: the task says "confirm the exact set to keep before unbinding." Under C-; ! the bindings are shutdown (s), reboot (r), restart-Emacs (e), and friends. Tell me which to keep bound and which to drop (the completing-read menu still reaches the rare ones), and I'll unbind the rest.
-=modules/system-commands.el= binds shutdown (=C-; ! s=), reboot (=C-; ! r=), restart-Emacs (=C-; ! e=) and friends under the =C-; != prefix. Craig rarely uses them and wants the key real-estate back. Drop the bindings he doesn't use; the completing-read menu can still reach the rare ones. Confirm the exact set to keep before unbinding. From the roam inbox.
-
** PROJECT [#A] Manual testing and validation
Exercised once the phases above land.
-*** VERIFY ai-term keybindings land on C-; a + M-SPC
-What we're verifying: the relocated ai-term keys work in a live frame, including from inside an agent buffer, and the no-agent fallback launches the picker.
-- Press M-SPC from a normal buffer with at least one agent open.
-- Press M-SPC again from inside an agent buffer (ghostel).
-- With no agent running, press M-SPC.
-- Walk C-; a a, C-; a s, C-; a n, C-; a k (which-key should show the ai-term menu under C-; a).
-- Press F9, C-F9, s-F9, M-F9.
-Expected: M-SPC swaps to the next agent (rotating, wrapping) both from a normal buffer and from inside an agent. With no agent running, M-SPC opens the project picker rather than erroring. C-; a a toggles the most-recent agent, s opens the picker, n swaps, k closes. The F9 family does nothing (unbound). Note: the running daemon still has gptel in memory from before the archive, so a full Emacs restart is the clean confirmation that nothing regressed at startup.
*** TODO theme-studio preview-locate discoverability read
What we're verifying: the locate hover/flash actually feels discoverable in a live frame — the subjective read the deterministic gates can't make.
- Open theme-studio in Chrome (=make theme-studio-open=, or open theme-studio.html).
- Hover several preview elements across the UI mock and a package pane.
- Click an on-pane element, then click an off-pane element.
Expected: hovering updates the preview-label info line immediately with "section > face — value" (no wait on the native tooltip); an on-pane click scrolls to and flashes the right assignment row; off-pane elements don't respond and their title explains why. The flow reads like a legend you can interrogate. If it feels broken or unclear, note where and reopen the relevant phase.
-*** VERIFY deferred game commands still work after a restart (load-graph Phase 4)
-What we're verifying: with games-config no longer eagerly required, malyon and 2048-game still launch from a fresh Emacs, and games-config loads on first use rather than at startup. Batch tests cover the autoload chain; this is the interactive confirmation the spec asks for after each deferral batch.
-- Restart Emacs (daemon or standalone) so games-config is no longer pre-loaded from this session.
-- Confirm it's not loaded at startup:
-#+begin_src emacs-lisp
-(featurep 'games-config)
-#+end_src
-- M-x malyon — it should load games-config and the malyon package, then start interactive fiction (stories under ~/sync/org/text.games/).
-- M-x 2048-game — should start the 2048 puzzle.
-- Re-check (featurep 'games-config) — now non-nil.
-Expected: at startup (featurep 'games-config) is nil; both commands launch normally; after invoking one, games-config is loaded. If a command errors instead of launching, capture it and reopen the deferral as a TODO.
-*** VERIFY native-comp + gcmh survive a daemon restart cleanly
-What we're verifying: re-enabling JIT native compilation and switching GC to gcmh holds up across a full daemon restart and a real work session. The fix is live in the current daemon and a throwaway daemon launched clean, but the already-loaded modules only get natively compiled on a fresh start (a background async burst), and the old "Selecting deleted buffer" race needs a real GUI session to rule out on 30.2.
-- Restart the Emacs daemon (clean state): kill it and start fresh, or reboot.
-- Use Emacs normally for a while — the first session after restart triggers background native compilation of ~100 modules. Watch for any "Selecting deleted buffer" errors or compilation crashes (check the *Async-native-compile-log* buffer and comp-warnings.log).
-- After things settle, confirm the settings are live:
-#+begin_src emacs-lisp
-(list :jit native-comp-jit-compilation
- :gcmh gcmh-mode
- :gcmh-high gcmh-high-cons-threshold)
-#+end_src
-- Edit normally (completion, agenda, AI buffers) and notice whether the periodic GC jank is gone.
-Expected: restart is clean (no backtrace); the background native-comp burst finishes without "Selecting deleted buffer" errors; the form returns (:jit t :gcmh t :gcmh-high 1073741824); editing feels smoother with no frequent GC pauses. If the async race recurs on 30.2, capture the error and reopen as a TODO — the fallback is an AOT sweep or going back to JIT-off.
-*** VERIFY mu4e buffers are themed (headers, main, message view)
-What we're verifying: with the mu4e modes excluded from global font-lock, mu4e's manual face properties survive, so the buffers pick up the theme. The headers + main + view-headers are the ones global font-lock was stripping.
-- Restart Emacs (cleanest), or kill and reopen the mu4e buffers
-- Open mu4e, look at the headers list and the main menu
-- Open a message and read the body
-Expected: headers list shows unread/flagged/date/subject in their theme colors (mu4e-unread-face gold, mu4e-header-face green, etc.); the main menu and the message-view headers (From/To/Subject) are themed; the message body still renders correctly (gnus does the body, so it's unaffected). NOTE: a plain "g" refresh in an already-open *mu4e-headers* won't fix it on its own unless font-lock is off there; a restart is the reliable check.
-*** VERIFY C-c ; reaches the custom command family in a real terminal frame
-What we're verifying: the TTY mirror prefix C-c ; reaches the same cj/custom-keymap as the GUI C-; prefix, so the whole command family works in a terminal. The unit tests + a live daemon eval already confirm both prefixes resolve to the one keymap; this is the end-to-end in an actual TTY frame, which the batch harness can't drive.
-- Open a terminal Emacs frame: emacsclient -nw (or emacs -nw, or Emacs inside vterm/tmux)
-- Press C-c ; L (pearl), C-c ; a (AI), C-c ; g (calendar) — the same leaf keys you use under C-; in GUI
-- Confirm which-key shows the custom prefix under C-c ;
-Expected: each C-c ; <leaf> runs the same command its C-; <leaf> counterpart runs in GUI; which-key lists the family under C-c ;. C-; itself stays working in GUI frames (unchanged).
-*** VERIFY theme-studio gnus view package themes the article headers
-What we're verifying: gnus is now its own view package in theme-studio (it drives the mu4e article view), so the bright-green article headers can be themed and exported. #gnustest confirms the package is registered and its preview emits only real gnus faces; this is the visual read plus the live-green retirement.
-- Reload theme-studio (or make theme-studio-open)
-- Pick "gnus (mu4e article view)" from the view dropdown (sits among the g entries)
-- Confirm the preview shows a header block, an emphasized body, an 11-level quoted reply chain, and a signature
-- Theme a few gnus faces (e.g. gnus-header-name, gnus-header-from, gnus-cite-1) to obvious colors, export to WIP.json, then deploy
-#+begin_src sh :results output
-make -C /home/cjennings/.emacs.d deploy-wip
-#+end_src
-- Restart Emacs (or reload the theme), reopen a mu4e message
-Expected: the studio preview renders each gnus face in its theme color; after export + deploy, the *mu4e-article* From/Subject/To/Date headers show the themed colors instead of the gnus green defaults.
-*** VERIFY theme-studio markdown preview reads like a real README
-What we're verifying: selecting markdown-mode in the view dropdown shows a realistic README (not the generic face-name list), and the markdown faces render legibly in context. #mdtest already confirms the wiring + that every element's face is real; this is the visual read.
-- Reload theme-studio (or make theme-studio-open)
-- Pick "markdown-mode" from the view dropdown
-Expected: a README preview with headers, bold/italic, code, links, lists/checkboxes, blockquote, table, etc., each in its theme face. Clicking an element flashes its row in the faces table.
-*** VERIFY dashboard theming — banner gold, headings themed, items show per-filetype icons
-What we're verifying: with the dashboard out of global font-lock (Fix A) and file icons on (Fix C), the live dashboard shows the theme colors and icons. Eyeball it.
-- Open the dashboard (F1)
-Expected: the "Emacs:" banner title is gold, the "Projects:/Bookmarks:/Recent Files:" headings are themed blue, and the project/recent-file rows each show a colored per-filetype icon (org files greenish, dirs yellow; bookmarks a plain icon).
-*** VERIFY org-faces color set in theme-studio reaches the agenda
-What we're verifying: editing an org-faces-* row in theme-studio, exporting, and deploying lands the new color on the real agenda's keyword/priority. The build-theme -> deftheme half and the live org-todo-keyword-faces / org-priority-faces wiring are already verified mechanically; this confirms the visual end-to-end with a human eye.
-- Open theme-studio in Chrome and pick "org-faces" from the application dropdown (it sits beside elfeed and mu4e)
-- Confirm the preview shows the focused agenda block over the auto-dim block, and that the rows read "todo", "priority a", etc.
-- Edit org-faces-todo to an obviously different color (e.g. bright magenta) and export the theme to WIP.json
-#+begin_src sh :results output
-make -C /home/cjennings/.emacs.d deploy-wip
-#+end_src
-- Open the org agenda (or any todo.org buffer) and look at a TODO keyword
-Expected: the TODO keyword renders in the color just set; the priority cookies and other keywords keep their own colors; an unfocused window shows the dimmed variants.
-*** VERIFY slack keys are safe before slack loads
-What we're verifying: the C-; S slack keys don't error before slack has started, and the prefix shows in which-key. Fixed in modules/slack-config.el; restart to apply (not reloaded into the live session).
-- Restart Emacs but do NOT run cj/slack-start
-- Press C-; S Q (close all), and C-; S w / @ / # (these previously void-function'd or void-variable'd before load)
-- Press C-; S and check which-key shows the "slack" prefix
-Expected: C-; S Q reports "Closed 0 Slack buffers" with no error; w/@/# either run or autoload slack cleanly (no void-function); the which-key popup lists the slack prefix.
-*** VERIFY ERC fires one mention notification and lists real servers
-What we're verifying: a mention pops a single desktop notification (not two), and cj/erc-connected-servers lists only live server connections. Fixed in modules/erc-config.el; takes effect after an Emacs restart (not reloaded into the live IRC session).
-- Restart Emacs and reconnect ERC
-- Have someone mention your nick in a channel (or trigger erc-text-matched-hook)
-- Run M-x cj/erc-connected-servers with one server connected and a few channels open
-Expected: exactly one desktop notification per mention; cj/erc-connected-servers reports just the connected server(s), not every channel/query buffer.
-*** VERIFY modeline still shows the git branch and state
-What we're verifying: the VC-cache simplification didn't change what the modeline shows on a normal repo. Fixed in modules/modeline-config.el (live in the daemon after reload).
-- Open a file inside a git repo
-- Glance at the mode-line VC segment
-Expected: the branch name and state still render as before (e.g. "main" with the usual state face). The change only drops a per-render stat and guards against git errors; normal display is unchanged.
-*** VERIFY info-mode open is non-destructive and cancels cleanly
-What we're verifying: opening a .info file no longer auto-kills the buffer, and the explicit cj/open-with-info-mode prompt cancels cleanly on decline. Fixed in modules/help-config.el; stale daemon state already cleared, so this also survives a fresh restart.
-- find-file a .info file (e.g. one under elpa) — it should open as an ordinary buffer, not vanish into Info
-- In that buffer, edit something, then M-x cj/open-with-info-mode; at the save prompt answer no
-- Repeat M-x cj/open-with-info-mode on an unmodified .info buffer
-Expected: find-file leaves the buffer intact (no auto-kill); declining the save prompt prints "Operation canceled" with no "No catch for tag" error; on an unmodified buffer it opens the file in Info.
-*** VERIFY dwim-shell zip/backup/menu-key behave
-What we're verifying: single-file zip makes a valid <name>.zip, the dated backup gets a real timestamp, and the dwim-shell menu is reachable on M-D in plain dired. Fixed in modules/dwim-shell-config.el, reloaded into the daemon.
-- In dired, mark a single file, run the dwim-shell menu (M-D), pick Zip
-- Mark a file, run the menu, pick "Backup with date"
-- Open a plain dired buffer (not dirvish) and press M-D
-Expected: zip produces foo.zip (a valid archive, openable); backup produces foo.ext.YYYYMMDD_HHMMSS.bak with a real date; M-D opens the dwim-shell command menu in plain dired (before the fix it did nothing there).
-*** VERIFY markdown live preview renders in the browser
-What we're verifying: F2 in a markdown buffer runs the custom cj/markdown-preview (not markdown-mode's own command) and the impatient-mode strapdown preview actually renders. Fixed in modules/markdown-config.el, reloaded into the daemon.
-- Open a .md file with some markdown content
-- M-x cj/markdown-preview-server-start (starts simple-httpd on :8080)
-- Press F2 in the markdown buffer
-Expected: a browser opens http://localhost:8080/imp showing the rendered markdown, and edits to the buffer update the preview live. Pressing F2 before starting the server gives a user-error telling you to start it.
-*** VERIFY orderless matching works inside a vertico session
-What we're verifying: vertico-prescient no longer overrides completion-styles, so orderless's space-separated, out-of-order matching is live in the minibuffer (prescient still sorts). Fixed in modules/selection-framework.el, applied live in the daemon.
-- Run a command with a vertico minibuffer (e.g. M-x, or C-x b)
-- Type two space-separated fragments out of order, e.g. "mode buf" to match "switch-to-buffer-other-... mode" style candidates
-Expected: candidates match on both fragments regardless of order (orderless), and the ordering still reflects prescient frecency. Before the fix, space-separated out-of-order input would not match.
-*** VERIFY C-; b d diffs, C-; b D deletes
-What we're verifying: the buffer-and-file keymap now puts diff on the easy lowercase key and the destructive delete on the capital. Swapped in modules/custom-buffer-file.el and re-bound live in the daemon.
-- Open a file buffer and edit it without saving
-- Press C-; b d
-- Press C-; b D, then cancel at the delete confirmation
-Expected: C-; b d runs the diff (buffer vs saved file); C-; b D starts delete-buffer-and-file (offers to delete the file). Before the swap these were reversed.
-*** TODO C-s C-s repeats the last search
-What we're verifying: the second consecutive C-s repeats the previous consult-line search instead of erroring "No Vertico session". Fix in modules/selection-framework.el (vertico-repeat-save now on minibuffer-setup-hook), live in the daemon.
-- Press C-s, type a search term, RET to dismiss (or just narrow then exit)
-- Press C-s again, then C-s a second time without any command in between
-Expected: the second C-s reopens the last search (vertico-repeat) rather than signalling "No Vertico session".
*** TODO reconcile-open-repos includes dot-named repos
What we're verifying: M-P (reconcile open repos) now visits repos whose directory name has a dot (mcp.el, capture.el, etc.), which the old "^[^.]+$" filter silently skipped. Fix in modules/reconcile-open-repos.el, live in the daemon; live-daemon check already confirmed discovery, this is the through-the-command spot-check.
- Run M-P (or M-x cj/reconcile-open-repos)
- Watch the per-repo progress / final summary
Expected: dot-named repos under ~/code (mcp.el, gptel-mcp.el, capture.el, google-contacts.el, …) appear in the reconciliation pass, not just dot-free ones.
-*** 2026-06-15 Mon @ 12:10:06 -0500 org-capture popup single-Task into inbox verified
-Craig confirmed: Super+Shift+N pops straight into a Task capture (no menu), single full-frame window, files under "Inbox" in ~/org/roam/inbox.org, and the frame closes cleanly. Passed.
-*** TODO Lock screen actually locks on Wayland
-What we're verifying: C-; ! l locks the screen on Wayland. slock (X11-only) never worked here; the locker now runs loginctl lock-session, which logind turns into a Lock signal that hypridle handles by running hyprlock — the same path idle/sleep locking already uses. Fix in modules/system-commands.el, live in the daemon.
-- Press C-; ! l (or run M-x cj/system-cmd-lock)
-- The screen should lock with hyprlock
-- Unlock with your password
-Expected: the screen locks immediately and unlocks with your password. (Before the fix it printed "Running lockscreen-cmd..." and nothing happened.)
-*** TODO Irreversible actions require a typed "yes" after a daemon restart
-What we're verifying: the strong-confirm tier is restored for irreversible actions. The global (fset 'yes-or-no-p 'y-or-n-p) was removed and those sites now call cj/confirm-strong, which forces a typed "yes"/"no". The fset is baked into the running daemon and can't be cleared from Lisp, so this only takes effect after a restart. Ordinary yes-or-no-p prompts stay single-key (use-short-answers t).
-- Restart the Emacs daemon (clean state)
-- Trigger an irreversible action, e.g. M-x cj/system-cmd-shutdown (then abort), or attempt to overwrite a file via the rename/move commands
-Expected: the irreversible prompt requires typing the full word "yes" (not a single y); a benign yes-or-no-p prompt elsewhere still accepts a single keystroke.
-*** 2026-06-11 Thu @ 18:29:39 -0500 Verified UI-face preview and contrast survive a ground bg change
-Craig walked the repro: mode-line with its own fg/bg kept its preview bg and ratio through a ground change; ground-dependent rows re-rated; package-faces contrast column updated. Pass. Closed the [#A] contrast-cell and [#B] preview-bg parents.
-*** 2026-06-11 Thu @ 18:29:39 -0500 Verified seeded package-face defaults, with steel tuning
-Craig read org/magit/elfeed against the ground. Pass with tuning: steel reads a bit dark — flipped to steel+1 on magit (better), but org wanted darker; these are updated selections, NOT final — he expects to adjust many more before the theme ships. His export saved to scripts/theme-studio/theme.json (replaced the 2026-06-09 state, prior version in git at 4f2d00eb). Side find: the org preview's heading-three ↔ headline-todo flash linkage is cross-wired — filed as its own bug task.
-*** 2026-06-11 Thu @ 18:29:39 -0500 Verified large face tables stay usable
-Craig scrolled the org table, filtered on "agenda", reassigned a face — grouping, narrowing, and live preview update all behaved. Pass.
-*** 2026-06-11 Thu @ 18:29:39 -0500 Verified perceptual readouts in the picker
-Craig validated the readouts against computed reference values (default fg #f0fef0 on ground #000000: APCA Lc -104.7 / WCAG 20.14; keyword blue #67809c: Lc -33.7 / WCAG 5.14 — negative polarity correct for light-on-dark). Legible, uncrowded. Pass. Side find filed separately: the picker panel itself blends into the page background ([#C] picker-visibility task).
-*** 2026-06-11 Thu @ 18:29:39 -0500 Verified ΔE warnings read clearly
-Craig built a near-duplicate pair and a well-spread palette: the close pair was named with its ΔE, sorted closest-first with the cap behaving; no warning on the spread palette. Pass.
-*** TODO OKLCH editor feels right
-What we're verifying: the OKLCH sliders / C×L plane edit cleanly and clamping is visible.
-- Switch the picker to OKLCH mode and drag L, then C, then H
-- Push chroma past the sRGB gamut, then toggle the AA/AAA mask
-Expected: each axis moves independently; the C×L plane (once 4b lands) opens on the current color; "chroma clamped to sRGB" shows on clamp; toggling the mask does not reset OKLCH mode.
-*** TODO Generated ramp harmonizes
-What we're verifying: a ramp generated from a base color reads as one family, not a grab-bag (the aesthetic the math is meant to produce).
-- Open =scripts/theme-studio/theme-studio.html= in Chrome
-- Pick a mid-lightness base swatch (e.g. a blue) and generate its ramp at the defaults
-- Read the row of steps left to right, then try a near-black and a near-white base
-Expected: the steps share an obvious hue and step evenly in lightness; the chroma-ease keeps the extreme steps from going muddy or garish; nothing looks like it belongs to a different color.
*** TODO Safe-lightness guidance reads clearly
What we're verifying: the L_max marker and unsafe-band shade are legible and land in the right place when editing a covered face.
- Open the picker in OKLCH mode on region (or hl-line), with syntax colors assigned
@@ -324,31 +80,12 @@ What we're verifying: a background tint the tool calls safe really keeps every t
- Build the theme and load it in Emacs, open a code buffer with varied syntax, and select a region spanning many token colors
- Read every token through the region highlight, paying attention to the limiting foreground the tool named
Expected: every token stays readable over the tint, including the limiting one; a tint pushed just past L_max (readout FAIL) shows a visibly strained or unreadable token, confirming the floor matches reality.
-*** TODO Color families group the way the eye reads them
-What we're verifying: the OKLCH hue clustering (25° gap) splits and merges families the way you'd expect, and renaming never moves a color.
-- Open =scripts/theme-studio/theme-studio.html= in Chrome and load a real theme (e.g. sterling)
-- Read the strips top to bottom: are "the blues" one strip, "the greens" another, neutrals and ground pinned at the top
-- Find a pair you'd consider one family that landed in two strips (or two you'd consider separate that merged)
-- Rename any swatch to something absurd and confirm it stays in the same strip
-Expected: families match your mental grouping; the few that don't are the cue to revisit the 25° gap; renaming never regroups.
*** TODO Regenerate-replace reads as deliberate
What we're verifying: the count control clearly signals it rewrites the whole family, so replacing hand-added same-hue colors isn't a surprise.
- Add two unrelated colors at a similar hue so they share a strip
- Set that strip's count to 2
- Watch what happens to the two colors
Expected: the strip becomes a clean base±2 ramp, the two loose colors are gone, and the control made it obvious that's what it would do before you committed.
-*** TODO Removed-step references read clearly as "(gone)"
-What we're verifying: lowering a family's count leaves a referencing face visibly stale, not silently re-pointed.
-- Assign a UI or syntax element to an outer step of a family (e.g. region = a blue+3)
-- Lower that family's count to 2 so blue+3 disappears
-- Read the assignment's dropdown
-Expected: the dropdown shows "(gone)" for the removed step, never a silent jump to a different color; re-pointing it is a deliberate choice.
-*** TODO Calibre bookmark default name is "Author, Title"
-What we're verifying: a new nov bookmark takes the "Author, Title" form parsed from the filename, not the raw EPUB filename.
-- Open an EPUB in Calibre (nov buffer).
-- Hit m to set a bookmark.
-Expected: the default bookmark name is "Author, Title" (underscores stripped, colon restored), e.g. "Agatha Christie, The A.B.C. Murders".
-
*** TODO Calibre curated ? menu and docked description
What we're verifying: the curated ? transient, the docked description, and the full dispatch all work in a live calibredb buffer.
- In a calibredb search buffer, press ? and confirm the curated menu (library / filter / sort / open / describe) appears.
@@ -368,26 +105,46 @@ What we're verifying: the suppression predicate gates the toast when you're read
- Have the same sender message you again.
Expected: the message renders in the buffer, but no desktop toast appears.
-*** TODO Project-aware capture files into the right todo.org
+*** DONE Project-aware capture files into the right todo.org
+CLOSED: [2026-06-24 Wed 11:48]
What we're verifying: C-c c t and C-c c b file into the current projectile project's todo.org under its "<Project> Open Work" header, and fall back to the global inbox outside a project.
- Inside a projectile project that has a todo.org, run C-c c t (Task), capture a test entry, and confirm it lands under "<Project> Open Work".
- Run C-c c b (Bug) similarly and confirm it lands as "* TODO [#C] ..." under the same header.
- Run a capture from outside any project (or a project with no todo.org) and confirm the global-inbox fallback with a warning.
Expected: in-project captures land in that project's Open Work; out-of-project captures fall back to the global inbox with a warning.
-*** VERIFY Dirvish d duplicates, D force-deletes with a confirm
-What we're verifying: in dirvish, d now duplicates the file at point (delete-to-trash removed), and D force-deletes the marked files via sudo rm -rf after a yes-or-no-p naming the targets. The pure command builder is unit-tested; this is the live keypress plus the guarded destructive path.
-- Open dirvish on a scratch directory holding a couple of throwaway files
-- Put point on a file and press d — confirm a "<name>-copy.<ext>" appears (a duplicate, nothing deleted)
-- Mark one or two throwaway files, press D, and read the "Force-delete (sudo rm -rf, NO undo): <names>?" prompt
-- Answer no first (confirm nothing happens), then press D again and answer yes
-- Note whether sudo prompts for a password and whether the file actually disappears
-Expected: d duplicates; D names the exact targets and only deletes on yes; the files are gone with no trash copy. If sudo needs a password that shell-command can't supply, flag it — the delete may need to route through a tty instead.
-*** 2026-06-20 Sat @ 22:11:00 -0400 F9 agent toggle no longer shrinks after a C-; b pull-away
-Craig confirmed in his live GUI frame: the agent window keeps its height across repeated F9 toggles after a C-; b pull-away, even under the WIP theme's near-zero mode-line-inactive. The total-height capture/replay fix holds (dbee95ae).
-*** 2026-06-20 Sat @ 22:11:00 -0400 F9 toggle preserves all windows in a 3-window layout
-Craig confirmed in his live GUI frame: toggling the agent off then on in a 3-window layout returns the same three windows — both working windows survive and the agent re-splits its own bottom strip. The reversible-toggle fix holds (64916462).
-*** 2026-06-24 Wed @ 00:37:18 -0400 C-<up> copy-mode scroll verified in a real terminal
-Craig confirmed in a live terminal: C-<up> enters copy-mode and scrolls up, repeated C-<up> keep scrolling without resetting, the other modified arrows are left alone (C-<left>/C-<right> still do readline word-motion at the prompt). The C-<up>-only fix + already-in-copy guard (commit 7696ff76) holds.
+*** VERIFY C-c ; reaches the custom command family in a real terminal frame
+What we're verifying: the TTY mirror prefix C-c ; reaches the same cj/custom-keymap as the GUI C-; prefix, so the whole command family works in a terminal. The unit tests + a live daemon eval already confirm both prefixes resolve to the one keymap; this is the end-to-end in an actual TTY frame, which the batch harness can't drive.
+- Open a terminal Emacs frame: emacsclient -nw (or emacs -nw, or Emacs inside vterm/tmux)
+- Press C-c ; L (pearl), C-c ; a (AI), C-c ; g (calendar) — the same leaf keys you use under C-; in GUI
+- Confirm which-key shows the custom prefix under C-c ;
+Expected: each C-c ; <leaf> runs the same command its C-; <leaf> counterpart runs in GUI; which-key lists the family under C-c ;. C-; itself stays working in GUI frames (unchanged).
+*** VERIFY org-faces color set in theme-studio reaches the agenda
+What we're verifying: editing an org-faces-* row in theme-studio, exporting, and deploying lands the new color on the real agenda's keyword/priority. The build-theme -> deftheme half and the live org-todo-keyword-faces / org-priority-faces wiring are already verified mechanically; this confirms the visual end-to-end with a human eye.
+- Open theme-studio in Chrome and pick "org-faces" from the application dropdown (it sits beside elfeed and mu4e)
+- Confirm the preview shows the focused agenda block over the auto-dim block, and that the rows read "todo", "priority a", etc.
+- Edit org-faces-todo to an obviously different color (e.g. bright magenta) and export the theme to WIP.json
+#+begin_src sh :results output
+make -C /home/cjennings/.emacs.d deploy-wip
+#+end_src
+- Open the org agenda (or any todo.org buffer) and look at a TODO keyword
+Expected: the TODO keyword renders in the color just set; the priority cookies and other keywords keep their own colors; an unfocused window shows the dimmed variants.
+*** VERIFY ERC fires one mention notification and lists real servers
+What we're verifying: a mention pops a single desktop notification (not two), and cj/erc-connected-servers lists only live server connections. Fixed in modules/erc-config.el; takes effect after an Emacs restart (not reloaded into the live IRC session).
+- Restart Emacs and reconnect ERC
+- Have someone mention your nick in a channel (or trigger erc-text-matched-hook)
+- Run M-x cj/erc-connected-servers with one server connected and a few channels open
+Expected: exactly one desktop notification per mention; cj/erc-connected-servers reports just the connected server(s), not every channel/query buffer.
+*** VERIFY dwim-shell zip/backup/menu-key behave
+What we're verifying: single-file zip makes a valid <name>.zip, the dated backup gets a real timestamp, and the dwim-shell menu is reachable on M-D in plain dired. Fixed in modules/dwim-shell-config.el, reloaded into the daemon.
+- In dired, mark a single file, run the dwim-shell menu (M-D), pick Zip
+- Mark a file, run the menu, pick "Backup with date"
+- Open a plain dired buffer (not dirvish) and press M-D
+Expected: zip produces foo.zip (a valid archive, openable); backup produces foo.ext.YYYYMMDD_HHMMSS.bak with a real date; M-D opens the dwim-shell command menu in plain dired (before the fix it did nothing there).
+*** VERIFY orderless matching works inside a vertico session
+What we're verifying: vertico-prescient no longer overrides completion-styles, so orderless's space-separated, out-of-order matching is live in the minibuffer (prescient still sorts). Fixed in modules/selection-framework.el, applied live in the daemon.
+- Run a command with a vertico minibuffer (e.g. M-x, or C-x b)
+- Type two space-separated fragments out of order, e.g. "mode buf" to match "switch-to-buffer-other-... mode" style candidates
+Expected: candidates match on both fragments regardless of order (orderless), and the ordering still reflects prescient frecency. Before the fix, space-separated out-of-order input would not match.
*** VERIFY face-name buttons open describe-face
What we're verifying: the face names in the Face Diagnosis report are live buttons. The button text properties (action + face data) are confirmed in the daemon; this is the click/RET confirmation.
- Put point on themed text and run =C-h F= (=cj/describe-face-at-point=).
@@ -406,14 +163,19 @@ What we're verifying: the cmail trash-folder + per-message refile fix (shipped 2
- On a cmail message, press =r= (refile) then =x=; confirm it lands in cmail/Archive.
- On a gmail (or dmail) message, press =r=.
Expected: cmail trash → cmail/Trash, cmail refile → cmail/Archive, both real maildirs mbsync syncs. Refile on gmail/dmail signals a user-error (no move) rather than offering to create an unsynced phantom folder. If any move targets a folder mbsync doesn't sync, capture it and reopen.
-*** VERIFY nerd-icons colors are theme-driven (legend + live icons)
-What we're verifying: the nerd-icons v1 feature reads right end to end. The Python/Node/browser gates pass; this is the visual confirmation the gates can't make — the legend pane and the real per-filetype icon colors after the tint removal.
-- In theme-studio, open the nerd-icons pane: the legend should show each filetype's real nerd-font glyph in its mapped color (el purple, py dark-blue, dir yellow, …), with the 34 color faces editable on the left.
-- Recolor a face (e.g. nerd-icons-purple) and confirm every legend row mapped to it repaints immediately.
-- Restart Emacs (the running daemon still has the old darkgoldenrod tint baked into the faces until restart).
-- In a fresh frame, look at icons in completing-read (find-file), dirvish, the dashboard, and ibuffer.
-Expected: the legend renders glyphs in their assigned colors and recolor repaints live; after restart, file/dir/buffer icons show nerd-icons' per-filetype multicolor palette driven by the theme (not a uniform darkgoldenrod), and directory icons are yellow. If icons are still uniform or uncolored, capture it and reopen.
-*** VERIFY ai-term wrap-teardown + shutdown end-to-end
+*** STALLED markdown live preview renders in the browser
+What we're verifying: F2 in a markdown buffer runs the custom cj/markdown-preview (not markdown-mode's own command) and the impatient-mode strapdown preview actually renders. Fixed in modules/markdown-config.el, reloaded into the daemon.
+- Open a .md file with some markdown content
+- M-x cj/markdown-preview-server-start (starts simple-httpd on :8080)
+- Press F2 in the markdown buffer
+Expected: a browser opens http://localhost:8080/imp showing the rendered markdown, and edits to the buffer update the preview live.
+Pressing F2 before starting the server gives a user-error telling you to start it.
+
+#+begin_src cj: comment
+ we should simply have the server start if it's not already started.
+#+end_src
+
+*** STALLED ai-term wrap-teardown + shutdown end-to-end
What we're verifying: the three headless functions drive the rulesets wrap-it-up workflow correctly, including the real tmux/shutdown side effects the ERT tests can't exercise. The .emacs.d functions are in and unit-verified; the rulesets half (workflow + Stop hook) is already built. Test the countdown with a stubbed shutdown command first — do not power off during the check.
#+begin_src emacs-lisp
;; temporarily stub the shutdown so the countdown can't power off:
@@ -429,6 +191,191 @@ What we're verifying: the three headless functions drive the rulesets wrap-it-up
#+end_src
Expected: teardown removes exactly the right session/buffer and restores layout; the with-summary variants keep the buffer; the multi-session shutdown refuses; the sole-session countdown renders, cancels on C-g, and fires only at zero. If any step misbehaves, capture it and reopen. Once the stubbed run looks right, a single real shutdown test confirms the live path.
+#+begin_src cj: comment
+ I would like to test this in separate steps naturally as I need them across sessions. please add one child task for each item to test above.
+#+end_src
+
+*** 2026-06-15 Mon @ 12:10:06 -0500 org-capture popup single-Task into inbox verified
+Craig confirmed: Super+Shift+N pops straight into a Task capture (no menu), single full-frame window, files under "Inbox" in ~/org/roam/inbox.org, and the frame closes cleanly. Passed.
+*** 2026-06-11 Thu @ 18:29:39 -0500 Verified UI-face preview and contrast survive a ground bg change
+Craig walked the repro: mode-line with its own fg/bg kept its preview bg and ratio through a ground change; ground-dependent rows re-rated; package-faces contrast column updated. Pass. Closed the [#A] contrast-cell and [#B] preview-bg parents.
+*** 2026-06-11 Thu @ 18:29:39 -0500 Verified seeded package-face defaults, with steel tuning
+Craig read org/magit/elfeed against the ground. Pass with tuning: steel reads a bit dark — flipped to steel+1 on magit (better), but org wanted darker; these are updated selections, NOT final — he expects to adjust many more before the theme ships. His export saved to scripts/theme-studio/theme.json (replaced the 2026-06-09 state, prior version in git at 4f2d00eb). Side find: the org preview's heading-three ↔ headline-todo flash linkage is cross-wired — filed as its own bug task.
+*** 2026-06-11 Thu @ 18:29:39 -0500 Verified large face tables stay usable
+Craig scrolled the org table, filtered on "agenda", reassigned a face — grouping, narrowing, and live preview update all behaved. Pass.
+*** 2026-06-11 Thu @ 18:29:39 -0500 Verified perceptual readouts in the picker
+Craig validated the readouts against computed reference values (default fg #f0fef0 on ground #000000: APCA Lc -104.7 / WCAG 20.14; keyword blue #67809c: Lc -33.7 / WCAG 5.14 — negative polarity correct for light-on-dark). Legible, uncrowded. Pass. Side find filed separately: the picker panel itself blends into the page background ([#C] picker-visibility task).
+*** 2026-06-11 Thu @ 18:29:39 -0500 Verified ΔE warnings read clearly
+Craig built a near-duplicate pair and a well-spread palette: the close pair was named with its ΔE, sorted closest-first with the cap behaving; no warning on the spread palette. Pass.
+*** 2026-06-20 Sat @ 22:11:00 -0400 F9 agent toggle no longer shrinks after a C-; b pull-away
+Craig confirmed in his live GUI frame: the agent window keeps its height across repeated F9 toggles after a C-; b pull-away, even under the WIP theme's near-zero mode-line-inactive. The total-height capture/replay fix holds (dbee95ae).
+*** 2026-06-20 Sat @ 22:11:00 -0400 F9 toggle preserves all windows in a 3-window layout
+Craig confirmed in his live GUI frame: toggling the agent off then on in a 3-window layout returns the same three windows — both working windows survive and the agent re-splits its own bottom strip. The reversible-toggle fix holds (64916462).
+*** 2026-06-24 Wed @ 00:37:18 -0400 C-<up> copy-mode scroll verified in a real terminal
+Craig confirmed in a live terminal: C-<up> enters copy-mode and scrolls up, repeated C-<up> keep scrolling without resetting, the other modified arrows are left alone (C-<left>/C-<right> still do readline word-motion at the prompt). The C-<up>-only fix + already-in-copy guard (commit 7696ff76) holds.
+*** DONE theme-studio markdown preview reads like a real README
+CLOSED: [2026-06-24 Wed 11:47]
+What we're verifying: selecting markdown-mode in the view dropdown shows a realistic README (not the generic face-name list), and the markdown faces render legibly in context. #mdtest already confirms the wiring + that every element's face is real; this is the visual read.
+- Reload theme-studio (or make theme-studio-open)
+- Pick "markdown-mode" from the view dropdown
+Expected: a README preview with headers, bold/italic, code, links, lists/checkboxes, blockquote, table, etc., each in its theme face. Clicking an element flashes its row in the faces table.
+*** DONE C-s C-s repeats the last search
+CLOSED: [2026-06-24 Wed 11:37]
+What we're verifying: the second consecutive C-s repeats the previous consult-line search instead of erroring "No Vertico session". Fix in modules/selection-framework.el (vertico-repeat-save now on minibuffer-setup-hook), live in the daemon.
+- Press C-s, type a search term, RET to dismiss (or just narrow then exit)
+- Press C-s again, then C-s a second time without any command in between
+Expected: the second C-s reopens the last search (vertico-repeat) rather than signalling "No Vertico session".
+*** DONE Irreversible actions require a typed "yes" after a daemon restart
+CLOSED: [2026-06-24 Wed 11:36]
+What we're verifying: the strong-confirm tier is restored for irreversible actions. The global (fset 'yes-or-no-p 'y-or-n-p) was removed and those sites now call cj/confirm-strong, which forces a typed "yes"/"no". The fset is baked into the running daemon and can't be cleared from Lisp, so this only takes effect after a restart. Ordinary yes-or-no-p prompts stay single-key (use-short-answers t).
+- Restart the Emacs daemon (clean state)
+- Trigger an irreversible action, e.g. M-x cj/system-cmd-shutdown (then abort), or attempt to overwrite a file via the rename/move commands
+Expected: the irreversible prompt requires typing the full word "yes" (not a single y); a benign yes-or-no-p prompt elsewhere still accepts a single keystroke.
+*** DONE Calibre bookmark default name is "Author, Title"
+CLOSED: [2026-06-24 Wed 10:56]
+What we're verifying: a new nov bookmark takes the "Author, Title" form parsed from the filename, not the raw EPUB filename.
+- Open an EPUB in Calibre (nov buffer).
+- Hit m to set a bookmark.
+Expected: the default bookmark name is "Author, Title" (underscores stripped, colon restored), e.g. "Agatha Christie, The A.B.C. Murders".
+
+*** DONE theme-studio gnus view package themes the article headers
+CLOSED: [2026-06-24 Wed 11:29]
+What we're verifying: gnus is now its own view package in theme-studio (it drives the mu4e article view), so the bright-green article headers can be themed and exported. #gnustest confirms the package is registered and its preview emits only real gnus faces; this is the visual read plus the live-green retirement.
+- Reload theme-studio (or make theme-studio-open)
+- Pick "gnus (mu4e article view)" from the view dropdown (sits among the g entries)
+- Confirm the preview shows a header block, an emphasized body, an 11-level quoted reply chain, and a signature
+- Theme a few gnus faces (e.g. gnus-header-name, gnus-header-from, gnus-cite-1) to obvious colors, export to WIP.json, then deploy
+#+begin_src sh :results output
+make -C /home/cjennings/.emacs.d deploy-wip
+#+end_src
+- Restart Emacs (or reload the theme), reopen a mu4e message
+Expected: the studio preview renders each gnus face in its theme color; after export + deploy, the *mu4e-article* From/Subject/To/Date headers show the themed colors instead of the gnus green defaults.
+*** DONE dashboard theming — banner gold, headings themed, items show per-filetype icons
+CLOSED: [2026-06-24 Wed 11:29]
+What we're verifying: with the dashboard out of global font-lock (Fix A) and file icons on (Fix C), the live dashboard shows the theme colors and icons. Eyeball it.
+- Open the dashboard (F1)
+Expected: the "Emacs:" banner title is gold, the "Projects:/Bookmarks:/Recent Files:" headings are themed blue, and the project/recent-file rows each show a colored per-filetype icon (org files greenish, dirs yellow; bookmarks a plain icon).
+*** DONE info-mode open is non-destructive and cancels cleanly
+CLOSED: [2026-06-24 Wed 11:41]
+What we're verifying: opening a .info file no longer auto-kills the buffer, and the explicit cj/open-with-info-mode prompt cancels cleanly on decline. Fixed in modules/help-config.el; stale daemon state already cleared, so this also survives a fresh restart.
+- find-file a .info file (e.g. one under elpa) — it should open as an ordinary buffer, not vanish into Info
+- In that buffer, edit something, then M-x cj/open-with-info-mode; at the save prompt answer no
+- Repeat M-x cj/open-with-info-mode on an unmodified .info buffer
+Expected: find-file leaves the buffer intact (no auto-kill); declining the save prompt prints "Operation canceled" with no "No catch for tag" error; on an unmodified buffer it opens the file in Info.
+*** DONE C-; b d diffs, C-; b D deletes
+CLOSED: [2026-06-24 Wed 11:43]
+What we're verifying: the buffer-and-file keymap now puts diff on the easy lowercase key and the destructive delete on the capital. Swapped in modules/custom-buffer-file.el and re-bound live in the daemon.
+- Open a file buffer and edit it without saving
+- Press C-; b d
+- Press C-; b D, then cancel at the delete confirmation
+Expected: C-; b d runs the diff (buffer vs saved file); C-; b D starts delete-buffer-and-file (offers to delete the file). Before the swap these were reversed.
+*** DONE nerd-icons colors are theme-driven (legend + live icons)
+CLOSED: [2026-06-24 Wed 11:35]
+What we're verifying: the nerd-icons v1 feature reads right end to end. The Python/Node/browser gates pass; this is the visual confirmation the gates can't make — the legend pane and the real per-filetype icon colors after the tint removal.
+- In theme-studio, open the nerd-icons pane: the legend should show each filetype's real nerd-font glyph in its mapped color (el purple, py dark-blue, dir yellow, …), with the 34 color faces editable on the left.
+- Recolor a face (e.g. nerd-icons-purple) and confirm every legend row mapped to it repaints immediately.
+- Restart Emacs (the running daemon still has the old darkgoldenrod tint baked into the faces until restart).
+- In a fresh frame, look at icons in completing-read (find-file), dirvish, the dashboard, and ibuffer.
+Expected: the legend renders glyphs in their assigned colors and recolor repaints live; after restart, file/dir/buffer icons show nerd-icons' per-filetype multicolor palette driven by the theme (not a uniform darkgoldenrod), and directory icons are yellow. If icons are still uniform or uncolored, capture it and reopen.
+*** DONE ai-term keybindings land on C-; a + M-SPC
+CLOSED: [2026-06-24 Wed 10:36]
+What we're verifying: the relocated ai-term keys work in a live frame, including from inside an agent buffer, and the no-agent fallback launches the picker.
+- Press M-SPC from a normal buffer with at least one agent open.
+- Press M-SPC again from inside an agent buffer (ghostel).
+- With no agent running, press M-SPC.
+- Walk C-; a a, C-; a s, C-; a n, C-; a k (which-key should show the ai-term menu under C-; a).
+- Press F9, C-F9, s-F9, M-F9.
+Expected: M-SPC swaps to the next agent (rotating, wrapping) both from a normal buffer and from inside an agent. With no agent running, M-SPC opens the project picker rather than erroring. C-; a a toggles the most-recent agent, s opens the picker, n swaps, k closes. The F9 family does nothing (unbound). Note: the running daemon still has gptel in memory from before the archive, so a full Emacs restart is the clean confirmation that nothing regressed at startup.
+*** DONE deferred game commands still work after a restart (load-graph Phase 4)
+CLOSED: [2026-06-24 Wed 10:37]
+What we're verifying: with games-config no longer eagerly required, malyon and 2048-game still launch from a fresh Emacs, and games-config loads on first use rather than at startup. Batch tests cover the autoload chain; this is the interactive confirmation the spec asks for after each deferral batch.
+- Restart Emacs (daemon or standalone) so games-config is no longer pre-loaded from this session.
+- Confirm it's not loaded at startup:
+#+begin_src emacs-lisp
+(featurep 'games-config)
+#+end_src
+- M-x malyon — it should load games-config and the malyon package, then start interactive fiction (stories under ~/sync/org/text.games/).
+- M-x 2048-game — should start the 2048 puzzle.
+- Re-check (featurep 'games-config) — now non-nil.
+Expected: at startup (featurep 'games-config) is nil; both commands launch normally; after invoking one, games-config is loaded. If a command errors instead of launching, capture it and reopen the deferral as a TODO.
+*** DONE native-comp + gcmh survive a daemon restart cleanly
+CLOSED: [2026-06-24 Wed 10:37]
+What we're verifying: re-enabling JIT native compilation and switching GC to gcmh holds up across a full daemon restart and a real work session. The fix is live in the current daemon and a throwaway daemon launched clean, but the already-loaded modules only get natively compiled on a fresh start (a background async burst), and the old "Selecting deleted buffer" race needs a real GUI session to rule out on 30.2.
+- Restart the Emacs daemon (clean state): kill it and start fresh, or reboot.
+- Use Emacs normally for a while — the first session after restart triggers background native compilation of ~100 modules. Watch for any "Selecting deleted buffer" errors or compilation crashes (check the *Async-native-compile-log* buffer and comp-warnings.log).
+- After things settle, confirm the settings are live:
+#+begin_src emacs-lisp
+(list :jit native-comp-jit-compilation
+ :gcmh gcmh-mode
+ :gcmh-high gcmh-high-cons-threshold)
+#+end_src
+- Edit normally (completion, agenda, AI buffers) and notice whether the periodic GC jank is gone.
+Expected: restart is clean (no backtrace); the background native-comp burst finishes without "Selecting deleted buffer" errors; the form returns (:jit t :gcmh t :gcmh-high 1073741824); editing feels smoother with no frequent GC pauses. If the async race recurs on 30.2, capture the error and reopen as a TODO — the fallback is an AOT sweep or going back to JIT-off.
+*** DONE mu4e buffers are themed (headers, main, message view)
+CLOSED: [2026-06-24 Wed 10:38]
+What we're verifying: with the mu4e modes excluded from global font-lock, mu4e's manual face properties survive, so the buffers pick up the theme. The headers + main + view-headers are the ones global font-lock was stripping.
+- Restart Emacs (cleanest), or kill and reopen the mu4e buffers
+- Open mu4e, look at the headers list and the main menu
+- Open a message and read the body
+Expected: headers list shows unread/flagged/date/subject in their theme colors (mu4e-unread-face gold, mu4e-header-face green, etc.); the main menu and the message-view headers (From/To/Subject) are themed; the message body still renders correctly (gnus does the body, so it's unaffected). NOTE: a plain "g" refresh in an already-open *mu4e-headers* won't fix it on its own unless font-lock is off there; a restart is the reliable check.
+*** DONE slack keys are safe before slack loads
+CLOSED: [2026-06-24 Wed 10:44]
+What we're verifying: the C-; S slack keys don't error before slack has started, and the prefix shows in which-key. Fixed in modules/slack-config.el; restart to apply (not reloaded into the live session).
+- Restart Emacs but do NOT run cj/slack-start
+- Press C-; S Q (close all), and C-; S w / @ / # (these previously void-function'd or void-variable'd before load)
+- Press C-; S and check which-key shows the "slack" prefix
+Expected: C-; S Q reports "Closed 0 Slack buffers" with no error; w/@/# either run or autoload slack cleanly (no void-function); the which-key popup lists the slack prefix.
+*** DONE modeline still shows the git branch and state
+CLOSED: [2026-06-24 Wed 10:45]
+What we're verifying: the VC-cache simplification didn't change what the modeline shows on a normal repo. Fixed in modules/modeline-config.el (live in the daemon after reload).
+- Open a file inside a git repo
+- Glance at the mode-line VC segment
+Expected: the branch name and state still render as before (e.g. "main" with the usual state face). The change only drops a per-render stat and guards against git errors; normal display is unchanged.
+
+*** DONE Lock screen actually locks on Wayland
+CLOSED: [2026-06-24 Wed 10:45]
+What we're verifying: C-; ! l locks the screen on Wayland. slock (X11-only) never worked here; the locker now runs loginctl lock-session, which logind turns into a Lock signal that hypridle handles by running hyprlock — the same path idle/sleep locking already uses. Fix in modules/system-commands.el, live in the daemon.
+- Press C-; ! l (or run M-x cj/system-cmd-lock)
+- The screen should lock with hyprlock
+- Unlock with your password
+Expected: the screen locks immediately and unlocks with your password. (Before the fix it printed "Running lockscreen-cmd..." and nothing happened.)
+*** DONE OKLCH editor feels right
+CLOSED: [2026-06-24 Wed 10:47]
+What we're verifying: the OKLCH sliders / C×L plane edit cleanly and clamping is visible.
+- Switch the picker to OKLCH mode and drag L, then C, then H
+- Push chroma past the sRGB gamut, then toggle the AA/AAA mask
+Expected: each axis moves independently; the C×L plane (once 4b lands) opens on the current color; "chroma clamped to sRGB" shows on clamp; toggling the mask does not reset OKLCH mode.
+*** DONE Generated ramp harmonizes
+CLOSED: [2026-06-24 Wed 10:47]
+What we're verifying: a ramp generated from a base color reads as one family, not a grab-bag (the aesthetic the math is meant to produce).
+- Open =scripts/theme-studio/theme-studio.html= in Chrome
+- Pick a mid-lightness base swatch (e.g. a blue) and generate its ramp at the defaults
+- Read the row of steps left to right, then try a near-black and a near-white base
+Expected: the steps share an obvious hue and step evenly in lightness; the chroma-ease keeps the extreme steps from going muddy or garish; nothing looks like it belongs to a different color.
+*** DONE Color families group the way the eye reads them
+CLOSED: [2026-06-24 Wed 10:51]
+What we're verifying: the OKLCH hue clustering (25° gap) splits and merges families the way you'd expect, and renaming never moves a color.
+- Open =scripts/theme-studio/theme-studio.html= in Chrome and load a real theme (e.g. sterling)
+- Read the strips top to bottom: are "the blues" one strip, "the greens" another, neutrals and ground pinned at the top
+- Find a pair you'd consider one family that landed in two strips (or two you'd consider separate that merged)
+- Rename any swatch to something absurd and confirm it stays in the same strip
+Expected: families match your mental grouping; the few that don't are the cue to revisit the 25° gap; renaming never regroups.
+*** DONE Removed-step references read clearly as "(gone)"
+CLOSED: [2026-06-24 Wed 10:46]
+What we're verifying: lowering a family's count leaves a referencing face visibly stale, not silently re-pointed.
+- Assign a UI or syntax element to an outer step of a family (e.g. region = a blue+3)
+- Lower that family's count to 2 so blue+3 disappears
+- Read the assignment's dropdown
+Expected: the dropdown shows "(gone)" for the removed step, never a silent jump to a different color; re-pointing it is a deliberate choice.
+*** DONE Dirvish d duplicates, D force-deletes with a confirm
+CLOSED: [2026-06-24 Wed 10:52]
+What we're verifying: in dirvish, d now duplicates the file at point (delete-to-trash removed), and D force-deletes the marked files via sudo rm -rf after a yes-or-no-p naming the targets. The pure command builder is unit-tested; this is the live keypress plus the guarded destructive path.
+- Open dirvish on a scratch directory holding a couple of throwaway files
+- Put point on a file and press d — confirm a "<name>-copy.<ext>" appears (a duplicate, nothing deleted)
+- Mark one or two throwaway files, press D, and read the "Force-delete (sudo rm -rf, NO undo): <names>?" prompt
+- Answer no first (confirm nothing happens), then press D again and answer yes
+- Note whether sudo prompts for a password and whether the file actually disappears
+Expected: d duplicates; D names the exact targets and only deletes on yes; the files are gone with no trash copy. If sudo needs a password that shell-command can't supply, flag it — the delete may need to route through a tty instead.
** PROJECT [#A] Theme-Studio Open Work
Parent grouping the open theme-studio / theming issues; close each child independently.
*** TODO [#A] theme-studio: consistent assignment-view table columns :feature:studio:next:
@@ -624,6 +571,59 @@ Phase 1. Palette anchors + OKLCH shade generation (reusing colormath.js), the RO
Phase 2. Initial state from seed() plus seedPkgmap for the non-org packages; all-tier reseed button with a scope-named overwrite warning, resetting non-org to their APPS defaults; regenerate dupre-revised.json. Gate: #selftest PASS; default-on-open equals seed(); artifact round-trip (regenerated dupre-revised.json imports back to the same seeded state); Chrome eyeball.
**** TODO Seeding-engine test surface :solo:test:
Keep #seedtest, #selftest, the default-on-open check, the dupre-revised round-trip, node --check, and Chrome validation green.
+** TODO [#A] Unified popup and messenger UX — placement, dismissal, one library :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-20
+:END:
+Merged 2026-06-20 from the config-wide popup-policy task and the messenger-unification
+task — they're the same policy at two scopes (the messenger windows are the first
+concrete application of the general popup rules). Two parts:
+
+(A) Config-wide popup policy. All transient popups follow one set of principles.
+Placement: when the Emacs frame is wider than tall, the popup rises from the right;
+when square or taller, from the bottom — settle the aspect-ratio threshold and the
+pop-out percentage. Dismissal: C-c C-c when there's an accept action, C-c C-k when
+there's a cancel, otherwise =q= closes the window. Generalizes ai-term adaptive
+placement (the aspect-ratio docking) and the messenger window/key rules below into
+one config-wide policy. From the roam inbox.
+
+(B) Messenger unification (first application of the policy above).
+Spec: [[file:docs/specs/messenger-unification-spec.org][messenger-unification-spec.org]] ([[id:4bfc2011-8ffc-4765-8886-91df12141171][by id]], Draft, 2026-06-11; keybinding-alphabet section + smoke-first parity added 2026-06-16). One library (=cj-messenger-lib.el=) gives every messenger the same shape: chat windows rise from the bottom (the signel rule, generalized), C-c C-c confirms, C-c C-k cancels, C-c C-a attaches — dispatched per backend through a registry + minor mode. Signel already conforms (reference backend); telega and slack join in phases 2-3; ERC later. All eight decisions settled 2026-06-11 (cancel closes an idle window; telega's filter-cancel shadow accepted; slack rooms join the bottom rule). Spec held open — Craig has more ideas to fold in before it's marked Ready.
+
+** TODO [#B] Un-pin ghostel from 0.33.0 once upstream fixes #422/#423 :bug:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-20
+:END:
+ghostel is held at 0.33.0 (=ghostel-20260604.2049=, commit 5779a2adceb2) in =modules/term-config.el= to dodge the 0.35.x native-PTY crash. When dakra/ghostel ships a fix for #422 (Linux malloc/signal reentrancy) and #423 (macOS recursive lock), restore =:ensure t= (drop the pin comment) and =package-upgrade ghostel=, then re-run the open-ghostel-in-a-GUI-frame survival check. Watch the two issues for the fixing commit.
+
+archsetup automated the zig 0.15.2 pin (managed =install_zig_pin= step, sha-verified, unit-tested). If the un-pinned ghostel bumps its ghostty dependency to a newer zig, send archsetup the new version + sha256 so it bumps its =ZIG_VERSION= / =ZIG_SHA256= constants (=inbox-send archsetup=).
+
+** VERIFY [#B] calendar-sync robustness: atomic writes, curl --fail, zero-event false errors :bug:solo:next:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-20
+:END:
+Deferred, pairs with the calendar-sync recurrence VERIFY above. The mechanical parts (write to a temp file + rename, add curl --fail, guard the zero-event case) are doable, but any calendar-sync change needs verification against a real .ics feed to avoid masking a genuine empty/failed sync. Do this together with the recurrence fix once you provide a fixture / confirm the live feed.
+From the 2026-06 config audit, =modules/calendar-sync.el=:
+- =:1309= — agenda file written via =with-temp-file= directly on the target (truncate-in-place); org-agenda/chime reading mid-write sees a partial calendar, hourly. Write temp + =rename-file= (atomic same-fs). Same for =--save-state= :258.
+- =:1284= — curl runs without =--fail=: an HTTP 404/500 error page exits 0 and the HTML proceeds into conversion.
+- =:1229-1233= — =--parse-ics= returns nil for both garbage and a valid calendar with zero in-window events, so healthy near-empty calendars report "parse failed" in =calendar-sync-status=. Distinguish the cases.
+
+** VERIFY [#B] org-roam :config triggers the 15-20s refile scan synchronously at first idle :bug:solo:next:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-20
+:END:
+Needs from Craig: this is measurement-first (perf), not a blind fix — it's the same bottleneck as the "optimize org-capture target building" debug task. Run /debug with debug-profiling to measure what actually costs the 15-20s (file count? regex? agenda rebuild?), then fix from the data. I won't restructure the refile/agenda scan without a profile. Say "let's debug it" and I'll profile + fix.
+=modules/org-roam-config.el:78-79= — org-roam is =:defer 1=, so its :config calls =cj/build-org-refile-targets= at 1s idle, BEFORE the 5s background timer (=org-refile-config.el:144-151=); on a cold cache the 30k-file scan runs inline and freezes Emacs at first idle. Drop the call — org-roam is loaded long before the 5s timer fires. Likely a player in the filed org-capture 15-20s perf task (=[#B] Optimize org-capture target building performance=) — check both together. From the 2026-06 config audit.
+
+** VERIFY [#B] transcription: stderr never reaches the log, video transcripts stranded in /tmp :bug:solo:next:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-20
+:END:
+Deferred from the batch (no blocker; needs a focused pass with live verification). Plan: (1) transcription-config.el:210 — make-process :stderr with a file path creates a buffer, not a file; route stderr into the process buffer and write the captured text out in the sentinel, then drop the leaked buffer. (2) :370-374 — derive the txt/log base from the VIDEO path, not the temp mp3's /tmp path, so transcripts land alongside the source. The path-derivation half is cleanly unit-testable; the stderr half needs a real transcription run to verify, which is why I held it for a focused session rather than the batch.
+From the 2026-06 config audit, =modules/transcription-config.el=:
+- =:210= — =make-process :stderr= with a file PATH creates a BUFFER named like the path (verified by probe); the "Errored. Logs in <file>" notification points at a log without the error text, and the hidden stderr buffer leaks per transcription. Route stderr into the process buffer or write it out in the sentinel.
+- =:370-374= — video path derives txt/log from the temp mp3's /tmp path; the transcript lands in /tmp and dies on reboot, contradicting the "alongside the source" docstring. Pass the video's path as the output base.
+
** PROJECT [#B] Architecture review follow-up from 2026-05-03 :refactor:
High-level pass over =init.el=, =early-init.el=, and all 104 files in
@@ -2332,6 +2332,598 @@ From the color-assignment guide work (2026-06-08): make the tool support the gui
[[id:b70b37f2-37df-4c8e-ac2f-1f20d12e33dd][theme-studio-seeding-engine-spec-doing.org]] — role table + face→role maps for syntax/UI/org, OKLCH shade generation, reseed dupre-revised to the compact mapping. Codex-reviewed, Ready. Implementation tracked under the seeding-engine parent below.
*** TODO Guide-support views and advisories spec
Five optional surfaces, all dismissible and non-blocking, in one collapsible panel where they advise: (1) CVD-simulation toggle on previews (deuteranopia/protanopia/tritanopia); (2) squint/blur preview toggle; (3) lightness-ramp view + palette advisories (accent count over 6-8, roles separated only by red/green) — depends on the OKLCH/ΔE core; (4) definition-vs-call / weight advisories; (5) state-over-syntax preview (region/search/diff tint over real syntax-colored text). Sequence: rewritten guide reviewed → seeding-engine spec → this. Advisories (3, 4) layer on the perceptual-metrics feature.
+** TODO [#B] Auto-dim: org headings, links, and tags do not dim in unfocused windows :bug:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-20
+:END:
+auto-dim-other-buffers-affected-faces (auto-dim-config.el) remaps font-lock and a few org faces to the flat dim face, but not org-level-1..8, org-link, or org-tag, so headings, links (seen in daily-prep.org), and tags like :solo: stay lit when the window loses focus. Decide the dim approach: a flat-dim remap like font-lock (quick) versus dedicated -dim variants surfaced through org-faces / theme-studio (richer, matches the keyword work; Craig flagged org-tags may want the org-faces treatment). Consolidates three roam-inbox captures.
+** TODO [#B] "? = curated help menu" convention across modes :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-20
+:END:
+From the calibredb keybindings work 2026-06-06. The pattern that worked: in a modal/major-mode buffer (calibredb), bind =?= to a curated transient of the frequent workflows, and move the package's own full dispatch to =H=. It fixes the "I can't discover the keys" problem that which-key can't help with (which-key only pops up after a prefix, not for top-level single keys in a mode-map).
+
+Task: survey the modes/modules Craig works in and identify where a =?= -> curated-help-menu (transient) makes sense. Candidates: any major-mode buffer with single-key bindings and no good discovery affordance -- calibredb (done), nov, dirvish, mu4e, ghostel/term, signel, pearl/linear, ELFeed, etc. For each, note whether =?= is free or already a help dispatch, and whether a curated menu (vs the package's own) adds value. Establish it as a convention (and maybe a small helper/macro to define a curated =?= menu consistently).
+
+** TODO [#B] Dupre diff-changed / diff-refine-changed legibility :bug:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-21
+:END:
+Surfaced 2026-06-07 from a pearl session designing its modified-ticket indicator (pearl marks a changed field by inheriting =diff-changed=). dupre's =diff-refine-changed= is bright gold (#ffd700) under near-white text (#f0fef0) -- WCAG contrast ~1.35, unreadable as a plain background. It only looks fine inside diff-mode because diff-mode overlays its own dark foreground. =diff-changed= (#875f00 amber) is ~5.49, readable but off the modus model. Every modus variant keeps both faces legible (contrast 9-16) by pairing a dark low-saturation background with a hue-matched foreground.
+
+Ask:
+1. Rework dupre's =diff-changed= and =diff-refine-changed= on modus lines: dark low-saturation background, legible foreground (plain default fg for simplicity, or hue-tinted per modus -- decide), and keep refine slightly stronger than changed (refine is the word-level emphasis inside a changed region; modus keeps them distinct).
+2. While there, audit dupre's broader diff/palette faces against modus conventions (background/foreground tinting, contrast targets) and flag where it diverges.
+
+Reference values -- modus-vivendi: refine-changed bg #4a4a00 fg #efef80, changed bg #363300 fg #efef80. modus-operandi: refine-changed bg #fac090 fg #553d00, changed bg #ffdfa9 fg #553d00.
+
+Side-by-side legibility render: [[file:assets/2026-06-07-dupre-diff-face-legibility-compare.png][assets/2026-06-07-dupre-diff-face-legibility-compare.png]].
+** TODO [#B] Fix up test runner :feature:refactor:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-21
+:END:
+*** 2026-05-16 Sat @ 11:15:51 -0500 Ideas
+**** Current State
+=modules/test-runner.el= is a solid first pass for an Emacs-config-specific ERT
+workflow:
+- project-scoped focus lists
+- run all vs focused mode
+- run ERT test at point
+- load all test files
+- clear ERT tests from other project roots
+- keybindings under =C-; t=
+
+The universal test-running direction is currently split across modules:
+- =test-runner.el= owns ERT focus/state/UI.
+- =dev-fkeys.el= owns F6 language detection and command generation for Elisp,
+ Python, Go, and partial TypeScript.
+
+That split is the biggest architectural pressure point. The test runner should
+eventually own runner discovery, scopes, command construction, result handling,
+and UI. F6 should become a thin entry point into the runner.
+
+**** Critical Design Issues
+***** Too ERT-specific at the core
+The current state model is named generically, but most operations assume:
+- test files live in =test/= or =tests/=
+- files match =test-*.el=
+- tests are ERT forms
+- individual tests can be selected by ERT selector regex
+- loading tests into the current Emacs process is acceptable
+
+This makes the module hard to extend cleanly to pytest, Jest, Vitest, Go, Rust,
+or shell test runners. The common abstraction should be "test run request" and
+"test runner adapter", not "ERT file list".
+
+***** In-process ERT causes state contamination
+=cj/test-load-all= and focused runs load test files into the current Emacs
+session. This is fast and ergonomic, but it can leak:
+- global variables
+- advice
+- loaded features
+- overridden functions
+- ERT test definitions
+- load-path mutations
+
+The runner should support two ERT execution modes:
+- =interactive= / in-process for fast local TDD
+- =isolated= / batch Emacs for reliable verification
+
+The isolated path should be preferred for "before commit", CI parity, and
+agent-driven verification.
+
+***** Test discovery is regex-based and fragile
+=cj/test--extract-test-names= scans files with a regex for =ert-deftest=.
+That misses or mishandles:
+- macro-generated tests
+- commented forms in unusual shapes
+- multiline or reader-conditional forms
+- non-ERT Elisp tests such as Buttercup
+- stale ERT tests already loaded in the session
+
+Better approach:
+- for ERT in isolated mode, let ERT discover tests after loading files
+- for source navigation, use syntax-aware forms where possible
+- store discovered tests as structured records with file, line, name, framework,
+ tags, and runner
+
+***** Path containment has at least one suspicious edge
+=cj/test--do-focus-add-file= checks:
+
+#+begin_src elisp
+(string-prefix-p (file-truename testdir) (file-truename filepath))
+#+end_src
+
+That should use =cj/test--file-in-directory-p= or ensure the directory has a
+trailing slash. Otherwise sibling paths with a shared prefix are a recurring
+class of bug.
+
+***** Runner commands are shell strings too early
+=cj/--f6-test-runner-cmd-for= returns shell command strings. That makes it
+harder to:
+- inspect command parts
+- safely quote arguments
+- offer command editing
+- run via =make-process= / =compilation-start= without shell ambiguity
+- attach metadata
+- rerun exact invocations
+- convert commands into UI labels
+
+Prefer a structured command object:
+
+#+begin_src elisp
+(:program "pytest"
+ :args ("tests/test_foo.py" "-q")
+ :default-directory "/project/"
+ :env (("PYTHONPATH" . "..."))
+ :runner pytest
+ :scope file)
+#+end_src
+
+Render to a shell string only at the final compilation boundary.
+
+***** F6 and =C-; t= workflows duplicate the same domain
+F6 already handles "all tests" and "current file's tests" for multiple
+languages. =C-; t= handles ERT-only focus and run state. These should converge
+on one runner service:
+- F6: quick entry point
+- =C-; t=: full runner menu
+- both call the same scope/adapter engine
+
+***** Test directory discovery is too narrow
+Current discovery prefers =test/= then =tests/=, with a global fallback. Real
+projects often need:
+- Python: =tests/=, package-local =test_*.py=, =pytest.ini=, =pyproject.toml=
+- JS/TS: =package.json= scripts, =vitest.config.*=, =jest.config.*=,
+ =*.test.ts=, =*.spec.ts=
+- Go: package directories, =go.mod=
+- Rust: =Cargo.toml=, integration tests under =tests/=
+- Elisp packages: =Makefile=, =Eask=, =ert-runner=, Buttercup, =tests/=
+
+Discovery should be adapter-specific and project-config-aware.
+
+***** No structured result model
+=cj/test-last-results= exists but is not meaningfully populated. A powerful
+runner needs a normalized result model:
+- run id
+- started/finished timestamps
+- status: passed/failed/errored/cancelled/skipped/xfail/xpass
+- command
+- runner adapter
+- scope
+- exit code
+- duration
+- failed test records
+- file/line locations
+- raw output buffer
+- coverage artifact paths
+
+This enables last-failed, failures-first, summaries, dashboards, and AI-assisted
+failure explanation.
+
+***** No failure parser / navigation layer
+Compilation buffers are useful, but the runner should parse common failure
+formats and provide:
+- next/previous failure
+- jump to source line
+- failure summary buffer
+- copy failure context
+- rerun failed test at point
+- annotate failing tests in source buffers
+
+Adapters can provide regexes/parsers for ERT, pytest, Jest/Vitest, Go, Rust,
+and shell.
+
+***** Missing watch/rerun modes
+Modern test runners optimize the feedback loop:
+- pytest supports selecting tests, markers, last-failed, failures-first,
+ stepwise, fixtures, xfail/skip, plugins, and cache state.
+- Jest/Vitest support watch workflows, changed-file selection, coverage,
+ snapshots, and rich interactive filtering. Vitest also defaults to watch in
+ development and run mode in CI.
+- Go and Rust runners commonly support package-level runs, regex selection,
+ race/coverage flags, and cached test behavior.
+
+The Emacs runner should expose the subset that maps well to editor workflows:
+- current test
+- current file
+- related test file
+- focused set
+- last failed
+- failed first
+- changed since git base
+- watch current scope
+- full project
+- coverage for current scope
+
+**** Proposed Architecture
+***** Core Types
+Use plain plists initially; promote to =cl-defstruct= only if helpful.
+
+#+begin_src elisp
+;; Test runner adapter
+(:id pytest
+ :name "pytest"
+ :languages (python)
+ :detect cj/test-pytest-detect
+ :discover cj/test-pytest-discover
+ :build-command cj/test-pytest-build-command
+ :parse-results cj/test-pytest-parse-results
+ :capabilities (:current-test :file :project :last-failed :coverage :watch))
+
+;; Test run request
+(:project-root "/repo/"
+ :language python
+ :framework pytest
+ :scope file
+ :file "/repo/tests/test_api.py"
+ :test-name "test_create_user"
+ :extra-args ("-q")
+ :profile default)
+
+;; Test run result
+(:run-id "..."
+ :status failed
+ :exit-code 1
+ :duration 2.14
+ :failures (...)
+ :output-buffer "*test pytest*"
+ :artifacts (...))
+#+end_src
+
+***** Adapter Registry
+Create a registry like:
+
+#+begin_src elisp
+(defvar cj/test-runner-adapters nil)
+(cj/test-register-adapter 'pytest ...)
+(cj/test-register-adapter 'ert ...)
+(cj/test-register-adapter 'vitest ...)
+#+end_src
+
+Runner selection should consider:
+- buffer file extension
+- project files
+- explicit user override
+- available executables
+- package manager scripts
+- existing Makefile targets
+
+***** Scope Model
+Make scopes explicit and shared across languages:
+- =test-at-point=
+- =current-file=
+- =related-file=
+- =focused-files=
+- =last-failed=
+- =changed=
+- =package/module=
+- =project=
+- =coverage=
+- =watch=
+
+Each adapter can say which scopes it supports. Unsupported scopes should produce
+clear user-errors with suggestions.
+
+***** Command Builder Pipeline
+1. Detect project.
+2. Detect language/framework candidates.
+3. Resolve user-requested scope.
+4. Build structured command object.
+5. Optionally let user edit command.
+6. Run via =compilation-start= or =make-process=.
+7. Parse output/result artifacts.
+8. Store normalized result.
+9. Update UI/modeline/messages/failure buffer.
+
+***** Keep Makefile Support But Do Not Require It
+For this Emacs config, =make test-file= and =make test-name= are useful and
+should remain the default Elisp isolated path. But adapter detection should
+support:
+- direct =emacs --batch= ERT invocation
+- =make test=
+- =make test-file=
+- =make test-name=
+- Eask
+- Buttercup
+
+**** Elisp-Specific Improvements
+***** Add isolated ERT runs
+Support batch commands for:
+- all project tests
+- one test file
+- one test name
+- focused files
+- last failed, once result parsing exists
+
+Use the same Makefile targets in this repo, but design the adapter so other
+Elisp projects can run without this Makefile.
+
+***** Support Buttercup/Eask Later
+Buttercup uses BDD-style =describe= / =it= suites and is common in Elisp
+package testing. Eask is often used to run package tests. Add adapter slots
+for these instead of hard-coding ERT forever.
+
+***** Avoid unnecessary global ERT deletion
+=cj/ert-clear-tests= is a pragmatic fix for project contamination, but the
+stronger long-term answer is isolated runs plus project-scoped discovery. Keep
+the cleanup command, but do not make correctness depend on deleting global ERT
+state.
+
+**** Python / pytest Ideas
+- Detect pytest by =pyproject.toml=, =pytest.ini=, =tox.ini=, =setup.cfg=, or
+ presence of =tests/=.
+- Build commands for:
+ - project: =pytest=
+ - file: =pytest path/to/test_file.py=
+ - test at point: =pytest path/to/test_file.py::test_name=
+ - class method: =pytest path::TestClass::test_method=
+ - marker: =pytest -m marker=
+ - last failed: =pytest --lf=
+ - failed first: =pytest --ff=
+ - stop after first: =pytest -x=
+ - coverage: =pytest --cov=...=
+- Parse output for failing node ids and =file:line= references.
+- Read pytest cache for last-failed where useful.
+- Offer marker completion by parsing =pytest --markers= or config files.
+- Surface xfail/skip separately from hard failures.
+
+**** TypeScript / JavaScript Ideas
+***** Detection
+Detect runner by project files and scripts:
+- =vitest.config.ts/js/mts/mjs=
+- =jest.config.ts/js/mjs/cjs=
+- =package.json= scripts: =test=, =test:watch=, =vitest=, =jest=
+- lockfile/package manager: =pnpm-lock.yaml=, =yarn.lock=, =package-lock.json=,
+ =bun.lockb=
+
+Prefer project scripts over raw =npx= when present:
+- =pnpm test -- path=
+- =npm test -- path=
+- =yarn test path=
+- =bun test path=
+
+***** Scopes
+- current file: =vitest run path= or =jest path=
+- test at point: use nearest =it= / =test= / =describe= string and pass =-t=
+- watch current file
+- changed tests where runner supports it
+- coverage current file/project
+- update snapshots
+
+***** Result Parsing
+Parse:
+- failing test names
+- file paths and line numbers
+- snapshot failures
+- coverage summary
+
+Treat snapshot updates as an explicit command, not an automatic side effect.
+
+**** Go Ideas
+- Detect =go.mod=.
+- Current file/source: run package =go test ./pkg=.
+- Test at point: nearest =func TestXxx= and run =go test ./pkg -run '^TestXxx$'=.
+- Bench at point: nearest =BenchmarkXxx= and run =go test -bench '^BenchmarkXxx$'=.
+- Add toggles for =-race=, =-cover=, =-count=1=, =-v=.
+- Parse =file.go:line:= output and package failure summaries.
+
+**** Rust Ideas
+- Detect =Cargo.toml=.
+- Use =cargo test= by default, optionally =cargo nextest run= when available.
+- Current test at point: nearest =#[test]= function.
+- Current file/module where possible.
+- Integration test file: =cargo test --test name=.
+- Support =-- --nocapture= toggle.
+- Parse compiler/test failures and =file:line= links.
+
+**** Shell / Generic Ideas
+- Adapter for Makefile targets:
+ - detect =make test=, =make check=, =make coverage=
+ - expose project-level commands even when language-specific detection fails
+- Adapter for arbitrary project command configured in dir-locals or a project
+ config plist.
+- Let users register custom command templates per project:
+
+#+begin_src elisp
+((:name "unit"
+ :command ("npm" "run" "test:unit" "--" "{file}"))
+ (:name "integration"
+ :command ("pytest" "tests/integration" "-q")))
+#+end_src
+
+**** UI Ideas
+***** Transient Menu
+Replace or complement the raw keymap with a =transient= menu:
+- scope: current test/file/focused/last failed/project
+- runner: auto/ert/pytest/vitest/jest/go/cargo/make
+- toggles: watch, coverage, debug, fail-fast, verbose, update snapshots
+- actions: run, rerun, edit command, show failures, open report
+
+***** Result Buffer
+Create a normalized =*Test Results*= buffer:
+- latest status per project
+- command and duration
+- pass/fail/skip counts
+- failure list with clickable =file:line=
+- actions to rerun failed/current/all
+- links to coverage artifacts
+
+***** Modeline / Headerline Signal
+Show the last run status for the current project:
+- green passed
+- red failed
+- yellow running
+- gray no run
+
+Keep it quiet and optional.
+
+***** History
+Store recent run requests per project:
+- rerun last
+- rerun last failed
+- choose previous command
+- compare duration/status against previous run
+
+**** Configuration Ideas
+- =cj/test-runner-default-scope=
+- =cj/test-runner-prefer-isolated-elisp=
+- =cj/test-runner-project-overrides=
+- =cj/test-runner-known-adapters=
+- =cj/test-runner-enable-watch=
+- =cj/test-runner-result-retention=
+- per-project override through =.dir-locals.el=
+
+Example:
+
+#+begin_src elisp
+((nil . ((cj/test-runner-project-overrides
+ . (:adapter pytest
+ :default-args ("-q")
+ :coverage-args ("--cov=src"))))))
+#+end_src
+
+**** Safety And Robustness
+- Use structured commands until the final boundary.
+- Quote only at render time.
+- Avoid shell when =make-process= / =process-file= is sufficient.
+- Keep command preview/editing available for surprising cases.
+- Detect missing executables before running.
+- Add timeouts/cancel commands for long-running or hung tests.
+- Do not silently fall back from a missing runner to a different runner unless
+ the fallback is visible in the command preview.
+- Avoid mutating global =load-path= permanently.
+- Keep remote/TRAMP behavior explicit; do not accidentally run local commands
+ for remote projects.
+
+**** Coverage Integration
+Tie this into the existing coverage work:
+- run coverage for current file/scope
+- open latest coverage report
+- summarize uncovered lines for current file
+- support Elisp SimpleCov/Undercover, pytest-cov, Vitest coverage, Go cover,
+ and Rust coverage later
+- store coverage artifact paths in the normalized run result
+
+**** AI-Assisted Debugging Ideas
+- Summarize failing tests from the parsed failure records and raw output.
+- Include command, changed files, failure snippets, and relevant source/test
+ locations.
+- Redact env vars, tokens, Authorization headers, and secrets before sending to
+ =gptel=.
+- Add commands:
+ - =cj/test-runner-explain-failure=
+ - =cj/test-runner-suggest-related-tests=
+ - =cj/test-runner-summarize-coverage-gap=
+
+**** Migration Plan
+***** Phase 1: Internal cleanup
+- Fix the task typo and rename current ERT-specific functions or wrap them under
+ an ERT adapter.
+- Move F6 language detection/command construction from =dev-fkeys.el= into
+ =test-runner.el= or a new =test-runner-core.el=.
+- Replace shell-string command builders with structured command plists.
+- Fix path containment in =cj/test--do-focus-add-file=.
+- Make =cj/test-last-results= real for ERT runs.
+
+***** Phase 2: ERT adapter
+- Implement adapter registry.
+- Add ERT adapter with in-process and isolated modes.
+- Preserve all current keybindings by routing them through the adapter.
+- Add failure/result normalization for ERT.
+- Add "rerun last" and "rerun failed" for ERT.
+
+***** Phase 3: Python and JS/TS adapters
+- Add pytest adapter.
+- Add Vitest/Jest adapter with package-manager/script detection.
+- Support current file and test-at-point for both.
+- Add parser/navigation for common failures.
+
+***** Phase 4: UI and watch modes
+- Add transient menu.
+- Add result buffer.
+- Add cancellation and rerun history.
+- Add watch commands where supported.
+
+***** Phase 5: Coverage and AI
+- Connect coverage commands to adapter capabilities.
+- Add failure summarization with redaction.
+- Add coverage-gap summarization.
+
+**** Acceptance Criteria For First Fix-Up Pass
+- Existing ERT workflow still works.
+- F6 and =C-; t= use the same underlying runner API.
+- Current-file test command generation is covered for Elisp, Python, Go,
+ TypeScript, and JavaScript.
+- At least one isolated ERT command path exists.
+- Path containment checks are robust against sibling-prefix paths and symlinks.
+- Runner requests and results are represented as data, not only messages.
+- Missing runner/tool errors are clear and actionable.
+- Tests cover adapter detection, command building, scope resolution, result
+ storage, and key interactive paths.
+
+** TODO [#B] Keymap consolidation — resolve decisions, run Phase 1-2 :feature:refactor:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+Spec: [[id:540bf06b-16b8-46c6-b459-c40d1b9c795d][keybinding-console-safety-spec-doing.org]]. Phase 0 (revert 4a1ecf64) is done and pushed. Decisions D1-D5 are open TODOs in the spec; D2/D4/D5 gate the primary work (Phase 1 prune via Appendix D, Phase 2 consolidate + retire the translation block), while D1/D3 (the console-safe prefix) gate only the optional Phase 3 and can stay open indefinitely. Resolve D2/D4/D5, then run Phase 1-2. Appendix D is the keybinding pruning checklist. Add a =#+TODO: TODO | DONE SUPERSEDED CANCELLED= header line to the spec if adopting those decision keywords (rulesets convention update, 2026-06-12).
+
+** TODO [#B] ledger-config is orphaned — ledger-mode never configured :bug:quick:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-21
+:END:
+Nothing requires =modules/ledger-config.el= (verified by grep), so .dat/.ledger/.journal open without ledger-mode, reports, or flycheck-ledger. The module looks finished, not staged (unlike duet-config, which documents its pre-alpha orphaning). Decide: wire into init.el (+ =cj/executable-find-or-warn= for the ledger binary) or delete. From the 2026-06 config audit.
+
+** TODO [#C] ai-term multi-LLM support — Claude / Codex / ollama :feature:
+Allow creating an ai-term that launches any of Claude, Codex, or a local LLM via ollama, switchable at session start. From rulesets/Craig via the roam inbox. Spec note: =inbox/PROCESSED-2026-06-23-2123-from-rulesets-ai-term-multi-llm-support-from-craig.org=.
+** TODO [#C] theme-studio: package coverage for pearl, wttrin, chime :feature:studio:
+Three projects shipped themeable faces and asked theme-studio to render accurate previews. Data lives in the PROCESSED handoff files.
+*** TODO pearl — 6 faces + overlay-driven appearance
+Six faces in the =pearl= customize group plus overlay-driven appearance a raw buffer read won't show. =inbox/PROCESSED-2026-06-23-2239-from-pearl-theme-studio-pearl-spec.org= + cover + =sample-pearl-buffer.org=.
+*** TODO emacs-wttrin — 4 new faces
+Was hardcoded "gray60"; now four customizable faces (branch =feature/themeable-faces=). =inbox/PROCESSED-2026-06-23-2253-from-emacs-wttrin-wttrin-faces-handoff.org= + rendered sample.
+*** TODO chime — 4 themeable modeline faces
+Four modeline faces shipped (081d76e). =inbox/PROCESSED-2026-06-23-2326-from-chime-chime-added-four-themeable-modeline.org=.
+** TODO [#C] VAMP — extract music-config into a standalone player :feature:refactor:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-21
+:END:
+Build VAMP ("VAMP Audio Music Player"), a standalone, publishable Emacs music player at =~/code/vamp= — derived from a maintained subset of EMMS, depending on the EMMS package not at all, with MPV and mpd behind a generalized adapter API. =.emacs.d= keeps thin glue (=vamp-config.el=: keybindings, paths, dashboard); archsetup owns OS wiring (Super+/ launcher, m3u MIME). Models the =linear-config= → =pearl= migration.
+
+Brainstorm complete 2026-06-22 — validated design at [[file:docs/design/vamp-music-player.org][docs/design/vamp-music-player.org]]. It builds on the prior EMMS-removal work ([[file:docs/specs/music-config-without-emms-spec.org][spec]] + [[file:docs/design/music-config-without-emms-review.org][2026-05-15 review]]), confirming its B1/B2/B4/S3 decisions and pivoting four things (publishable-now, two adapters + generalized API, VAMP name, desktop integration).
+
+Next: (1) revise the spec to the new direction; (2) spike the risky assumptions (mpd dumb-single-file-player contract; m3u =.desktop= on Hyprland); (3) =/start-work= against the revised spec — pure-helper extraction (review Migration Plan step 1) is the safe first phase. Priority [#C] is a placeholder pending Craig's call.
+
+** TODO [#C] ai-term: step between running ai-terms even when detached :feature:solo:next:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-22
+:END:
+The step-to-next-agent family (s-F9 and friends) should cycle to a running ai-term even when that ai-term is currently detached, instead of skipping it. Today the step only lands on attached/visible ai-terms, so a detached-but-running agent gets passed over and there's no keyboard path back to it — re-attach/display it on landing. From the roam inbox.
+
+** TODO [#C] ai-term: multi-backend (Claude / Codex / local ollama) :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-22
+:END:
+Allow creating an ai-term backed by any of Claude, Codex, or a local LLM via ollama, with the backend chosen seamlessly at the start of the session. ai-term currently assumes Claude; generalize the launch path so the agent backend is a selectable parameter and switching between them at session start is frictionless. Routed here from the rulesets roam-inbox item "multiple agent source improvements" (its bullet 3 asked to send emacs this note); the item's other bullets — naming the agent so non-Claude agents aren't called "Claude", and tightening workflow wording for Codex's more literal reading — stay with rulesets.
+
+** TODO [#C] Compare terminal themeability: EAT vs vterm vs ghostel :feature:solo:next:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-22
+:END:
+Research how completely each of EAT, vterm, and ghostel can be themed — in particular how far theme studio can theme each terminal and what it leaves out. Produce a comparison document, then review it with an eye to whether ai-term should move off ghostel (current) to EAT or vterm. Connects to the chime/emacs-wttrin/pearl face-exposure theme-studio thread. From the roam inbox.
+
+** VERIFY [#C] Remove unused system-power keybindings :refactor:quick:next:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-20
+:END:
+Needs from Craig: the task says "confirm the exact set to keep before unbinding." Under C-; ! the bindings are shutdown (s), reboot (r), restart-Emacs (e), and friends. Tell me which to keep bound and which to drop (the completing-read menu still reaches the rare ones), and I'll unbind the rest.
+
+=modules/system-commands.el= binds shutdown (=C-; ! s=), reboot (=C-; ! r=), restart-Emacs (=C-; ! e=) and friends under the =C-; != prefix. Craig rarely uses them and wants the key real-estate back. Drop the bindings he doesn't use; the completing-read menu can still reach the rare ones. Confirm the exact set to keep before unbinding. From the roam inbox.
+
+#+begin_src cj: comment
+ remove them all.
+#+end_src
+
** PROJECT [#C] Music Open Work
Parent grouping the open music / EMMS issues; close each child independently.
*** VERIFY [#C] music: extract faces for music config :refactor:quick:solo:next:
@@ -2972,51 +3564,6 @@ Restart the daemon, open a GUI frame, trigger an encrypted decrypt, confirm =pin
*** TODO [#C] Archive the original L3813 task
After this work lands, mark the original "Finish terminal GPG pinentry configuration" task DONE with a =CLOSED:= stamp and a one-line note pointing at this parent task.
-** TODO [#A] Unified popup and messenger UX — placement, dismissal, one library :feature:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-20
-:END:
-Merged 2026-06-20 from the config-wide popup-policy task and the messenger-unification
-task — they're the same policy at two scopes (the messenger windows are the first
-concrete application of the general popup rules). Two parts:
-
-(A) Config-wide popup policy. All transient popups follow one set of principles.
-Placement: when the Emacs frame is wider than tall, the popup rises from the right;
-when square or taller, from the bottom — settle the aspect-ratio threshold and the
-pop-out percentage. Dismissal: C-c C-c when there's an accept action, C-c C-k when
-there's a cancel, otherwise =q= closes the window. Generalizes ai-term adaptive
-placement (the aspect-ratio docking) and the messenger window/key rules below into
-one config-wide policy. From the roam inbox.
-
-(B) Messenger unification (first application of the policy above).
-Spec: [[file:docs/specs/messenger-unification-spec.org][messenger-unification-spec.org]] ([[id:4bfc2011-8ffc-4765-8886-91df12141171][by id]], Draft, 2026-06-11; keybinding-alphabet section + smoke-first parity added 2026-06-16). One library (=cj-messenger-lib.el=) gives every messenger the same shape: chat windows rise from the bottom (the signel rule, generalized), C-c C-c confirms, C-c C-k cancels, C-c C-a attaches — dispatched per backend through a registry + minor mode. Signel already conforms (reference backend); telega and slack join in phases 2-3; ERC later. All eight decisions settled 2026-06-11 (cancel closes an idle window; telega's filter-cancel shadow accepted; slack rooms join the bottom rule). Spec held open — Craig has more ideas to fold in before it's marked Ready.
-
-** TODO [#B] Auto-dim: org headings, links, and tags do not dim in unfocused windows :bug:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-20
-:END:
-auto-dim-other-buffers-affected-faces (auto-dim-config.el) remaps font-lock and a few org faces to the flat dim face, but not org-level-1..8, org-link, or org-tag, so headings, links (seen in daily-prep.org), and tags like :solo: stay lit when the window loses focus. Decide the dim approach: a flat-dim remap like font-lock (quick) versus dedicated -dim variants surfaced through org-faces / theme-studio (richer, matches the keyword work; Craig flagged org-tags may want the org-faces treatment). Consolidates three roam-inbox captures.
-** TODO [#B] "? = curated help menu" convention across modes :feature:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-20
-:END:
-From the calibredb keybindings work 2026-06-06. The pattern that worked: in a modal/major-mode buffer (calibredb), bind =?= to a curated transient of the frequent workflows, and move the package's own full dispatch to =H=. It fixes the "I can't discover the keys" problem that which-key can't help with (which-key only pops up after a prefix, not for top-level single keys in a mode-map).
-
-Task: survey the modes/modules Craig works in and identify where a =?= -> curated-help-menu (transient) makes sense. Candidates: any major-mode buffer with single-key bindings and no good discovery affordance -- calibredb (done), nov, dirvish, mu4e, ghostel/term, signel, pearl/linear, ELFeed, etc. For each, note whether =?= is free or already a help dispatch, and whether a curated menu (vs the package's own) adds value. Establish it as a convention (and maybe a small helper/macro to define a curated =?= menu consistently).
-
-** TODO [#B] Dupre diff-changed / diff-refine-changed legibility :bug:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-21
-:END:
-Surfaced 2026-06-07 from a pearl session designing its modified-ticket indicator (pearl marks a changed field by inheriting =diff-changed=). dupre's =diff-refine-changed= is bright gold (#ffd700) under near-white text (#f0fef0) -- WCAG contrast ~1.35, unreadable as a plain background. It only looks fine inside diff-mode because diff-mode overlays its own dark foreground. =diff-changed= (#875f00 amber) is ~5.49, readable but off the modus model. Every modus variant keeps both faces legible (contrast 9-16) by pairing a dark low-saturation background with a hue-matched foreground.
-
-Ask:
-1. Rework dupre's =diff-changed= and =diff-refine-changed= on modus lines: dark low-saturation background, legible foreground (plain default fg for simplicity, or hue-tinted per modus -- decide), and keep refine slightly stronger than changed (refine is the word-level emphasis inside a changed region; modus keeps them distinct).
-2. While there, audit dupre's broader diff/palette faces against modus conventions (background/foreground tinting, contrast targets) and flag where it diverges.
-
-Reference values -- modus-vivendi: refine-changed bg #4a4a00 fg #efef80, changed bg #363300 fg #efef80. modus-operandi: refine-changed bg #fac090 fg #553d00, changed bg #ffdfa9 fg #553d00.
-
-Side-by-side legibility render: [[file:assets/2026-06-07-dupre-diff-face-legibility-compare.png][assets/2026-06-07-dupre-diff-face-legibility-compare.png]].
** TODO [#C] Migrate tests off mocking primitives (native-comp robustness) :test:refactor:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-21
@@ -3029,522 +3576,6 @@ This task is the durable fix the ecosystem and =elisp-testing.md= point to: rest
Full mechanism, the three failure modes, the research (Emacs bug#51140, bug#61880, buttercup #230, Debian #1021842, the emacs-29 redefine-primitive warning, the manual on =native-comp-enable-subr-trampolines=), and the decision: [[file:docs/native-comp-subr-mocking.org][docs/native-comp-subr-mocking.org]].
-** TODO [#B] Fix up test runner :feature:refactor:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-21
-:END:
-*** 2026-05-16 Sat @ 11:15:51 -0500 Ideas
-**** Current State
-=modules/test-runner.el= is a solid first pass for an Emacs-config-specific ERT
-workflow:
-- project-scoped focus lists
-- run all vs focused mode
-- run ERT test at point
-- load all test files
-- clear ERT tests from other project roots
-- keybindings under =C-; t=
-
-The universal test-running direction is currently split across modules:
-- =test-runner.el= owns ERT focus/state/UI.
-- =dev-fkeys.el= owns F6 language detection and command generation for Elisp,
- Python, Go, and partial TypeScript.
-
-That split is the biggest architectural pressure point. The test runner should
-eventually own runner discovery, scopes, command construction, result handling,
-and UI. F6 should become a thin entry point into the runner.
-
-**** Critical Design Issues
-***** Too ERT-specific at the core
-The current state model is named generically, but most operations assume:
-- test files live in =test/= or =tests/=
-- files match =test-*.el=
-- tests are ERT forms
-- individual tests can be selected by ERT selector regex
-- loading tests into the current Emacs process is acceptable
-
-This makes the module hard to extend cleanly to pytest, Jest, Vitest, Go, Rust,
-or shell test runners. The common abstraction should be "test run request" and
-"test runner adapter", not "ERT file list".
-
-***** In-process ERT causes state contamination
-=cj/test-load-all= and focused runs load test files into the current Emacs
-session. This is fast and ergonomic, but it can leak:
-- global variables
-- advice
-- loaded features
-- overridden functions
-- ERT test definitions
-- load-path mutations
-
-The runner should support two ERT execution modes:
-- =interactive= / in-process for fast local TDD
-- =isolated= / batch Emacs for reliable verification
-
-The isolated path should be preferred for "before commit", CI parity, and
-agent-driven verification.
-
-***** Test discovery is regex-based and fragile
-=cj/test--extract-test-names= scans files with a regex for =ert-deftest=.
-That misses or mishandles:
-- macro-generated tests
-- commented forms in unusual shapes
-- multiline or reader-conditional forms
-- non-ERT Elisp tests such as Buttercup
-- stale ERT tests already loaded in the session
-
-Better approach:
-- for ERT in isolated mode, let ERT discover tests after loading files
-- for source navigation, use syntax-aware forms where possible
-- store discovered tests as structured records with file, line, name, framework,
- tags, and runner
-
-***** Path containment has at least one suspicious edge
-=cj/test--do-focus-add-file= checks:
-
-#+begin_src elisp
-(string-prefix-p (file-truename testdir) (file-truename filepath))
-#+end_src
-
-That should use =cj/test--file-in-directory-p= or ensure the directory has a
-trailing slash. Otherwise sibling paths with a shared prefix are a recurring
-class of bug.
-
-***** Runner commands are shell strings too early
-=cj/--f6-test-runner-cmd-for= returns shell command strings. That makes it
-harder to:
-- inspect command parts
-- safely quote arguments
-- offer command editing
-- run via =make-process= / =compilation-start= without shell ambiguity
-- attach metadata
-- rerun exact invocations
-- convert commands into UI labels
-
-Prefer a structured command object:
-
-#+begin_src elisp
-(:program "pytest"
- :args ("tests/test_foo.py" "-q")
- :default-directory "/project/"
- :env (("PYTHONPATH" . "..."))
- :runner pytest
- :scope file)
-#+end_src
-
-Render to a shell string only at the final compilation boundary.
-
-***** F6 and =C-; t= workflows duplicate the same domain
-F6 already handles "all tests" and "current file's tests" for multiple
-languages. =C-; t= handles ERT-only focus and run state. These should converge
-on one runner service:
-- F6: quick entry point
-- =C-; t=: full runner menu
-- both call the same scope/adapter engine
-
-***** Test directory discovery is too narrow
-Current discovery prefers =test/= then =tests/=, with a global fallback. Real
-projects often need:
-- Python: =tests/=, package-local =test_*.py=, =pytest.ini=, =pyproject.toml=
-- JS/TS: =package.json= scripts, =vitest.config.*=, =jest.config.*=,
- =*.test.ts=, =*.spec.ts=
-- Go: package directories, =go.mod=
-- Rust: =Cargo.toml=, integration tests under =tests/=
-- Elisp packages: =Makefile=, =Eask=, =ert-runner=, Buttercup, =tests/=
-
-Discovery should be adapter-specific and project-config-aware.
-
-***** No structured result model
-=cj/test-last-results= exists but is not meaningfully populated. A powerful
-runner needs a normalized result model:
-- run id
-- started/finished timestamps
-- status: passed/failed/errored/cancelled/skipped/xfail/xpass
-- command
-- runner adapter
-- scope
-- exit code
-- duration
-- failed test records
-- file/line locations
-- raw output buffer
-- coverage artifact paths
-
-This enables last-failed, failures-first, summaries, dashboards, and AI-assisted
-failure explanation.
-
-***** No failure parser / navigation layer
-Compilation buffers are useful, but the runner should parse common failure
-formats and provide:
-- next/previous failure
-- jump to source line
-- failure summary buffer
-- copy failure context
-- rerun failed test at point
-- annotate failing tests in source buffers
-
-Adapters can provide regexes/parsers for ERT, pytest, Jest/Vitest, Go, Rust,
-and shell.
-
-***** Missing watch/rerun modes
-Modern test runners optimize the feedback loop:
-- pytest supports selecting tests, markers, last-failed, failures-first,
- stepwise, fixtures, xfail/skip, plugins, and cache state.
-- Jest/Vitest support watch workflows, changed-file selection, coverage,
- snapshots, and rich interactive filtering. Vitest also defaults to watch in
- development and run mode in CI.
-- Go and Rust runners commonly support package-level runs, regex selection,
- race/coverage flags, and cached test behavior.
-
-The Emacs runner should expose the subset that maps well to editor workflows:
-- current test
-- current file
-- related test file
-- focused set
-- last failed
-- failed first
-- changed since git base
-- watch current scope
-- full project
-- coverage for current scope
-
-**** Proposed Architecture
-***** Core Types
-Use plain plists initially; promote to =cl-defstruct= only if helpful.
-
-#+begin_src elisp
-;; Test runner adapter
-(:id pytest
- :name "pytest"
- :languages (python)
- :detect cj/test-pytest-detect
- :discover cj/test-pytest-discover
- :build-command cj/test-pytest-build-command
- :parse-results cj/test-pytest-parse-results
- :capabilities (:current-test :file :project :last-failed :coverage :watch))
-
-;; Test run request
-(:project-root "/repo/"
- :language python
- :framework pytest
- :scope file
- :file "/repo/tests/test_api.py"
- :test-name "test_create_user"
- :extra-args ("-q")
- :profile default)
-
-;; Test run result
-(:run-id "..."
- :status failed
- :exit-code 1
- :duration 2.14
- :failures (...)
- :output-buffer "*test pytest*"
- :artifacts (...))
-#+end_src
-
-***** Adapter Registry
-Create a registry like:
-
-#+begin_src elisp
-(defvar cj/test-runner-adapters nil)
-(cj/test-register-adapter 'pytest ...)
-(cj/test-register-adapter 'ert ...)
-(cj/test-register-adapter 'vitest ...)
-#+end_src
-
-Runner selection should consider:
-- buffer file extension
-- project files
-- explicit user override
-- available executables
-- package manager scripts
-- existing Makefile targets
-
-***** Scope Model
-Make scopes explicit and shared across languages:
-- =test-at-point=
-- =current-file=
-- =related-file=
-- =focused-files=
-- =last-failed=
-- =changed=
-- =package/module=
-- =project=
-- =coverage=
-- =watch=
-
-Each adapter can say which scopes it supports. Unsupported scopes should produce
-clear user-errors with suggestions.
-
-***** Command Builder Pipeline
-1. Detect project.
-2. Detect language/framework candidates.
-3. Resolve user-requested scope.
-4. Build structured command object.
-5. Optionally let user edit command.
-6. Run via =compilation-start= or =make-process=.
-7. Parse output/result artifacts.
-8. Store normalized result.
-9. Update UI/modeline/messages/failure buffer.
-
-***** Keep Makefile Support But Do Not Require It
-For this Emacs config, =make test-file= and =make test-name= are useful and
-should remain the default Elisp isolated path. But adapter detection should
-support:
-- direct =emacs --batch= ERT invocation
-- =make test=
-- =make test-file=
-- =make test-name=
-- Eask
-- Buttercup
-
-**** Elisp-Specific Improvements
-***** Add isolated ERT runs
-Support batch commands for:
-- all project tests
-- one test file
-- one test name
-- focused files
-- last failed, once result parsing exists
-
-Use the same Makefile targets in this repo, but design the adapter so other
-Elisp projects can run without this Makefile.
-
-***** Support Buttercup/Eask Later
-Buttercup uses BDD-style =describe= / =it= suites and is common in Elisp
-package testing. Eask is often used to run package tests. Add adapter slots
-for these instead of hard-coding ERT forever.
-
-***** Avoid unnecessary global ERT deletion
-=cj/ert-clear-tests= is a pragmatic fix for project contamination, but the
-stronger long-term answer is isolated runs plus project-scoped discovery. Keep
-the cleanup command, but do not make correctness depend on deleting global ERT
-state.
-
-**** Python / pytest Ideas
-- Detect pytest by =pyproject.toml=, =pytest.ini=, =tox.ini=, =setup.cfg=, or
- presence of =tests/=.
-- Build commands for:
- - project: =pytest=
- - file: =pytest path/to/test_file.py=
- - test at point: =pytest path/to/test_file.py::test_name=
- - class method: =pytest path::TestClass::test_method=
- - marker: =pytest -m marker=
- - last failed: =pytest --lf=
- - failed first: =pytest --ff=
- - stop after first: =pytest -x=
- - coverage: =pytest --cov=...=
-- Parse output for failing node ids and =file:line= references.
-- Read pytest cache for last-failed where useful.
-- Offer marker completion by parsing =pytest --markers= or config files.
-- Surface xfail/skip separately from hard failures.
-
-**** TypeScript / JavaScript Ideas
-***** Detection
-Detect runner by project files and scripts:
-- =vitest.config.ts/js/mts/mjs=
-- =jest.config.ts/js/mjs/cjs=
-- =package.json= scripts: =test=, =test:watch=, =vitest=, =jest=
-- lockfile/package manager: =pnpm-lock.yaml=, =yarn.lock=, =package-lock.json=,
- =bun.lockb=
-
-Prefer project scripts over raw =npx= when present:
-- =pnpm test -- path=
-- =npm test -- path=
-- =yarn test path=
-- =bun test path=
-
-***** Scopes
-- current file: =vitest run path= or =jest path=
-- test at point: use nearest =it= / =test= / =describe= string and pass =-t=
-- watch current file
-- changed tests where runner supports it
-- coverage current file/project
-- update snapshots
-
-***** Result Parsing
-Parse:
-- failing test names
-- file paths and line numbers
-- snapshot failures
-- coverage summary
-
-Treat snapshot updates as an explicit command, not an automatic side effect.
-
-**** Go Ideas
-- Detect =go.mod=.
-- Current file/source: run package =go test ./pkg=.
-- Test at point: nearest =func TestXxx= and run =go test ./pkg -run '^TestXxx$'=.
-- Bench at point: nearest =BenchmarkXxx= and run =go test -bench '^BenchmarkXxx$'=.
-- Add toggles for =-race=, =-cover=, =-count=1=, =-v=.
-- Parse =file.go:line:= output and package failure summaries.
-
-**** Rust Ideas
-- Detect =Cargo.toml=.
-- Use =cargo test= by default, optionally =cargo nextest run= when available.
-- Current test at point: nearest =#[test]= function.
-- Current file/module where possible.
-- Integration test file: =cargo test --test name=.
-- Support =-- --nocapture= toggle.
-- Parse compiler/test failures and =file:line= links.
-
-**** Shell / Generic Ideas
-- Adapter for Makefile targets:
- - detect =make test=, =make check=, =make coverage=
- - expose project-level commands even when language-specific detection fails
-- Adapter for arbitrary project command configured in dir-locals or a project
- config plist.
-- Let users register custom command templates per project:
-
-#+begin_src elisp
-((:name "unit"
- :command ("npm" "run" "test:unit" "--" "{file}"))
- (:name "integration"
- :command ("pytest" "tests/integration" "-q")))
-#+end_src
-
-**** UI Ideas
-***** Transient Menu
-Replace or complement the raw keymap with a =transient= menu:
-- scope: current test/file/focused/last failed/project
-- runner: auto/ert/pytest/vitest/jest/go/cargo/make
-- toggles: watch, coverage, debug, fail-fast, verbose, update snapshots
-- actions: run, rerun, edit command, show failures, open report
-
-***** Result Buffer
-Create a normalized =*Test Results*= buffer:
-- latest status per project
-- command and duration
-- pass/fail/skip counts
-- failure list with clickable =file:line=
-- actions to rerun failed/current/all
-- links to coverage artifacts
-
-***** Modeline / Headerline Signal
-Show the last run status for the current project:
-- green passed
-- red failed
-- yellow running
-- gray no run
-
-Keep it quiet and optional.
-
-***** History
-Store recent run requests per project:
-- rerun last
-- rerun last failed
-- choose previous command
-- compare duration/status against previous run
-
-**** Configuration Ideas
-- =cj/test-runner-default-scope=
-- =cj/test-runner-prefer-isolated-elisp=
-- =cj/test-runner-project-overrides=
-- =cj/test-runner-known-adapters=
-- =cj/test-runner-enable-watch=
-- =cj/test-runner-result-retention=
-- per-project override through =.dir-locals.el=
-
-Example:
-
-#+begin_src elisp
-((nil . ((cj/test-runner-project-overrides
- . (:adapter pytest
- :default-args ("-q")
- :coverage-args ("--cov=src"))))))
-#+end_src
-
-**** Safety And Robustness
-- Use structured commands until the final boundary.
-- Quote only at render time.
-- Avoid shell when =make-process= / =process-file= is sufficient.
-- Keep command preview/editing available for surprising cases.
-- Detect missing executables before running.
-- Add timeouts/cancel commands for long-running or hung tests.
-- Do not silently fall back from a missing runner to a different runner unless
- the fallback is visible in the command preview.
-- Avoid mutating global =load-path= permanently.
-- Keep remote/TRAMP behavior explicit; do not accidentally run local commands
- for remote projects.
-
-**** Coverage Integration
-Tie this into the existing coverage work:
-- run coverage for current file/scope
-- open latest coverage report
-- summarize uncovered lines for current file
-- support Elisp SimpleCov/Undercover, pytest-cov, Vitest coverage, Go cover,
- and Rust coverage later
-- store coverage artifact paths in the normalized run result
-
-**** AI-Assisted Debugging Ideas
-- Summarize failing tests from the parsed failure records and raw output.
-- Include command, changed files, failure snippets, and relevant source/test
- locations.
-- Redact env vars, tokens, Authorization headers, and secrets before sending to
- =gptel=.
-- Add commands:
- - =cj/test-runner-explain-failure=
- - =cj/test-runner-suggest-related-tests=
- - =cj/test-runner-summarize-coverage-gap=
-
-**** Migration Plan
-***** Phase 1: Internal cleanup
-- Fix the task typo and rename current ERT-specific functions or wrap them under
- an ERT adapter.
-- Move F6 language detection/command construction from =dev-fkeys.el= into
- =test-runner.el= or a new =test-runner-core.el=.
-- Replace shell-string command builders with structured command plists.
-- Fix path containment in =cj/test--do-focus-add-file=.
-- Make =cj/test-last-results= real for ERT runs.
-
-***** Phase 2: ERT adapter
-- Implement adapter registry.
-- Add ERT adapter with in-process and isolated modes.
-- Preserve all current keybindings by routing them through the adapter.
-- Add failure/result normalization for ERT.
-- Add "rerun last" and "rerun failed" for ERT.
-
-***** Phase 3: Python and JS/TS adapters
-- Add pytest adapter.
-- Add Vitest/Jest adapter with package-manager/script detection.
-- Support current file and test-at-point for both.
-- Add parser/navigation for common failures.
-
-***** Phase 4: UI and watch modes
-- Add transient menu.
-- Add result buffer.
-- Add cancellation and rerun history.
-- Add watch commands where supported.
-
-***** Phase 5: Coverage and AI
-- Connect coverage commands to adapter capabilities.
-- Add failure summarization with redaction.
-- Add coverage-gap summarization.
-
-**** Acceptance Criteria For First Fix-Up Pass
-- Existing ERT workflow still works.
-- F6 and =C-; t= use the same underlying runner API.
-- Current-file test command generation is covered for Elisp, Python, Go,
- TypeScript, and JavaScript.
-- At least one isolated ERT command path exists.
-- Path containment checks are robust against sibling-prefix paths and symlinks.
-- Runner requests and results are represented as data, not only messages.
-- Missing runner/tool errors are clear and actionable.
-- Tests cover adapter detection, command building, scope resolution, result
- storage, and key interactive paths.
-
-** TODO [#B] Keymap consolidation — resolve decisions, run Phase 1-2 :feature:refactor:solo:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-13
-:END:
-Spec: [[id:540bf06b-16b8-46c6-b459-c40d1b9c795d][keybinding-console-safety-spec-doing.org]]. Phase 0 (revert 4a1ecf64) is done and pushed. Decisions D1-D5 are open TODOs in the spec; D2/D4/D5 gate the primary work (Phase 1 prune via Appendix D, Phase 2 consolidate + retire the translation block), while D1/D3 (the console-safe prefix) gate only the optional Phase 3 and can stay open indefinitely. Resolve D2/D4/D5, then run Phase 1-2. Appendix D is the keybinding pruning checklist. Add a =#+TODO: TODO | DONE SUPERSEDED CANCELLED= header line to the spec if adopting those decision keywords (rulesets convention update, 2026-06-12).
-
-** TODO [#B] ledger-config is orphaned — ledger-mode never configured :bug:quick:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-21
-:END:
-Nothing requires =modules/ledger-config.el= (verified by grep), so .dat/.ledger/.journal open without ledger-mode, reports, or flycheck-ledger. The module looks finished, not staged (unlike duet-config, which documents its pre-alpha orphaning). Decide: wire into init.el (+ =cj/executable-find-or-warn= for the ledger binary) or delete. From the 2026-06 config audit.
-
** TODO [#C] buffer-differs save prompt: 4-way yes/no/diff/cancel :feature:next:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-21
@@ -3709,6 +3740,12 @@ These may override useful defaults - review and pick better bindings:
:END:
Display slack.el message and thread buffers in a dedicated popup window (side or bottom) and reuse that one window instead of spawning a new window per buffer. Likely a =display-buffer-alist= rule (or popper integration) in =modules/slack-config.el=.
+** TODO [#D] Evaluate google-keep Emacs package :quick:
+From the roam inbox. Look at the google-keep Emacs package — worth adding for in-editor Keep, or does the existing google-keep MCP cover it? Triage / shortlist, not a commitment.
+** TODO [#D] Theme Studio nerd-icons vNext follow-ups :feature:
+Deferred from [[file:docs/specs/theme-studio-nerd-icons-colors-spec.org][theme-studio-nerd-icons-colors-spec.org]]: extend the legend to
+buffer-mode and command/symbol categories if the file set proves insufficient;
+add a "reset to nerd-icons native palette" button.
** TODO [#D] Dashboard over-scroll: pin last line to window bottom :bug:
:PROPERTIES:
:LAST_REVIEWED: 2026-05-22