aboutsummaryrefslogtreecommitdiff
path: root/todo.org
diff options
context:
space:
mode:
Diffstat (limited to 'todo.org')
-rw-r--r--todo.org2113
1 files changed, 1072 insertions, 1041 deletions
diff --git a/todo.org b/todo.org
index e8edadf45..a9551bfe6 100644
--- a/todo.org
+++ b/todo.org
@@ -55,369 +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
-** DONE [#B] C-<left>/<right>/<down> wrongly enter terminal copy-mode :bug:quick:
-CLOSED: [2026-06-24 Wed]
-Fixed 2026-06-24: per Craig, only C-<up> enters copy-mode now — all other arrows (C-<down>/<left>/<right> and the M-arrows) were dropped from both the ghostel-mode-map binding and ghostel-keymap-exceptions in modules/term-config.el, so C-<left>/C-<right> reach the shell as readline word-motion again. Also per Craig: C-<up> pressed while already in copy-mode just moves up — cj/term-copy-mode-up checks tmux pane_in_mode (and ghostel--input-mode without tmux) and skips re-entry, which would otherwise reset the cursor. 6 ERT tests rewritten; byte-compile clean; the live daemon was stripped of the stale bindings/exceptions and reloaded (C-<up> bound + an exception, C-<left> forwarded to the pty). Real-terminal scroll is the VERIFY under Manual testing and validation.
-** DONE [#B] ai-term wrap-teardown + shutdown functions :feature:
-CLOSED: [2026-06-24 Wed]
-Done 2026-06-24: added the three headless functions to =modules/ai-term.el= per the rulesets contract — =cj/ai-term-quit= (kill aiv- session + agent buffer + restore layout, idempotent), =cj/ai-term-live-count= (integer gate), =cj/ai-term-shutdown-countdown= (gate re-check → abort-able run-at-time countdown → =cj/ai-term-shutdown-command=, a defcustom). Reused the existing kill/close helpers. 13 ERT tests (live-count parsing, quit kill+idempotency, gate-abort/cancel/tick); byte-compile + validate-modules + launch smoke clean; headless contracts verified live in the daemon (live-count→3, quit no-op returns the session name, countdown aborted with sessions live — no shutdown). The tmux/shutdown side effects and the both-sides end-to-end are a VERIFY under Manual testing and validation. Original task body:
-The .emacs.d half of the rulesets wrap-it-up teardown / shutdown feature. Implement three functions in =modules/ai-term.el=, all callable headlessly via =emacsclient -e= (no interactive frame): =cj/ai-term-quit "<project>"= (teardown a project's aiv- tmux session + buffer + geometry restore), =cj/ai-term-live-count= (integer, the safety gate), =cj/ai-term-shutdown-countdown= (run-at-time timer). Craig's 2026-06-23 decisions: non-destructive qualifier = "with summary"/"and summarize"; countdown is a run-at-time timer (not a tty writer); safety gate uses cj/ai-term-live-count. Lands with the rulesets half (workflow + Stop hook already built/pushed). Spec: =inbox/PROCESSED-2026-06-23-2331-from-rulesets-ai-term-teardown-companion.org= (rulesets proposal: docs/design/2026-06-23-wrap-teardown-shutdown-proposal.org). Own focused session.
-** 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.
-** DONE [#C] README holistic pass
-CLOSED: [2026-06-24 Wed]
-Holistic pass over README.org, changes approved by Craig: bumped the Emacs floor to 30 (developed on 30.2); corrected the module count (~100 → ~120); added docs/ to the layout and reworded scripts/ (now also theme-studio); added Theme Studio, the ghostel native terminal, and ai-term to Features; added make coverage-summary to the dev targets. From the roam inbox.
-** DONE [#B] Theme-driven nerd-icons colors + filetype legend :feature:
-CLOSED: [2026-06-24 Wed]
-Dropped the runtime nerd-icons tint so icon color is theme-driven, and added a
-theme-studio filetype-legend representation over the 34 =nerd-icons-*= color
-faces. Spec:
-[[file:docs/specs/theme-studio-nerd-icons-colors-spec.org][theme-studio-nerd-icons-colors-spec.org]].
-Three Codex spec-review rounds (3 + 6 + 1 findings) incorporated; findings
-[10/10], decisions [6/6]. Ready confirmed 2026-06-24 and implemented in a
-no-approvals speedrun as the four dated phases below — full run-tests.sh and
-=make test= green, all pushed. Live visual confirmation is a VERIFY under
-Manual testing and validation. vNext follow-ups promoted to their own [#D] task.
-*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 1 — legend capture shipped
-=scripts/theme-studio/build-nerd-icons-legend.el= resolves the 13 v1 rows from the live nerd-icons alists into =nerd-icons-legend.json= (committed); =generate.py='s =load_nerd_icons_legend= validates and falls back to the generic app on absent/malformed/empty/bad-row, with a warning. 7 Python tests. Committed (feat phase 1).
-*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 2 — bespoke legend preview shipped
-nerd-icons registers as a bespoke app whenever the legend is valid (=add_nerd_icons_app=); =renderNerdIconsPreview= draws each row's glyph in its mapped face color through the shared registry, so recolor repaints live; the 34 faces stay editable. =#nerdiconstest= gate covers the wiring, the dir-row owner, and the recolor-repaint. Committed (feat phase 2).
-*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 3 — tint removed, theme drives color
-Removed =cj/nerd-icons-tint-color= + =cj/--nerd-icons-color-faces= + =cj/nerd-icons-apply-tint= and both call sites from =nerd-icons-config.el=; the WIP theme already owned the 34 faces (theme-studio auto-discovered them), so color is theme-driven now. Kept =cj/--nerd-icons-color-dir=. Deleted the apply-tint test. validate-modules + launch smoke clean. Committed (feat phase 3).
-*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 4 — dir-precedence probe + round-trip
-ERT probe locks the dir-precedence decision (prepended =nerd-icons-yellow= is first in the face list, wins over =nerd-icons-completion-dir-face=); =#nerdiconstest= extended with the export/import round-trip over an assigned nerd-icons color and a dir-face-stays-out check. Full run-tests.sh + =make test= green. Committed (test phase 4). Live visual is the VERIFY under Manual testing.
-** 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.
-** DONE [#B] ai-term keybinding home :feature:
-CLOSED: [2026-06-23 Tue]
-Done 2026-06-23 (commit be772bc0): family moved to C-; a (a toggle, s select/launch, n next, k kill), swap also on M-SPC, F9 family retired, jumper's M-SPC binding removed (rehome pending). cj/ai-term-next now opens the picker when no agent is running instead of erroring. Bindings verified live in the daemon; Craig's hands-on check is filed under Manual testing and validation.
-Move the ai-term commands off the F9 family. F9 sits somewhere semi-dangerous
-to hit, and F8 (org-agenda) is slow to load, which reads as Emacs being
-unresponsive. Craig wants three commands on an easy near-home-row chord: open
-the ai-term selection menu, switch to the next agent, and kill the current one
-(=cj/ai-term=, =cj/ai-term-next=, =cj/ai-term-close=). Explore C-, M-, and C-M-
-with SPC. Likely collides with jumper, but ai-term is used far more, so jumper
-yields. Archiving gptel this session freed the =C-; a= prefix, so the whole
-ai-term family could live under =C-; a= (or another near-home-row key).
-Related: the s-F9 detached-agent landing task and the tmux copy-mode binding
-task elsewhere in this section. From the roam inbox.
-** DONE [#C] Face coloring completion-read icons :quick:solo:
-CLOSED: [2026-06-23 Tue]
-Answered 2026-06-23 (investigation, no code change). There is no single
-"completion icon" face — each icon inherits a per-type =nerd-icons-*= color
-face (a .el file icon inherits =nerd-icons-purple=, an M-x command icon
-=nerd-icons-blue=, etc.; nerd-icons picks the face per glyph/filetype). What
-makes every completion icon render the SAME color here is this config's bulk
-tint: =cj/nerd-icons-tint-color= (defcustom in =nerd-icons-config.el=, default
-"darkgoldenrod") sets the foreground of all ~33 =nerd-icons-*= color faces via
-=cj/nerd-icons-apply-tint=, applied in the =nerd-icons= =:config=. Verified live:
-=nerd-icons-icon-for-file "init.el"= -> =:inherit nerd-icons-purple=, and that
-face's foreground is "darkgoldenrod". Directory icons additionally get
-=nerd-icons-yellow= layered on by =cj/--nerd-icons-color-dir= advice
-(=nerd-icons-completion-dir-face= is unset, so it isn't the driver here).
-To theme: change =cj/nerd-icons-tint-color= (one color for all icons, then call
-=cj/nerd-icons-apply-tint=), or drop the bulk tint and set the individual
-=nerd-icons-*= color faces for per-filetype colors. For theme-studio, the knob
-to expose is =cj/nerd-icons-tint-color= plus the =nerd-icons-*= face family.
-** DONE [#C] Org formatting inside cj comments :feature:
-CLOSED: [2026-06-23 Tue]
-Done 2026-06-23: mapped the "cj:" src-block language to org-mode via
-=org-src-lang-modes= in =org-babel-config.el=. Effect: a cj comment block's
-prose now gets org font-lock in place (links, *bold*, lists styled — verified
-live, the link inside a block carries the =org-link= face), and =C-c '= opens a
-full org-mode buffer to edit it. Approach A from the design walk: non-breaking,
-the =cj:= grep marker and the whole cj-processing pipeline are unchanged. The
-block stays a src block, so org's parser still treats its body as code — links
-are followed from the =C-c '= buffer rather than clicked in place. If that
-in-place limitation bites, Approach B (migrate to a =#+begin_cj= special block)
-is the documented escalation.
-Craig writes free-form prose inside cj comment blocks (=#+begin_src cj: ...=)
-and wants org formatting available there.
-From the roam inbox.
-** 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.
-
-** DONE [#C] term: M-<arrow> enters tmux copy-mode :feature:
-CLOSED: [2026-06-24 Wed]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-22
-:END:
-Done 2026-06-24: C-<up>/<down>/<left>/<right> and M-<arrow> in =ghostel-mode-map= enter copy-mode and carry their direction in one stroke (=cj/term-copy-mode-up= & friends -> =cj/term-copy-mode-move= -> =cj/term-copy-mode-dwim= then =cj/--term-copy-mode-move-step=). tmux path writes the arrow escape sequence into the pty; non-tmux path moves point in =ghostel-copy-mode=. All 8 keys added to =ghostel-keymap-exceptions= + =ghostel--rebuild-semi-char-keymap= (the gotcha). Ghostel-only. 6 new ERT tests; bindings + exceptions + the dwim sequence verified live in the daemon. The real tmux copy-mode scroll is a VERIFY under Manual testing and validation.
-
-Folded 2026-06-23 from the roam inbox: Craig also wants C-<up> (control + up arrow) to enter tmux copy-mode and move up in one stroke — i.e. a modified arrow both enters copy-mode and passes the movement (copy-mode + arrow). So the binding set is the modified arrow keys (M-arrow and/or C-arrow), each entering copy-mode and carrying its own direction.
-
-** 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.
-
-** CANCELLED [#C] page-signal pager account deregistered — re-registration needs your hands
-CLOSED: [2026-06-21 Sun]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-12
-:END:
-Reported by .emacs.d 2026-06-12 01:01: the dedicated pager number (+15045173983, the Claude Pager Google Voice number on signal-cli) returns "User ... is not registered" on every send — Signal appears to have deregistered it (GV numbers get periodically re-verified). Re-registration requires captcha/SMS, which only you can do. Until then every page-signal call fails; .emacs.d's config-audit page fell back to email. Wrapper lives at claude-templates/bin/page-signal.
-
-** 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.
-
-** DONE [#B] mu4e: cmail can't trash, no account can refile :bug:quick:solo:
-CLOSED: [2026-06-24 Wed]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-13
-:END:
-=modules/mail-config.el:217-220= — the cmail context (primary account) sets only drafts/sent, so D falls back to default "/trash" which doesn't exist under ~/.mail (=/cmail/Trash= does); and NO context sets =mu4e-refile-folder=, so r targets nonexistent "/archive" everywhere. Accepting mu4e's offer to create the maildir strands mail in a directory mbsync never syncs — messages silently vanish from the server's view. Add =mu4e-trash-folder= to cmail + per-context =mu4e-refile-folder=. From the 2026-06 config audit.
-Fixed 2026-06-13: cmail gets =mu4e-trash-folder= "/cmail/Trash"; refile is a per-message function (=cj/mu4e--refile-folder=) instead of a per-context string — mu4e context :vars are sticky, so a per-context refile leaks one account's archive folder into another. cmail → "/cmail/Archive"; gmail/dmail signal a =user-error= rather than move mail into an unsynced phantom folder (Craig chose the fail-safe over syncing [Gmail]/All Mail — the All Mail option means a multi-GB pull + cross-folder duplicates; revisit if local Gmail archiving is wanted). Applies on next mu4e open; pure dispatch helper covered by tests.
-
-** CANCELLED [#C] Lock screen silently fails — slock is X11-only :bug:quick:
-CLOSED: [2026-06-21 Sun]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-13
-:END:
-=modules/system-commands.el:105= binds the lockscreen command to =slock=, which can't grab a Wayland session; =cj/system-cmd= launches it detached with output silenced, so C-; ! l does nothing and the screen never locks. Security issue: Craig believes the screen locks when it doesn't. Fix: =hyprlock= (or =swaylock=), ideally resolved per session type via =env-wayland-p= so an X11 fallback survives for other machines. From the 2026-06 config audit.
-Fixed 2026-06-13: lockscreen-cmd resolves to =loginctl lock-session= on Wayland (logind Lock → hypridle → hyprlock, the path idle/sleep locking already uses), =slock= on X11; also added the missing =(require 'host-environment)=. Live in the daemon; manual lock test under the Manual testing parent.
** 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
@@ -430,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.
@@ -474,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=).
@@ -512,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:
@@ -535,8 +191,197 @@ 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.
+*** 2026-06-24 Wed @ 18:09:26 -0400 theme-studio tier-1 simplifications landed
+Behavior-preserving simplifications from the four-agent refactor/simplify assessment, all test-verified (full suite green). Landed: syncMockHeight + syncPkgHeight merged into syncPaneHeight(tableId, paneId); the dead generatorHues "manual" branch deleted (identical to fallback); locateInfoLine removed (fn + export + test, orphaned this session); the redundant pkgbody guard dropped (buildPkgTable self-guards); displayHex/displayName closures inlined; paintUI now calls worstCellHtml; generate.py's two nerd-icons loaders share _load_nerd_icons_artifact (sentinel keeps the null-file edge exact); face_coverage.classify rewritten with named locals (with a new characterization test). Two agent findings were wrong and skipped on verification: LOCATE_REG is live (read by previewSpan), and normalizePaletteEntryCore doesn't exist (hallucinated). Skipped on judgment: a RELEASED_BOX constant (mutable-dict aliasing hazard, only ~10 sites) and inlining apply_hover_box_default (its why-docstring earns the named function). Open for Craig: previewFaceAttrs (app-core.js) is test-only with a stale "the gate calls it" docstring — confirm delete vs keep.
+*** TODO [#D] theme-studio app.js module split (tier 2) :refactor:studio:
+Optional structural change. app.js (927 lines) holds seven responsibility clusters. Split into controls.js / picker.js / locate.js / io.js / tables.js — near-zero runtime risk since generate.py concatenates source by token. Navigability win only; do it only if scrolling app.js becomes real friction.
*** TODO [#A] theme-studio: consistent assignment-view table columns :feature:studio:next:
All view-assignment tables should use one consistent column set and order, whatever view is selected: element name (sortable), lock, fg, bg, style, box (with a side expansion showing the selected color, as in UI faces), contrast, inheritance, size, preview text. No other columns at this design stage. When a view's elements can't take a given section, raise a signal and disable that section for that view; the disabled state is the visual cue. From the roam inbox 2026-06-16.
*** TODO [#B] Route hardcoded theme colors through the theme :refactor:
@@ -730,27 +575,58 @@ 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.
-** CANCELLED [#B] AI Open Work
-CLOSED: [2026-06-23 Tue]
-gptel archived 2026-06-23 to archive/gptel/ (rarely used). The child issues below — ai-rewrite directive plumbing, ai-conversations bugs, the stale-elpa / gptel-magit shadow, model-switch dedup — are all moot against archived code. Kept for reference; detail also in git history.
-*** CANCELLED [#B] ai-rewrite: chosen directive never reaches the request :bug:solo:
-=modules/ai-rewrite.el:64= — the directive is let-bound around =(call-interactively #'gptel-rewrite)=, but gptel-rewrite is a transient prefix that returns when the menu shows; the send resolves the directive AFTER the binding unwound (verified against ~/code/gptel/gptel-rewrite.el:780-799). The picker's choice is silently dropped — the module's core feature is inert. Set =gptel--rewrite-directive= buffer-locally (restore via =gptel-post-rewrite-functions=) or use a self-removing global hook entry. From the 2026-06 config audit.
+** 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:
-*** CANCELLED [#B] Stale elpa gptel shadows the local fork — likely the gptel-magit root :bug:quick:solo:next:
-Needs from Craig: can't be done standalone. I tried deleting elpa/gptel-0.9.8.5 — the fork loaded fine and gptel-magit still worked via use-package autoloads, but package activation then printed "Unable to activate gptel-magit / Required gptel-0.9.8 unavailable" on every startup, so I reverted. To remove the shadow we must also resolve gptel-magit's package dependency: either drop gptel-magit's package dep (load it via load-path like the gptel fork), or repackage the fork into .localrepo as gptel. Tell me which and I'll do it; this pairs with the gptel-magit investigation.
-=elpa/gptel-0.9.8.5= is still installed alongside the =~/code/gptel= fork (=ai-config.el:383=); package activation puts the elpa dir + autoloads on load-path, so which copy wins depends on ordering, and a mixed load (fork .el + elpa .elc) produces "impossible" bugs. =gptel-magit= (elpa) declares gptel as a dependency, so IT may be pulling the stale copy — check this first when working the open "[#B] Investigate gptel-magit not working properly" task. Fix: =package-delete= the elpa gptel + remove from .localrepo so the fork is the only copy on disk. From the 2026-06 config audit.
+(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.
-2026-06-15: tried deleting =elpa/gptel-0.9.8.5= standalone. The fork loaded correctly and gptel-magit still worked via use-package =:commands= autoloads, BUT package activation then printed "Unable to activate package gptel-magit / Required package gptel-0.9.8 unavailable" on every startup and test run (gptel-magit declares gptel as a package dependency that no longer resolves). Reverted. This can't be done standalone — it must be paired with the gptel-magit dependency fix (drop gptel-magit's package dep, or repackage the fork into .localrepo as gptel). Do it together with the gptel-magit investigation task.
+(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.
-*** CANCELLED [#C] ai-conversations: dead-buffer load, role flattening, non-atomic writes :bug:solo:
-From the 2026-06 config audit, =modules/ai-conversations.el=:
-- =:324= — load in a fresh session does =get-buffer-create "*AI-Assistant*"= (plain fundamental-mode buffer); =--ensure-ai-buffer= then sees it exists and never calls =(gptel)=. Sending doesn't work, autosave self-cancels (requires gptel-mode). Use =get-buffer= for the check; let ensure create. The browser RET/l path inherits this.
-- =:240= — persistence drops gptel's =response= text properties, so a reloaded history replays to the model as ONE user message (model re-reads its own answers as Craig's words). Adopt gptel's native bounds persistence or re-mark on load from the "* Backend:" headings.
-- =:248= — =write-region= straight at the target; crash mid-write truncates the only copy of the history (autosave hits this constantly). Temp + rename.
-- =:140= — three overlapping autosave mechanisms (after-send advice that fires before the response exists, post-response hook, 60s timer). Keep the hook; drop the advice (and likely the timer).
+** 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.
-*** CANCELLED [#C] Dedup gptel model-switch commands — keep switch-backend or fold into change-model :bug:
-=cj/gptel-change-model= (C-; a m) already does backend+model switching and interns correctly, so =cj/gptel-switch-backend= (C-; a B) is arguably redundant now that its crash is fixed. Decision for Craig: keep both, or delete =cj/gptel-switch-backend= plus its C-; a B binding and keep one model-switch command. From the 2026-06 config-audit follow-up.
+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:
@@ -2460,6 +2336,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:
@@ -3100,68 +3568,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.
-
-** DONE [#B] agenda sources: roam Projects missing, no existence filtering :bug:solo:
-CLOSED: [2026-06-24 Wed]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-20
-:END:
-Done 2026-06-24, both parts: (1) per Craig, corrected the docs rather than implementing roam-Project agenda scanning — the commentary + two docstrings claimed org-roam "Project" nodes are agenda sources, but they were never scanned; roam Project/Topic notes are refile targets (org-refile-config.el), not agenda sources. (2) =cj/--org-agenda-base-files= now drops non-existent files and =org-agenda-skip-unavailable-files= is set as a backstop, in the one shared helper so the agenda builders, single-project view, and chime initializer all get it. base-files tests reworked to drive real temp files (+ a drops-missing case); byte-compile clean; live-verified (skip var t, base-files returns only existing). From the 2026-06 config audit, =modules/org-agenda-config.el=:
-- =:182-191= — commentary and docstrings promise org-roam nodes tagged "Project" as agenda sources, but =cj/--org-agenda-scan-files= never scans them, and files added by the roam finalize-hook are wiped on the next =cj/build-org-agenda-list= cache rebuild (≤1h). Add a roam Project pass (mirror =org-refile-config.el:101-109=) or correct the docs.
-- =:186,456= — agenda file list built unconditionally (inbox/calendars may not exist on a fresh machine) and =org-agenda-skip-unavailable-files= is unset — the exact interactive-prompt class that once hung the chime daemon. Filter with =file-exists-p= + set the var as backstop.
-
-** 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]].
-** DONE [#B] F7 diff-aware coverage classifies every changed file "not tracked" :bug:solo:
-CLOSED: [2026-06-22 Mon]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-20
-:END:
-Fixed 2026-06-22: simplecov keys are absolute, git-diff keys repo-relative, so the exact-key intersect never matched. Added =cj/--coverage-relativize-keys= and normalize both tables to repo-relative in =cj/--coverage-read-and-display= before the intersect; intersect unchanged. New =test-coverage-core--relativize-keys.el= (5 unit + 1 integration through the real parsers). Full suite green.
-=modules/coverage-core.el:252= — =cj/--coverage-intersect= joins covered×changed by exact string key, but simplecov.json keys are ABSOLUTE paths while the git-diff parser returns repo-RELATIVE ones — zero matches ever, so working-tree/staged/branch scopes report ":tracked nil" for everything and F7's main feature is inert (whole-project scope works, same-source keys). Unit tests hand-build matching keys so they pass; add one integration test feeding a real undercover report + real diff. Normalize both sides to repo-relative. From the 2026-06 config audit.
-
** TODO [#C] Migrate tests off mocking primitives (native-comp robustness) :test:refactor:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-21
@@ -3174,533 +3580,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.
-
-** DONE [#B] jumper: register collisions and dead-marker errors :bug:solo:
-CLOSED: [2026-06-22 Mon]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-13
-:END:
-Fixed 2026-06-22: (1) store now allocates the first unused register char in the live slice (=jumper--first-free-register=) instead of by next-index, and removal clears the freed register, so a store after a removal no longer overwrites a surviving slot's marker; (2) =jumper--with-marker-at= guards =(buffer-live-p (marker-buffer marker))= so killed-buffer entries are skipped instead of signaling wrong-type errors; (3) the single-location toggle jumps back to the last-location register when set (returns =jumped-back=). New =test-jumper--register-hygiene.el= (8 tests); all 42 jumper tests green. Pre-existing unused-lexical =i= warning in =jumper--location-exists-p= left alone (separate nit).
-Two related defects from the 2026-06 config audit:
-- =modules/jumper.el:155= — removal shifts the vector without renumbering registers, so a later store allocates a register still held by a surviving location and silently overwrites it. Allocate the first free register char in the live slice; =set-register nil= on removal so freed markers don't pin buffers.
-- =modules/jumper.el:117,132= — guards check =(markerp marker)= but not =(buffer-live-p (marker-buffer marker))=; after killing a buffer holding a location, M-SPC SPC and M-SPC j signal wrong-type errors. Treat dead entries as skippable/removable.
-Also =jumper.el:178= — the promised single-location toggle never toggles back ('already-there branch should =jump-to-register= z when set).
-
-** 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
@@ -3742,12 +3621,6 @@ F2 is the universal preview key. Currently bound only in markdown-mode (markdown
Keep the binding mode-local so F2 stays available as a global candidate where no preview makes sense.
-** DONE [#C] face-diagnostic: face-name buttons + header allowlist :feature:quick:solo:
-CLOSED: [2026-06-24 Wed]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-21
-:END:
-Done 2026-06-24: (a) =cj/--face-diag-face-button= renders each real face name in the report as a =buttonize='d button that runs =describe-face= on it (carries the face as button-data); anonymous specs and non-faces stay plain. Routed through the stack, overlay, remap, and provenance render sites. (b) Added =face-diagnostic= to =test-init-header--classified-modules= (it's required in init.el and already carries the header contract). 5 new ERT tests; button text properties confirmed live in a rendered *Face Diagnosis* buffer. Click/RET sign-off is a VERIFY under Manual testing and validation. Spec: [[id:98f065cf-8bd5-46a0-ac24-da94d66855ad][face-font-diagnostic-popup-spec-implemented.org]].
** TODO [#C] Gold text in auto-dimmed buffers :bug:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-21
@@ -3765,13 +3638,6 @@ From the 2026-06-11 brainstorm. Goal: keep [[file:~/sync/org/contacts.org][conta
:END:
From the 2026-06-11 messenger-unification brainstorm. Google Voice has no official API; the viable routes ride the Matrix bridge ecosystem's reverse engineering (mautrix-gvoice). Research pass to establish the 2026 state of play: (1) is mautrix-gvoice healthy and what does its auth flow look like now; (2) any better-maintained alternative (CLI/daemon) for the signel-pattern architecture (external daemon + JSON-RPC + thin Emacs chat client); (3) does call initiation (ring-linked-phone-then-connect, Emacs as dialer) survive in the current protocol — two-way audio in Emacs is out of scope (WebRTC); (4) ToS/account-flag risk assessment for Craig's account. Output: a recommendation doc in docs/design/ naming the architecture (signel-pattern daemon vs Matrix bridge + ement.el) or a no-go with reasons. If go, GV becomes a registered backend under the messenger-unification convention (see the [#B] task below).
-** DONE [#C] latexmk workflow never activates (two breaks) :bug:quick:solo:
-CLOSED: [2026-06-24 Wed]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-21
-:END:
-Done 2026-06-24: changed the :hook key from =TeX-mode-hook= to =TeX-mode= (use-package appends "-hook" only to non-"-mode" symbols, so this now registers on the real =TeX-mode-hook= instead of the unbound =TeX-mode-hook-hook=), and auctex-latexmk from =:defer t= to =:after tex= so =auctex-latexmk-setup= runs when AUCTeX loads. Confirmed both breaks via macroexpand (the dump showed =add-hook 'TeX-mode-hook-hook= before, =TeX-mode-hook= after). 2 new regression ERT tests; live-verified in a real .tex buffer: =TeX-command-default= is "latexmk" and "LatexMk" is in =TeX-command-list=. Actual C-c C-c compile is a VERIFY under Manual testing and validation. From the 2026-06 config audit.
-
** TODO [#C] Org-noter custom workflow — fix and finish :feature:bug:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-21
@@ -3878,36 +3744,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=.
-** CANCELLED [#C] the preview splits an already split window into 3 temporarily. :bug:
-CLOSED: [2026-06-21 Sun]
-looks strange. potentially problematic for ai-terms.
-
-** CANCELLED [#C] TRAMP/dirvish "?" for remote dates — verify the fix per host :bug:
-CLOSED: [2026-06-21 Sun]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-02
-:END:
-
-Root cause is traced (see the dated investigation entry below). What's left needs a live remote: open each remote host in dirvish and run the three diagnostic evals to find which gate is closed, then close it.
-
-Diagnostics (run with point in a remote dirvish buffer):
-- =M-: (dirvish-prop :remote-async)= — nil means =tramp-direct-async-process-p= is failing for this method/host, so dirvish's remote attribute fetch never runs.
-- =M-: (dirvish-prop :gnuls)= — nil means the remote has no GNU =ls= (the =ls --version= probe failed), so the parser gate stays shut. Likely on truenas (FreeBSD).
-- =M-: (tramp-direct-async-process-p)= — confirms whether direct-async is actually active for the connection.
-
-Likely fixes, by which gate is closed:
-- =:gnuls= nil → install GNU coreutils on the remote (FreeBSD: =pkg install coreutils=) and make =ls= resolve to GNU on the TRAMP path, or accept "?" on that host.
-
- - Constraint: nothing gets installed on the remote host, so the =:gnuls= gate is resolved by accepting "?" on that host rather than installing coreutils.
-- =:remote-async= nil → the scp/sshx method isn't advertising direct-async; switch to a method that supports it or check =tramp-direct-async-process= is taking effect for that protocol.
-
-Files involved: =modules/tramp-config.el=, =modules/dirvish-config.el=.
-
-*** 2026-05-22 Fri @ 20:24:44 -0500 Traced the root cause through dirvish source
-Remote dates/sizes don't come from the dired =ls= listing or =dired-listing-switches=. They come from =dirvish-data-for-dir= (=dirvish-tramp.el:95=), which runs =ls -1lahi= on the remote and parses the columns into the attribute cache. That method only fires when both =(dirvish-prop :remote-async)= is a number and =(dirvish-prop :gnuls)= is a string. When either gate is shut, dirvish falls back to its default, which deliberately skips =(file-attributes f-name)= for remote files (=dirvish.el:904=, a perf guard) — leaving attrs nil, so the file-size and file-time widgets render "?" (=dirvish-widgets.el:216,247=).
-
-That explains why every prior fix missed: dired-listing-switches feed a different code path entirely, and disabling =tramp-direct-async-process= shuts the =:remote-async= gate, which is the one path that populates remote attributes — exactly backwards. The config already enables direct-async for ssh/sshx (=tramp-config.el:79-88=), so the remaining closed gate is per-host: =:gnuls= (no GNU ls on FreeBSD-based truenas) or direct-async not taking effect for the method. Could not verify on a live remote from the work session — handed the per-host diagnostics up into the task body.
-
+** 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
@@ -9033,3 +8875,192 @@ Re-reviewed [[id:b54c94a0-d762-4b41-afd7-cf5593ce6675][docs/specs/vterm-to-ghost
** DONE [#A] erc-yank silently publishes >5-line pastes as public gists :bug:quick:solo:
CLOSED: [2026-06-20 Sat]
Dropped erc-yank 2026-06-20 (Craig's call: drop, not harden). The package turned a >5-line paste into a PUBLIC gist (=gist -P=, the clipboard-paste flag, no =--private=) behind a single y-or-n-p, with no executable-find guard for =gist=. It also gisted the system clipboard rather than the kill-ring text being yanked. No replacement binding needed: =erc-mode-map= defines no C-y of its own, so removing the package lets C-y fall through to the ordinary global =yank=. Verified live: effective C-y in an ERC buffer = =yank=. (Audit's "no confirmation" was slightly off — the package did prompt — but public-by-default + one-keystroke confirm + no guard made dropping it the clean fix.)
+** DONE [#B] C-<left>/<right>/<down> wrongly enter terminal copy-mode :bug:quick:
+CLOSED: [2026-06-24 Wed]
+Fixed 2026-06-24: per Craig, only C-<up> enters copy-mode now — all other arrows (C-<down>/<left>/<right> and the M-arrows) were dropped from both the ghostel-mode-map binding and ghostel-keymap-exceptions in modules/term-config.el, so C-<left>/C-<right> reach the shell as readline word-motion again. Also per Craig: C-<up> pressed while already in copy-mode just moves up — cj/term-copy-mode-up checks tmux pane_in_mode (and ghostel--input-mode without tmux) and skips re-entry, which would otherwise reset the cursor. 6 ERT tests rewritten; byte-compile clean; the live daemon was stripped of the stale bindings/exceptions and reloaded (C-<up> bound + an exception, C-<left> forwarded to the pty). Real-terminal scroll is the VERIFY under Manual testing and validation.
+** DONE [#B] ai-term wrap-teardown + shutdown functions :feature:
+CLOSED: [2026-06-24 Wed]
+Done 2026-06-24: added the three headless functions to =modules/ai-term.el= per the rulesets contract — =cj/ai-term-quit= (kill aiv- session + agent buffer + restore layout, idempotent), =cj/ai-term-live-count= (integer gate), =cj/ai-term-shutdown-countdown= (gate re-check → abort-able run-at-time countdown → =cj/ai-term-shutdown-command=, a defcustom). Reused the existing kill/close helpers. 13 ERT tests (live-count parsing, quit kill+idempotency, gate-abort/cancel/tick); byte-compile + validate-modules + launch smoke clean; headless contracts verified live in the daemon (live-count→3, quit no-op returns the session name, countdown aborted with sessions live — no shutdown). The tmux/shutdown side effects and the both-sides end-to-end are a VERIFY under Manual testing and validation. Original task body:
+The .emacs.d half of the rulesets wrap-it-up teardown / shutdown feature. Implement three functions in =modules/ai-term.el=, all callable headlessly via =emacsclient -e= (no interactive frame): =cj/ai-term-quit "<project>"= (teardown a project's aiv- tmux session + buffer + geometry restore), =cj/ai-term-live-count= (integer, the safety gate), =cj/ai-term-shutdown-countdown= (run-at-time timer). Craig's 2026-06-23 decisions: non-destructive qualifier = "with summary"/"and summarize"; countdown is a run-at-time timer (not a tty writer); safety gate uses cj/ai-term-live-count. Lands with the rulesets half (workflow + Stop hook already built/pushed). Spec: =inbox/PROCESSED-2026-06-23-2331-from-rulesets-ai-term-teardown-companion.org= (rulesets proposal: docs/design/2026-06-23-wrap-teardown-shutdown-proposal.org). Own focused session.
+** DONE [#C] README holistic pass
+CLOSED: [2026-06-24 Wed]
+Holistic pass over README.org, changes approved by Craig: bumped the Emacs floor to 30 (developed on 30.2); corrected the module count (~100 → ~120); added docs/ to the layout and reworded scripts/ (now also theme-studio); added Theme Studio, the ghostel native terminal, and ai-term to Features; added make coverage-summary to the dev targets. From the roam inbox.
+** DONE [#B] Theme-driven nerd-icons colors + filetype legend :feature:
+CLOSED: [2026-06-24 Wed]
+Dropped the runtime nerd-icons tint so icon color is theme-driven, and added a
+theme-studio filetype-legend representation over the 34 =nerd-icons-*= color
+faces. Spec:
+[[file:docs/specs/theme-studio-nerd-icons-colors-spec.org][theme-studio-nerd-icons-colors-spec.org]].
+Three Codex spec-review rounds (3 + 6 + 1 findings) incorporated; findings
+[10/10], decisions [6/6]. Ready confirmed 2026-06-24 and implemented in a
+no-approvals speedrun as the four dated phases below — full run-tests.sh and
+=make test= green, all pushed. Live visual confirmation is a VERIFY under
+Manual testing and validation. vNext follow-ups promoted to their own [#D] task.
+*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 1 — legend capture shipped
+=scripts/theme-studio/build-nerd-icons-legend.el= resolves the 13 v1 rows from the live nerd-icons alists into =nerd-icons-legend.json= (committed); =generate.py='s =load_nerd_icons_legend= validates and falls back to the generic app on absent/malformed/empty/bad-row, with a warning. 7 Python tests. Committed (feat phase 1).
+*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 2 — bespoke legend preview shipped
+nerd-icons registers as a bespoke app whenever the legend is valid (=add_nerd_icons_app=); =renderNerdIconsPreview= draws each row's glyph in its mapped face color through the shared registry, so recolor repaints live; the 34 faces stay editable. =#nerdiconstest= gate covers the wiring, the dir-row owner, and the recolor-repaint. Committed (feat phase 2).
+*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 3 — tint removed, theme drives color
+Removed =cj/nerd-icons-tint-color= + =cj/--nerd-icons-color-faces= + =cj/nerd-icons-apply-tint= and both call sites from =nerd-icons-config.el=; the WIP theme already owned the 34 faces (theme-studio auto-discovered them), so color is theme-driven now. Kept =cj/--nerd-icons-color-dir=. Deleted the apply-tint test. validate-modules + launch smoke clean. Committed (feat phase 3).
+*** 2026-06-24 Wed @ 05:54:34 -0400 Phase 4 — dir-precedence probe + round-trip
+ERT probe locks the dir-precedence decision (prepended =nerd-icons-yellow= is first in the face list, wins over =nerd-icons-completion-dir-face=); =#nerdiconstest= extended with the export/import round-trip over an assigned nerd-icons color and a dir-face-stays-out check. Full run-tests.sh + =make test= green. Committed (test phase 4). Live visual is the VERIFY under Manual testing.
+** DONE [#B] ai-term keybinding home :feature:
+CLOSED: [2026-06-23 Tue]
+Done 2026-06-23 (commit be772bc0): family moved to C-; a (a toggle, s select/launch, n next, k kill), swap also on M-SPC, F9 family retired, jumper's M-SPC binding removed (rehome pending). cj/ai-term-next now opens the picker when no agent is running instead of erroring. Bindings verified live in the daemon; Craig's hands-on check is filed under Manual testing and validation.
+Move the ai-term commands off the F9 family. F9 sits somewhere semi-dangerous
+to hit, and F8 (org-agenda) is slow to load, which reads as Emacs being
+unresponsive. Craig wants three commands on an easy near-home-row chord: open
+the ai-term selection menu, switch to the next agent, and kill the current one
+(=cj/ai-term=, =cj/ai-term-next=, =cj/ai-term-close=). Explore C-, M-, and C-M-
+with SPC. Likely collides with jumper, but ai-term is used far more, so jumper
+yields. Archiving gptel this session freed the =C-; a= prefix, so the whole
+ai-term family could live under =C-; a= (or another near-home-row key).
+Related: the s-F9 detached-agent landing task and the tmux copy-mode binding
+task elsewhere in this section. From the roam inbox.
+** DONE [#C] Face coloring completion-read icons :quick:solo:
+CLOSED: [2026-06-23 Tue]
+Answered 2026-06-23 (investigation, no code change). There is no single
+"completion icon" face — each icon inherits a per-type =nerd-icons-*= color
+face (a .el file icon inherits =nerd-icons-purple=, an M-x command icon
+=nerd-icons-blue=, etc.; nerd-icons picks the face per glyph/filetype). What
+makes every completion icon render the SAME color here is this config's bulk
+tint: =cj/nerd-icons-tint-color= (defcustom in =nerd-icons-config.el=, default
+"darkgoldenrod") sets the foreground of all ~33 =nerd-icons-*= color faces via
+=cj/nerd-icons-apply-tint=, applied in the =nerd-icons= =:config=. Verified live:
+=nerd-icons-icon-for-file "init.el"= -> =:inherit nerd-icons-purple=, and that
+face's foreground is "darkgoldenrod". Directory icons additionally get
+=nerd-icons-yellow= layered on by =cj/--nerd-icons-color-dir= advice
+(=nerd-icons-completion-dir-face= is unset, so it isn't the driver here).
+To theme: change =cj/nerd-icons-tint-color= (one color for all icons, then call
+=cj/nerd-icons-apply-tint=), or drop the bulk tint and set the individual
+=nerd-icons-*= color faces for per-filetype colors. For theme-studio, the knob
+to expose is =cj/nerd-icons-tint-color= plus the =nerd-icons-*= face family.
+** DONE [#C] Org formatting inside cj comments :feature:
+CLOSED: [2026-06-23 Tue]
+Done 2026-06-23: mapped the "cj:" src-block language to org-mode via
+=org-src-lang-modes= in =org-babel-config.el=. Effect: a cj comment block's
+prose now gets org font-lock in place (links, *bold*, lists styled — verified
+live, the link inside a block carries the =org-link= face), and =C-c '= opens a
+full org-mode buffer to edit it. Approach A from the design walk: non-breaking,
+the =cj:= grep marker and the whole cj-processing pipeline are unchanged. The
+block stays a src block, so org's parser still treats its body as code — links
+are followed from the =C-c '= buffer rather than clicked in place. If that
+in-place limitation bites, Approach B (migrate to a =#+begin_cj= special block)
+is the documented escalation.
+Craig writes free-form prose inside cj comment blocks (=#+begin_src cj: ...=)
+and wants org formatting available there.
+From the roam inbox.
+** DONE [#C] term: M-<arrow> enters tmux copy-mode :feature:
+CLOSED: [2026-06-24 Wed]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-22
+:END:
+Done 2026-06-24: C-<up>/<down>/<left>/<right> and M-<arrow> in =ghostel-mode-map= enter copy-mode and carry their direction in one stroke (=cj/term-copy-mode-up= & friends -> =cj/term-copy-mode-move= -> =cj/term-copy-mode-dwim= then =cj/--term-copy-mode-move-step=). tmux path writes the arrow escape sequence into the pty; non-tmux path moves point in =ghostel-copy-mode=. All 8 keys added to =ghostel-keymap-exceptions= + =ghostel--rebuild-semi-char-keymap= (the gotcha). Ghostel-only. 6 new ERT tests; bindings + exceptions + the dwim sequence verified live in the daemon. The real tmux copy-mode scroll is a VERIFY under Manual testing and validation.
+
+Folded 2026-06-23 from the roam inbox: Craig also wants C-<up> (control + up arrow) to enter tmux copy-mode and move up in one stroke — i.e. a modified arrow both enters copy-mode and passes the movement (copy-mode + arrow). So the binding set is the modified arrow keys (M-arrow and/or C-arrow), each entering copy-mode and carrying its own direction.
+** CANCELLED [#C] page-signal pager account deregistered — re-registration needs your hands
+CLOSED: [2026-06-21 Sun]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-12
+:END:
+Reported by .emacs.d 2026-06-12 01:01: the dedicated pager number (+15045173983, the Claude Pager Google Voice number on signal-cli) returns "User ... is not registered" on every send — Signal appears to have deregistered it (GV numbers get periodically re-verified). Re-registration requires captcha/SMS, which only you can do. Until then every page-signal call fails; .emacs.d's config-audit page fell back to email. Wrapper lives at claude-templates/bin/page-signal.
+** DONE [#B] mu4e: cmail can't trash, no account can refile :bug:quick:solo:
+CLOSED: [2026-06-24 Wed]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+=modules/mail-config.el:217-220= — the cmail context (primary account) sets only drafts/sent, so D falls back to default "/trash" which doesn't exist under ~/.mail (=/cmail/Trash= does); and NO context sets =mu4e-refile-folder=, so r targets nonexistent "/archive" everywhere. Accepting mu4e's offer to create the maildir strands mail in a directory mbsync never syncs — messages silently vanish from the server's view. Add =mu4e-trash-folder= to cmail + per-context =mu4e-refile-folder=. From the 2026-06 config audit.
+Fixed 2026-06-13: cmail gets =mu4e-trash-folder= "/cmail/Trash"; refile is a per-message function (=cj/mu4e--refile-folder=) instead of a per-context string — mu4e context :vars are sticky, so a per-context refile leaks one account's archive folder into another. cmail → "/cmail/Archive"; gmail/dmail signal a =user-error= rather than move mail into an unsynced phantom folder (Craig chose the fail-safe over syncing [Gmail]/All Mail — the All Mail option means a multi-GB pull + cross-folder duplicates; revisit if local Gmail archiving is wanted). Applies on next mu4e open; pure dispatch helper covered by tests.
+** CANCELLED [#C] Lock screen silently fails — slock is X11-only :bug:quick:
+CLOSED: [2026-06-21 Sun]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+=modules/system-commands.el:105= binds the lockscreen command to =slock=, which can't grab a Wayland session; =cj/system-cmd= launches it detached with output silenced, so C-; ! l does nothing and the screen never locks. Security issue: Craig believes the screen locks when it doesn't. Fix: =hyprlock= (or =swaylock=), ideally resolved per session type via =env-wayland-p= so an X11 fallback survives for other machines. From the 2026-06 config audit.
+Fixed 2026-06-13: lockscreen-cmd resolves to =loginctl lock-session= on Wayland (logind Lock → hypridle → hyprlock, the path idle/sleep locking already uses), =slock= on X11; also added the missing =(require 'host-environment)=. Live in the daemon; manual lock test under the Manual testing parent.
+** CANCELLED [#B] AI Open Work
+CLOSED: [2026-06-23 Tue]
+gptel archived 2026-06-23 to archive/gptel/ (rarely used). The child issues below — ai-rewrite directive plumbing, ai-conversations bugs, the stale-elpa / gptel-magit shadow, model-switch dedup — are all moot against archived code. Kept for reference; detail also in git history.
+*** CANCELLED [#B] ai-rewrite: chosen directive never reaches the request :bug:solo:
+=modules/ai-rewrite.el:64= — the directive is let-bound around =(call-interactively #'gptel-rewrite)=, but gptel-rewrite is a transient prefix that returns when the menu shows; the send resolves the directive AFTER the binding unwound (verified against ~/code/gptel/gptel-rewrite.el:780-799). The picker's choice is silently dropped — the module's core feature is inert. Set =gptel--rewrite-directive= buffer-locally (restore via =gptel-post-rewrite-functions=) or use a self-removing global hook entry. From the 2026-06 config audit.
+
+*** CANCELLED [#B] Stale elpa gptel shadows the local fork — likely the gptel-magit root :bug:quick:solo:next:
+Needs from Craig: can't be done standalone. I tried deleting elpa/gptel-0.9.8.5 — the fork loaded fine and gptel-magit still worked via use-package autoloads, but package activation then printed "Unable to activate gptel-magit / Required gptel-0.9.8 unavailable" on every startup, so I reverted. To remove the shadow we must also resolve gptel-magit's package dependency: either drop gptel-magit's package dep (load it via load-path like the gptel fork), or repackage the fork into .localrepo as gptel. Tell me which and I'll do it; this pairs with the gptel-magit investigation.
+=elpa/gptel-0.9.8.5= is still installed alongside the =~/code/gptel= fork (=ai-config.el:383=); package activation puts the elpa dir + autoloads on load-path, so which copy wins depends on ordering, and a mixed load (fork .el + elpa .elc) produces "impossible" bugs. =gptel-magit= (elpa) declares gptel as a dependency, so IT may be pulling the stale copy — check this first when working the open "[#B] Investigate gptel-magit not working properly" task. Fix: =package-delete= the elpa gptel + remove from .localrepo so the fork is the only copy on disk. From the 2026-06 config audit.
+
+2026-06-15: tried deleting =elpa/gptel-0.9.8.5= standalone. The fork loaded correctly and gptel-magit still worked via use-package =:commands= autoloads, BUT package activation then printed "Unable to activate package gptel-magit / Required package gptel-0.9.8 unavailable" on every startup and test run (gptel-magit declares gptel as a package dependency that no longer resolves). Reverted. This can't be done standalone — it must be paired with the gptel-magit dependency fix (drop gptel-magit's package dep, or repackage the fork into .localrepo as gptel). Do it together with the gptel-magit investigation task.
+
+*** CANCELLED [#B] ai-conversations: dead-buffer load, role flattening, non-atomic writes :bug:solo:
+From the 2026-06 config audit, =modules/ai-conversations.el=:
+- =:324= — load in a fresh session does =get-buffer-create "*AI-Assistant*"= (plain fundamental-mode buffer); =--ensure-ai-buffer= then sees it exists and never calls =(gptel)=. Sending doesn't work, autosave self-cancels (requires gptel-mode). Use =get-buffer= for the check; let ensure create. The browser RET/l path inherits this.
+- =:240= — persistence drops gptel's =response= text properties, so a reloaded history replays to the model as ONE user message (model re-reads its own answers as Craig's words). Adopt gptel's native bounds persistence or re-mark on load from the "* Backend:" headings.
+- =:248= — =write-region= straight at the target; crash mid-write truncates the only copy of the history (autosave hits this constantly). Temp + rename.
+- =:140= — three overlapping autosave mechanisms (after-send advice that fires before the response exists, post-response hook, 60s timer). Keep the hook; drop the advice (and likely the timer).
+
+*** CANCELLED [#B] Dedup gptel model-switch commands — keep switch-backend or fold into change-model :bug:
+=cj/gptel-change-model= (C-; a m) already does backend+model switching and interns correctly, so =cj/gptel-switch-backend= (C-; a B) is arguably redundant now that its crash is fixed. Decision for Craig: keep both, or delete =cj/gptel-switch-backend= plus its C-; a B binding and keep one model-switch command. From the 2026-06 config-audit follow-up.
+** DONE [#B] agenda sources: roam Projects missing, no existence filtering :bug:solo:
+CLOSED: [2026-06-24 Wed]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-20
+:END:
+Done 2026-06-24, both parts: (1) per Craig, corrected the docs rather than implementing roam-Project agenda scanning — the commentary + two docstrings claimed org-roam "Project" nodes are agenda sources, but they were never scanned; roam Project/Topic notes are refile targets (org-refile-config.el), not agenda sources. (2) =cj/--org-agenda-base-files= now drops non-existent files and =org-agenda-skip-unavailable-files= is set as a backstop, in the one shared helper so the agenda builders, single-project view, and chime initializer all get it. base-files tests reworked to drive real temp files (+ a drops-missing case); byte-compile clean; live-verified (skip var t, base-files returns only existing). From the 2026-06 config audit, =modules/org-agenda-config.el=:
+- =:182-191= — commentary and docstrings promise org-roam nodes tagged "Project" as agenda sources, but =cj/--org-agenda-scan-files= never scans them, and files added by the roam finalize-hook are wiped on the next =cj/build-org-agenda-list= cache rebuild (≤1h). Add a roam Project pass (mirror =org-refile-config.el:101-109=) or correct the docs.
+- =:186,456= — agenda file list built unconditionally (inbox/calendars may not exist on a fresh machine) and =org-agenda-skip-unavailable-files= is unset — the exact interactive-prompt class that once hung the chime daemon. Filter with =file-exists-p= + set the var as backstop.
+** DONE [#B] F7 diff-aware coverage classifies every changed file "not tracked" :bug:solo:
+CLOSED: [2026-06-22 Mon]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-20
+:END:
+Fixed 2026-06-22: simplecov keys are absolute, git-diff keys repo-relative, so the exact-key intersect never matched. Added =cj/--coverage-relativize-keys= and normalize both tables to repo-relative in =cj/--coverage-read-and-display= before the intersect; intersect unchanged. New =test-coverage-core--relativize-keys.el= (5 unit + 1 integration through the real parsers). Full suite green.
+=modules/coverage-core.el:252= — =cj/--coverage-intersect= joins covered×changed by exact string key, but simplecov.json keys are ABSOLUTE paths while the git-diff parser returns repo-RELATIVE ones — zero matches ever, so working-tree/staged/branch scopes report ":tracked nil" for everything and F7's main feature is inert (whole-project scope works, same-source keys). Unit tests hand-build matching keys so they pass; add one integration test feeding a real undercover report + real diff. Normalize both sides to repo-relative. From the 2026-06 config audit.
+** DONE [#B] jumper: register collisions and dead-marker errors :bug:solo:
+CLOSED: [2026-06-22 Mon]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+Fixed 2026-06-22: (1) store now allocates the first unused register char in the live slice (=jumper--first-free-register=) instead of by next-index, and removal clears the freed register, so a store after a removal no longer overwrites a surviving slot's marker; (2) =jumper--with-marker-at= guards =(buffer-live-p (marker-buffer marker))= so killed-buffer entries are skipped instead of signaling wrong-type errors; (3) the single-location toggle jumps back to the last-location register when set (returns =jumped-back=). New =test-jumper--register-hygiene.el= (8 tests); all 42 jumper tests green. Pre-existing unused-lexical =i= warning in =jumper--location-exists-p= left alone (separate nit).
+Two related defects from the 2026-06 config audit:
+- =modules/jumper.el:155= — removal shifts the vector without renumbering registers, so a later store allocates a register still held by a surviving location and silently overwrites it. Allocate the first free register char in the live slice; =set-register nil= on removal so freed markers don't pin buffers.
+- =modules/jumper.el:117,132= — guards check =(markerp marker)= but not =(buffer-live-p (marker-buffer marker))=; after killing a buffer holding a location, M-SPC SPC and M-SPC j signal wrong-type errors. Treat dead entries as skippable/removable.
+Also =jumper.el:178= — the promised single-location toggle never toggles back ('already-there branch should =jump-to-register= z when set).
+** DONE [#C] face-diagnostic: face-name buttons + header allowlist :feature:quick:solo:
+CLOSED: [2026-06-24 Wed]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-21
+:END:
+Done 2026-06-24: (a) =cj/--face-diag-face-button= renders each real face name in the report as a =buttonize='d button that runs =describe-face= on it (carries the face as button-data); anonymous specs and non-faces stay plain. Routed through the stack, overlay, remap, and provenance render sites. (b) Added =face-diagnostic= to =test-init-header--classified-modules= (it's required in init.el and already carries the header contract). 5 new ERT tests; button text properties confirmed live in a rendered *Face Diagnosis* buffer. Click/RET sign-off is a VERIFY under Manual testing and validation. Spec: [[id:98f065cf-8bd5-46a0-ac24-da94d66855ad][face-font-diagnostic-popup-spec-implemented.org]].
+** DONE [#C] latexmk workflow never activates (two breaks) :bug:quick:solo:
+CLOSED: [2026-06-24 Wed]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-21
+:END:
+Done 2026-06-24: changed the :hook key from =TeX-mode-hook= to =TeX-mode= (use-package appends "-hook" only to non-"-mode" symbols, so this now registers on the real =TeX-mode-hook= instead of the unbound =TeX-mode-hook-hook=), and auctex-latexmk from =:defer t= to =:after tex= so =auctex-latexmk-setup= runs when AUCTeX loads. Confirmed both breaks via macroexpand (the dump showed =add-hook 'TeX-mode-hook-hook= before, =TeX-mode-hook= after). 2 new regression ERT tests; live-verified in a real .tex buffer: =TeX-command-default= is "latexmk" and "LatexMk" is in =TeX-command-list=. Actual C-c C-c compile is a VERIFY under Manual testing and validation. From the 2026-06 config audit.
+** CANCELLED [#C] the preview splits an already split window into 3 temporarily. :bug:
+CLOSED: [2026-06-21 Sun]
+looks strange. potentially problematic for ai-terms.
+** CANCELLED [#C] TRAMP/dirvish "?" for remote dates — verify the fix per host :bug:
+CLOSED: [2026-06-21 Sun]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-02
+:END:
+
+Root cause is traced (see the dated investigation entry below). What's left needs a live remote: open each remote host in dirvish and run the three diagnostic evals to find which gate is closed, then close it.
+
+Diagnostics (run with point in a remote dirvish buffer):
+- =M-: (dirvish-prop :remote-async)= — nil means =tramp-direct-async-process-p= is failing for this method/host, so dirvish's remote attribute fetch never runs.
+- =M-: (dirvish-prop :gnuls)= — nil means the remote has no GNU =ls= (the =ls --version= probe failed), so the parser gate stays shut. Likely on truenas (FreeBSD).
+- =M-: (tramp-direct-async-process-p)= — confirms whether direct-async is actually active for the connection.
+
+Likely fixes, by which gate is closed:
+- =:gnuls= nil → install GNU coreutils on the remote (FreeBSD: =pkg install coreutils=) and make =ls= resolve to GNU on the TRAMP path, or accept "?" on that host.
+
+ - Constraint: nothing gets installed on the remote host, so the =:gnuls= gate is resolved by accepting "?" on that host rather than installing coreutils.
+- =:remote-async= nil → the scp/sshx method isn't advertising direct-async; switch to a method that supports it or check =tramp-direct-async-process= is taking effect for that protocol.
+
+Files involved: =modules/tramp-config.el=, =modules/dirvish-config.el=.
+
+*** 2026-05-22 Fri @ 20:24:44 -0500 Traced the root cause through dirvish source
+Remote dates/sizes don't come from the dired =ls= listing or =dired-listing-switches=. They come from =dirvish-data-for-dir= (=dirvish-tramp.el:95=), which runs =ls -1lahi= on the remote and parses the columns into the attribute cache. That method only fires when both =(dirvish-prop :remote-async)= is a number and =(dirvish-prop :gnuls)= is a string. When either gate is shut, dirvish falls back to its default, which deliberately skips =(file-attributes f-name)= for remote files (=dirvish.el:904=, a perf guard) — leaving attrs nil, so the file-size and file-time widgets render "?" (=dirvish-widgets.el:216,247=).
+
+That explains why every prior fix missed: dired-listing-switches feed a different code path entirely, and disabling =tramp-direct-async-process= shuts the =:remote-async= gate, which is the one path that populates remote attributes — exactly backwards. The config already enables direct-async for ssh/sshx (=tramp-config.el:79-88=), so the remaining closed gate is per-host: =:gnuls= (no GNU ls on FreeBSD-based truenas) or direct-async not taking effect for the method. Could not verify on a live remote from the work session — handed the per-host diagnostics up into the task body.