aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-12 02:06:18 -0500
committerCraig Jennings <c@cjennings.net>2026-06-12 02:06:18 -0500
commit86ecdc39944c14ec7175a4a578397035f92f7d6b (patch)
tree3b9775e62ab2b98ddec4d4c1d2c788c94dc93e6c
parent0176cae46ffbb14f713e303b28963a77b38a01f4 (diff)
downloaddotemacs-86ecdc39944c14ec7175a4a578397035f92f7d6b.tar.gz
dotemacs-86ecdc39944c14ec7175a4a578397035f92f7d6b.zip
chore(todo): move palette-columns VERIFY to section end
-rw-r--r--todo.org2090
1 files changed, 1045 insertions, 1045 deletions
diff --git a/todo.org b/todo.org
index 2731c8c5d..33959ccd0 100644
--- a/todo.org
+++ b/todo.org
@@ -41,9 +41,65 @@ 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
-** VERIFY Palette-columns spec review :theme-studio:
-SCHEDULED: <2026-06-12 Fri>
-Read [[file:docs/theme-studio-palette-columns-spec.org][docs/theme-studio-palette-columns-spec.org]] (Draft, from the 2026-06-10 design discussion) and bless or amend. Decisions 9 and 10 are the two session calls awaiting your word: strips flip to lightest→darkest top→bottom to match the dropdown, and each dropdown column run places the base at its natural lightness position (vs bg/fg bases leading before any steps). On "spec's good": mark Ready, file the phase breakdown, cancel the [#C] hint-override task, start Phase 1.
+** TODO [#A] Calibre Open Work :calibre:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-06
+:END:
+Parent grouping the open Calibre / ebook-workflow issues; close each child independently. The EPUB reading-width tasks were already resolved (2026-05-12/14).
+
+*** DOING Calibre bookmark title format :feature:solo:quick:
+When I hit m in calibre, I'm making my place in the book with a bookmark.
+While sometimes, the books look fine: "The A.B.C. Murders - Agatha Christie.epub"
+Sometimes they look not so good: Engines of Logic_ Mathematicians and the O - Martin Davis.pdf or Software Architecture_ The Hard Parts _ Mo - Neal Ford.pdf
+
+What I would like to do is to have the bookmarks be saved in the following format:
+
+Author, Title [no extension]. Underscores should be stripped.
+
+Root cause: in a nov buffer =m= is =bookmark-set= (rebound at calibredb-epub-config.el:311); nov's =nov-bookmark-make-record= names the record =(buffer-name)= -- the EPUB filename.
+
+Implemented 2026-06-06. Source decision: parse the *filename*, not the embedded EPUB metadata -- under Calibre's "<Title> - <Author>.epub" naming the filename is more complete (the embedded metadata had truncated titles, author-sort "Last, First" forms, and lost punctuation; see the separate metadata-cleanup task). A =:filter-return= advice on =nov-bookmark-make-record= rebuilds the name from the record's filename: split on the last " - " into title/author, restore the colon Calibre sanitized to "_ " (-> ": "), reorder to "Author, Title". Pure helpers =cj/--nov-clean-title= + =cj/--nov-bookmark-name-from-file= in =modules/calibredb-epub-config.el=; 10 ERT tests in =tests/test-calibredb-epub-config--bookmark-name.el=. Live in the daemon.
+
+Existing bookmarks: the 3 nov bookmarks in =~/sync/org/emacs_bookmarks= were renamed by hand (one-pass, in the daemon + saved; backup at =emacs_bookmarks.bak-2026-06-06=): "Edward Kanterian, Frege: A Guide for the Perplexed", "Agatha Christie, The A.B.C. Murders", "Edward Abbey, The Fool's Progress: An Honest Novel".
+
+Awaiting Craig's manual confirm: make a NEW bookmark (open an EPUB, hit m) and check the default name is "Author, Title" from the filename.
+
+*** DOING [#A] Reconsider Calibre keybindings :feature:ux:
+Relocated from the global capture inbox 2026-06-06. Want a discoverable set of keybindings (visible in which-key) for the most frequent calibredb workflows:
+- Switch to a library (e.g. Literature), sort by last name, scroll the list.
+- Scope/filter the list in place, keeping the current library scope:
+ - by format (e.g. epubs only)
+ - by author last name (exact == or ^begins-with some text)
+ - sort by title, publication date, or group by format
+- One key pops up the selected book's description in a bottom-30% buffer, dismissed with q (same display pattern as the signel chat dock).
+- RET opens the book in the appropriate viewer.
+Survey finding 2026-06-06: calibredb already binds almost all of this in calibredb-search-mode-map (S/L library, g filter [f format, a author, t tag, d date], o sort [t title, a author, p pubdate, f format], RET open) and even ships transient menus (? = calibredb-dispatch, g, o). The real problem was discoverability -- they are top-level single keys (which-key never pops up) and Craig didn't know ? opened a menu. calibredb-quick-look is macOS-only; the detail view (v -> *calibredb-entry*, q quits) is the description but opens full-window.
+
+Implemented 2026-06-06 in =modules/calibredb-epub-config.el=:
+- A curated transient =cj/calibredb-menu= (library switch; filter format/author/reset; sort author/title/pubdate/format; open; describe; H = full calibredb-dispatch) bound to =?= in calibredb-search-mode-map. calibredb's own full dispatch moved to =H=. Defined in the use-package =:config= (needs the elpa transient, which batch doesn't load) -- the "? brings up a curated help menu" convention.
+- Bottom-30% description dock: =calibredb-show-entry-switch= -> =pop-to-buffer= + a =display-buffer-alist= rule for =*calibredb-entry*= (display-buffer-at-bottom, height 0.3); =cj/calibredb-describe-at-point= shows the entry without switching focus so q dismisses it. Same pattern as the signel chat dock.
+1 ERT test (the describe command; the transient/bindings/dock need the elpa transient + live calibredb, verified in the daemon). Author "begins-with" is covered well enough by g a's completing-read over "Last, First"; a true regex filter was not built. Awaiting Craig's manual verify (M-B -> ? menu; d/v docked description; H full menu).
+
+*** TODO Embed Calibre DB metadata into the EPUB files :data:maintenance:
+Surfaced 2026-06-06 while building the bookmark naming: the metadata embedded in the EPUB files' OPF is worse than Calibre's database metadata. nov reads the embedded OPF and got truncated titles ("Frege" vs the filename's "Frege: A Guide for the Perplexed"), author-sort "Last, First" forms ("Christie, Agatha"), and lost punctuation ("A.B.C." -> "A B C"). The filenames (from Calibre's curated DB) are the good copy. Fix on the Calibre side: select all (or by library), run "Edit metadata -> Embed metadata into book files" so the DB metadata is written into each EPUB's OPF. Consider auditing author vs author_sort first. After embedding, the in-file metadata matches the library and any tool reading the files (nov, other readers, re-imports) gets the good data. Not an Emacs task; Calibre-side bulk maintenance.
+
+** TODO [#A] Lock screen silently fails — slock is X11-only :bug:
+=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.
+
+** TODO [#A] mu4e: cmail can't trash, no account can refile :bug:
+=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.
+
+** TODO [#A] calendar-sync drops final occurrences and resurrects cancelled meetings :bug:solo:
+RFC 5545 conformance holes in =modules/calendar-sync.el=, all agenda-visible (from the 2026-06 config audit):
+- =:973,1015,1024= — UNTIL treated as exclusive (strict =calendar-sync--before-date-p=); RFC and Google make it inclusive, so the LAST instance of every UNTIL-bounded series vanishes. Tests assert loose count ranges, so it's unpinned. Allow equality.
+- =:578= — comma-separated EXDATE lists (Google emits them) never parse; the exclusion drops silently and cancelled occurrences reappear on the agenda. Split on "," before parsing; no comma-case test exists.
+- =:902= — timed events without DTEND render as all-day (time lost); multi-day all-day spans collapse to one day (end date unused, exclusive-DTEND unhandled). Emit start-time-only stamps and org date ranges.
+
+** TODO [#A] Native compilation disabled config-wide; GC at stock 800KB :bug:
+From the 2026-06 config audit (verified against the live daemon). =early-init.el:69= =(setq native-comp-deferred-compilation nil)= — the obsolete alias of =native-comp-jit-compilation= — turns JIT native compilation OFF entirely, not "synchronous" as the comment claims: 19 .eln files exist for 184 packages, ~100 of 121 modules run interpreted for the daemon's lifetime, and system-defaults.el:42-44's speed-3/8-jobs/always-compile settings are dead. Plus =early-init.el:113-116= restores =gc-cons-threshold= to the captured STOCK default (800000, verified) post-startup — frequent small GC pauses forever. Together these plausibly feed the filed org-capture 15-20s task more than anything in the capture path itself. Actions: retest the old "Selecting deleted buffer" race on 30.2 and re-enable JIT (or AOT sweep); set a deliberate 16-64MB threshold (or gcmh). Check both before burning time on the capture-perf debug task.
+
+** TODO [#A] Global yes-or-no-p fset defeats every strong confirmation :bug:quick:
+=modules/system-defaults.el:203= =(fset 'yes-or-no-p 'y-or-n-p)= — verified live. Several modules deliberately chose yes-or-no-p as the strong tier for irreversible actions: shutdown/reboot (=system-commands.el:74=, whose comment explicitly says "so a stray RET/space can't trigger them"), "permanently destroy files" (=dwim-shell-config.el:804=), file overwrites (=custom-buffer-file.el:159,199=, =music-config.el:374=). The fset makes all of them single-keystroke — the two-tier design is dead. Drop the fset, or provide a real =cj/confirm-strong= (typed "yes") for the irreversible set. From the 2026-06 config audit.
** TODO [#B] theme-studio preview face mislinks (org, erc, flycheck) :bug:theme-studio:quick:solo:
:PROPERTIES:
@@ -57,36 +113,12 @@ Found by Craig 2026-06-11 during the manual-test walk (org case), then a full au
Pin with a browser-gate assertion that these preview elements link the right faces (e.g. the org headline-todo span sits after an org-todo span; the erc my-message line uses input-face).
-** TODO [#C] theme-studio picker panel blends into the page :bug:theme-studio:ux:quick:solo:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-11
-:END:
-Craig, 2026-06-11 manual-test walk: the color picker's background is hard to distinguish from the page background. Give the picker panel a visibly distinct background or a highlighted border so it stands out. Pin with a gate asserting the picker element carries the distinct style.
-
-** TODO [#C] theme-studio Rust + Zig language previews :feature:theme-studio:solo:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-11
-:END:
-Requested by Craig 2026-06-11: add Rust and Zig code samples to the language previews (samples.py currently carries Elisp, Go, Python, TypeScript, Java, C, C++, Shell). Each sample should exercise the treesit token categories distinctive to its language (Rust: lifetimes, macros, attributes, traits; Zig: comptime, builtins, error unions), then regenerate theme-studio.html and extend the test surface.
-
** TODO [#B] Split window opens the dashboard in the other window :feature:ux:windows:quick:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-10
:END:
When splitting with C-x 2 (=split-window-below=) or C-x 3 (=split-window-right=), the new/other window should default to the =*dashboard*= buffer instead of mirroring the current buffer. Advise =split-window-below= / =split-window-right= (or rebind the keys) to select the dashboard in the freshly-created window. Keep point in the original window.
-** TODO [#C] theme-studio face-consistency check :feature:theme-studio:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-10
-:END:
-Rule taxonomy captured in [[file:docs/design/theme-studio-face-rules.org][docs/design/theme-studio-face-rules.org]] (Design Rules vs Fidelity Rules). The two checks below map to those two rule kinds. Both surface structural-attribute (weight/slant/underline/box/overline/height) issues; color is the theme's design and out of scope.
-
-1. Theme cross-cutting consistency (primary, per Craig 2026-06-09): the theme has deliberate cross-cutting rules — e.g. headings/titles are bold, links are underlined, errors/warnings/success are bold. Flag where the theme BREAKS ITS OWN rule (a heading that isn't bold, a link that isn't underlined). The designer declares the rules; the check finds the violators. This is the "tell me where I broke the rule" guardrail.
-
-2. defface-baseline divergence (secondary): flag where a face's structural attrs differ from its package =defface= so each divergence is deliberate, not an accidental drop. Would have caught the dropped underline/bold defaults and the contradictions (shr-h3 bold-vs-italic, erc-action italic-vs-bold) from the package-face audit as they were introduced.
-
-Bake into the tool (a lint surfaced in the UI) or run as a build-time check (seeds vs live deffaces via emacsclient).
-
** TODO [#B] theme-studio palette ramps + contrast safety v1 :feature:theme-studio:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-10
@@ -127,24 +159,6 @@ Phase 5 (commit =9daeff15=). Editing a family base recolors the whole family (sh
*** 2026-06-10 Wed @ 01:17:45 -0500 Warnings, seeding, export, README close-out landed
Phase 6 (commit =c175e2be=). Export stays a flat palette and import needs no reconstruction (#roundtriptest: export→import→export byte-identical). =seedPkgmap= reads the flat palette unchanged. The too-similar warning stays on the full palette — the planned ramp-step exemption was dropped after analysis: ramp steps are a stepL apart (well above the ΔE threshold) so they never warn, and exempting same-family pairs would hide genuine near-duplicates (caught by #deltatest). README documents families, the ground strip, the count control/regenerate, removed-step references, and the ramp-panel removal.
-** TODO [#C] Color-family per-hex hint override :feature:theme-studio:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-11
-:END:
-For the ~1 color per palette that sits on a ramp-collision point (e.g. yellow+2 on the distinguished palette, which by every hex signal belongs to the olive ramp though its name says gold), automatic grouping cannot recover the designer's intent. Add a per-hex family override: drag a swatch to a different column, store the override keyed by hex (never the name, so renaming is still free), consult it after the LCCL clustering, and drop/mark-stale it when the hex changes substantially. Export stays mostly flat; only overrides are extra metadata. Both reviews recommend this exact shape; details in =~/color-sorting-fable.org= (§ "The irreducible case") and =~/color-sorting-codex.org= (§ "What to store").
-
-** TODO [#C] Internet radio now-playing song :feature:music:emms:solo:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-11
-:END:
-Show the currently-playing song while streaming an internet radio station. Lives in =modules/music-config.el= (EMMS + MPV backend, M3U radio stations). The track title comes from the stream's ICY metadata — EMMS exposes it via =emms-track-description= / =emms-playing-time= and updates it on the metadata-change hook; MPV reports the ICY title too. Add an option to show the song in the minibuffer (e.g. echo on track change, or an on-demand command). Consider also a mode-line indicator as a second surface.
-
-** TODO [#C] Evaluate jamescherti essential-emacs-packages list :packages:research:quick:solo:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-11
-:END:
-Review [[https://www.jamescherti.com/essential-emacs-packages/][James Cherti's essential Emacs packages]] for anything worth installing. Cross-check each candidate against what is already in the config (=modules/= + =init.el=), skip the ones already present, and shortlist the genuinely new ones with a one-line rationale. Future-installation research, not a commitment to install.
-
** TODO [#B] Dupre diff-changed / diff-refine-changed legibility :bug:dupre:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-11
@@ -171,64 +185,6 @@ Decided 2026-06-11 (Craig): #0d0b0a is the canonical background — the three dr
*** TODO org-todo color mismatch: test expects #ff2a00, theme renders #a7502d
=dupre-theme-org-todo= (test:130) asserts the org-todo foreground is "#ff2a00" (intense-red), but the theme renders "#a7502d" (red-1). Design call: should org-todo be the bright intense-red or the muted red-1? Fix whichever side loses the decision.
-** TODO [#C] dupre-clear theme — contrast-first AAA sibling :feature:theme:dupre:
-Build a new theme (working name "dupre-clear", final name TBD) that takes dupre's color identity and rebuilds it Prot's way: contrast-first, targeting WCAG AAA (~7:1 on the ground), where the in-progress dupre revision is mood/depth-first and lands at AA. Same hues (dupre blue, emerald, gold, terracotta, regal violet, mint) brightened to clear the AAA floor; same modus-style role mapping (blue keywords bold, gold functions, violet types, emerald strings, terracotta constants, silver default, warm-grey comments, metallic greys, navy + regal fills). Build the dupre revision first; this reuses its hue choices as the starting point.
-
-Full design + methodology + starting palette + open questions in the spec: [[file:docs/design/dupre-clear-theme.org][docs/design/dupre-clear-theme.org]]. Key prerequisite/context: the dupre-redesign entry in =.ai/session-context.org= (the AA palette this brightens). Hardest slot: blue keywords (a deep dupre blue can't be AAA on near-black — decide brighten vs keep-AA-exception vs lift-the-ground).
-** TODO Manual testing and validation :verify:theme-studio:
-Exercised once the phases above land.
-*** 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
-- Read the L_max marker and the shaded unsafe band on the lightness slider
-- Drag lightness up toward and past the marker
-Expected: the marker is visible and correctly placed, the band above it reads as "unsafe," and crossing it is obvious; an out-of-scope face shows no marker.
-*** TODO Safe tint actually reads in real Emacs
-What we're verifying: a background tint the tool calls safe really keeps every token readable behind real syntax-colored text — the whole point of the worst-case floor.
-- In the tool, set a covered face (e.g. region) to a tint at or just below its L_max with the worst-case readout showing PASS
-- 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 [#B] theme-studio guide-support features :feature:theme-studio:
From the color-assignment guide work (2026-06-08): make the tool support the guide without mandating it — everything a seed, an advisory, or a view, never a gate. Two specs to write, both deriving from the rewritten guide and its seed table ([[file:scripts/theme-studio/theme-coloring-guide.org][theme-coloring-guide.org]]).
*** 2026-06-08 Mon @ 19:08:00 -0500 Seeding-engine spec written and Ready
@@ -243,67 +199,11 @@ 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:tests:
Keep #seedtest, #selftest, the default-on-open check, the dupre-revised round-trip, node --check, and Chrome validation green.
-** TODO [#C] theme-studio terminal/ANSI colors :feature:theme-studio:
-theme-studio represents GUI faces only; terminal colors aren't surfaced at all. Scope decided 2026-06-09: GUI-first faces, NOT full per-face display-class fallback. Two pieces:
-
-1. ANSI-16 panel. Map the 16 ANSI slots (black/red/green/yellow/blue/magenta/cyan/white + bright variants) to palette colors, with a preview, and export them so =build-theme.el= emits the =ansi-color-*= / =term-color-*= faces. This matters even in pure-GUI Emacs: colored shell output, compilation buffers, eshell, and vterm/eat all draw from these. Signals must line up with their ANSI slot (error red→ansi red, success→green, warning→yellow, info/link→blue) so a signal reads the same in a terminal.
-
-2. Core-face 16-color fallback. Only the ~10 faces that decide console legibility get a =(((class color) (min-colors 16)) ...)= clause plus a =(t ...)= floor: default/fg, bg, keyword, string, comment, constant, error, warning, region, mode-line, line-number. Tune these for contrast — push it UP, legibility over fidelity, because the only 16-color target is the bare Linux virtual console (an occasional emergency context). The long tail stays GUI-first and auto-approximates.
-
-Why this scope: the GUI and the normal terminal (foot + tmux, truecolor / ≥256-color) both render the GUI hexes fine; GUI-first is correct there. Only the Linux VT is 16-color, and a low-contrast palette approximates badly down to 16 — so a few core faces get a deliberately higher-contrast 16-color fallback rather than every face carrying a multi-spec. Tool work: the ANSI-16 panel + a flag on the core faces to also capture a 16-color value; =build-theme.el= emits multi-spec only for those. Full per-face fallback is revisited only if console work becomes regular.
-** TODO [#D] theme-studio per-tier reseed controls :feature:theme-studio:
-Deferred from the seeding-engine spec (vNext). V1 reseeds all three guide-owned tiers at once; later consider separate "reseed syntax", "reseed UI", and "reseed package/org" controls if all-at-once proves too blunt. Spec: [[file:docs/design/theme-studio-seeding-engine-spec.org][spec]] (vNext; review folded in 2026-06-08).
-** TODO [#D] theme-studio low-contrast preset/mask mode :feature:theme-studio:
-Deferred from the perceptual color metrics spec (vNext). After raw OKLCH/APCA/DeltaE readouts exist, decide whether to add a named low-contrast workflow: APCA Lc bands, a contrast ceiling/floor mask, or a "soft" sibling to the existing any/AA+/AAA picker mask. Spec: [[file:docs/design/theme-studio-perceptual-color-metrics-spec.org][spec]] (vNext candidates; review folded in 2026-06-08).
-** TODO [#D] theme-studio CIEDE2000 DeltaE option :feature:theme-studio:
-Deferred from the perceptual color metrics spec (vNext). v1 uses DeltaE-OK on its native scale with a 0.02 threshold (decided); revisit CIEDE2000 only if the native OKLab scale proves too unfamiliar or poorly calibrated for palette distinguishability. Spec: [[file:docs/design/theme-studio-perceptual-color-metrics-spec.org][spec]] (vNext candidates; review folded in 2026-06-08).
** TODO [#B] Dashboard keybinding changes :quick:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-06
:END:
pressing g has should refresh. find another binding for Telegram.
-** TODO [#A] Calibre Open Work :calibre:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-06
-:END:
-Parent grouping the open Calibre / ebook-workflow issues; close each child independently. The EPUB reading-width tasks were already resolved (2026-05-12/14).
-
-*** DOING Calibre bookmark title format :feature:solo:quick:
-When I hit m in calibre, I'm making my place in the book with a bookmark.
-While sometimes, the books look fine: "The A.B.C. Murders - Agatha Christie.epub"
-Sometimes they look not so good: Engines of Logic_ Mathematicians and the O - Martin Davis.pdf or Software Architecture_ The Hard Parts _ Mo - Neal Ford.pdf
-
-What I would like to do is to have the bookmarks be saved in the following format:
-
-Author, Title [no extension]. Underscores should be stripped.
-
-Root cause: in a nov buffer =m= is =bookmark-set= (rebound at calibredb-epub-config.el:311); nov's =nov-bookmark-make-record= names the record =(buffer-name)= -- the EPUB filename.
-
-Implemented 2026-06-06. Source decision: parse the *filename*, not the embedded EPUB metadata -- under Calibre's "<Title> - <Author>.epub" naming the filename is more complete (the embedded metadata had truncated titles, author-sort "Last, First" forms, and lost punctuation; see the separate metadata-cleanup task). A =:filter-return= advice on =nov-bookmark-make-record= rebuilds the name from the record's filename: split on the last " - " into title/author, restore the colon Calibre sanitized to "_ " (-> ": "), reorder to "Author, Title". Pure helpers =cj/--nov-clean-title= + =cj/--nov-bookmark-name-from-file= in =modules/calibredb-epub-config.el=; 10 ERT tests in =tests/test-calibredb-epub-config--bookmark-name.el=. Live in the daemon.
-
-Existing bookmarks: the 3 nov bookmarks in =~/sync/org/emacs_bookmarks= were renamed by hand (one-pass, in the daemon + saved; backup at =emacs_bookmarks.bak-2026-06-06=): "Edward Kanterian, Frege: A Guide for the Perplexed", "Agatha Christie, The A.B.C. Murders", "Edward Abbey, The Fool's Progress: An Honest Novel".
-
-Awaiting Craig's manual confirm: make a NEW bookmark (open an EPUB, hit m) and check the default name is "Author, Title" from the filename.
-
-*** DOING [#A] Reconsider Calibre keybindings :feature:ux:
-Relocated from the global capture inbox 2026-06-06. Want a discoverable set of keybindings (visible in which-key) for the most frequent calibredb workflows:
-- Switch to a library (e.g. Literature), sort by last name, scroll the list.
-- Scope/filter the list in place, keeping the current library scope:
- - by format (e.g. epubs only)
- - by author last name (exact == or ^begins-with some text)
- - sort by title, publication date, or group by format
-- One key pops up the selected book's description in a bottom-30% buffer, dismissed with q (same display pattern as the signel chat dock).
-- RET opens the book in the appropriate viewer.
-Survey finding 2026-06-06: calibredb already binds almost all of this in calibredb-search-mode-map (S/L library, g filter [f format, a author, t tag, d date], o sort [t title, a author, p pubdate, f format], RET open) and even ships transient menus (? = calibredb-dispatch, g, o). The real problem was discoverability -- they are top-level single keys (which-key never pops up) and Craig didn't know ? opened a menu. calibredb-quick-look is macOS-only; the detail view (v -> *calibredb-entry*, q quits) is the description but opens full-window.
-
-Implemented 2026-06-06 in =modules/calibredb-epub-config.el=:
-- A curated transient =cj/calibredb-menu= (library switch; filter format/author/reset; sort author/title/pubdate/format; open; describe; H = full calibredb-dispatch) bound to =?= in calibredb-search-mode-map. calibredb's own full dispatch moved to =H=. Defined in the use-package =:config= (needs the elpa transient, which batch doesn't load) -- the "? brings up a curated help menu" convention.
-- Bottom-30% description dock: =calibredb-show-entry-switch= -> =pop-to-buffer= + a =display-buffer-alist= rule for =*calibredb-entry*= (display-buffer-at-bottom, height 0.3); =cj/calibredb-describe-at-point= shows the entry without switching focus so q dismisses it. Same pattern as the signel chat dock.
-1 ERT test (the describe command; the transient/bindings/dock need the elpa transient + live calibredb, verified in the daemon). Author "begins-with" is covered well enough by g a's completing-read over "Last, First"; a true regex filter was not built. Awaiting Craig's manual verify (M-B -> ? menu; d/v docked description; H full menu).
-
-*** TODO Embed Calibre DB metadata into the EPUB files :data:maintenance:
-Surfaced 2026-06-06 while building the bookmark naming: the metadata embedded in the EPUB files' OPF is worse than Calibre's database metadata. nov reads the embedded OPF and got truncated titles ("Frege" vs the filename's "Frege: A Guide for the Perplexed"), author-sort "Last, First" forms ("Christie, Agatha"), and lost punctuation ("A.B.C." -> "A B C"). The filenames (from Calibre's curated DB) are the good copy. Fix on the Calibre side: select all (or by library), run "Edit metadata -> Embed metadata into book files" so the DB metadata is written into each EPUB's OPF. Consider auditing author vs author_sort first. After embedding, the in-file metadata matches the library and any tool reading the files (nov, other readers, re-imports) gets the good data. Not an Emacs task; Calibre-side bulk maintenance.
-
** TODO [#B] TTY-accessible personal C-; keymap :feature:ux:solo:quick:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-05
@@ -319,504 +219,6 @@ Easy prefix candidates (home-row-leaning, TTY-safe), same leaf keys under each:
While in here, audit individual leaf chords for other non-TTY keys (any =C-RET=, super/hyper bindings — terminals can't send super/hyper either) and note or remap them. Verify the result in an actual =emacs -nw= / =emacsclient -nw= frame, not just GUI. Relates to the standing "org-mode keybinding consolidation" reminder.
-** DOING [#B] Signel Client Open Work
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-06
-:END:
-Parent task for the Emacs Signal client. Engine: signal-cli (linked secondary device). Front end: a fork of signel at =~/code/signel=, wired through =modules/signal-config.el=. Design: [[file:docs/design/signal-client.org][docs/design/signal-client.org]]. Child issues below.
-
-*** 2026-05-26 Tue @ 20:06:58 -0500 Decided: fork signel rather than depend on it
-signel is on MELPA but stale (one-author v0.1, all commits in a Jan-2026 burst, unattended tracker, no PRs). The spec needs internal edits (notify behavior, input-clobber fix), which are clean in a fork and hacky via advice, and a dead upstream means no divergence cost. Rejected: adopt-from-MELPA + advice, build-from-scratch, signal-cli-rest-api (Docker), MCP-tool, ERC bridge. Full rationale in the design doc.
-
-*** 2026-05-26 Tue @ 20:06:58 -0500 Linked as secondary device; contact parser verified against live shape
-Installed signal-cli 0.14.4.1 (AUR; imported AsamK's signing key FA10826A... to clear the makepkg verification). Linked the account via QR. Built and unit-tested the pure helper layer in =modules/signal-config.el= (contact-list parsing, notify-when-not-viewing predicate) with =tests/test-signal-config.el=. Confirmed the live =listContacts= shape: givenName/familyName are top-level in 0.14, not under profile as first assumed; corrected the parser and verified it produces a picker entry for all 94 real contacts. Sent a request to archsetup to add signal-cli to the standard install.
-
-*** 2026-05-27 Wed @ 22:08:40 -0500 Shipped initiate-message workflow: picker + Note-to-Self + keymap
-=cj/signel-message= (=C-; M m=) names contacts via =completing-read= over the cj-owned =cj/signel--contact-cache=, with "Note to Self" pinned first. =cj/signel-message-self= (=C-; M s=) sends straight to =signel-account=. Daemon guard =cj/signel--ensure-started= auto-starts the daemon when =signel-account= is set and =user-error='s with the remedy when it isn't; on start it pre-warms the cache. =cj/signel--fetch-contacts= rides the new RPC callback contract (=signel--send-rpc= with success-callback), the result feeds =cj/signal--parse-contacts=, and =cj/signel-refresh-contacts= (=C-; M no leaf=) clears + refetches. Cold-cache invocations =accept-process-output= up to =cj/signel-fetch-timeout= seconds (3s default) and =user-error= on timeout so a wedged daemon can't hang Emacs. Prefix keymap =cj/signel-prefix-map= bound under =C-; M= via =keybindings.el='s =cj/custom-keymap=: m / s / d / q / SPC. 15 new ERT tests in =tests/test-signal-config.el= cover ensure-started branches, fetch contract, cache empty-vs-failure, refresh, picker happy-path + cold-cache resolves + cold-cache timeout, message-self, and the prefix map bindings.
-
-*** 2026-05-27 Wed @ 21:55:57 -0500 Added JSON-RPC success-result dispatch in the signel fork
-Fork commit 4740d97 added =signel--request-handler-map= (id → success callback), extended =signel--send-rpc= with an optional =success-callback= that registers under the new request id, and gave =signel--dispatch= a result branch that invokes the callback and removes the handler. Error responses also remhash the handler entry, and =signel-start= / =signel-stop= both =clrhash= the map so reconnect is reliably empty. Backward-compatible: existing callers that don't pass a callback hit the same code path as before. Five ERT tests in this project (=tests/test-signel-rpc-dispatch.el=, dotemacs commit bfec0eab) lock the contract: Normal (result invokes callback + cleanup, send-rpc registers), Boundary (unknown id is a no-op), Error (error response cleans up handler), reconnect (=signel-stop= empties the map). Refactor audit surfaced a separate pre-existing leak in =signel--handle-error= (request-buffer-map entries aren't removed on error); filed as the [#C] follow-up below.
-
-*** TODO [#C] signel--handle-error leaks request-buffer-map entries :bug:no-sync:
-Surfaced during the JSON-RPC dispatch refactor audit. =signel--handle-error= reads =signel--request-buffer-map= by id but never =remhash='es the entry, so every error response leaves the request-id → buffer-name mapping behind for the life of the process. Low impact (the map clears on stop/start, and id collisions are unlikely at the counter scale), but unbounded growth in a long-lived session and inconsistent with how the new request-handler-map is cleaned up on error.
-
-*** DOING [#B] Notify only for the unviewed conversation :feature:
-Wire =cj/signal--should-notify-p= (done) into signel's =signel--handle-receive= notify block (signel.el:277), route through Craig's notify script instead of bare =notifications-notify=, and gate sound behind a defcustom that defaults off. Spec addendum (the four notify details + wiring architecture) accepted 2026-06-11 — see [[file:docs/design/signal-client.org][signal-client.org]] "Notification slice".
-
-Built 2026-06-11 (TDD; fork commit e263367, dotemacs 9afc6128): =signel-notify-function= customization point in the fork; =cj/signel--notify= + =cj/signal--format-notify-body= + =cj/signel-notify-sound= in signal-config.el, wired in =:config= with a load-time =cj/executable-find-or-warn=. 17 new ERT tests green; full launch smoke clean; live-reloaded into the daemon and a synthetic toast fired through the script path. Stays DOING until the two manual checks below pass.
-
-**** Signel: real incoming message raises a toast through the notify script
-What we're verifying: the full receive path (signal-cli → signel --handle-receive → cj/signel--notify → notify script) fires on a real message.
-- Make sure you are NOT viewing the sender's chat buffer.
-- Have a real message sent to you on Signal (or send one from your phone to a second device thread that lands here).
-Expected: a transient info toast titled "Signal: <sender>" with the message text (one line, truncated if long), no sound.
-
-**** Signel: actively-viewed chat stays quiet
-What we're verifying: the suppression predicate gates the toast when you're reading that chat.
-- Open the sender's chat buffer (=C-; M m=) and keep it the selected window in a focused frame.
-- Have the same sender message you again.
-Expected: the message renders in the buffer, but no desktop toast appears.
-
-*** 2026-05-27 Wed @ 22:08:40 -0500 Shipped clobber fix for both insert paths
-Fork commit 5ec56c0 added =signel--pending-input= (capture from input-marker to point-max) and =signel--restore-input= (re-insert after the redrawn prompt; nil-safe), and wired both into =signel--insert-msg= (the receive path) and =signel--insert-system-msg= (the error path). A mid-type send now survives both an incoming message and a system-error insertion. Four ERT tests in =tests/test-signel-input-preservation.el= cover the helpers (typed text, empty) and both insert paths via a temp =signel-chat-mode= buffer.
-
-*** TODO [#B] Link command with QR :feature:
-=cj/signel-link= wrapping =signal-cli link -n NAME=, capturing the =sgnl://linkdevice= URI and rendering it as a scannable QR (qrencode). Convenience for re-linking; the first link was done by hand this session.
-
-*** 2026-05-27 Wed @ 22:08:40 -0500 use-package wired with C-; M keymap and local account config
-=use-package signel :load-path "~/code/signel" :ensure nil= already wired earlier with =signel-auto-open-buffer nil=. Account source is =signel-account= set from =cj/signal-private-config-file= (=signal-config.local.el=, gitignored) loaded in =:config=, decided in the workflow spec. Keymap prefix =C-; M= attached via =with-eval-after-load 'keybindings= so the binding survives load-order.
-
-*** TODO [#D] Include Signal groups in the picker :feature:no-sync:
-vNext after the 1:1 initiate-message flow is stable. Merge =listGroups= with =listContacts=, label groups distinctly, and preserve the current v1 behavior where the picker is contacts-only.
-
-*** 2026-06-06 Sat @ 12:29:24 -0500 Fixed C-; M load-order bug via canonical register-prefix-map
-Root cause: signal-config.el was the only feature module that violated the prefix-registration contract documented in =keybindings.el:41-45=. Every other prefix map uses =(require 'keybindings)= + a top-level =(cj/register-prefix-map "X" map)=; signal-config had neither, mutating =cj/custom-keymap= directly through a =(with-eval-after-load 'keybindings (when (boundp 'cj/custom-keymap) ...))= form. The =boundp= guard turned a load-order miss into a SILENT no-op — no error, the binding just never happened — which is why a live-reload (keybindings definitely loaded by then) papered over it.
-Fix: added =(require 'keybindings)= at the top of signal-config.el and replaced the guarded form with =(cj/register-prefix-map "M" cj/signel-prefix-map "signal messages")=, matching the 25+ other prefix maps.
-Verified: (1) new contract test =test-signal-config-prefix-map-registered-under-c-semi-m= asserts =C-; M= resolves to =cj/signel-prefix-map= (35/35 green); (2) full =emacs --batch= init.el launch — the exact failing scenario — now shows =C-; M= bound; (3) clean byte-compile; (4) live-reloaded into the daemon, binding confirmed. No unit-level red was possible: the =boundp= guard is robust under all standard test timings, which is the CLAUDE.md launch-only-failure class.
-
-*** 2026-05-28 Thu @ 03:09:18 -0500 Chat buffer docks bottom 30% and C-c C-k cancels
-=display-buffer-alist= entry in =modules/signal-config.el= matches =^\*Signel: = chat buffers and routes them through =display-buffer-at-bottom= with =window-height . 0.3=, so the chat docks to the bottom 30% of the frame. The signel fork's =signel-chat= switched from =switch-to-buffer= to =pop-to-buffer= so the rule can apply (=switch-to-buffer= ignores =display-buffer-alist=). =C-c C-c= was already bound to =signel--send-input= in the mode; =C-c C-k= now binds =signel--cancel-input=, a new fork helper that clears the editable region between =signel--input-marker= and =point-max= and then calls =quit-window=. Buffer stays alive so chat history above the marker survives revisits; cleared input means the next visit lands on a fresh prompt. Five ERT tests in =tests/test-signel-cancel-input.el= (clears pending, empty-area no-op, quit-window called, buffer preserved, keymap binding) and two new tests in =tests/test-signal-config.el= (entry shape + regex match set). Dotemacs commit 998e9c7a, fork commit df02d79.
-
-** DOING [#B] Migrate All Terminals From Vterm to Ghostel :terminal:ghostel:
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-04
-:END:
-Replace vterm with ghostel (libghostty-vt) as the single terminal engine across every workflow, and rename ai-vterm → ai-term. References: [[file:docs/2026-05-25-emacs-terminal-comparison.org][docs/2026-05-25-emacs-terminal-comparison.org]] (vterm vs eat vs ghostel research); migration spec [[file:docs/design/vterm-to-ghostel-migration-spec.org][docs/design/vterm-to-ghostel-migration-spec.org]] (READY; external review incorporated 2026-06-04, D1-D7 agreed). Build in 5 phases (0-4); see the spec's Implementation tasks block.
-
-Decisions D1-D7 are settled in the spec's Agreed-decisions section. Build order below; each phase stays green (suite + byte-compile) at every step.
-
-*** 2026-06-04 Thu @ 23:57:09 -0500 Phase 0 done: characterization baseline green
-=make test= green except the 5 documented pre-existing failures (4 test-dupre-theme, 1 test-init-module-headers), none terminal-related. Characterization coverage already present + green for all six must-survive behaviors: vterm-toggle--dispatch/display/buffer-filter, vterm-tmux-history, ai-vterm--show-or-create/launch-command/f9-in-vterm, ui-config--buffer-cursor-state + vterm-copy-mode-cursor, dashboard-config-launchers. Add a characterization test before any behavior change in later phases if a gap appears.
-
-*** 2026-06-05 Fri @ 00:38:34 -0500 Phase 1 done: ghostel + term-config.el
-=modules/term-config.el= written (full port of vterm-config: tmux history/copy-mode-dwim preserved via process-tty-name + ghostel-send-string; F12 toggle + display rule + geometry; cj/term-map C-; x menu → ghostel commands; which-key "terminal menu"; ghostel-max-scrollback 10MB; C-; added to ghostel-keymap-exceptions; F12 + C-; in ghostel-mode-map; use-package ghostel guarded per D6). Dropped: mouse-wheel SGR forwarding, vterm-timer-delay hacks, copy-mode cursor hook, goto-address hook. ghostel installed into elpa (MELPA + auto-downloaded native module). Tests: test-term-toggle--{dispatch,display,buffer-filter} + test-term-tmux-history (16) ported with a ghostel stub in testutil-ghostel-buffers; all green.
-
-*** 2026-06-05 Fri @ 00:38:34 -0500 Phase 2 done: ai-vterm→ai-term on ghostel
-=modules/ai-vterm.el= → =modules/ai-term.el=: 6 vterm call sites swapped to ghostel (buffer named via let-bound ghostel-buffer-name + pinned ghostel-buffer-name-function so OSC titles don't rename agent buffers); F9/C-F9/M-F9 on global + ghostel-mode-map; refuse-in-terminal guard removed (D4 — F9 launches in TTY frames); tmux-suppression invariant preserved (cj/--ai-term-suppress-tmux). 23 ai-vterm tests renamed → test-ai-term--* (terminal-guard test deleted, obsolete); show-or-create + f9-in-term rewritten for ghostel; all green. ui-config cursor-state ported (ghostel-mode + ghostel--input-mode; copy/emacs = read-only, else writeable) + its test. init.el now requires term-config + ai-term; vterm-config.el + ai-vterm.el deleted. Full suite green except the 5 documented pre-existing failures (4 dupre-theme, 1 init-module-headers/popper-config-missing — both unrelated). validate-modules ✓; full early-init+init smoke clean (no ghostel/term/ai-term errors). vterm package still installed (Phase 4) — dashboard "Launch VTerm" + dormant auto-dim still reference it until Phase 3/4. Restart Emacs to pick up ghostel (load-order + use-package :config change).
-
-*** TODO [#B] Phase 2: rename ai-vterm→ai-term on ghostel :terminal:ghostel:
-Swap the 6 vterm call sites; F9 family on global + ghostel-mode-map; drop refuse-in-terminal guard (D4); preserve the tmux-suppression invariant. Rename engine-agnostic tests after green; rework coupled tests; add D4 + F12-excludes-agent regression tests.
-
-*** 2026-06-05 Fri @ 00:50:58 -0500 Phase 3 done: satellites ported to ghostel
-Deleted auto-dim's vterm color-advice + redraw integration (~165 lines; D1 — terminals don't dim, ghostel bakes its palette per-terminal so there's no per-window color hook); dashboard launcher → =(ghostel)= + "Launch Terminal" label; cj-window-geometry/toggle-lib doc comments; module-inventory + init-load-graph doc refs. (ui-config cursor-state + init.el requires landed in Phase 2.) Trimmed test-auto-dim-config (dropped the 6 vterm tests) + updated the dashboard-launcher test stub. Incidental: removed the stale =popper-config= entry from the test-init-module-headers allowlist (the file doesn't exist + isn't required) — fixes the long-standing pre-existing test failure.
-
-*** 2026-06-05 Fri @ 00:50:58 -0500 Phase 4 done: vterm + vterm-toggle removed
-=package-delete='d vterm + vterm-toggle from elpa. No vterm refs remain in modules/init except intentional historical comments. Suite green except the 4 pre-existing dupre-theme failures (the popper-config one is now fixed). validate-modules ✓; full early-init+init batch smoke = INIT-SMOKE-OK. The migration parent stays DOING until Craig restarts Emacs and walks the ghostel manual-verify matrix under "Emacs Manual Testing and Validation".
-
-*** TODO [#B] Follow-up: theme ghostel ANSI faces in dupre :terminal:ghostel:dupre:
-D2 — set the 16 ghostel-color-* + ghostel-default faces in dupre-faces/palette.
-
-*** TODO [#B] Follow-up: evaluate ghostel-eshell + ghostel-compile :terminal:ghostel:eval:
-D3 — ghostel-eshell as eshell visual backend; ghostel-compile against F4 dev-fkeys.
-
-*** 2026-06-05 Fri @ 14:24:02 -0500 Auto-dim revisit cancelled — current no-dim behavior is fine
-Craig confirmed the shipped auto-dim setup works fine as-is: terminal buffers don't participate in unfocused-window dimming (D1), and the rest of auto-dim behaves. That is the measured decision the original task asked for — option (a), keep no-dim — so no rework (the focus-loss palette-blend in option (b) or an upstream per-window hook in option (c)) is needed. Closing without further investigation. Context: [[file:docs/design/vterm-to-ghostel-migration-spec.org][migration spec]] D1.
-
-*** TODO [#B] Investigate ghostel selection/highlight color :terminal:ghostel:
-Look at how selected text is highlighted in a ghostel buffer — the region face in =ghostel-copy-mode= and any live selection — surfaced during the copy-mode debugging. Check whether the highlight is legible against the dupre background and consistent with the rest of the config; if it needs theming, fold it in with D2 (theming the ghostel faces in dupre).
-
-*** 2026-05-26 Tue @ 15:15:43 -0500 Direction confirmed; Claude Code in eat needs a caveat
-Craig confirmed the consolidation: one terminal engine everywhere — eat for standalone terminal buffers (replacing vterm) plus =eat-eshell-mode= as eshell's visual backend, keeping eshell as the shell. Not dropping eshell for eat + zsh.
-
-Researched whether Claude Code runs cleanly in eat (Craig runs it in his Emacs terminal). Verdict: mostly, with caveats. eat is the default backend for claude-code.el and renders the TUI with color and full key handling, but there is an eat-specific bug where Claude Code's input handling makes the buffer scroll-pop to the top on window-buffer changes and the input box can get stuck mid-buffer (recoverable, but it does not happen in vterm or ghostel), and eat runs about 1.5x slower than vterm on heavy streaming output. claude-code.el's own docs name ghostel as the most faithful Claude TUI renderer.
-
-Recommendation: consolidate everyday terminals onto eat, but keep ghostel (or vterm) for the Claude Code workflow specifically — the scroll-pop / stuck-input bug and the slower heavy-stream handling are exactly what bites a long Claude session. Sources: [[https://github.com/cpoile/claudemacs][claudemacs]], [[https://github.com/stevemolitor/claude-code.el][claude-code.el]], [[https://codeberg.org/akib/emacs-eat][emacs-eat]].
-
-Eval plan (from the research doc): install EAT alongside vterm, run the same workloads through both, decide. Test matrix: Claude Code TUI, lazygit, htop/btop, yazi, a heavy-output build, ssh to a remote, and eshell with =eat-eshell-mode=. Assess rendering fidelity, stability under heavy output, and Emacs-native line editing. Switch only if it covers every workflow without regression.
-
-*** 2026-06-02 Tue @ 14:12:48 -0500 Audit: eval plan not yet run; back to TODO
-Task audit found no eval work recorded since the 2026-05-26 direction-confirmed note. The test matrix above is unrun, so the task isn't actively in progress — moved DOING back to TODO until the eval starts.
-
-*** 2026-06-04 Thu @ 22:40:27 -0500 Pivot: ghostel as the single engine (not eat)
-Direction changed from eat-everyday + ghostel-for-Claude to ghostel-for-everything, and the task is now a migration rather than an eval. Rationale: ghostel is claude-code.el's most-faithful Claude TUI renderer and the fastest engine (81 vs vterm 34 vs eat 4.9 MB/s), and an audit confirmed it exposes an analog for every vterm primitive this config uses (=ghostel-send-string=, =ghostel-keymap-exceptions=, =ghostel-copy-mode=, =ghostel-clear-scrollback=, =ghostel-send-next-key=, =ghostel-next-prompt= / =ghostel-previous-prompt=, =ghostel-max-scrollback=, =ghostel-kill-buffer-on-exit=). eat's washed colors, the scroll-pop / stuck-input bug under Claude Code, and slowest throughput made it the weaker single-engine pick; one engine beats running two. Surface audited: 2 main modules (=vterm-config.el=, =ai-vterm.el=) + 4 satellites (=auto-dim-config.el= is the heavy one) + ~35 test files + init.el. Next: spike ghostel read-only to answer the open migration questions (auto-dim rework — ARCHITECTURE.md forbids the around-redraw color advice vterm uses; tmux pane-id via =process-tty-name= on a ghostel process; buffer naming; TTY-frame behavior; copy-mode keybinding parity), then write the migration spec under =docs/design/= and review it.
-
-*** 2026-06-04 Thu @ 23:17:54 -0500 Spec review: not ready until decisions and handoff shape are closed
-Ran the spec-review workflow against [[file:docs/design/vterm-to-ghostel-migration-spec.org][docs/design/vterm-to-ghostel-migration-spec.org]] and wrote a companion review file (incorporated and deleted 2026-06-04). Verdict: =Not ready=. Direction is sound, but the draft still has open D1-D5 decisions, lacks the workflow-required =Implementation phases= section and acceptance criteria, and needs explicit ghostel package/native-module failure behavior before implementation tasks can be emitted.
-
-*** 2026-06-04 Thu @ 23:24:28 -0500 Spec-response: review incorporated, raised to READY
-Folded the external review via spec-response. Craig accepted D1-D5; baked them plus D6 (module-failure = degrade-with-warning, modifying the reviewer's fail-loud) and D7 (=ghostel-max-scrollback= 10 MB) into a new Agreed-decisions section. Added Implementation phases (0-4), Acceptance criteria, Dependency/module-failure behavior, Test strategy, per-phase key/menu ownership, the tmux-suppression contract, and an Implementation-tasks drop-in block. Status DRAFT → READY; review file deleted. Build is now unblocked.
-
-*** 2026-06-04 Thu @ 23:30:18 -0500 External re-review: ready
-Re-reviewed [[file:docs/design/vterm-to-ghostel-migration-spec.org][docs/design/vterm-to-ghostel-migration-spec.org]] after incorporation. Verdict: =Ready=. No further blocking review notes; implementation can start from the phase plan and acceptance criteria in the spec.
-
-** PROJECT [#B] Implement ai-kb :feature:ai:kb:
-Build v1 of the AI knowledge base per [[file:docs/design/ai-kb.org][docs/design/ai-kb.org]] (Ready; six reviews incorporated, all decisions resolved 2026-05-24). Step 1 splits into 1a (the safe write path — minimum usable) and 1b (retrieval, maintenance, push), since =remember= depends on =index=+=lint= and the adapter depends on =remember=. Step 2 is the Emacs layer: a full org-roam profile on switch, the human-edit safety model (same write path as the agent), and the browsing surface. Step 3 and the LLM-Wiki layer are vNext. Children are ordered by build sequence; the server bootstrap is the prerequisite.
-
-*** TODO [#B] ai-kb bare repo on cjennings.net :ai-kb:
-Prerequisite, one-time server bootstrap (not doable by the local script): =sudo git init --bare /var/git/ai-kb.git= + chown on cjennings.net. Leave the github-mirror hook OFF — this repo is private. Required before every per-machine clone.
-
-*** TODO [#B] ai-kb store + contract + seed :ai-kb:
-Step 1a. Clone =git@cjennings.net:ai-kb.git= to =~/.local/share/ai-kb=. Author =AGENT_CONTRACT.org= (canonical repo-resident contract: node format, write protocol, operations, routing) and seed =index.org= + a README/index node with a generated =:ID:=. Node format per spec — a *required* one-line =:SUMMARY:= (the index/query read it straight, no inference/LLM), provenance (=:CREATED_BY:/:CONFIDENCE:/:VISIBILITY:/:SOURCE:/:STATUS:=), =:PROJECTS:= slugs, type filetags, relation labels. Define the durable external-pointer format as *ID-first*: =ai-kb: <Title> (<UUID>)=, resolved by ID with title fallback (filenames can change in curation).
-
-*** TODO [#B] ai-kb CLI 1a: index, lint, remember, doctor :ai-kb:
-Step 1a. Shell wrapper calling Emacs for org work — =emacsclient= when a daemon is up, =emacs --batch= fallback, lint+index in *one* invocation per =remember=. =index= regenerates =index.org= from node properties incl. =:SUMMARY:= (never hand-maintained); the index references nodes as plain =Title (UUID)= text, never =[[id:]]= links, and is excluded from the scan so it can't manufacture backlinks or hide orphans. =lint= = org-lint fatal checks + duplicate IDs + broken id-links (excl =raw/= + index) + missing required props (incl =:SUMMARY:=) + bad project slugs + stale/incomplete index + credential scan of nodes *and* =raw/= text files (binaries skipped). =remember= = the write protocol: fetch + =pull --ff-only= (abort on diverge/dirty), write, regenerate index, then run the *full =ai-kb lint=* over the change as the commit gate (not just node org-lint — this is the safety boundary), commit locally, =flock=; no push. =doctor= / =status= = health + push-state + raw-dir-size report (repo, private remote, CLI on PATH, =graphviz= if the map needs it, adapter linked, db buildable, no secrets, "ahead N"/"push failed"/"diverged"); =status= is the fast non-diagnostic mode for the dashboard/nudge.
-
-*** TODO [#B] claude-rules/ai-kb.md adapter :ai-kb:
-Step 1a. Global L1 rule in rulesets pointing at the repo-resident =AGENT_CONTRACT.org=: path, routing (T1/T2/T3 tiers; per-project =MEMORY.md= shrinks to ID-first pointers into ai-kb), proactive + contradiction rules, concrete "read the index first" triggers, link-grep recipes, "use =ai-kb remember=, never bypass =ai-kb lint=", one-line nudge on unpushed commits / recorded push rejection. =make install= symlinks it into =~/.claude/rules/=.
-
-*** TODO [#B] ai-kb provisioning: setup-ai-kb.sh + make ai-kb-init :ai-kb:
-Step 1a (core; the timer-install line is added with 1b). Idempotent =scripts/setup-ai-kb.sh=: clone (or init+add-remote on first machine), seed, install the CLI on PATH, =ai-kb index=, =ai-kb doctor=. =make ai-kb-init= wraps it. The one-time server bootstrap stays a separate documented step.
-
-*** TODO [#B] ai-kb Step-1a tests :ai-kb:tests:
-Write-path: a write with the remote unreachable still commits locally and does not error; =flock= serializes concurrent =remember=; each org-lint *fatal* check (malformed drawer, missing/dup =:ID:=, invalid required property, missing =#+title:=, unparseable org) rejects the commit, a style warning does not; a node missing =:SUMMARY:= fails lint; =remember= aborts the commit when the *full* lint fails (stale index, broken link, secret in a node or =raw/= text file); the credential scan skips binaries. Index: regen from a fixture produces expected entries; an out-of-band node appears only after regen; a node referenced only by =index.org= still reports as an orphan (the index is not a backlink source). Link recipes: backlink (excl =raw/= + index) + forward correct. Provisioning (bats): idempotent, valid =:ID:= + =:SUMMARY:=, =doctor= passes.
-
-*** TODO [#B] ai-kb CLI 1b: query, curate, sync :ai-kb:
-Step 1b. =query <context>= with a *testable contract*: plain-text default + =--json=; fields title/ID/summary/projects/status/updated/path + *match reason*; searches index rows + title/tags/properties/body; ranks by lexical score — sum of each matched field's weight, counted once per field: title 100, tag/project/status 50 each, summary 20, body 5; no term-frequency weighting in v1 — with most-recently-updated (=:UPDATED:=) only as the *tie-break* on equal scores (recency alone buries stable old preferences); default max-results; =raw/= paths only as source references; exit codes for no-match / invalid KB / lint-index failure. =show <id-or-title>= (resolve ID-first, print the node) and =backlinks <id>= (excl =raw/= + index) as the inspection primitives the Emacs commands wrap. =curate --dry-run= (four buckets; also flags orphan =raw/= captures and any =raw/= file over 256 KB; destructive ops human-only). =sync= (=org-roam-db-sync= against ai-kb) only when the db is missing/stale or forced.
-
-*** TODO [#B] ai-kb push timer + failure observability :ai-kb:
-Step 1b. =ai-kb-push.timer= + =ai-kb-push.service= =systemd --user= units: push only if ahead, ~15 min; installed + =enable --now= by the setup script (add this line to =setup-ai-kb.sh=). A failed push is logged to a state file (=$XDG_STATE_HOME/ai-kb=), never fatal; surfaced by =ai-kb doctor= and the adapter's startup nudge.
-
-*** TODO [#B] ai-kb-curate workflow in rulesets :ai-kb:
-Step 1b. =~/code/rulesets/.ai/workflows/ai-kb-curate.org= — human-gated curation: the four buckets, node-count trigger (nudge at 150 nodes, re-fire every +50), =:LAST_CURATED:= rotation, pointer-integrity (merge/supersede changes the canonical ID, so grep inbound =[[id:]]= + =MEMORY.md= =ai-kb: ... (UUID)= refs and repoint before deleting). Surfaced by =ai-kb doctor= + session startup when due.
-
-*** TODO [#B] ai-kb Step-1b tests :ai-kb:tests:
-=query --json= returns the specified fields (incl. match reason)/exit-codes on a fixture KB and =raw/= appears only as a source ref; a title match outranks a body-only match with recency only breaking ties (an old preference is not buried under a newer body-only hit); a simulated push failure is recorded to the state file and surfaced by =ai-kb doctor= / =status=. Performance (=:perf= tag): 100- and 1,000-node fixtures keep =index=/=query=/=lint=/=remember= under a stated time budget (catches an accidental per-check Emacs startup or an O(n²) scan).
-
-*** TODO [#B] Emacs: org-roam ai-kb profile + switch :ai-kb:
-Step 2.
-=org-roam-config.el=: =cj/org-roam-switch-to-ai-kb= / =cj/org-roam-switch-to-personal= install a full org-roam *profile*, not a two-variable swap — dir + =org-roam-ai.db= + =org-roam-file-exclude-regexp= (=raw/= + =index*.org=), and dailies, capture templates, topic/project/recipe find wrappers, and the agenda/refile + completed-task→daily hooks all rescoped or neutralized so ai-kb nodes never leak into personal journals/agenda. Restore everything exactly on exit; re-assert personal state at startup (abnormal-exit safety). =cj/ai-kb-db-sync= syncs only when the db is missing/stale or forced, with a status indicator.
-
-*** TODO [#B] Emacs: ai-kb edit safety (same write path) :ai-kb:
-Step 2. An =ai-kb= minor mode whose =after-save-hook= runs the agent's post-write sequence under =flock= — =ai-kb index=, full =ai-kb lint=, commit, push-state update — so a human Emacs edit can't bypass index/lint/commit. One write path for both agent and human. Failure UX: the save always writes to disk and the buffer stays editable (never read-only/blocked); on lint failure it does *not* commit, pops findings to a =*ai-kb-lint*= buffer (no focus steal), and shows the uncommitted-failing state in the modeline + dashboard — Craig fixes and re-saves, a clean save commits. Recursion guard, two layers: the mode's activation predicate excludes =index*.org= + =raw/=, and the pipeline binds a re-entrancy flag (=cj/ai-kb--in-pipeline=) the hook early-returns on; index regen prefers =write-region= over =save-buffer=.
-
-*** TODO [#B] Emacs: ai-kb browsing surface :ai-kb:
-Step 2. =cj/ai-kb-dashboard= (status banner: active KB, node count, unpushed commits, push-failure state, curation due, last index/sync), =cj/ai-kb-find-node= (=org-roam-node-find= in the ai-kb profile), =cj/ai-kb-search= (=ai-kb query= or scoped =consult-ripgrep=), =cj/ai-kb-show-node= (resolve ID-first, open), =cj/ai-kb-backlinks= (excl =raw/= + index), =cj/ai-kb-map= (built-in =org-roam-graph= *first* — the profile's exclude regexp already keeps =raw/= + index out of the db, so the graph inherits the right scope; custom DOT export only if project/tag/status filtering proves necessary; =graphviz= dep). Simple wrappers over the CLI primitives where possible.
-
-*** TODO [#B] Emacs: ai-kb keybindings + which-key :ai-kb:
-Bind the switch + sync + browsing commands under the =C-c n= roam prefix (e.g. =C-c n a= → ai-kb, =C-c n A= → personal, a small transient for the browsing commands), avoiding the dense existing set; which-key labels.
-
-*** TODO [#B] Emacs: ai-kb Step-2 ERT tests :ai-kb:tests:
-Profile: switch installs the ai-kb dir + db + exclude regexp and switch-back restores personal *exactly* — completed-task hook, agenda/refile finalize hook, dailies, and capture templates all untouched by ai-kb while switched; startup re-asserts personal state after a simulated abnormal exit. Edit path: a save in an ai-kb buffer runs index+lint+commit (a bad save surfaces the lint failure rather than committing). Sync runs only when stale.
-
-** PROJECT [#B] Architecture review follow-up from 2026-05-03 :refactor:nosync:
-
-High-level pass over =init.el=, =early-init.el=, and all 104 files in
-=modules/=. The main theme: the config works, but load order, startup side
-effects, credentials, and test measurement are more implicit than they should
-be. Use this project as the parent tracker; each child below should land as a
-small, reviewable change.
-
-Review snapshot:
-- =modules/= has 104 files and about 24k lines including =init.el= and
- =early-init.el=.
-- =init.el= eagerly =require=s nearly every module.
-- =make coverage= passed when allowed to write the test scratch directory.
-- Coverage report: =3240/4952= executable lines, =65.43%=, across 49 module
- files. Caveat: 55 module files do not appear in the report at all, so the
- real project confidence is lower than the raw percentage suggests.
-
-*** 2026-05-15 Fri Consolidate shared utility helpers :architecture:refactor:
-CLOSED: [2026-05-15 Fri]
-
-Helpers are scattered across feature modules where they were first needed.
-Some are duplicated, and some private helpers are generic enough to belong in a
-shared foundation library. This is adjacent to the load-graph refactor because
-central helper ownership reduces hidden inter-module dependencies, but it
-should remain a sibling project so load-order batches stay small and
-reviewable.
-
-Guidance:
-- Do not extract a helper until at least two callers are clearly the same
- shape.
-- Prefer growing =system-lib.el= first; split into topic libraries only if it
- becomes too broad or starts pulling coarse dependencies into foundation
- startup.
-- Keep one helper extraction per commit.
-- Move unit tests with the helper. Consumers should keep behavior/integration
- coverage.
-- Do not add heavy package dependencies to foundation helpers.
-
-**** DONE [#B] Write full utility consolidation design spec :architecture:refactor:
-CLOSED: [2026-05-04 Mon]
-
-Create a design document that inventories candidate helper extractions,
-recommends grouping and naming, explains how the helpers fit into existing
-library modules, defines migration phases, and identifies testing/rollback
-rules.
-
-Spec: [[file:docs/design/utility-consolidation.org][docs/design/utility-consolidation.org]]
-
-Verify 2026-05-04:
-- Added [[file:docs/design/utility-consolidation.org][docs/design/utility-consolidation.org]].
-- Spec includes framing questions, existing library fit, proposed grouping,
- concrete pull/rename table, migration phases, test strategy, acceptance
- criteria, risks, open questions, and recommended first commits.
-- Parsed the spec and =todo.org= with =org-element=.
-- Committed the tracked spec as =3ea4707=.
-- Incorporated complete review feedback in =dd77ebd=, including API behavior
- contracts, speculative-extraction rules, =system-lib= dependency budget,
- inventory/audit artifacts, test relocation policy, commit type guidance,
- =use-package :if= load-order policy, and Phase 5 cache-design addendum
- requirement.
-
-**** DONE [#B] Inventory private helpers across modules :refactor:
-CLOSED: [2026-05-10 Sun]
-
-Walk every module and tag private helpers as genuinely module-specific,
-generic-but-trapped, or duplicated. Capture likely consumers and any dependency
-cost before extracting.
-
-Candidate families:
-- shell argument formatting,
-- executable lookup with user-visible warnings,
-- argv-based process runners,
-- path containment/safe-base predicates,
-- Org-safe heading/property/body text sanitizers,
-- cache-with-TTL plus invalidation hooks,
-- warning/message wrappers.
-
-Verify 2026-05-10:
-- Added [[file:docs/design/utility-inventory.org][docs/design/utility-inventory.org]] covering the 30 entries in the spec's
- Candidate Extraction Table grouped by family (executable discovery, shell
- quoting, process runner, file/path, external-open, Org-safe text, cache,
- logging, macros/debug, theme I/O, string).
-- For each helper recorded: visibility, dependencies, side effects, callers
- (production + test), test files, priority, decision (Migrate / Leave / Defer)
- with rationale.
-- Decisions Summary: 11 Migrate, 3 Leave, 13 Defer.
-- Concrete next-action list groups Migrate items by Phase (2 = foundation
- helpers, 3 = Org-safe text, 4 = external-open consolidation) for the order
- the spec recommends.
-- Discoveries: =cj/log-silently= has 10 production callers (more than the
- spec's table suggested -- defer is the right call); =cj/--file-manager-program-for=
- shipped today in =dirvish-config.el= is the new form of OS-dispatch
- consolidation and should fold into =cj/external-open-command= during Phase 4.
-
-**** DONE [#B] Extract executable lookup with warning helper :refactor:
-CLOSED: [2026-05-10 Sun]
-
-Create a generic helper such as =cj/find-executable-or-warn= from the useful
-=mail-config= pattern. It should return the executable path or nil and produce
-a clear warning when the executable is missing.
-
-Done 2026-05-10:
-- Shipped as =cj/executable-find-or-warn= in =modules/system-lib.el=
- (commit =c75e36f4=, extracted from =mail-config=).
-- First consumer rewired in =12c2cb14= (=cj/set-wallpaper= in
- =dirvish-config.el=).
-
-**** DONE [#B] Extract argv-based process runner helper :refactor:
-CLOSED: [2026-05-10 Sun]
-
-Generalize the =coverage-core= process pattern into a dependency-light helper
-that captures output and signals a clear =user-error= with command/status/output
-on failure. Consider a small git wrapper only after the generic runner exists.
-
-Done 2026-05-10:
-- Shipped =cj/process-output-or-error= plus the =cj/git-output-or-error=
- wrapper in =modules/system-lib.el= (commit =57e558ce=, extracted from
- =coverage-core=).
-
-**** DONE [#B] Extract Org-safe text sanitizers :refactor:
-CLOSED: [2026-05-10 Sun]
-
-Move heading/property/body sanitization into a shared helper once at least one
-non-calendar consumer is ready. Keep behavior explicit so external text cannot
-accidentally create headings or malformed properties.
-
-Done 2026-05-10:
-- Shipped =modules/cj-org-text-lib.el= (renamed to its final =-lib= form in
- commit =0f9e3087=) with three sanitizers: =cj/org-sanitize-body-text=,
- =cj/org-sanitize-property-value=, =cj/org-sanitize-heading=.
-
-*** 2026-05-15 Fri Make coverage reporting account for untracked modules :tests:
-CLOSED: [2026-05-15 Fri]
-
-The current coverage result is useful but easy to overread. =make coverage=
-reported =65.43%= for files that undercover saw, but only 49 of 104 module
-files appeared in =.coverage/simplecov.json=.
-
-Definition: in this task, "untracked modules" means repository-owned
-=modules/*.el= files that should be part of the Emacs configuration coverage
-universe but have no entry in =.coverage/simplecov.json= after =make coverage=
-runs. These files may be missing because no test required them, because loading
-was skipped due to package/environment guards, or because instrumentation did
-not see them. They are distinct from tracked modules with 0% covered lines,
-which already appear in SimpleCov and can be scored directly.
-
-Completed 2026-05-15:
-- Both child tasks are done.
-- =make coverage-summary= reports missing modules explicitly and also reports a
- separate project-module score where missing modules count as 0%.
-- Focused summary tests and byte-compilation of the summary helper passed.
-
-**** 2026-05-15 Fri Teach the coverage report to list modules missing from SimpleCov
-CLOSED: [2026-05-15 Fri]
-
-Expected outcome:
-- Compare =modules/*.el= against paths present in =.coverage/simplecov.json=.
-- Show a separate "not in report" section.
-- Do not silently fold those files into the percentage until we decide the
- semantics. A visible missing-file count is enough for v1.
-
-Done 2026-05-15:
-- =make coverage-summary= now compares direct =modules/*.el= files on disk
- against the module paths present in =.coverage/simplecov.json=.
-- The terminal report appends a =Not in SimpleCov report= section with a count
- and the missing module paths.
-- Missing modules are explicitly excluded from the displayed percentage for
- now; the policy question below remains open.
-- Added focused tests in =tests/test-coverage-summary.el= for missing-module
- reporting and for ignoring =.elc= files and nested paths outside direct
- =modules/*.el= ownership.
-
-**** 2026-05-15 Fri Decide whether unreported modules count as 0% coverage
-CLOSED: [2026-05-15 Fri]
-
-This is a policy decision:
-- Counting missing modules as 0% gives a more honest project-level number.
-- Keeping the current number is useful for "instrumented executable lines only".
-
-Recommendation: display both:
-- Instrumented coverage: current SimpleCov percentage.
-- Project module coverage: includes unreported module files as 0% or reports
- them separately with an explicit caveat.
-
-Decision 2026-05-15:
-- Keep the existing SimpleCov percentage as the line-weighted
- =instrumented coverage= number. It only covers modules that SimpleCov saw and
- has real executable-line denominators for.
-- Also display a separate module-weighted =project module coverage= score over
- all direct =modules/*.el= files. Modules present in SimpleCov contribute their
- per-file coverage percentage; modules absent from SimpleCov count as 0%.
-- Do not pretend missing modules have known executable-line counts. Counting
- them as 0% at the module level is honest about risk without inventing a line
- denominator.
-
-Done 2026-05-15:
-- =make coverage-summary= now prints both the existing line-weighted summary
- and a separate =Project module coverage= line that includes missing modules
- as 0%.
-- The missing-module section now states that missing modules count as 0% in the
- project-module score.
-- Updated =tests/test-coverage-summary.el= to assert the policy and the
- displayed project-module percentage.
-
-*** 2026-05-15 Fri Add a lightweight architecture smoke test for startup contracts :tests:
-CLOSED: [2026-05-15 Fri]
-
-After the above refactors start, add one or two smoke tests that protect the
-architecture instead of individual functions.
-
-Candidate checks:
-- All modules can be loaded directly with only =modules/= on =load-path=, or
- skipped with a clear external package reason.
-- No module other than =keybindings.el= binds =C-;= itself.
-- Startup-only modules do not run timers in batch test mode.
-
-Keep this small. The goal is to catch accidental return to hidden load-order
-coupling, not to build a full static analyzer.
-
-Done 2026-05-15:
-- Added =tests/test-architecture-startup-contracts.el= with two source-level
- smoke checks:
- - only =keybindings.el= may globally own the exact =C-;= prefix;
- - top-level timer scheduling forms must be guarded by =noninteractive= so
- batch/test loads do not schedule startup timers.
-- Gated existing startup timers in =org-agenda-config.el=,
- =org-refile-config.el=, =quick-video-capture.el=, and =wrap-up.el=.
-- Focused tests passed for the new architecture smoke file and the affected
- agenda/refile helpers.
-
-*** PROJECT [#A] Un tangle the eager =init.el= load graph :architecture:refactor:
-
-=init.el= currently functions as the dependency graph by eagerly requiring
-almost every module in a fixed order. That makes modules harder to test in
-isolation and hides real dependencies behind "loaded earlier in init.el"
-assumptions.
-
-Spec: [[file:docs/design/init-load-graph.org][docs/design/init-load-graph.org]]
-
-**** 2026-05-25 Mon @ 07:59:20 -0500 Wrote full design spec for the =init.el= load-graph refactor :architecture:refactor:
-
-Create a design document that defines the target architecture, module
-categories, migration phases, test strategy, acceptance criteria, and risk
-controls for untangling the eager =init.el= load graph.
-
-Review incorporation:
-- Treat helper consolidation as adjacent architecture work, not a direct
- acceptance criterion for the load-graph refactor.
-- Mention utility extraction guardrails in the spec so Phase 2 dependency work
- has a clear rule for duplicated helpers found along the way.
-
-Verify 2026-05-04:
-- Added [[file:docs/design/init-load-graph.org][docs/design/init-load-graph.org]].
-- Incorporated review feedback by making utility consolidation an explicit
- sibling project with guardrails and candidate helper families.
-- Parsed the spec and =todo.org= with =org-element=.
-- Committed the tracked spec as =0528475=.
-
-**** 2026-05-24 Sun @ 17:07:03 -0500 Classified modules by role and startup requirement
-Built [[file:docs/design/module-inventory.org][docs/design/module-inventory.org]] across 9 batches: 101 of 102 init.el-required modules annotated with the load-graph header contract (Layer, Category, Load shape, Eager reason, Top-level side effects, Runtime requires, Direct test load) and tabulated in the inventory. Added =tests/test-init-module-headers.el= to enforce the contract on each classified module. Retired the three vague =init.el= comments (latex-config WIP, prog-shell "combine elsewhere", "Modules In Test" banner) into real tasks. Recorded seven hidden =cj/custom-keymap= / cross-module dependencies for the Phase 2 dependency pass. Tagged the span =load-graph-classify-start..load-graph-classify-end=. elfeed-config is the one module left, pulled to its own task below.
-
-**** 2026-05-25 Mon @ 08:35:33 -0500 Annotated elfeed-config load-graph header
-Added the load-graph header to elfeed-config (Layer 4, O/D/P, current load shape eager with an eager reason, target command-loaded; runtime requires user-constants, system-lib, media-utils), added it to the header-contract allowlist in =tests/test-init-module-headers.el= (Batch 8), and moved it in =docs/design/module-inventory.org= from the Deferred/Pending sections into the Batch 8 table. Inventory now 102 of 102 classified. The header's "Load shape" records the current shape (eager, required in init.el) per the weather-config/games-config convention; "command-loaded" is the target, in the inventory's Target column. Shipped as a522e553.
-
-**** 2026-05-24 Sun @ 18:35:06 -0500 Made hidden module dependencies explicit
-Fixed the seven hidden dependencies the classification surfaced: system-defaults now requires host-environment and user-constants at runtime (was eval-when-compile); custom-buffer-file, dev-fkeys, calendar-sync, and video-audio-recording require keybindings and drop their =(when (boundp 'cj/custom-keymap) ...)= shims; flycheck-config and mail-config require keybindings for their cj/custom-keymap bindings. Removed a dead =eval-when-compile (defvar cj/custom-keymap)= in transcription-config (the var was never used).
-
-No init.el load-order change — keybindings and the foundation modules already load before these, so the explicit requires are no-ops at startup and only fix standalone/test loading.
-
-Verified each fix with a fresh =emacs --batch (require 'X)=, then swept all ~100 modules standalone: every one loads or fails only with a clear missing-package message (the spec's Phase 2 exit bar). Full =make test=, =make validate-modules=, and an init smoke all pass. Module headers and the inventory's hidden-dependency section updated to mark the seven resolved.
-
-**** TODO [#B] Defer feature modules behind autoloads, hooks, and commands :refactor:
-
-Once dependencies are explicit, reduce the number of modules required at
-startup. Start with lower-risk feature modules:
-- Entertainment and optional integrations: =games-config=, =music-config=,
- =weather-config=, =slack-config=, =erc-config=.
-- Heavy document/media modules: =pdf-config=, =calibredb-epub-config=,
- =video-audio-recording=, =transcription-config=.
-- AI/rest tooling: =ai-config=, =restclient-config=, =ai-conversations=.
-
-Do this incrementally. After each batch:
-- Restart Emacs interactively.
-- Run =make test= or at least targeted tests.
-- Check that keybindings still resolve and which-key labels still appear.
-
-**** 2026-05-24 Sun @ 19:59:01 -0500 Centralized custom keymap registration
-Added cj/register-prefix-map and cj/register-command to keybindings.el (commit 47f222f6) with test-init-keymap-registration.el, then migrated all 31 cj/custom-keymap registration sites across 24 modules onto the API. Consumers no longer reference cj/custom-keymap directly — keybindings.el is the sole owner of the prefix, and modules require keybindings to reach the API.
-
-Verified behavior-preserving by dumping every C-; binding before and after: identical, 279 bindings, each resolving to the same command. Byte-compiled all 24 migrated files (no new free-variable warnings — the cj/custom-keymap coupling is gone), and full make test, validate-modules, and an init load all pass. which-key label blocks were left intact; they use string key descriptions and never assumed cj/custom-keymap existed.
-
-Related existing task: [#B] "Review and rebind M-S- keybindings".
-
-*** PROJECT [#A] Move package bootstrap out of =early-init.el= where possible :startup:refactor:
-
-=early-init.el= currently handles package archives, package refresh, installing
-=use-package=, and =use-package-always-ensure=. That is more than early startup
-needs and can make startup network-sensitive.
-
-**** TODO [#B] Split early startup from package bootstrap :refactor:
-
-Keep =early-init.el= focused on things that must happen before package and UI
-startup:
-- GC/file-name-handler startup tuning.
-- =load-prefer-newer=.
-- frame/UI suppression.
-- minimal debug behavior.
-
-Move package archive setup and =use-package= installation to a normal module or
-bootstrap command, unless there is a specific reason it must run in
-=early-init.el=.
-
-Acceptance criteria:
-- Fresh install/bootstrap still works from a documented command or script.
-- Normal startup does not refresh archives or install packages unexpectedly.
-- Offline startup remains quiet and predictable.
-
-**** TODO [#A] Revisit package signature policy
-
-=package-check-signature= is disabled. Decide whether that is still necessary
-for the localrepo/mirror workflow.
-
-Expected outcome:
-- Prefer signatures on by default.
-- If signatures must be disabled for local mirrors, scope that exception and
- document why.
-- Add a note to the local repository docs so future package failures do not
- lead to permanent insecure defaults.
-
** TODO [#B] F-key Completion :feature:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-02
@@ -1349,6 +751,865 @@ Tie this into the existing coverage work:
- Tests cover adapter detection, command building, scope resolution, result
storage, and key interactive paths.
+** TODO [#B] Add Signal to the dashboard :quick:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-01
+:END:
+** TODO [#B] Messenger window/key unification :feature:ux:
+Spec: [[file:docs/design/messenger-unification-spec.org][messenger-unification-spec.org]] (Draft, 2026-06-11). One library (=cj-messenger-lib.el=) gives every messenger the same shape: chat windows rise from the bottom (the signel rule, generalized), C-c C-c confirms, C-c C-k cancels, C-c C-a attaches — dispatched per backend through a registry + minor mode. Signel already conforms (reference backend); telega and slack join in phases 2-3; ERC later. All eight decisions settled 2026-06-11 (cancel closes an idle window; telega's filter-cancel shadow accepted; slack rooms join the bottom rule). Spec held open — Craig has more ideas to fold in before it's marked Ready.
+
+** TODO [#B] cj/undo-kill-buffer off-by-one on plain invocation :bug:quick:solo:
+=modules/ui-navigation.el:181= — =(interactive "p")= makes arg always ≥1, and the body does =(if arg (nth arg list) (car list))=, so the nth branch always runs and plain M-S-z reopens the SECOND-most-recently-killed file. The existing test passes 0 explicitly, masking it. Fix the indexing (=(interactive "P")= + =prefix-numeric-value=, or =nth (1- arg)=) and fix the test to cover the no-prefix path. From the 2026-06 config audit.
+
+** TODO [#B] reconcile-open-repos skips any repo with a dot in its name :bug:solo:
+=modules/reconcile-open-repos.el:174= — discovery regexp ="^[^.]+$"= matches only dot-free names, so =~/code/mcp.el=, =capture.el=, =google-contacts.el=, =auto-dim-other-buffers.el= etc. are never reconciled while M-P still reports "Complete." Replace with =directory-files-no-dot-files-regexp= + a hidden-dir check; add a regression test with a dotted repo name. From the 2026-06 config audit.
+
+** TODO [#B] jumper: register collisions and dead-marker errors :bug:solo:
+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] C-s C-s vertico-repeat path never works :bug:quick:solo:
+=modules/selection-framework.el:263= — =cj/consult-line-or-repeat= calls =vertico-repeat= on the second consecutive C-s, but nothing adds =vertico-repeat-save= to =minibuffer-setup-hook= (grep: zero hits config-wide), so it always signals "No Vertico session". Add the hook next to the vertico use-package block. From the 2026-06 config audit.
+
+** TODO [#B] dashboard-config setq wipes recentf-exclude list :bug:quick:solo:
+=modules/dashboard-config.el:199= =(setq recentf-exclude '("/emms/history"))= discards the five exclusions system-defaults.el:239-243 added earlier in init order (bookmarks, elpa, recentf, ElfeedDB, airootfs). Change to =add-to-list=. From the 2026-06 config audit.
+
+** TODO [#B] auth-config: unguarded gpg-connect-agent call + compile-time require :bug:quick:solo:
+From the 2026-06 config audit. =modules/auth-config.el:88= — bare =(call-process "gpg-connect-agent" ...)= in a =:demand t= :config signals file-missing and aborts init on machines without the binary; guard with =cj/executable-find-or-warn=. =auth-config.el:36= — =user-constants= is required only =eval-when-compile= but =authinfo-file= is read at load time; works from .el source, fails from standalone .elc. Use a runtime require (system-defaults.el:32-35 documents this exact trap).
+
+** TODO [#B] system-defaults: top-level server-start unguarded in batch :bug:quick:solo:
+=modules/system-defaults.el:140= — raw module load under =--batch= (make validate-modules on a machine with no daemon socket) starts a server from a batch process; the suite only passes because the testutil stubs it. Wrap in =(unless noninteractive ...)= — the repo's established guard for this defect class; same guard stops the =custom-file= =make-temp-file= at line 104 littering temp files per batch load. From the 2026-06 config audit.
+
+** TODO [#B] markdown live preview clobbered by markdown-mode :bug:quick:solo:
+=modules/markdown-config.el:54= defines bare =markdown-preview=, which markdown-mode redefines the moment the first .md loads — the impatient-mode live preview is dead and F2 silently runs the package command (agent verified in the live daemon). Also =:61= guards on =(boundp 'httpd-process)=, a variable that doesn't exist in simple-httpd — use =(httpd-running-p)=. And the =:config= =(setq imp-set-user-filter 'markdown-html)= at line 41 is doubly dead (function-not-variable, symbol names nothing) — delete. Rename to =cj/markdown-preview=, rebind F2. From the 2026-06 config audit.
+
+** TODO [#B] org-roam dailies template writes FILETAGS and TITLE on one line :bug:quick:solo:
+=modules/org-roam-config.el:42= — the "d" dailies head is ="#+FILETAGS: Journal #+TITLE: %<%Y-%m-%d>"= with no newline, so every C-c n d daily is malformed: no parsed #+TITLE, FILETAGS value "Journal #+TITLE: ...". The journal-copy template (lines 213-216) has it right. Add the newline; consider a sweep of existing dailies for the malformed first line. From the 2026-06 config audit.
+
+** TODO [#B] agenda sources: roam Projects missing, no existence filtering :bug:solo:
+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] org-roam :config triggers the 15-20s refile scan synchronously at first idle :bug:solo:
+=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.
+
+** TODO [#B] heavy-box comment inserts non-comment lines :bug:solo:
+=modules/custom-comments.el:427= — =cj/--comment-heavy-box= interior/empty lines carry no comment prefix, so in line-comment languages (elisp, Python) C-; C h injects syntax-breaking bare =*...= lines. The existing test characterizes the broken output (asserts =^\*.*\*$=). Prefix interiors like =cj/--comment-box= does; add the missing min-length validation (negative width hits make-string with a raw error); fix the test to assert corrected output. From the 2026-06 config audit.
+
+** TODO [#B] calendar-sync robustness: atomic writes, curl --fail, zero-event false errors :bug:solo:
+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.
+
+** TODO [#B] drill-refile clobbers global org-refile-targets with an invalid spec :bug:quick:solo:
+=modules/org-drill-config.el:95-98= — =setq org-refile-targets= replaces the session-wide value, so after one drill refile every org-refile everywhere offers only drill targets until restart; and the =(drill-dir :maxlevel . 1)= spec names a directory-path variable where org expects files, so the drill side yields nothing usable. Let-bind around the call with =((directory-files drill-dir t "\\.org$") :maxlevel . 1)=. From the 2026-06 config audit.
+
+** TODO [#B] ERC: double mention notifications + tautological server list :bug:quick:solo:
+From the 2026-06 config audit, =modules/erc-config.el=:
+- =:281= — =erc-modules= includes the built-in =notifications= module AND :config adds =cj/erc-notify-on-mention= to the same hook — every mention fires two desktop notifications. Pick one path (keep the custom one, slated for messenger unification).
+- =:100= — =cj/erc-connected-servers=: inside =with-current-buffer=, the free =erc-server-process= is the buffer's own local value, so the eq test is tautologically true — returns ALL ERC buffers (channels, dead connections). Use =erc-server-buffer-p= + =erc-server-process-alive=.
+- =:238= — =user-whole-name= read at load but =user-constants= only required at compile time (same trap as auth-config/keyboard-macros).
+
+** TODO [#B] slack-config lifecycle gaps :bug:quick:solo:
+From the 2026-06 config audit, =modules/slack-config.el=:
+- =:265= — w / @ / # bound to commands neither autoloaded nor in :commands — void-function before slack loads. Add to :commands.
+- =:246= — =cj/slack-close-all-buffers= reads =slack-current-buffer= (declared but unbound) without the boundp guard its sibling has — void-variable on C-; S Q before slack loads.
+- =:259= — raw =global-set-key= for C-; S bypasses =cj/register-prefix-map= (signal/erc use it); invisible to the keybindings registry and the planned unification enumeration.
+
+** TODO [#B] erc-yank silently publishes >5-line pastes as public gists :bug:
+=modules/erc-config.el:345= — C-y in any ERC buffer auto-creates a public gist for anything over 5 lines: clipboard content goes to a public URL with no confirmation, and no executable-find guard for =gist= (errors mid-send if absent). Privacy trap. Add a =yes-or-no-p= gate or drop the package for plain C-y. From the 2026-06 config audit.
+
+** TODO [#B] F7 diff-aware coverage classifies every changed file "not tracked" :bug:solo:
+=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 [#B] eshell: visual-commands nested-list + xterm-color dead hook :bug:quick:solo:
+=modules/eshell-config.el:104= — =add-to-list= pushes one LIST into the flat string list =eshell-visual-commands=, so lf/ranger/htop/top never get a visual terminal (and the r→ranger alias garbles). dolist the strings. =:166= — =:hook (eshell-before-prompt-hook . ...)= gets "-hook" appended → registers on nonexistent =eshell-before-prompt-hook-hook=; and =xterm-color-filter= is never added to =eshell-preoutput-filter-functions= anyway while TERM advertises xterm-256color. Wire xterm-color fully per its README or drop it + the TERM override. From the 2026-06 config audit.
+
+** TODO [#B] dirvish M (mark all files) marks every other file :bug:quick:solo:
+=modules/dirvish-config.el:218= — =dired-mark= advances point to the next line itself; the loop's extra =forward-line 1= then skips it, so consecutive files are marked alternately. Live mis-marking on a key that feeds batch operations (delete/copy on marked files) — data-loss adjacent. Drop the manual forward-line when a mark was made (or =dired-unmark-all-marks= + mark dirs + =dired-toggle-marks=). The trivial line-predicate helper is tested; the loop isn't — add the marked-count test. From the 2026-06 config audit.
+
+** TODO [#B] dwim-shell: zip overwrites its own name, backup timestamp never expands, dired menu key dead :bug:quick:solo:
+From the 2026-06 config audit, =modules/dwim-shell-config.el=:
+- =:338= — single-file zip is =zip -r '<<fne>>.<<e>>' '<<f>>'= — reconstructs the input filename as the archive ("Zip file structure invalid"; directories produce =foo.=). Should be ='<<fne>>.zip'= like the tar-gzip sibling.
+- =:549= — backup destination single-quotes =$(date ...)= so the substitution is literal: =foo.txt.$(date +%Y%m%d_%H%M%S).bak=. Move it outside the quotes or format-time-string in Elisp.
+- =:932= — dired-mode binding "M-S-d" is unreachable (Meta+Shift+d generates M-D); the dirvish binding two lines down is correctly "M-D". Fix + the stale commentary at dirvish-config.el:30.
+
+** TODO [#B] Go: format key void-functions, go-mode :config never runs :bug:quick:solo:
+=modules/prog-go.el:99,113-118= — .go maps to go-ts-mode so the go-mode package never loads, and =gofmt= isn't autoloaded in go-mode 1.6.0 — C-; f signals void-function, and the :config (exec-path += ~/go/bin, =gofmt-command "goimports"=) never executes. Wrapper that requires go-mode first (or autoload gofmt), move the setup to top level. From the 2026-06 config audit.
+
+** TODO [#B] prog hooks mutate global state per buffer :bug:quick:solo:
+From the 2026-06 config audit: =prog-go.el:64=, =prog-c.el:73=, =prog-shell.el:77= call global =(electric-pair-mode t)= from buffer setup hooks — one Go/C/shell buffer turns on pairing in org/text everywhere (python/webdev correctly use =electric-pair-local-mode=). =prog-general.el:79-80= — =display-line-numbers-type 'relative= setq/setq-default run from the hook AFTER the mode is enabled, so the first prog buffer of a session gets absolute numbers. Local-mode for the three; move the line-number setqs to top level.
+
+** TODO [#B] M-S- launcher keys dead: eww, elfeed, calibredb unreachable :bug:quick:solo:
+=eww-config.el:70= (M-S-e), =elfeed-config.el:36= (M-S-r), =calibredb-epub-config.el:115= (M-S-b) — Meta+Shift+letter generates the uppercase event (M-E/M-R/M-B), which never matches an explicit S- spec on a lowercase letter; verified dead in the live daemon (chord falls through to M-r move-to-window-line etc.). Same class as the text-config M-S-i finding. Write them as "M-E"/"M-R"/"M-B". Weather's M-S-w works only via the keyboard-compat translation layer — audit that layer's coverage while here. From the 2026-06 config audit.
+
+** TODO [#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 [#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).
+
+** TODO [#B] cj/gptel-switch-backend reintroduces the string-model crash :bug:quick:solo:
+=modules/ai-config.el:272= — =(setq gptel-model model)= with the raw completing-read STRING — the documented wrong-type-argument-symbolp modeline hang (CLAUDE.md gotcha), reachable from C-; a B today. =cj/gptel-change-model= (C-; a m) already does backend+model switching and interns correctly. Intern here, or delete switch-backend and keep one command. From the 2026-06 config audit.
+
+** TODO [#B] transcription: stderr never reaches the log, video transcripts stranded in /tmp :bug:solo:
+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.
+
+** TODO [#B] ledger-config is orphaned — ledger-mode never configured :bug:quick:
+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 [#B] eww quick-add bookmarks split the store and break the default file :bug:quick:solo:
+=modules/eww-config.el:116-126= — quick-add let-binds =eww-bookmarks-directory= to ~/.emacs.d/eww-bookmarks/ (creating a DIRECTORY at the path where the daemon's default store expects a FILE ~/.emacs.d/eww-bookmarks). After one quick-add, B reads an unreadable path and quick-added bookmarks are invisible post-restart. Drop the let-binding or setq the directory once in :config so both commands share one store. From the 2026-06 config audit.
+
+** TODO [#B] help-config: three defects in one small file :bug:quick:solo:
+From the 2026-06 config audit, =modules/help-config.el=:
+- =:67= — =cl-return-from= inside a plain =defun= (no cl-block): declining the save prompt signals "No catch for tag" instead of canceling. =cl-defun= or restructure.
+- =:108= — =:hook (info-mode . info-persist-history-mode)= is dead twice: Info's hook is =Info-mode-hook= (capital I), and =info-persist-history-mode= doesn't exist anywhere. Implement the intent or delete.
+- =:111= — auto-mode-alist maps .info to an interactive command that KILLS the buffer mid find-file — programmatic =find-file-noselect= of any .info destroys buffers and pops Info windows. Drop the entry; keep the explicit command. Zero test coverage on this module (the two broken paths are exactly the untested ones).
+
+** TODO [#B] modeline runs synchronous git on the redisplay path, unguarded :bug:solo:
+=modules/modeline-config.el:173,154,145= — the mode-line :eval calls vc-backend/vc-state/vc-working-revision (synchronous git) on TTL expiry; a slow or unmounted filesystem stalls ALL redisplay. The cache key computes =file-truename= on every render (the "one stat per refresh" comment is wrong), and nothing is condition-case-wrapped, so a signal lands inside the mode-line eval. Defer the truename behind the TTL check; wrap the fetch in condition-case caching nil. From the 2026-06 config audit.
+
+** TODO [#B] Stale elpa gptel shadows the local fork — likely the gptel-magit root :bug:quick:solo:
+=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.
+
+** TODO [#B] vertico-prescient clobbers orderless filtering :bug:quick:solo:
+=modules/selection-framework.el:250= — =vertico-prescient-mode= defaults =vertico-prescient-enable-filtering t=, overriding =completion-styles= to prescient inside vertico sessions; the orderless config at :151 is dead exactly where it matters. Set =vertico-prescient-enable-filtering nil= — orderless matches, prescient sorts (and this resolves the dead =vertico-sort-function= finding in the buffer/window-libs child the other way around). From the 2026-06 config audit.
+
+** TODO [#B] 2026-06 full config audit — findings backlog :refactor:
+Module-by-module review of all 121 modules + init/early-init, holistic passes (startup/perf, stability, UX consistency, package strategy), and spin-offs into pearl, chime, emacs-wttrin. Method: parallel read-only review agents per module group; key claims spot-verified (incl. against the live daemon) before filing. Run 2026-06-11/12, COMPLETE. Tally: ~165 module findings + ~40 holistic + 30 spin-off ≈ 235 total; 40 high-impact bugs filed as standalone tasks above this parent; the rest live in the group children below. Spin-off findings delivered as inbox handoffs to pearl, chime, and emacs-wttrin (2026-06-12-0057). Start with the synthesis child below for the recommended attack order.
+
+*** Synthesis: the overall picture and attack order
+Six cross-cutting themes, then the order I'd work them.
+
+Themes:
+1. Performance has one systemic lever, not many small ones: native-comp is accidentally OFF config-wide and GC sits at the stock 800KB ([#A] task). Daemon init itself is healthy (1.11s measured). Fix the lever before any micro-deferral work, and before burning time on the org-capture-perf debug.
+2. A "dangerous defaults" safety cluster: yes-or-no-p fset (single-keystroke shutdown/file-destruction), the silently-failing Wayland lock screen, erc-yank's public gists, mu4e's broken trash/refile on the primary account. All four are [#A]/[#B] standalones; do these first — they're where the config can actually hurt you.
+3. Calendar/agenda data correctness: calendar-sync's RFC trio (vanishing final occurrences, resurrected cancelled meetings, collapsed multi-day events) + agenda sources missing roam Projects. Meetings are missed over this.
+4. Recurring mechanical defect classes worth sweeping as one commit each, config-wide: use-package :hook "-hook" suffix trap (org-babel, eshell, latex); eval-when-compile-only requires read at runtime (auth-config, keyboard-macros, erc-config); M-S-<letter> bindings vs uppercase events (4 dead keys + 1 asymmetry); raw C-; entries bypassing cj/register-prefix-map (8 modules); unreachable modules (prog-lsp, ledger-config, show-kill-ring, mu4e-org-contacts-setup); config for package versions long gone (mu4e 1.7 block, dashboard override, org timeline, checkdoc-arguments).
+5. The test suite has a blind-spot class: characterization tests asserting BROKEN output (reverse-lines, heavy-box, undo-kill's explicit 0), unit tests hand-building data that hides integration mismatches (F7 coverage paths), and an integration gate that prints green over "Ran 0 tests" (chime). When fixing any standalone bug above, fix its test to assert correct behavior — and consider extending the architecture smoke test to mechanically pin the class-4 sweeps (hooks must be bound after load, no raw C-; binds, no M-S-<letter> specs, no eval-when-compile requires of runtime vars).
+6. Consistency wants conventions, not patches: one notification facade (cj/notify — messenger spec addendum already covers the messenger half), one confirmation tier (the fset fix), one prefix-registration mechanism with labels, one buffer-naming shape. The messenger-unification registry mindset generalizes.
+
+Attack order: (a) the three [#A]s + gptel-shadow (it's blocking the filed gptel-magit investigation); (b) the daily-data pair — mail trash/refile + calendar RFC trio; (c) the :quick:solo: standalone sweep — roughly 20 one-to-five-line fixes, a satisfying solo batch; (d) the class-4 mechanical sweeps, one commit per class, each with its smoke-test guard; (e) the consistency conventions, opportunistically as those modules get touched.
+
+*** TODO Findings: foundation/system group
+From agents 2026-06-11; spot-verified sample. Remaining findings beyond the standalone bug tasks:
+- [BUG] =keyboard-compat.el:121= — terminal arrow-key fix runs once on emacs-startup-hook; =input-decode-map= is terminal-local, so =emacsclient -t= frames under the daemon never get it. Register on =tty-setup-hook= (GUI half already uses =server-after-make-frame-hook=).
+- [BUG] =config-utilities.el:142= — =cj/recompile-emacs-home=: =(boundp 'native-compile-async)= is always nil (it's a function — needs =fboundp=), so native compilation is never selected; and the helper deletes =<dir>/eln= when the real cache is =eln-cache/= (derive from =native-comp-eln-load-path=). Extend the existing test.
+- [BUG] =system-utils.el:94= — success message args swapped: prints "Running notes.txt on mpv...". Trivial; wired into dirvish (O) and calibredb so it shows regularly.
+- [REMOVE] =local-repository.el:51= — =localrepo-initialize=, its three defcustoms, and unprefixed =car-member= are dead; early-init owns archive setup with its own divergent path constant. Shrink to =cj/update-localrepo-repository= pointed at early-init's =localrepo-location=.
+- [REMOVE] =keybindings.el:146-147= — C-x C-f unset/reset is a no-op (already find-file); comment wrong. Delete or retarget.
+- [COVERAGE] =local-repository.el= — only module in the group with no test file.
+
+*** TODO Findings: UI core group
+From agents 2026-06-11; spot-verified sample. Remaining findings beyond the standalone bug tasks:
+- [BUG] =font-config.el:262= — emojify =:defer 1= means :config runs before any daemon GUI frame exists; =env-gui-p= picks ='unicode= permanently, GUI frames never get image emojis. Compute per-frame (=server-after-make-frame-hook=) or test =(daemonp)=.
+- [BUG] =font-config.el:283= — =cj/display-available-fonts= errors on second invocation: first call's =special-mode= sets read-only; next call's erase/insert signals. Wrap in =inhibit-read-only=. (Also [COVERAGE]: untested — a call-twice test catches it.)
+- [UX] =undead-buffers.el:82= — =cj/kill-other-window= in a single-window frame kills the buffer you're looking at (other-window no-ops; only delete-window is guarded). Add the sibling's =(user-error "No other window")= guard.
+- [UX] =undead-buffers.el:48= — C-u C-x k silently marks a buffer undead (then it refuses to die with no explanation later). Undocumented mode-switch inside a core-command remap; document or split into its own command.
+- [ENHANCE] =ui-theme.el:87= — theme persistence silently fails on a fresh machine until =persist/= exists; =make-directory= before the writability check.
+- [REMOVE] =dashboard-config.el:32-58= — =dashboard-insert-bookmarks= override is dead code: the :demand t require lets upstream dashboard-widgets.el redefine it; behavior survives only because upstream natively honors the settings now. Delete.
+- [REMOVE] =font-config.el:199-220= — all-the-icons stack (2 =:demand t= packages + unprompted network font install on fresh machines) likely redundant with nerd-icons everywhere; verify keyboard-compat's reference then drop.
+- [REMOVE] =ui-config.el:185= — duplicate =(use-package nerd-icons :defer t)= stanza; nerd-icons-config owns it. Delete stanza + stale Commentary bullet.
+
+*** TODO Findings: buffer/window libs group
+From agents 2026-06-11; spot-verified sample. Remaining findings beyond the standalone bug tasks:
+- [REMOVE] =show-kill-ring.el= — loaded by nothing (init require deliberately removed in b785a19d), so its M-S-k binding is dead; =keyboard-compat.el:177= still installs the M-K → M-S-k translation whose only purpose was this module. Re-add or delete module + stale translation/comment (consult-yank-pop largely supersedes it).
+- [UX] =selection-framework.el:38= — =vertico-sort-function= custom is dead config: =vertico-prescient-mode= (line 250) replaces sorting when it activates. Pick one policy (drop the custom, or =vertico-prescient-enable-sorting nil=).
+- [BUG] =custom-buffer-file.el:486= — =cj/view-email-in-buffer= leaks MIME handles when no displayable part: =user-error= fires before =mm-destroy-parts=. unwind-protect.
+- [ENHANCE] =custom-buffer-file.el:49= — eager =(require 'mm-decode)= at startup only for macro expansion; runtime require already exists at line 481. Make it =eval-when-compile=.
+- [UX] =custom-buffer-file.el:221= — =cj/copy-link-to-buffer-file= is a silent no-op in non-file buffers while siblings signal =user-error=. Match them.
+
+*** TODO Findings: editing helpers group
+From agents 2026-06-11; spot-verified sample (jump-paren, sortable-time confirmed). Beyond the standalone heavy-box task:
+- [BUG] =custom-misc.el:48= — jump-to-matching-paren with point ON a closer lands at the last inner sexp, not the opener (batch-verified). =(forward-char)= before =(backward-sexp)= in the char-after-closer case; the test only covers the after-closer position.
+- [BUG] =custom-datetime.el:71= — "sortable" time format is 12-hour ="%I:%M:%S %p %Z"= — "01:00:00 PM" sorts before "09:00:00 AM". Should be ="%H:%M:%S"=.
+- [BUG] =custom-comments.el:82= — =cj/comment-reformat= prints "No region was selected" even on success (message outside the if-else), and the fill-column shrink/restore isn't unwind-protected — an error leaves fill-column permanently -3. Use let-binding + =user-error=; also =mark-active= vs the config's usual =use-region-p=.
+- [BUG] =custom-line-paragraph.el:52= — join-line-or-region without region inserts a spurious blank line mid-buffer (verified); only insert the newline at eobp.
+- [BUG] =custom-line-paragraph.el:77= — duplicate-line-or-region splits a mid-line-ending region via open-line and duplicates an extra empty line when the region ends at bol. Normalize bounds to whole lines.
+- [BUG] =custom-ordering.el:158= — reverse-lines and number-lines mishandle the trailing newline ("a\nb\n" → "\nb\na"); the trailing-newline test asserts the broken output. =cj/--arrayify= (line 43) has the correct pattern — apply it; fix the characterization test.
+- [BUG] =custom-comments.el:152= — inline-border lines come out 2 chars short for even-length or empty text (parity computed from text length instead of remaining width); stacked dividers misalign.
+- [UX] =custom-text-enclose.el:216= — indent-lines =(interactive "p\nP")= couples COUNT and USE-TABS to one prefix arg — multi-column space indent is impossible interactively; docstrings claim "default 4" but "p" defaults to 1 (same in dedent :256).
+- [REMOVE] =custom-ordering.el:90= — =cj/arrayify-python= is byte-identical to =cj/arrayify-json= (two bindings, same output). Delete one or differentiate (single quotes for Python).
+- [UX] =custom-case.el:66= — title-case contradicts its docstring: "is" is in word-skip despite "linking verbs are major words"; no sentence-restart capitalization after periods; no capitalize-last-word rule. Align list + docstring.
+
+*** TODO Findings: text/prose tools group
+From agents 2026-06-11. Beyond the standalone markdown/latex tasks:
+- [BUG] =text-config.el:72= — "M-S-i" for edit-indirect-region is unreachable: Meta+Shift+i generates the event M-I, not M-S-i, so the keypress falls back to M-i tab-to-tab-stop. Rebind as "M-I" (the "was M-I" comment thought the rename was a no-op; it wasn't).
+- [BUG] =keyboard-macros.el:46= — user-constants required only =eval-when-compile= but =macros-file= is read at runtime; works only because init.el loads user-constants first. Plain require (same trap as auth-config).
+- [BUG] =keyboard-macros.el:137= — kill-emacs-hook fires =y-or-n-p= + an interactive name prompt whenever any last-kbd-macro exists — hazardous for daemon/systemd shutdown (no one to answer) and noisy for throwaway macros. Guard =(and last-kbd-macro (not noninteractive))= minimum; consider dropping the prompt (M-F3 already persists named macros).
+- [BUG] =lorem-optimum.el:221= — empty Markov chain (missing assets/liber-primus.txt) makes =cj/lipsum-insert= do =(insert nil)= — cryptic wrong-type error far from cause. Signal =user-error= naming the fix; also Commentary advertises "M-x cj/lipsum" but it has no interactive spec.
+- [UX] =flyspell-and-abbrev.el:230= — every C-' press re-runs =flyspell-buffer= over the whole buffer while flyspell-mode is off (the documented word-by-word workflow = O(buffer) per keypress in large files). Call =cj/flyspell-on-for-buffer-type= so the mode sticks and the scan runs once.
+- [ENHANCE] =text-config.el:121= — accent is wired to the company backend (=accent-company=); the filed Company→Corfu migration task doesn't list it, so C-` breaks silently post-migration. Add to the migration scope or switch to =accent-menu= now.
+
+*** TODO Findings: org core group
+From agents 2026-06-11; spot-verified sample (dailies head, babel hook, void bindings confirmed). Beyond the standalone tasks:
+- [BUG] =org-babel-config.el:27= — =:hook (org-babel-after-execute-hook . org-redisplay-inline-images)= gets a second "-hook" appended (symbol unbound at expansion, doesn't end in -mode) → registers on nonexistent =org-babel-after-execute-hook-hook=; inline dot-graph images never refresh after C-c C-c. Write =(org-babel-after-execute . ...)= or add-hook in :config.
+- [BUG] =org-roam-config.el:67,71= — C-c n p / C-c n w bound (and which-key-labeled) to =cj/org-roam-find-node-project= / =-webclip=, defined nowhere — keypress errors "autoloading failed to define function". Define via =cj/org-roam-find-node= (a project template exists) or drop bindings + labels.
+- [BUG] =org-export-config.el:74-81= — ox-texinfo block can never run (=:defer t=, no trigger, excluded from line-47 dolist and =org-export-backends=); commentary still advertises Texinfo. Add to the dolist or delete; also commentary says "subtree default scope" vs actual ='buffer= (line 61).
+- [UX] =org-roam-config.el:50-63= — two parallel template dirs drift: :custom templates read =~/.emacs.d/org-roam-templates/= while find-node-topic/recipe read =roam-dir/templates/= — overlapping recipe/topic/v2mom files, edits don't propagate. Pick one canonical dir.
+- [REMOVE] =org-agenda-config.el:84= — dead =timeline= entry in org-agenda-prefix-format (removed in org 9.1). Also =org-config.el:47-48= — the TASK note claiming =org-indent-indentation-per-level= "doesn't exist" is wrong (real org-indent defcustom); restore the setq or fix the comment.
+- [REMOVE] =org-babel-config.el:161= — =org-html-footnote-separator= is an ox-html setting parked in the babel module with a wrong comment; =org-roam-config.el:76= similarly hides =org-agenda-timegrid-use-ampm= in roam's :config (only takes effect after roam loads). Move both to their owning modules.
+- [REMOVE] =org-roam-config.el:363-390= — 28-line commented consult-org-roam block on a TASK comment; its proposed C-c n l / C-c n r now collide with live bindings, so it can't ship as written. Decide + delete (git keeps the draft).
+- [COVERAGE] =org-agenda-config.el:423= cj/add-timestamp-to-org-entry (defvar-inside-defun smell), =org-roam-config.el:115,185= node-insert-immediate + finalize-hook — untested.
+
+*** TODO Findings: org apps + calendar-sync group
+From agents 2026-06-11/12; spot-verified sample (UNTIL comparisons, EXDATE regex, drill setq confirmed). Beyond the standalone tasks:
+- [BUG] =org-reveal-config.el:241= — seven raw =global-set-key= "C-; p ..." calls carry a hidden load-order dependency on keybindings.el (signals "non-prefix key" otherwise); every sibling uses =defvar-keymap= + =cj/register-prefix-map=. Convert.
+- [BUG] =org-drill-config.el:131= — =:load-path "~/code/org-drill"= dev checkout breaks drill on machines without it (velox already diverges per the gptel-magit task). Guard with =file-directory-p= fallback to :vc.
+- [UX] =org-contacts-config.el:146= — =cj/org-contacts-find= visits the file BEFORE prompting (C-g strands you at point-min) and plain =search-forward= can match body text in another entry. Collect heading positions in org-map-entries, goto after prompt.
+- [REMOVE] =calendar-sync.el:1240= — =calendar-sync--fetch-ics= (buffer-string variant) is dead; the sync path uses the temp-file variant exclusively. 30 lines of duplicate curl/sentinel logic that will drift.
+- [REMOVE] =org-webclipper.el:216-241= dead commented keymap blocks; =org-contacts-config.el:118-124= commented duplicate capture template flagged "TASK: duplicate?!?". Delete both (git keeps drafts).
+- [COVERAGE] =calendar-sync.el:1274= — fetch sentinel branches (curl failure, temp-file cleanup, signal exit) untested; dispatch tests stub above this layer.
+
+*** TODO Findings: mail group
+From agents 2026-06-12; spot-verified sample (cmail trash gap, no refile folders, gmail-first contexts confirmed). Beyond the standalone [#A] task:
+- [BUG] =mail-config.el:392-407= — C-; e account nav lambdas call =mu4e-search=, not autoloaded — void-function before first mu4e launch. Add to :commands or require first.
+- [BUG] =mail-config.el:481-484= — unconditional =org-msg-edit-mode= :after advice on replies defeats the =(reply-to-text . (text))= alternative at :459 and re-runs a major mode org-msg already set up. Gate or remove.
+- [BUG] =mu4e-attachments.el:222= — the *mu4e attachments* selection buffer saves through stale MIME handles if the view changed before s — errors or saves the wrong message's parts. Check =buffer-live-p= per handle at save.
+- [BUG] =mail-config.el:329= — "save attachment" in =mu4e-headers-actions= can't work from headers (MIME vars are view-buffer-local, nil in headers-mode). Drop it there.
+- [BUG] =mail-config.el:282-305= — HTML view block sets variables obsolete since mu4e 1.7 (installed 1.14.1): =mu4e-view-prefer-html=, =mu4e-html2text-command= (also set twice: 186, 285), =mu4e-view-show-images=, =mu4e-view-image-max-width=. The pandoc/w3m selection never runs; shr renders regardless. Delete the dead block (image/privacy reconciliation already filed separately).
+- [BUG] =mail-config.el:45-49,80-89= — top-level =(defvar message-send-mail-function nil)= pre-empts message.el's defcustom default; with msmtp absent the fallback leaves it nil → "invalid function: nil" on first send. Explicit =smtpmail-send-it= fallback or descriptive user-error.
+- [UX] =mail-config.el:171,196-199= — =pick-first= + gmail listed first makes gmail the startup context though cmail reads as primary everywhere else — quiet wrong-account hazard for the first compose. Reorder contexts.
+- [REMOVE] =mu4e-org-contacts-setup.el= — unreachable (nothing requires it; mail-config calls activation directly) and its featurep gate would be nil at init anyway. Delete or fold its two setqs into mail-config.
+- [REMOVE] =mail-config.el:208,232= — =mu4e-starred-folder= isn't a mu4e variable (invented, no effect); =:174= =mu4e-maildir= is the obsolete alias of root-maildir set on the previous line. Drop all three.
+- [REMOVE] =mu4e-org-contacts-integration.el:158,171-172= — hook surgery on =mu4e--compose-setup-completion= is a no-op on mu4e 1.14 (called directly, not via hook; already gated by the var activation sets). Delete both hook calls.
+- [COVERAGE] =mu4e-attachments.el:101-105= — mid-batch save-failure path and stale-handle scenario untested.
+
+*** TODO Findings: messengers group
+From agents 2026-06-12. Beyond the standalone tasks; several feed the messenger-unification spec:
+- [BUG] =signal-config.el:201= — contact cache docstring claims "cleared on signel-stop/restart"; nothing clears it (grep: fork never references it). Stale list after relink/reconnect. Advise =signel-stop= or clear on start.
+- [BUG] =signal-config.el:298= — fetched-and-empty contact list is indistinguishable from cold cache (nil), so a zero-contact account re-runs the blocking fetch (up to fetch-timeout) on every C-; M m. Cache a sentinel.
+- [UX] =slack-config.el:208= — =cj/slack-notify= lacks signel's hardening: no truncation (giant toasts), no sound gating, no notifications-notify fallback when the script is absent. Unification-relevant: extract a shared =cj/messenger-notify= (title prefix, truncation, sound flag, script-with-fallback) — noted in the unification spec.
+- [ENHANCE] =telega-config.el:52= — telega has NO notification path (=telega-notifications-mode= not enabled); incoming Telegram messages invisible unless the buffer is on screen. Enable, or route through the shared notifier. Unification-relevant.
+- [COVERAGE] — =cj/erc-join-channel-with-completion= (erc:148, four-way reconnect branching), =cj/erc-connected-servers= (would have caught the tautology), =cj/slack-notify= predicates, =cj/signel--ensure-started= branches — all untested.
+
+*** TODO Findings: programming group
+From agents 2026-06-12; spot-verified sample (prog-lsp unreachable confirmed by grep). Beyond the standalone tasks:
+- [BUG→FOLD] =prog-lsp.el= — the module is UNREACHABLE: nothing requires it, so its entire LSP policy (TRAMP guard, file-watch ignores, read-process-output-max, idle-delay 0.5) is dead while prog-general.el:388-416's older conflicting block wins (idle 0.1, lsp-ui-doc on). Fold this fact into the filed "Make prog-lsp.el the single owner of generic LSP policy" task — it doesn't currently record that prog-lsp never loads.
+- [BUG] =flycheck-config.el:68-70= — =checkdoc-arguments= isn't a real variable (invented name + invented format); the intended checkdoc suppression has never worked. Use =flycheck-emacs-lisp-checkdoc-variables= or drop.
+- [BUG] =prog-json.el:87-90= — C-c C-q → jq-interactively binding defers to eval-after-load of jq-mode, which nothing loads — dead key. Bind in =cj/json-setup= via local-set-key (jq-interactively IS autoloaded).
+- [BUG] =prog-python.el:129-132= — lsp-pyright's :hook lambda calls =lsp-deferred= unguarded on the same hook as the guarded =cj/python-setup= — pyright-absent machines still get the LSP attach prompt the guard exists to prevent. Move the require into the guarded branch; delete the hook.
+- [BUG] =prog-lisp.el:122-125= — =:after (flycheck package-lint)= waits for a manual M-x to load package-lint, so =flycheck-package-setup= effectively never runs. Hook on flycheck load + require inside.
+- [UX] =prog-python.el:111-115=, =prog-go.el:111-114=, =prog-webdev.el:128-147= — setup hooks attach to ts-modes only (C/shell hook both variants); grammar-unavailable fallback to classic modes silently loses indent/keys/formatter/LSP. Add classic-mode hooks.
+- [UX] =prog-webdev.el:165-173= — web-mode gets the format key but none of the promised setup (no company/flyspell/LSP in HTML buffers). Add to the setup hook or fix the Commentary.
+- [ENHANCE] gopls, clangd, bash-language-server, shfmt, shellcheck lack the =cj/executable-find-or-warn= load-time warnings pyright/prettier have; prog-shell's =:if (executable-find ...)= evaluates once at startup and silently disables shfmt/flycheck setup forever.
+- [REMOVE] =prog-training.el:36-37= — =(url-debug t)= turns on GLOBAL url.el debug logging once leetcode loads. Debugging leftover; delete.
+- [REMOVE] =prog-webdev.el:85=, =prog-json.el:44=, =prog-yaml.el:39= — three byte-identical format-region helpers. Extract one shared tested helper (system-lib).
+
+*** TODO Findings: dev tooling group
+From agents 2026-06-12; spot-verified sample. Beyond the standalone F7 task:
+- [BUG] =vc-config.el:138-144= — =cj/goto-git-gutter-diff-hunks= (C-; v d) never did what it claims: consult-line over "^[+\\-]" matches source text, not gutter hunks. Build candidates from =git-gutter:diffinfos= or drop the binding (C-; v n/p covers it).
+- [BUG] =dev-fkeys.el:116-122= — F4 compile+run one-shot hook installs on GLOBAL =compilation-finish-functions= before the prompt; C-g leaves it armed and the next unrelated compile triggers projectile-run-project. Use the buffer-local pattern the module already uses for cache-revert (same in =--f4-clean-rebuild-impl=:143).
+- [BUG] =test-runner.el:84,222= — documented ~/.emacs.d/tests fallback doesn't exist (=cj/test-global-directory= defvar'd nil, never set); outside a project =(file-directory-p nil)= crashes in three commands. Initialize the defvar or guard with user-error. (Adds specifics to the open "Fix up test runner" task — fold.)
+- [BUG] =test-runner.el:288= — focus-add prefix check lacks the trailing slash so =tests-scratch/= passes the "inside tests/" check; the correct helper =cj/test--file-in-directory-p= exists at :168 — use it.
+- [BUG] =vc-config.el:217-219= — difftastic blame map binds D and S to the same command (show); D should be diff per the transient four lines down.
+- [UX] =diff-config.el:37= — =ediff-diff-options "-w"= ignores ALL whitespace in every ediff session — indentation-only Python changes compare as identical. Drop the default; toggle per-session.
+- [UX] =restclient-config.el:64-65= — raw global-set-key "C-; R n" hides a load-order dependency (header claims "Runtime requires: none"); use defvar-keymap + =cj/register-prefix-map= like siblings (same class as org-reveal, slack).
+- [UX] =vc-config.el:196= — clipboard clone via synchronous =call-process= freezes every emacsclient frame for the whole clone. make-process + sentinel.
+- [REMOVE] =vc-config.el:80-82= — phantom autoload =git-timemachine-show-selected-revision= (no such function in the package) appears in M-x and errors. Drop from :commands.
+- [REMOVE] =httpd-config.el:19-30= — pointless =:defer 1= (impatient-mode loads simple-httpd on demand) + unprefixed eager globals =wwwdir=/=check-or-create-wwwdir= creating www/ on every startup. =:defer t=, prefix, or fold into markdown-config.
+- [COVERAGE] — intersect/parse unit tests hand-build matching keys (the F7 bug's escape route); =--coverage-elisp-run='s compilation-finish wiring, goto-git-gutter-diff-hunks, timemachine candidate round-trip untested. F-key sweep clean: no collisions; F5 free for the debug-backend task.
+
+*** TODO Findings: shell/term/files group
+From agents 2026-06-12; spot-verified sample (eshell nested list confirmed). Beyond the standalone tasks:
+- [BUG] =dirvish-config.el:37= — =cj/xdg-open= attributed to system-utils in the require-comment but defined in external-open.el; neither dirvish-config nor dwim-shell-config (caller at :876) requires it — "Direct test load: yes" headers are false. Require external-open (or move the fn into external-open-lib) + fix comment.
+- [UX] =tramp-config.el:73= — =revert-without-query '(".*")= kills revert confirmation for EVERY file in Emacs, buried in the TRAMP module. Scope to =tramp-file-name-regexp= or move deliberately to an editing module.
+- [UX] =dirvish-config.el:403= — quick-access entries lx (~/archive/lectures), phl (~/projects/homelab), pn (~/projects/nextjob) point at directories that don't exist on this machine. Prune or create.
+- [REMOVE] =dwim-shell-config.el:474,507= — open-externally (raw xdg-open) and open-file-manager (thunar/nautilus probe chain) duplicate cj/xdg-open (dirvish o) and cj/dirvish-open-file-manager-here (f); ascii-art references jp2a, the module's only absent binary. Delete the two duplicates; install jp2a or drop ascii-art.
+- [REMOVE] =tramp-config.el:115= — custom =sshfast= method referenced nowhere (everything uses sshx); =tramp-own-remote-path= added twice (:39,:128); =dirtrack-list= and =magit-git-executable "/usr/bin/git"= are unrelated globals hiding here. Prune/relocate.
+- [COVERAGE] — eshell visual-commands/xterm-color wiring and the dirvish mark-all loop had no load-and-assert tests (both standalone bugs above); TRAMP perf settings look sound for the DUET latency concern (attr caching, no remote VC, direct-async + controlmaster).
+
+*** TODO Findings: AI group
+From agents 2026-06-12; spot-verified sample (string-model setq confirmed). Beyond the standalone tasks:
+- [BUG] =ai-term.el:875= — close derives the tmux session name from =default-directory=, which ghostel retargets via OSC 7; after a cd the kill-session misses (orphaned agent session) or name-collides with a different aiv- session. Derive from the buffer name's immutable basename.
+- [UX] =ai-term.el:827= — multi-window F9 toggle-off unconditionally delete-windows, never restoring the displaced edge-window buffer the Commentary (:24) and reuse-edge docstring (:521) promise. Restore when quit-restore still matches, or fix the docs to describe delete-window reality.
+- [UX] =ai-conversations-browser.el:191= — browser load stubs =y-or-n-p= to nil, silently discarding an unsaved in-progress conversation (the direct C-; a l path offers to save). Give ai-conversations a file-arg internal instead of puppeting the interactive command via cl-letf; also the =(caar cands)= fallback loads the newest conversation on a filename mismatch — fail loudly.
+- [ENHANCE] =ai-quick-ask.el:103= — dismiss mid-stream kills the buffer without =gptel-abort= — request keeps streaming to a dead buffer (wasted tokens).
+- [NOTE] =ai-mcp.el= — unreachable from init, consistent with the paused Phase 1.5; add a one-line Commentary note ("not wired until Phase 2") so future audits don't re-flag, and revisit =cj/mcp-enabled-servers= defaulting to all nine servers before wiring.
+- [COVERAGE] — load/autosave lifecycle untested (fresh-session load, timer self-cancel, close-buffer session-name derivation).
+
+*** TODO Findings: media/reading group
+From agents 2026-06-12; spot-verified sample (M-S- bindings, eww store split confirmed). Beyond the standalone tasks:
+- [BUG] =music-config.el:585= — =cj/music-add-dired-selection= gates =dired-get-marked-files= on =(use-region-p)= — but dired marks aren't a region; marked files are ignored, + adds only file-at-point. Drop the conditional (the function already falls back correctly). Note for the EMMS-free rewrite: dirvish + shadows =dired-create-directory= — deliberate decision needed before carrying it over.
+- [UX] =media-utils.el:195-204= — =cj/yt-dl-it= watches tsp (which enqueues and exits), so "Finished downloading" fires immediately while yt-dlp may fail later, silently; also affects elfeed d. Message "queued" honestly or watch the real job (tsp -f).
+- [UX] =browser-config.el:34-47,171= — first-run fallback picks EWW (first, "always available") over installed real browsers; fresh machines get org links in a text browser until cj/choose-browser runs. Prefer the first external match.
+- [REMOVE] =video-audio-recording.el:442-488= — =cj/recording-group-devices-by-hardware= is dead code (nothing calls it) carrying a hardcoded "Jabra SPEAK 510 USB" branch. Delete + its test file.
+- [REMOVE] =calibredb-epub-config.el:198-212= — =set-auto-mode= :around advice for .epub is redundant with nov's :mode registration (auto-mode-alist wins before magic-fallback); overhead + failure surface on every file visit. Remove and verify.
+- [COVERAGE] — eww interactive commands (switch-search-engine, bookmark-quick-add, copy-url) and =cj/nov-center-images= untested.
+
+*** TODO Findings: apps/misc group
+From agents 2026-06-12. Beyond the standalone tasks:
+- [BUG] =hugo-config.el:49= — =cj/hugo-new-post= void-functions on =org-hugo-slug= in a fresh session (ox-hugo is :after ox, which loads on first export); =cj/hugo-export-post= already requires ox-hugo — do the same here.
+- [BUG] =help-utils.el:73= — arch-wiki search signals raw file-missing when the docs dir is absent; the friendly install hint at :81 is unreachable. Guard with =file-directory-p= + user-error up front.
+- [UX] =hugo-config.el:244= — eight raw global-set-key C-; h calls + hand-rolled which-key mutate cj/custom-keymap directly, against keybindings.el's own instruction. Convert to defvar-keymap + =cj/register-prefix-map= (same class as org-reveal, restclient, slack).
+- [ENHANCE] =games-config.el:25= — =:defer 1= pulls malyon + 2048 into every session for nothing; use =:commands=. Also :config references =org-dir= without requiring user-constants (free-variable warning at byte-compile).
+- [REMOVE] =wrap-up.el:29= — =elisp-compile-mode= doesn't exist (real mode emacs-lisp-compilation-mode derives from compilation-mode, already covered at :27); dead line. (The prior unguarded-timer fix is intact.)
+- [REMOVE] =help-config.el:99-106= — stray empty :preface + dead commented Info-directory-list block. Delete.
+- [NOTE] =duet-config.el= — orphaned BY DESIGN (Commentary documents pre-alpha staging; Stage 1 is the wire-in trigger). Audit record only.
+- [COVERAGE] — help-config and help-utils have zero test files; the two broken paths above are exactly the untested branches.
+
+*** TODO Findings: holistic — startup & performance
+From the 2026-06-12 holistic pass; daemon init measured at 1.11s (healthy). Beyond the standalone [#A] native-comp/GC task:
+- [BUG→FOLD] the eager-org chain: =org-config.el:352= org-appear has no defer trigger (only :custom) → requires all of org at init; org-agenda (=:after org :demand t=) cascades; chime's =:demand t= pulls it anyway. org-config is the most expensive require (0.229s of 1.11s). Decide fully-eager vs fully-deferred — and =init.el:146='s "calendar-sync must come after org-agenda" contract exists only as a comment (three uncoordinated writers of =org-agenda-files=). Both facts belong in the filed defer-modules task before that refactor starts.
+- [PERF] =dirvish-config.el:385-387= — =:defer 0.5= defeated by :init calling autoloaded =dirvish-override-dired-mode= → dirvish fully loads at init (0.072s, third most expensive; trace-confirmed). Own the eager load or defer the override to a dired-mode-hook shim.
+- [PERF] timed =:defer N= loads unused packages into every start: simple-httpd (:1s + startup mkdir despite the defer), malyon, 2048-game, emojify (may hit network), ligature. Convert to :commands/mode hooks.
+- [UX] =early-init.el:235-256= — synchronous =package-refresh-contents= on the startup path when any archive cache is >7 days old (MELPA ~6MB) — multi-second network-bound start, fires in batch too. Make async post-startup or push into the localrepo update script (distinct from the filed bootstrap-relocation task).
+- [PERF] =early-init.el:228= — no =package-quickstart= with 184 packages; activation walks every package dir each start (~0.3s of early-init). Free win; regenerate after package ops.
+- [PERF] =prog-general.el:298= — =yas-reload-all= immediately before =yas-global-mode= scans snippet dirs twice per start (doubled "[yas] Prepared..." message). Delete the line.
+- [REMOVE] cross-ref: the all-the-icons stack (already in UI core findings) is 2 of the :demand t packages plus a per-frame install-check hook.
+
+*** TODO Findings: holistic — daemon stability
+From the 2026-06-12 holistic pass. Architecture-level verdict good (timers cancelled/guarded, calendar-sync async well-contained, advice mostly named + guarded). Residual:
+- [STABILITY] =transcription-config.el:293= — sentinel chain has no unwind-protect; =--append-to-log='s =insert-file-contents= signals if the log is missing → process buffer leaks, entry stuck 'running in the modeline forever, no notification. Extends the filed transcription standalone — fix together.
+- [STABILITY] =calendar-sync.el:1646= — hourly timer body: fetch/parse guarded but the timezone check and =--require-calendars= run bare — any signal repeats hourly forever (the exact class fixed in four modules once). Condition-case the body; demote the hourly echo-area message to the silent log.
+- [STABILITY] =music-config.el:865= — four anonymous-lambda advice in :config stack per live reload (verified: lambdas don't dedupe) and can't be advice-removed. Name the function.
+- [STABILITY] =system-defaults.el:69= — =display-warning= advice appends to comp-warnings-log with no condition-case (unwritable path → every async comp warning signals from inside display-warning) and the log grows unbounded. Guard + cap.
+- [STABILITY] =media-utils.el:164= — playback sentinel assumes the process buffer is alive (user killed *player:...* → sentinel error, diagnostics lost); sibling yt-dl sentinel shares the kill-buffer gap. buffer-live-p guards.
+- [ENHANCE] =system-commands.el:86= — =#'ignore= sentinel + output to /dev/null makes failing lock/suspend indistinguishable from success — the user walks away from an unlocked machine. Message on nonzero exit. (Compounds the [#A] slock task: the broken lock currently fails through exactly this silent path.)
+- [ENHANCE] =ui-config.el:153= — post-command cursor hook unguarded: any future signal self-removes it silently (cursor stops signaling modified/read-only until restart); frame-hook lambda also accumulates per reload. with-demoted-errors + name it.
+- (kill-emacs-hook y-or-n-p prompt independently re-found here — already filed in the text/prose child; convergence noted.)
+
+*** TODO Findings: holistic — UX consistency
+From the 2026-06-12 holistic pass. Verdict: more coherent than most 120-module configs (~85% prefix-helper adoption, M-S translation fully covers its 18 bindings, F-keys collision-free, DEF-arg prompts dominate). Beyond the standalone [#A] fset task:
+- [BUG] notification env gate: =transcription-config.el:169-171= gates desktop notifications on =(getenv "DISPLAY")= — an X11 predicate that works only because XWayland exports it. Use =env-gui-p= (host-environment.el provides it).
+- [UX] four notification stacks beyond the messenger split (notify script ± fallback, alert.el, raw notifications-notify, echo-only for calendar-sync/recording completions). Proposed: one cj/notify facade (transcription's =cj/--notify= is the right shape) — config-wide companion to the messenger-notify addendum in the unification spec.
+- [UX] five more C-; entries bypass the register helpers or lack labels: =browser-config.el:182= (C-; B, no label), =org-babel-config.el:51= (C-; k, no label), =flycheck-config.el:62-64= (:bind into cj/custom-keymap), =pearl-config.el:43= (:bind-keymap C-; L, no label), =dev-fkeys.el:533= (helper but no label). Sweep onto cj/register-prefix-map with labels.
+- [UX] user-error vs message inconsistent for "nothing to act on" config-wide (examples: =custom-whitespace.el:190=, =jumper.el:202=, =chrono-tools.el:99= message; =mu4e-attachments.el:112=, =ai-rewrite.el:79= user-error; =test-runner.el:392/394= mixes both 2 lines apart). Convention: user-error when the command can't proceed; message when it ran and found nothing.
+- [ENHANCE] M-S translation layer: complete for GUI (18/18) but installs only on env-gui-p paths — terminal frames have no M-uppercase route; and =dwim-shell-config.el:932/934= binds M-S-d (dired) vs raw M-D (dirvish) asymmetrically. Feeds the filed M-S review task with the concrete map.
+- [ENHANCE] which-key labels: register-helper's LABEL arg used by exactly 1 of 23 registrants (rest use separate with-eval-after-load blocks); label style drifts ("X menu" vs bare nouns). Adopt LABEL arg + one style.
+- [ENHANCE] "?" curated-menu candidates (for the filed convention task): elfeed search/show, dirvish, signel chat/dashboard, music playlist, ai-conversations-browser, mu4e-attachments, transcription status, pearl. calibredb remains the model.
+- [UX] ="(Cancel)"= pseudo-candidate in =music-config.el:253-256= vs C-g everywhere else (90+ prompts). Drop it.
+- [UX] buffer-naming drifts across three conventions (*AI-Assistant* / *Kill Ring* / *dashboard*); pick Title Case + "*Name: param*", lowercase for process logs.
+- [ENHANCE] C-; f formatter shadowing implemented 3 ways (:bind :map vs local-set-key in hooks); unify on :bind. Also =keybindings.el:21= commentary still says "C-c j" for the jump prefix the code binds at C-; j.
+- [ENHANCE] initial-input anti-pattern at =dwim-shell-config.el:661,680= and =erc-config.el:176-177= against the config's DEF-arg norm.
+
+*** TODO Findings: holistic — package strategy
+From the 2026-06-12 holistic pass (184 elpa dirs). Core stack modern (vertico/consult/embark/orderless, treesit-auto, built-in which-key, current magit/forge/telega/slack). Beyond the standalone gptel/prescient tasks:
+- [REMOVE] true orphans, nothing references them: js2-mode, tide, json-mode (pre-treesit JS stack). package-delete + drop from .localrepo.
+- [REMOVE] emojify: 2021 snapshot, dormant upstream, crashes in lui (slack disabled it), Emacs 30 renders emoji natively. Drop the use-package + hooks (=font-config.el:253=, =erc-config.el:211=); it stays on disk only as slack's declared dep.
+- [BUG] legacy-mode hooks miss the ts modes: =prog-general.el:91-92= hooks =yaml-mode-hook=/=toml-mode-hook= but the config runs yaml-ts/toml-ts — general prog settings silently don't apply in YAML/TOML buffers. Rehook; delete toml-mode + eldoc-toml + yaml-mode packages (superseded by treesit).
+- [RISK→FOLD] localrepo priority 200 is absolute, so package-upgrade silently no-ops on everything mirrored — the engine that fossilized emojify@2021/toml-mode@2016/js2@2023. The filed refresh-script task at [#D] deserves [#B] + a quarterly cadence, else every orphan finding regrows.
+- [RISK] fork fleet sync-back stories: org-drill flip back to :vc when done (filed dev-checkout finding); auto-dim-other-buffers local checkout with :vc commented — decide its home; org-msg pins =:rev :newest= (unpinned moving target) — pin a known rev. signel/duet/pearl/wttrin/gloss/chime self-owned remotes are fine.
+- [UPGRADE] wiki-summary (2018, dead upstream, predates Wikipedia's REST API; sole caller help-utils) — the audit's one write-your-own: ~30-line url-retrieve against the REST summary endpoint. Delete the package, inline the helper.
+- [UPGRADE] xterm-color droppable in eshell on Emacs 30's native ansi-color (its only use; also doubly-broken per the eshell standalone task — fixing by deletion is an option).
+- [ENHANCE] Python tier: poetry.el (sluggish) + pyvenv (2021) keep only if Poetry projects are still real; blacken fine until ruff-format (reformatter.el already installed). lsp-pyright current.
+- [DECIDED] projectile, lsp-mode, dirvish: keep (wired into 10/7/many modules, maintained, migration cost > benefit). On the record so future audits don't relitigate.
+
+*** TODO Findings: spin-off repos (pearl, chime, emacs-wttrin)
+Full findings delivered as handoffs to each repo's inbox/ (2026-06-12-0057-from-.emacs.d-handoff-*.org); each repo's next session files them through its own value gate. Highlights:
+- pearl (10 findings; suite green, 66 ERT files): auth-source negative-cache trap in pearl-clear-cache (the 2026-06-01 incident class, unfixed); sync wrapper ignores pearl-request-timeout + async has no timeout; mutation errors discard Linear's GraphQL reason; no RATELIMITED handling; dead legacy API layer (~150 lines).
+- chime (10 findings; suite green; the 2026-06-11 watchdog handoff VERIFIED landed in full): lookahead vars never injected into the async child (documented feature silently capped at 8 days — one-line fix); days-until-event nil crash on mixed timed/all-day events; stale-callback race after watchdog interrupt (generation counter needed); default test run prints green integration banner over "Ran 0 tests".
+- emacs-wttrin (10 findings; ~56 ERT files, CI; the face-flood reminder VERIFIED resolved — test 8f3c770 + fix c5e5e1d, reminder cleared from notes.org): no network timeouts (wttr.in stalls hang the loading buffer); error-path response-buffer leak; non-favorite cache never expires; 17 unreleased commits incl. two features — tag v0.4.0.
+
+** PROJECT [#B] Implement ai-kb :feature:ai:kb:
+Build v1 of the AI knowledge base per [[file:docs/design/ai-kb.org][docs/design/ai-kb.org]] (Ready; six reviews incorporated, all decisions resolved 2026-05-24). Step 1 splits into 1a (the safe write path — minimum usable) and 1b (retrieval, maintenance, push), since =remember= depends on =index=+=lint= and the adapter depends on =remember=. Step 2 is the Emacs layer: a full org-roam profile on switch, the human-edit safety model (same write path as the agent), and the browsing surface. Step 3 and the LLM-Wiki layer are vNext. Children are ordered by build sequence; the server bootstrap is the prerequisite.
+
+*** TODO [#B] ai-kb bare repo on cjennings.net :ai-kb:
+Prerequisite, one-time server bootstrap (not doable by the local script): =sudo git init --bare /var/git/ai-kb.git= + chown on cjennings.net. Leave the github-mirror hook OFF — this repo is private. Required before every per-machine clone.
+
+*** TODO [#B] ai-kb store + contract + seed :ai-kb:
+Step 1a. Clone =git@cjennings.net:ai-kb.git= to =~/.local/share/ai-kb=. Author =AGENT_CONTRACT.org= (canonical repo-resident contract: node format, write protocol, operations, routing) and seed =index.org= + a README/index node with a generated =:ID:=. Node format per spec — a *required* one-line =:SUMMARY:= (the index/query read it straight, no inference/LLM), provenance (=:CREATED_BY:/:CONFIDENCE:/:VISIBILITY:/:SOURCE:/:STATUS:=), =:PROJECTS:= slugs, type filetags, relation labels. Define the durable external-pointer format as *ID-first*: =ai-kb: <Title> (<UUID>)=, resolved by ID with title fallback (filenames can change in curation).
+
+*** TODO [#B] ai-kb CLI 1a: index, lint, remember, doctor :ai-kb:
+Step 1a. Shell wrapper calling Emacs for org work — =emacsclient= when a daemon is up, =emacs --batch= fallback, lint+index in *one* invocation per =remember=. =index= regenerates =index.org= from node properties incl. =:SUMMARY:= (never hand-maintained); the index references nodes as plain =Title (UUID)= text, never =[[id:]]= links, and is excluded from the scan so it can't manufacture backlinks or hide orphans. =lint= = org-lint fatal checks + duplicate IDs + broken id-links (excl =raw/= + index) + missing required props (incl =:SUMMARY:=) + bad project slugs + stale/incomplete index + credential scan of nodes *and* =raw/= text files (binaries skipped). =remember= = the write protocol: fetch + =pull --ff-only= (abort on diverge/dirty), write, regenerate index, then run the *full =ai-kb lint=* over the change as the commit gate (not just node org-lint — this is the safety boundary), commit locally, =flock=; no push. =doctor= / =status= = health + push-state + raw-dir-size report (repo, private remote, CLI on PATH, =graphviz= if the map needs it, adapter linked, db buildable, no secrets, "ahead N"/"push failed"/"diverged"); =status= is the fast non-diagnostic mode for the dashboard/nudge.
+
+*** TODO [#B] claude-rules/ai-kb.md adapter :ai-kb:
+Step 1a. Global L1 rule in rulesets pointing at the repo-resident =AGENT_CONTRACT.org=: path, routing (T1/T2/T3 tiers; per-project =MEMORY.md= shrinks to ID-first pointers into ai-kb), proactive + contradiction rules, concrete "read the index first" triggers, link-grep recipes, "use =ai-kb remember=, never bypass =ai-kb lint=", one-line nudge on unpushed commits / recorded push rejection. =make install= symlinks it into =~/.claude/rules/=.
+
+*** TODO [#B] ai-kb provisioning: setup-ai-kb.sh + make ai-kb-init :ai-kb:
+Step 1a (core; the timer-install line is added with 1b). Idempotent =scripts/setup-ai-kb.sh=: clone (or init+add-remote on first machine), seed, install the CLI on PATH, =ai-kb index=, =ai-kb doctor=. =make ai-kb-init= wraps it. The one-time server bootstrap stays a separate documented step.
+
+*** TODO [#B] ai-kb Step-1a tests :ai-kb:tests:
+Write-path: a write with the remote unreachable still commits locally and does not error; =flock= serializes concurrent =remember=; each org-lint *fatal* check (malformed drawer, missing/dup =:ID:=, invalid required property, missing =#+title:=, unparseable org) rejects the commit, a style warning does not; a node missing =:SUMMARY:= fails lint; =remember= aborts the commit when the *full* lint fails (stale index, broken link, secret in a node or =raw/= text file); the credential scan skips binaries. Index: regen from a fixture produces expected entries; an out-of-band node appears only after regen; a node referenced only by =index.org= still reports as an orphan (the index is not a backlink source). Link recipes: backlink (excl =raw/= + index) + forward correct. Provisioning (bats): idempotent, valid =:ID:= + =:SUMMARY:=, =doctor= passes.
+
+*** TODO [#B] ai-kb CLI 1b: query, curate, sync :ai-kb:
+Step 1b. =query <context>= with a *testable contract*: plain-text default + =--json=; fields title/ID/summary/projects/status/updated/path + *match reason*; searches index rows + title/tags/properties/body; ranks by lexical score — sum of each matched field's weight, counted once per field: title 100, tag/project/status 50 each, summary 20, body 5; no term-frequency weighting in v1 — with most-recently-updated (=:UPDATED:=) only as the *tie-break* on equal scores (recency alone buries stable old preferences); default max-results; =raw/= paths only as source references; exit codes for no-match / invalid KB / lint-index failure. =show <id-or-title>= (resolve ID-first, print the node) and =backlinks <id>= (excl =raw/= + index) as the inspection primitives the Emacs commands wrap. =curate --dry-run= (four buckets; also flags orphan =raw/= captures and any =raw/= file over 256 KB; destructive ops human-only). =sync= (=org-roam-db-sync= against ai-kb) only when the db is missing/stale or forced.
+
+*** TODO [#B] ai-kb push timer + failure observability :ai-kb:
+Step 1b. =ai-kb-push.timer= + =ai-kb-push.service= =systemd --user= units: push only if ahead, ~15 min; installed + =enable --now= by the setup script (add this line to =setup-ai-kb.sh=). A failed push is logged to a state file (=$XDG_STATE_HOME/ai-kb=), never fatal; surfaced by =ai-kb doctor= and the adapter's startup nudge.
+
+*** TODO [#B] ai-kb-curate workflow in rulesets :ai-kb:
+Step 1b. =~/code/rulesets/.ai/workflows/ai-kb-curate.org= — human-gated curation: the four buckets, node-count trigger (nudge at 150 nodes, re-fire every +50), =:LAST_CURATED:= rotation, pointer-integrity (merge/supersede changes the canonical ID, so grep inbound =[[id:]]= + =MEMORY.md= =ai-kb: ... (UUID)= refs and repoint before deleting). Surfaced by =ai-kb doctor= + session startup when due.
+
+*** TODO [#B] ai-kb Step-1b tests :ai-kb:tests:
+=query --json= returns the specified fields (incl. match reason)/exit-codes on a fixture KB and =raw/= appears only as a source ref; a title match outranks a body-only match with recency only breaking ties (an old preference is not buried under a newer body-only hit); a simulated push failure is recorded to the state file and surfaced by =ai-kb doctor= / =status=. Performance (=:perf= tag): 100- and 1,000-node fixtures keep =index=/=query=/=lint=/=remember= under a stated time budget (catches an accidental per-check Emacs startup or an O(n²) scan).
+
+*** TODO [#B] Emacs: org-roam ai-kb profile + switch :ai-kb:
+Step 2.
+=org-roam-config.el=: =cj/org-roam-switch-to-ai-kb= / =cj/org-roam-switch-to-personal= install a full org-roam *profile*, not a two-variable swap — dir + =org-roam-ai.db= + =org-roam-file-exclude-regexp= (=raw/= + =index*.org=), and dailies, capture templates, topic/project/recipe find wrappers, and the agenda/refile + completed-task→daily hooks all rescoped or neutralized so ai-kb nodes never leak into personal journals/agenda. Restore everything exactly on exit; re-assert personal state at startup (abnormal-exit safety). =cj/ai-kb-db-sync= syncs only when the db is missing/stale or forced, with a status indicator.
+
+*** TODO [#B] Emacs: ai-kb edit safety (same write path) :ai-kb:
+Step 2. An =ai-kb= minor mode whose =after-save-hook= runs the agent's post-write sequence under =flock= — =ai-kb index=, full =ai-kb lint=, commit, push-state update — so a human Emacs edit can't bypass index/lint/commit. One write path for both agent and human. Failure UX: the save always writes to disk and the buffer stays editable (never read-only/blocked); on lint failure it does *not* commit, pops findings to a =*ai-kb-lint*= buffer (no focus steal), and shows the uncommitted-failing state in the modeline + dashboard — Craig fixes and re-saves, a clean save commits. Recursion guard, two layers: the mode's activation predicate excludes =index*.org= + =raw/=, and the pipeline binds a re-entrancy flag (=cj/ai-kb--in-pipeline=) the hook early-returns on; index regen prefers =write-region= over =save-buffer=.
+
+*** TODO [#B] Emacs: ai-kb browsing surface :ai-kb:
+Step 2. =cj/ai-kb-dashboard= (status banner: active KB, node count, unpushed commits, push-failure state, curation due, last index/sync), =cj/ai-kb-find-node= (=org-roam-node-find= in the ai-kb profile), =cj/ai-kb-search= (=ai-kb query= or scoped =consult-ripgrep=), =cj/ai-kb-show-node= (resolve ID-first, open), =cj/ai-kb-backlinks= (excl =raw/= + index), =cj/ai-kb-map= (built-in =org-roam-graph= *first* — the profile's exclude regexp already keeps =raw/= + index out of the db, so the graph inherits the right scope; custom DOT export only if project/tag/status filtering proves necessary; =graphviz= dep). Simple wrappers over the CLI primitives where possible.
+
+*** TODO [#B] Emacs: ai-kb keybindings + which-key :ai-kb:
+Bind the switch + sync + browsing commands under the =C-c n= roam prefix (e.g. =C-c n a= → ai-kb, =C-c n A= → personal, a small transient for the browsing commands), avoiding the dense existing set; which-key labels.
+
+*** TODO [#B] Emacs: ai-kb Step-2 ERT tests :ai-kb:tests:
+Profile: switch installs the ai-kb dir + db + exclude regexp and switch-back restores personal *exactly* — completed-task hook, agenda/refile finalize hook, dailies, and capture templates all untouched by ai-kb while switched; startup re-asserts personal state after a simulated abnormal exit. Edit path: a save in an ai-kb buffer runs index+lint+commit (a bad save surfaces the lint failure rather than committing). Sync runs only when stale.
+
+** PROJECT [#B] Architecture review follow-up from 2026-05-03 :refactor:nosync:
+
+High-level pass over =init.el=, =early-init.el=, and all 104 files in
+=modules/=. The main theme: the config works, but load order, startup side
+effects, credentials, and test measurement are more implicit than they should
+be. Use this project as the parent tracker; each child below should land as a
+small, reviewable change.
+
+Review snapshot:
+- =modules/= has 104 files and about 24k lines including =init.el= and
+ =early-init.el=.
+- =init.el= eagerly =require=s nearly every module.
+- =make coverage= passed when allowed to write the test scratch directory.
+- Coverage report: =3240/4952= executable lines, =65.43%=, across 49 module
+ files. Caveat: 55 module files do not appear in the report at all, so the
+ real project confidence is lower than the raw percentage suggests.
+
+*** 2026-05-15 Fri Consolidate shared utility helpers :architecture:refactor:
+CLOSED: [2026-05-15 Fri]
+
+Helpers are scattered across feature modules where they were first needed.
+Some are duplicated, and some private helpers are generic enough to belong in a
+shared foundation library. This is adjacent to the load-graph refactor because
+central helper ownership reduces hidden inter-module dependencies, but it
+should remain a sibling project so load-order batches stay small and
+reviewable.
+
+Guidance:
+- Do not extract a helper until at least two callers are clearly the same
+ shape.
+- Prefer growing =system-lib.el= first; split into topic libraries only if it
+ becomes too broad or starts pulling coarse dependencies into foundation
+ startup.
+- Keep one helper extraction per commit.
+- Move unit tests with the helper. Consumers should keep behavior/integration
+ coverage.
+- Do not add heavy package dependencies to foundation helpers.
+
+**** DONE [#B] Write full utility consolidation design spec :architecture:refactor:
+CLOSED: [2026-05-04 Mon]
+
+Create a design document that inventories candidate helper extractions,
+recommends grouping and naming, explains how the helpers fit into existing
+library modules, defines migration phases, and identifies testing/rollback
+rules.
+
+Spec: [[file:docs/design/utility-consolidation.org][docs/design/utility-consolidation.org]]
+
+Verify 2026-05-04:
+- Added [[file:docs/design/utility-consolidation.org][docs/design/utility-consolidation.org]].
+- Spec includes framing questions, existing library fit, proposed grouping,
+ concrete pull/rename table, migration phases, test strategy, acceptance
+ criteria, risks, open questions, and recommended first commits.
+- Parsed the spec and =todo.org= with =org-element=.
+- Committed the tracked spec as =3ea4707=.
+- Incorporated complete review feedback in =dd77ebd=, including API behavior
+ contracts, speculative-extraction rules, =system-lib= dependency budget,
+ inventory/audit artifacts, test relocation policy, commit type guidance,
+ =use-package :if= load-order policy, and Phase 5 cache-design addendum
+ requirement.
+
+**** DONE [#B] Inventory private helpers across modules :refactor:
+CLOSED: [2026-05-10 Sun]
+
+Walk every module and tag private helpers as genuinely module-specific,
+generic-but-trapped, or duplicated. Capture likely consumers and any dependency
+cost before extracting.
+
+Candidate families:
+- shell argument formatting,
+- executable lookup with user-visible warnings,
+- argv-based process runners,
+- path containment/safe-base predicates,
+- Org-safe heading/property/body text sanitizers,
+- cache-with-TTL plus invalidation hooks,
+- warning/message wrappers.
+
+Verify 2026-05-10:
+- Added [[file:docs/design/utility-inventory.org][docs/design/utility-inventory.org]] covering the 30 entries in the spec's
+ Candidate Extraction Table grouped by family (executable discovery, shell
+ quoting, process runner, file/path, external-open, Org-safe text, cache,
+ logging, macros/debug, theme I/O, string).
+- For each helper recorded: visibility, dependencies, side effects, callers
+ (production + test), test files, priority, decision (Migrate / Leave / Defer)
+ with rationale.
+- Decisions Summary: 11 Migrate, 3 Leave, 13 Defer.
+- Concrete next-action list groups Migrate items by Phase (2 = foundation
+ helpers, 3 = Org-safe text, 4 = external-open consolidation) for the order
+ the spec recommends.
+- Discoveries: =cj/log-silently= has 10 production callers (more than the
+ spec's table suggested -- defer is the right call); =cj/--file-manager-program-for=
+ shipped today in =dirvish-config.el= is the new form of OS-dispatch
+ consolidation and should fold into =cj/external-open-command= during Phase 4.
+
+**** DONE [#B] Extract executable lookup with warning helper :refactor:
+CLOSED: [2026-05-10 Sun]
+
+Create a generic helper such as =cj/find-executable-or-warn= from the useful
+=mail-config= pattern. It should return the executable path or nil and produce
+a clear warning when the executable is missing.
+
+Done 2026-05-10:
+- Shipped as =cj/executable-find-or-warn= in =modules/system-lib.el=
+ (commit =c75e36f4=, extracted from =mail-config=).
+- First consumer rewired in =12c2cb14= (=cj/set-wallpaper= in
+ =dirvish-config.el=).
+
+**** DONE [#B] Extract argv-based process runner helper :refactor:
+CLOSED: [2026-05-10 Sun]
+
+Generalize the =coverage-core= process pattern into a dependency-light helper
+that captures output and signals a clear =user-error= with command/status/output
+on failure. Consider a small git wrapper only after the generic runner exists.
+
+Done 2026-05-10:
+- Shipped =cj/process-output-or-error= plus the =cj/git-output-or-error=
+ wrapper in =modules/system-lib.el= (commit =57e558ce=, extracted from
+ =coverage-core=).
+
+**** DONE [#B] Extract Org-safe text sanitizers :refactor:
+CLOSED: [2026-05-10 Sun]
+
+Move heading/property/body sanitization into a shared helper once at least one
+non-calendar consumer is ready. Keep behavior explicit so external text cannot
+accidentally create headings or malformed properties.
+
+Done 2026-05-10:
+- Shipped =modules/cj-org-text-lib.el= (renamed to its final =-lib= form in
+ commit =0f9e3087=) with three sanitizers: =cj/org-sanitize-body-text=,
+ =cj/org-sanitize-property-value=, =cj/org-sanitize-heading=.
+
+*** 2026-05-15 Fri Make coverage reporting account for untracked modules :tests:
+CLOSED: [2026-05-15 Fri]
+
+The current coverage result is useful but easy to overread. =make coverage=
+reported =65.43%= for files that undercover saw, but only 49 of 104 module
+files appeared in =.coverage/simplecov.json=.
+
+Definition: in this task, "untracked modules" means repository-owned
+=modules/*.el= files that should be part of the Emacs configuration coverage
+universe but have no entry in =.coverage/simplecov.json= after =make coverage=
+runs. These files may be missing because no test required them, because loading
+was skipped due to package/environment guards, or because instrumentation did
+not see them. They are distinct from tracked modules with 0% covered lines,
+which already appear in SimpleCov and can be scored directly.
+
+Completed 2026-05-15:
+- Both child tasks are done.
+- =make coverage-summary= reports missing modules explicitly and also reports a
+ separate project-module score where missing modules count as 0%.
+- Focused summary tests and byte-compilation of the summary helper passed.
+
+**** 2026-05-15 Fri Teach the coverage report to list modules missing from SimpleCov
+CLOSED: [2026-05-15 Fri]
+
+Expected outcome:
+- Compare =modules/*.el= against paths present in =.coverage/simplecov.json=.
+- Show a separate "not in report" section.
+- Do not silently fold those files into the percentage until we decide the
+ semantics. A visible missing-file count is enough for v1.
+
+Done 2026-05-15:
+- =make coverage-summary= now compares direct =modules/*.el= files on disk
+ against the module paths present in =.coverage/simplecov.json=.
+- The terminal report appends a =Not in SimpleCov report= section with a count
+ and the missing module paths.
+- Missing modules are explicitly excluded from the displayed percentage for
+ now; the policy question below remains open.
+- Added focused tests in =tests/test-coverage-summary.el= for missing-module
+ reporting and for ignoring =.elc= files and nested paths outside direct
+ =modules/*.el= ownership.
+
+**** 2026-05-15 Fri Decide whether unreported modules count as 0% coverage
+CLOSED: [2026-05-15 Fri]
+
+This is a policy decision:
+- Counting missing modules as 0% gives a more honest project-level number.
+- Keeping the current number is useful for "instrumented executable lines only".
+
+Recommendation: display both:
+- Instrumented coverage: current SimpleCov percentage.
+- Project module coverage: includes unreported module files as 0% or reports
+ them separately with an explicit caveat.
+
+Decision 2026-05-15:
+- Keep the existing SimpleCov percentage as the line-weighted
+ =instrumented coverage= number. It only covers modules that SimpleCov saw and
+ has real executable-line denominators for.
+- Also display a separate module-weighted =project module coverage= score over
+ all direct =modules/*.el= files. Modules present in SimpleCov contribute their
+ per-file coverage percentage; modules absent from SimpleCov count as 0%.
+- Do not pretend missing modules have known executable-line counts. Counting
+ them as 0% at the module level is honest about risk without inventing a line
+ denominator.
+
+Done 2026-05-15:
+- =make coverage-summary= now prints both the existing line-weighted summary
+ and a separate =Project module coverage= line that includes missing modules
+ as 0%.
+- The missing-module section now states that missing modules count as 0% in the
+ project-module score.
+- Updated =tests/test-coverage-summary.el= to assert the policy and the
+ displayed project-module percentage.
+
+*** 2026-05-15 Fri Add a lightweight architecture smoke test for startup contracts :tests:
+CLOSED: [2026-05-15 Fri]
+
+After the above refactors start, add one or two smoke tests that protect the
+architecture instead of individual functions.
+
+Candidate checks:
+- All modules can be loaded directly with only =modules/= on =load-path=, or
+ skipped with a clear external package reason.
+- No module other than =keybindings.el= binds =C-;= itself.
+- Startup-only modules do not run timers in batch test mode.
+
+Keep this small. The goal is to catch accidental return to hidden load-order
+coupling, not to build a full static analyzer.
+
+Done 2026-05-15:
+- Added =tests/test-architecture-startup-contracts.el= with two source-level
+ smoke checks:
+ - only =keybindings.el= may globally own the exact =C-;= prefix;
+ - top-level timer scheduling forms must be guarded by =noninteractive= so
+ batch/test loads do not schedule startup timers.
+- Gated existing startup timers in =org-agenda-config.el=,
+ =org-refile-config.el=, =quick-video-capture.el=, and =wrap-up.el=.
+- Focused tests passed for the new architecture smoke file and the affected
+ agenda/refile helpers.
+
+*** PROJECT [#A] Un tangle the eager =init.el= load graph :architecture:refactor:
+
+=init.el= currently functions as the dependency graph by eagerly requiring
+almost every module in a fixed order. That makes modules harder to test in
+isolation and hides real dependencies behind "loaded earlier in init.el"
+assumptions.
+
+Spec: [[file:docs/design/init-load-graph.org][docs/design/init-load-graph.org]]
+
+**** 2026-05-25 Mon @ 07:59:20 -0500 Wrote full design spec for the =init.el= load-graph refactor :architecture:refactor:
+
+Create a design document that defines the target architecture, module
+categories, migration phases, test strategy, acceptance criteria, and risk
+controls for untangling the eager =init.el= load graph.
+
+Review incorporation:
+- Treat helper consolidation as adjacent architecture work, not a direct
+ acceptance criterion for the load-graph refactor.
+- Mention utility extraction guardrails in the spec so Phase 2 dependency work
+ has a clear rule for duplicated helpers found along the way.
+
+Verify 2026-05-04:
+- Added [[file:docs/design/init-load-graph.org][docs/design/init-load-graph.org]].
+- Incorporated review feedback by making utility consolidation an explicit
+ sibling project with guardrails and candidate helper families.
+- Parsed the spec and =todo.org= with =org-element=.
+- Committed the tracked spec as =0528475=.
+
+**** 2026-05-24 Sun @ 17:07:03 -0500 Classified modules by role and startup requirement
+Built [[file:docs/design/module-inventory.org][docs/design/module-inventory.org]] across 9 batches: 101 of 102 init.el-required modules annotated with the load-graph header contract (Layer, Category, Load shape, Eager reason, Top-level side effects, Runtime requires, Direct test load) and tabulated in the inventory. Added =tests/test-init-module-headers.el= to enforce the contract on each classified module. Retired the three vague =init.el= comments (latex-config WIP, prog-shell "combine elsewhere", "Modules In Test" banner) into real tasks. Recorded seven hidden =cj/custom-keymap= / cross-module dependencies for the Phase 2 dependency pass. Tagged the span =load-graph-classify-start..load-graph-classify-end=. elfeed-config is the one module left, pulled to its own task below.
+
+**** 2026-05-25 Mon @ 08:35:33 -0500 Annotated elfeed-config load-graph header
+Added the load-graph header to elfeed-config (Layer 4, O/D/P, current load shape eager with an eager reason, target command-loaded; runtime requires user-constants, system-lib, media-utils), added it to the header-contract allowlist in =tests/test-init-module-headers.el= (Batch 8), and moved it in =docs/design/module-inventory.org= from the Deferred/Pending sections into the Batch 8 table. Inventory now 102 of 102 classified. The header's "Load shape" records the current shape (eager, required in init.el) per the weather-config/games-config convention; "command-loaded" is the target, in the inventory's Target column. Shipped as a522e553.
+
+**** 2026-05-24 Sun @ 18:35:06 -0500 Made hidden module dependencies explicit
+Fixed the seven hidden dependencies the classification surfaced: system-defaults now requires host-environment and user-constants at runtime (was eval-when-compile); custom-buffer-file, dev-fkeys, calendar-sync, and video-audio-recording require keybindings and drop their =(when (boundp 'cj/custom-keymap) ...)= shims; flycheck-config and mail-config require keybindings for their cj/custom-keymap bindings. Removed a dead =eval-when-compile (defvar cj/custom-keymap)= in transcription-config (the var was never used).
+
+No init.el load-order change — keybindings and the foundation modules already load before these, so the explicit requires are no-ops at startup and only fix standalone/test loading.
+
+Verified each fix with a fresh =emacs --batch (require 'X)=, then swept all ~100 modules standalone: every one loads or fails only with a clear missing-package message (the spec's Phase 2 exit bar). Full =make test=, =make validate-modules=, and an init smoke all pass. Module headers and the inventory's hidden-dependency section updated to mark the seven resolved.
+
+**** TODO [#B] Defer feature modules behind autoloads, hooks, and commands :refactor:
+
+Once dependencies are explicit, reduce the number of modules required at
+startup. Start with lower-risk feature modules:
+- Entertainment and optional integrations: =games-config=, =music-config=,
+ =weather-config=, =slack-config=, =erc-config=.
+- Heavy document/media modules: =pdf-config=, =calibredb-epub-config=,
+ =video-audio-recording=, =transcription-config=.
+- AI/rest tooling: =ai-config=, =restclient-config=, =ai-conversations=.
+
+Do this incrementally. After each batch:
+- Restart Emacs interactively.
+- Run =make test= or at least targeted tests.
+- Check that keybindings still resolve and which-key labels still appear.
+
+**** 2026-05-24 Sun @ 19:59:01 -0500 Centralized custom keymap registration
+Added cj/register-prefix-map and cj/register-command to keybindings.el (commit 47f222f6) with test-init-keymap-registration.el, then migrated all 31 cj/custom-keymap registration sites across 24 modules onto the API. Consumers no longer reference cj/custom-keymap directly — keybindings.el is the sole owner of the prefix, and modules require keybindings to reach the API.
+
+Verified behavior-preserving by dumping every C-; binding before and after: identical, 279 bindings, each resolving to the same command. Byte-compiled all 24 migrated files (no new free-variable warnings — the cj/custom-keymap coupling is gone), and full make test, validate-modules, and an init load all pass. which-key label blocks were left intact; they use string key descriptions and never assumed cj/custom-keymap existed.
+
+Related existing task: [#B] "Review and rebind M-S- keybindings".
+
+*** PROJECT [#A] Move package bootstrap out of =early-init.el= where possible :startup:refactor:
+
+=early-init.el= currently handles package archives, package refresh, installing
+=use-package=, and =use-package-always-ensure=. That is more than early startup
+needs and can make startup network-sensitive.
+
+**** TODO [#B] Split early startup from package bootstrap :refactor:
+
+Keep =early-init.el= focused on things that must happen before package and UI
+startup:
+- GC/file-name-handler startup tuning.
+- =load-prefer-newer=.
+- frame/UI suppression.
+- minimal debug behavior.
+
+Move package archive setup and =use-package= installation to a normal module or
+bootstrap command, unless there is a specific reason it must run in
+=early-init.el=.
+
+Acceptance criteria:
+- Fresh install/bootstrap still works from a documented command or script.
+- Normal startup does not refresh archives or install packages unexpectedly.
+- Offline startup remains quiet and predictable.
+
+**** TODO [#A] Revisit package signature policy
+
+=package-check-signature= is disabled. Decide whether that is still necessary
+for the localrepo/mirror workflow.
+
+Expected outcome:
+- Prefer signatures on by default.
+- If signatures must be disabled for local mirrors, scope that exception and
+ document why.
+- Add a note to the local repository docs so future package failures do not
+ lead to permanent insecure defaults.
+
+** DOING [#B] Signel Client Open Work
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-06
+:END:
+Parent task for the Emacs Signal client. Engine: signal-cli (linked secondary device). Front end: a fork of signel at =~/code/signel=, wired through =modules/signal-config.el=. Design: [[file:docs/design/signal-client.org][docs/design/signal-client.org]]. Child issues below.
+
+*** TODO [#C] signel--handle-error leaks request-buffer-map entries :bug:no-sync:
+Surfaced during the JSON-RPC dispatch refactor audit. =signel--handle-error= reads =signel--request-buffer-map= by id but never =remhash='es the entry, so every error response leaves the request-id → buffer-name mapping behind for the life of the process. Low impact (the map clears on stop/start, and id collisions are unlikely at the counter scale), but unbounded growth in a long-lived session and inconsistent with how the new request-handler-map is cleaned up on error.
+
+*** TODO [#B] Link command with QR :feature:
+=cj/signel-link= wrapping =signal-cli link -n NAME=, capturing the =sgnl://linkdevice= URI and rendering it as a scannable QR (qrencode). Convenience for re-linking; the first link was done by hand this session.
+
+*** TODO [#D] Include Signal groups in the picker :feature:no-sync:
+vNext after the 1:1 initiate-message flow is stable. Merge =listGroups= with =listContacts=, label groups distinctly, and preserve the current v1 behavior where the picker is contacts-only.
+
+*** DOING [#B] Notify only for the unviewed conversation :feature:
+Wire =cj/signal--should-notify-p= (done) into signel's =signel--handle-receive= notify block (signel.el:277), route through Craig's notify script instead of bare =notifications-notify=, and gate sound behind a defcustom that defaults off. Spec addendum (the four notify details + wiring architecture) accepted 2026-06-11 — see [[file:docs/design/signal-client.org][signal-client.org]] "Notification slice".
+
+Built 2026-06-11 (TDD; fork commit e263367, dotemacs 9afc6128): =signel-notify-function= customization point in the fork; =cj/signel--notify= + =cj/signal--format-notify-body= + =cj/signel-notify-sound= in signal-config.el, wired in =:config= with a load-time =cj/executable-find-or-warn=. 17 new ERT tests green; full launch smoke clean; live-reloaded into the daemon and a synthetic toast fired through the script path. Stays DOING until the two manual checks below pass.
+
+**** Signel: real incoming message raises a toast through the notify script
+What we're verifying: the full receive path (signal-cli → signel --handle-receive → cj/signel--notify → notify script) fires on a real message.
+- Make sure you are NOT viewing the sender's chat buffer.
+- Have a real message sent to you on Signal (or send one from your phone to a second device thread that lands here).
+Expected: a transient info toast titled "Signal: <sender>" with the message text (one line, truncated if long), no sound.
+
+**** Signel: actively-viewed chat stays quiet
+What we're verifying: the suppression predicate gates the toast when you're reading that chat.
+- Open the sender's chat buffer (=C-; M m=) and keep it the selected window in a focused frame.
+- Have the same sender message you again.
+Expected: the message renders in the buffer, but no desktop toast appears.
+
+*** 2026-05-26 Tue @ 20:06:58 -0500 Decided: fork signel rather than depend on it
+signel is on MELPA but stale (one-author v0.1, all commits in a Jan-2026 burst, unattended tracker, no PRs). The spec needs internal edits (notify behavior, input-clobber fix), which are clean in a fork and hacky via advice, and a dead upstream means no divergence cost. Rejected: adopt-from-MELPA + advice, build-from-scratch, signal-cli-rest-api (Docker), MCP-tool, ERC bridge. Full rationale in the design doc.
+
+*** 2026-05-26 Tue @ 20:06:58 -0500 Linked as secondary device; contact parser verified against live shape
+Installed signal-cli 0.14.4.1 (AUR; imported AsamK's signing key FA10826A... to clear the makepkg verification). Linked the account via QR. Built and unit-tested the pure helper layer in =modules/signal-config.el= (contact-list parsing, notify-when-not-viewing predicate) with =tests/test-signal-config.el=. Confirmed the live =listContacts= shape: givenName/familyName are top-level in 0.14, not under profile as first assumed; corrected the parser and verified it produces a picker entry for all 94 real contacts. Sent a request to archsetup to add signal-cli to the standard install.
+
+*** 2026-05-27 Wed @ 22:08:40 -0500 Shipped initiate-message workflow: picker + Note-to-Self + keymap
+=cj/signel-message= (=C-; M m=) names contacts via =completing-read= over the cj-owned =cj/signel--contact-cache=, with "Note to Self" pinned first. =cj/signel-message-self= (=C-; M s=) sends straight to =signel-account=. Daemon guard =cj/signel--ensure-started= auto-starts the daemon when =signel-account= is set and =user-error='s with the remedy when it isn't; on start it pre-warms the cache. =cj/signel--fetch-contacts= rides the new RPC callback contract (=signel--send-rpc= with success-callback), the result feeds =cj/signal--parse-contacts=, and =cj/signel-refresh-contacts= (=C-; M no leaf=) clears + refetches. Cold-cache invocations =accept-process-output= up to =cj/signel-fetch-timeout= seconds (3s default) and =user-error= on timeout so a wedged daemon can't hang Emacs. Prefix keymap =cj/signel-prefix-map= bound under =C-; M= via =keybindings.el='s =cj/custom-keymap=: m / s / d / q / SPC. 15 new ERT tests in =tests/test-signal-config.el= cover ensure-started branches, fetch contract, cache empty-vs-failure, refresh, picker happy-path + cold-cache resolves + cold-cache timeout, message-self, and the prefix map bindings.
+
+*** 2026-05-27 Wed @ 21:55:57 -0500 Added JSON-RPC success-result dispatch in the signel fork
+Fork commit 4740d97 added =signel--request-handler-map= (id → success callback), extended =signel--send-rpc= with an optional =success-callback= that registers under the new request id, and gave =signel--dispatch= a result branch that invokes the callback and removes the handler. Error responses also remhash the handler entry, and =signel-start= / =signel-stop= both =clrhash= the map so reconnect is reliably empty. Backward-compatible: existing callers that don't pass a callback hit the same code path as before. Five ERT tests in this project (=tests/test-signel-rpc-dispatch.el=, dotemacs commit bfec0eab) lock the contract: Normal (result invokes callback + cleanup, send-rpc registers), Boundary (unknown id is a no-op), Error (error response cleans up handler), reconnect (=signel-stop= empties the map). Refactor audit surfaced a separate pre-existing leak in =signel--handle-error= (request-buffer-map entries aren't removed on error); filed as the [#C] follow-up below.
+
+*** 2026-05-27 Wed @ 22:08:40 -0500 Shipped clobber fix for both insert paths
+Fork commit 5ec56c0 added =signel--pending-input= (capture from input-marker to point-max) and =signel--restore-input= (re-insert after the redrawn prompt; nil-safe), and wired both into =signel--insert-msg= (the receive path) and =signel--insert-system-msg= (the error path). A mid-type send now survives both an incoming message and a system-error insertion. Four ERT tests in =tests/test-signel-input-preservation.el= cover the helpers (typed text, empty) and both insert paths via a temp =signel-chat-mode= buffer.
+
+*** 2026-05-27 Wed @ 22:08:40 -0500 use-package wired with C-; M keymap and local account config
+=use-package signel :load-path "~/code/signel" :ensure nil= already wired earlier with =signel-auto-open-buffer nil=. Account source is =signel-account= set from =cj/signal-private-config-file= (=signal-config.local.el=, gitignored) loaded in =:config=, decided in the workflow spec. Keymap prefix =C-; M= attached via =with-eval-after-load 'keybindings= so the binding survives load-order.
+
+*** 2026-06-06 Sat @ 12:29:24 -0500 Fixed C-; M load-order bug via canonical register-prefix-map
+Root cause: signal-config.el was the only feature module that violated the prefix-registration contract documented in =keybindings.el:41-45=. Every other prefix map uses =(require 'keybindings)= + a top-level =(cj/register-prefix-map "X" map)=; signal-config had neither, mutating =cj/custom-keymap= directly through a =(with-eval-after-load 'keybindings (when (boundp 'cj/custom-keymap) ...))= form. The =boundp= guard turned a load-order miss into a SILENT no-op — no error, the binding just never happened — which is why a live-reload (keybindings definitely loaded by then) papered over it.
+Fix: added =(require 'keybindings)= at the top of signal-config.el and replaced the guarded form with =(cj/register-prefix-map "M" cj/signel-prefix-map "signal messages")=, matching the 25+ other prefix maps.
+Verified: (1) new contract test =test-signal-config-prefix-map-registered-under-c-semi-m= asserts =C-; M= resolves to =cj/signel-prefix-map= (35/35 green); (2) full =emacs --batch= init.el launch — the exact failing scenario — now shows =C-; M= bound; (3) clean byte-compile; (4) live-reloaded into the daemon, binding confirmed. No unit-level red was possible: the =boundp= guard is robust under all standard test timings, which is the CLAUDE.md launch-only-failure class.
+
+*** 2026-05-28 Thu @ 03:09:18 -0500 Chat buffer docks bottom 30% and C-c C-k cancels
+=display-buffer-alist= entry in =modules/signal-config.el= matches =^\*Signel: = chat buffers and routes them through =display-buffer-at-bottom= with =window-height . 0.3=, so the chat docks to the bottom 30% of the frame. The signel fork's =signel-chat= switched from =switch-to-buffer= to =pop-to-buffer= so the rule can apply (=switch-to-buffer= ignores =display-buffer-alist=). =C-c C-c= was already bound to =signel--send-input= in the mode; =C-c C-k= now binds =signel--cancel-input=, a new fork helper that clears the editable region between =signel--input-marker= and =point-max= and then calls =quit-window=. Buffer stays alive so chat history above the marker survives revisits; cleared input means the next visit lands on a fresh prompt. Five ERT tests in =tests/test-signel-cancel-input.el= (clears pending, empty-area no-op, quit-window called, buffer preserved, keymap binding) and two new tests in =tests/test-signal-config.el= (entry shape + regex match set). Dotemacs commit 998e9c7a, fork commit df02d79.
+
+** DOING [#B] Migrate All Terminals From Vterm to Ghostel :terminal:ghostel:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-04
+:END:
+Replace vterm with ghostel (libghostty-vt) as the single terminal engine across every workflow, and rename ai-vterm → ai-term. References: [[file:docs/2026-05-25-emacs-terminal-comparison.org][docs/2026-05-25-emacs-terminal-comparison.org]] (vterm vs eat vs ghostel research); migration spec [[file:docs/design/vterm-to-ghostel-migration-spec.org][docs/design/vterm-to-ghostel-migration-spec.org]] (READY; external review incorporated 2026-06-04, D1-D7 agreed). Build in 5 phases (0-4); see the spec's Implementation tasks block.
+
+Decisions D1-D7 are settled in the spec's Agreed-decisions section. Build order below; each phase stays green (suite + byte-compile) at every step.
+
+*** TODO [#B] Phase 2: rename ai-vterm→ai-term on ghostel :terminal:ghostel:
+Swap the 6 vterm call sites; F9 family on global + ghostel-mode-map; drop refuse-in-terminal guard (D4); preserve the tmux-suppression invariant. Rename engine-agnostic tests after green; rework coupled tests; add D4 + F12-excludes-agent regression tests.
+
+*** TODO [#B] Follow-up: theme ghostel ANSI faces in dupre :terminal:ghostel:dupre:
+D2 — set the 16 ghostel-color-* + ghostel-default faces in dupre-faces/palette.
+
+*** TODO [#B] Follow-up: evaluate ghostel-eshell + ghostel-compile :terminal:ghostel:eval:
+D3 — ghostel-eshell as eshell visual backend; ghostel-compile against F4 dev-fkeys.
+
+*** TODO [#B] Investigate ghostel selection/highlight color :terminal:ghostel:
+Look at how selected text is highlighted in a ghostel buffer — the region face in =ghostel-copy-mode= and any live selection — surfaced during the copy-mode debugging. Check whether the highlight is legible against the dupre background and consistent with the rest of the config; if it needs theming, fold it in with D2 (theming the ghostel faces in dupre).
+
+*** 2026-06-04 Thu @ 23:57:09 -0500 Phase 0 done: characterization baseline green
+=make test= green except the 5 documented pre-existing failures (4 test-dupre-theme, 1 test-init-module-headers), none terminal-related. Characterization coverage already present + green for all six must-survive behaviors: vterm-toggle--dispatch/display/buffer-filter, vterm-tmux-history, ai-vterm--show-or-create/launch-command/f9-in-vterm, ui-config--buffer-cursor-state + vterm-copy-mode-cursor, dashboard-config-launchers. Add a characterization test before any behavior change in later phases if a gap appears.
+
+*** 2026-06-05 Fri @ 00:38:34 -0500 Phase 1 done: ghostel + term-config.el
+=modules/term-config.el= written (full port of vterm-config: tmux history/copy-mode-dwim preserved via process-tty-name + ghostel-send-string; F12 toggle + display rule + geometry; cj/term-map C-; x menu → ghostel commands; which-key "terminal menu"; ghostel-max-scrollback 10MB; C-; added to ghostel-keymap-exceptions; F12 + C-; in ghostel-mode-map; use-package ghostel guarded per D6). Dropped: mouse-wheel SGR forwarding, vterm-timer-delay hacks, copy-mode cursor hook, goto-address hook. ghostel installed into elpa (MELPA + auto-downloaded native module). Tests: test-term-toggle--{dispatch,display,buffer-filter} + test-term-tmux-history (16) ported with a ghostel stub in testutil-ghostel-buffers; all green.
+
+*** 2026-06-05 Fri @ 00:38:34 -0500 Phase 2 done: ai-vterm→ai-term on ghostel
+=modules/ai-vterm.el= → =modules/ai-term.el=: 6 vterm call sites swapped to ghostel (buffer named via let-bound ghostel-buffer-name + pinned ghostel-buffer-name-function so OSC titles don't rename agent buffers); F9/C-F9/M-F9 on global + ghostel-mode-map; refuse-in-terminal guard removed (D4 — F9 launches in TTY frames); tmux-suppression invariant preserved (cj/--ai-term-suppress-tmux). 23 ai-vterm tests renamed → test-ai-term--* (terminal-guard test deleted, obsolete); show-or-create + f9-in-term rewritten for ghostel; all green. ui-config cursor-state ported (ghostel-mode + ghostel--input-mode; copy/emacs = read-only, else writeable) + its test. init.el now requires term-config + ai-term; vterm-config.el + ai-vterm.el deleted. Full suite green except the 5 documented pre-existing failures (4 dupre-theme, 1 init-module-headers/popper-config-missing — both unrelated). validate-modules ✓; full early-init+init smoke clean (no ghostel/term/ai-term errors). vterm package still installed (Phase 4) — dashboard "Launch VTerm" + dormant auto-dim still reference it until Phase 3/4. Restart Emacs to pick up ghostel (load-order + use-package :config change).
+
+*** 2026-06-05 Fri @ 00:50:58 -0500 Phase 3 done: satellites ported to ghostel
+Deleted auto-dim's vterm color-advice + redraw integration (~165 lines; D1 — terminals don't dim, ghostel bakes its palette per-terminal so there's no per-window color hook); dashboard launcher → =(ghostel)= + "Launch Terminal" label; cj-window-geometry/toggle-lib doc comments; module-inventory + init-load-graph doc refs. (ui-config cursor-state + init.el requires landed in Phase 2.) Trimmed test-auto-dim-config (dropped the 6 vterm tests) + updated the dashboard-launcher test stub. Incidental: removed the stale =popper-config= entry from the test-init-module-headers allowlist (the file doesn't exist + isn't required) — fixes the long-standing pre-existing test failure.
+
+*** 2026-06-05 Fri @ 00:50:58 -0500 Phase 4 done: vterm + vterm-toggle removed
+=package-delete='d vterm + vterm-toggle from elpa. No vterm refs remain in modules/init except intentional historical comments. Suite green except the 4 pre-existing dupre-theme failures (the popper-config one is now fixed). validate-modules ✓; full early-init+init batch smoke = INIT-SMOKE-OK. The migration parent stays DOING until Craig restarts Emacs and walks the ghostel manual-verify matrix under "Emacs Manual Testing and Validation".
+
+*** 2026-06-05 Fri @ 14:24:02 -0500 Auto-dim revisit cancelled — current no-dim behavior is fine
+Craig confirmed the shipped auto-dim setup works fine as-is: terminal buffers don't participate in unfocused-window dimming (D1), and the rest of auto-dim behaves. That is the measured decision the original task asked for — option (a), keep no-dim — so no rework (the focus-loss palette-blend in option (b) or an upstream per-window hook in option (c)) is needed. Closing without further investigation. Context: [[file:docs/design/vterm-to-ghostel-migration-spec.org][migration spec]] D1.
+
+*** 2026-05-26 Tue @ 15:15:43 -0500 Direction confirmed; Claude Code in eat needs a caveat
+Craig confirmed the consolidation: one terminal engine everywhere — eat for standalone terminal buffers (replacing vterm) plus =eat-eshell-mode= as eshell's visual backend, keeping eshell as the shell. Not dropping eshell for eat + zsh.
+
+Researched whether Claude Code runs cleanly in eat (Craig runs it in his Emacs terminal). Verdict: mostly, with caveats. eat is the default backend for claude-code.el and renders the TUI with color and full key handling, but there is an eat-specific bug where Claude Code's input handling makes the buffer scroll-pop to the top on window-buffer changes and the input box can get stuck mid-buffer (recoverable, but it does not happen in vterm or ghostel), and eat runs about 1.5x slower than vterm on heavy streaming output. claude-code.el's own docs name ghostel as the most faithful Claude TUI renderer.
+
+Recommendation: consolidate everyday terminals onto eat, but keep ghostel (or vterm) for the Claude Code workflow specifically — the scroll-pop / stuck-input bug and the slower heavy-stream handling are exactly what bites a long Claude session. Sources: [[https://github.com/cpoile/claudemacs][claudemacs]], [[https://github.com/stevemolitor/claude-code.el][claude-code.el]], [[https://codeberg.org/akib/emacs-eat][emacs-eat]].
+
+Eval plan (from the research doc): install EAT alongside vterm, run the same workloads through both, decide. Test matrix: Claude Code TUI, lazygit, htop/btop, yazi, a heavy-output build, ssh to a remote, and eshell with =eat-eshell-mode=. Assess rendering fidelity, stability under heavy output, and Emacs-native line editing. Switch only if it covers every workflow without regression.
+
+*** 2026-06-02 Tue @ 14:12:48 -0500 Audit: eval plan not yet run; back to TODO
+Task audit found no eval work recorded since the 2026-05-26 direction-confirmed note. The test matrix above is unrun, so the task isn't actively in progress — moved DOING back to TODO until the eval starts.
+
+*** 2026-06-04 Thu @ 22:40:27 -0500 Pivot: ghostel as the single engine (not eat)
+Direction changed from eat-everyday + ghostel-for-Claude to ghostel-for-everything, and the task is now a migration rather than an eval. Rationale: ghostel is claude-code.el's most-faithful Claude TUI renderer and the fastest engine (81 vs vterm 34 vs eat 4.9 MB/s), and an audit confirmed it exposes an analog for every vterm primitive this config uses (=ghostel-send-string=, =ghostel-keymap-exceptions=, =ghostel-copy-mode=, =ghostel-clear-scrollback=, =ghostel-send-next-key=, =ghostel-next-prompt= / =ghostel-previous-prompt=, =ghostel-max-scrollback=, =ghostel-kill-buffer-on-exit=). eat's washed colors, the scroll-pop / stuck-input bug under Claude Code, and slowest throughput made it the weaker single-engine pick; one engine beats running two. Surface audited: 2 main modules (=vterm-config.el=, =ai-vterm.el=) + 4 satellites (=auto-dim-config.el= is the heavy one) + ~35 test files + init.el. Next: spike ghostel read-only to answer the open migration questions (auto-dim rework — ARCHITECTURE.md forbids the around-redraw color advice vterm uses; tmux pane-id via =process-tty-name= on a ghostel process; buffer naming; TTY-frame behavior; copy-mode keybinding parity), then write the migration spec under =docs/design/= and review it.
+
+*** 2026-06-04 Thu @ 23:17:54 -0500 Spec review: not ready until decisions and handoff shape are closed
+Ran the spec-review workflow against [[file:docs/design/vterm-to-ghostel-migration-spec.org][docs/design/vterm-to-ghostel-migration-spec.org]] and wrote a companion review file (incorporated and deleted 2026-06-04). Verdict: =Not ready=. Direction is sound, but the draft still has open D1-D5 decisions, lacks the workflow-required =Implementation phases= section and acceptance criteria, and needs explicit ghostel package/native-module failure behavior before implementation tasks can be emitted.
+
+*** 2026-06-04 Thu @ 23:24:28 -0500 Spec-response: review incorporated, raised to READY
+Folded the external review via spec-response. Craig accepted D1-D5; baked them plus D6 (module-failure = degrade-with-warning, modifying the reviewer's fail-loud) and D7 (=ghostel-max-scrollback= 10 MB) into a new Agreed-decisions section. Added Implementation phases (0-4), Acceptance criteria, Dependency/module-failure behavior, Test strategy, per-phase key/menu ownership, the tmux-suppression contract, and an Implementation-tasks drop-in block. Status DRAFT → READY; review file deleted. Build is now unblocked.
+
+*** 2026-06-04 Thu @ 23:30:18 -0500 External re-review: ready
+Re-reviewed [[file:docs/design/vterm-to-ghostel-migration-spec.org][docs/design/vterm-to-ghostel-migration-spec.org]] after incorporation. Verdict: =Ready=. No further blocking review notes; implementation can start from the phase plan and acceptance criteria in the spec.
+
** DOING [#B] Module-by-module hardening :harden:nosync:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-05
@@ -2688,10 +2949,60 @@ configuration (=text-config=, =diff-config=, =ledger-config=,
=games-config=, =mu4e-org-contacts-setup=, =telega-config=,
=httpd-config=, =org-agenda-config-debug=).
-** TODO [#B] Add Signal to the dashboard :quick:
+** TODO [#C] theme-studio picker panel blends into the page :bug:theme-studio:ux:quick:solo:
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-01
+:LAST_REVIEWED: 2026-06-11
+:END:
+Craig, 2026-06-11 manual-test walk: the color picker's background is hard to distinguish from the page background. Give the picker panel a visibly distinct background or a highlighted border so it stands out. Pin with a gate asserting the picker element carries the distinct style.
+
+** TODO [#C] theme-studio Rust + Zig language previews :feature:theme-studio:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-11
+:END:
+Requested by Craig 2026-06-11: add Rust and Zig code samples to the language previews (samples.py currently carries Elisp, Go, Python, TypeScript, Java, C, C++, Shell). Each sample should exercise the treesit token categories distinctive to its language (Rust: lifetimes, macros, attributes, traits; Zig: comptime, builtins, error unions), then regenerate theme-studio.html and extend the test surface.
+
+** TODO [#C] theme-studio face-consistency check :feature:theme-studio:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-10
+:END:
+Rule taxonomy captured in [[file:docs/design/theme-studio-face-rules.org][docs/design/theme-studio-face-rules.org]] (Design Rules vs Fidelity Rules). The two checks below map to those two rule kinds. Both surface structural-attribute (weight/slant/underline/box/overline/height) issues; color is the theme's design and out of scope.
+
+1. Theme cross-cutting consistency (primary, per Craig 2026-06-09): the theme has deliberate cross-cutting rules — e.g. headings/titles are bold, links are underlined, errors/warnings/success are bold. Flag where the theme BREAKS ITS OWN rule (a heading that isn't bold, a link that isn't underlined). The designer declares the rules; the check finds the violators. This is the "tell me where I broke the rule" guardrail.
+
+2. defface-baseline divergence (secondary): flag where a face's structural attrs differ from its package =defface= so each divergence is deliberate, not an accidental drop. Would have caught the dropped underline/bold defaults and the contradictions (shr-h3 bold-vs-italic, erc-action italic-vs-bold) from the package-face audit as they were introduced.
+
+Bake into the tool (a lint surfaced in the UI) or run as a build-time check (seeds vs live deffaces via emacsclient).
+
+** TODO [#C] Color-family per-hex hint override :feature:theme-studio:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-11
:END:
+For the ~1 color per palette that sits on a ramp-collision point (e.g. yellow+2 on the distinguished palette, which by every hex signal belongs to the olive ramp though its name says gold), automatic grouping cannot recover the designer's intent. Add a per-hex family override: drag a swatch to a different column, store the override keyed by hex (never the name, so renaming is still free), consult it after the LCCL clustering, and drop/mark-stale it when the hex changes substantially. Export stays mostly flat; only overrides are extra metadata. Both reviews recommend this exact shape; details in =~/color-sorting-fable.org= (§ "The irreducible case") and =~/color-sorting-codex.org= (§ "What to store").
+
+** TODO [#C] Internet radio now-playing song :feature:music:emms:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-11
+:END:
+Show the currently-playing song while streaming an internet radio station. Lives in =modules/music-config.el= (EMMS + MPV backend, M3U radio stations). The track title comes from the stream's ICY metadata — EMMS exposes it via =emms-track-description= / =emms-playing-time= and updates it on the metadata-change hook; MPV reports the ICY title too. Add an option to show the song in the minibuffer (e.g. echo on track change, or an on-demand command). Consider also a mode-line indicator as a second surface.
+
+** TODO [#C] Evaluate jamescherti essential-emacs-packages list :packages:research:quick:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-11
+:END:
+Review [[https://www.jamescherti.com/essential-emacs-packages/][James Cherti's essential Emacs packages]] for anything worth installing. Cross-check each candidate against what is already in the config (=modules/= + =init.el=), skip the ones already present, and shortlist the genuinely new ones with a one-line rationale. Future-installation research, not a commitment to install.
+
+** TODO [#C] dupre-clear theme — contrast-first AAA sibling :feature:theme:dupre:
+Build a new theme (working name "dupre-clear", final name TBD) that takes dupre's color identity and rebuilds it Prot's way: contrast-first, targeting WCAG AAA (~7:1 on the ground), where the in-progress dupre revision is mood/depth-first and lands at AA. Same hues (dupre blue, emerald, gold, terracotta, regal violet, mint) brightened to clear the AAA floor; same modus-style role mapping (blue keywords bold, gold functions, violet types, emerald strings, terracotta constants, silver default, warm-grey comments, metallic greys, navy + regal fills). Build the dupre revision first; this reuses its hue choices as the starting point.
+
+Full design + methodology + starting palette + open questions in the spec: [[file:docs/design/dupre-clear-theme.org][docs/design/dupre-clear-theme.org]]. Key prerequisite/context: the dupre-redesign entry in =.ai/session-context.org= (the AA palette this brightens). Hardest slot: blue keywords (a deep dupre blue can't be AAA on near-black — decide brighten vs keep-AA-exception vs lift-the-ground).
+** TODO [#C] theme-studio terminal/ANSI colors :feature:theme-studio:
+theme-studio represents GUI faces only; terminal colors aren't surfaced at all. Scope decided 2026-06-09: GUI-first faces, NOT full per-face display-class fallback. Two pieces:
+
+1. ANSI-16 panel. Map the 16 ANSI slots (black/red/green/yellow/blue/magenta/cyan/white + bright variants) to palette colors, with a preview, and export them so =build-theme.el= emits the =ansi-color-*= / =term-color-*= faces. This matters even in pure-GUI Emacs: colored shell output, compilation buffers, eshell, and vterm/eat all draw from these. Signals must line up with their ANSI slot (error red→ansi red, success→green, warning→yellow, info/link→blue) so a signal reads the same in a terminal.
+
+2. Core-face 16-color fallback. Only the ~10 faces that decide console legibility get a =(((class color) (min-colors 16)) ...)= clause plus a =(t ...)= floor: default/fg, bg, keyword, string, comment, constant, error, warning, region, mode-line, line-number. Tune these for contrast — push it UP, legibility over fidelity, because the only 16-color target is the bare Linux virtual console (an occasional emergency context). The long tail stays GUI-first and auto-approximates.
+
+Why this scope: the GUI and the normal terminal (foot + tmux, truecolor / ≥256-color) both render the GUI hexes fine; GUI-first is correct there. Only the Linux VT is 16-color, and a low-contrast palette approximates badly down to 16 — so a few core faces get a deliberately higher-contrast 16-color fallback rather than every face carrying a multi-spec. Tool work: the ANSI-16 panel + a flag on the core faces to also capture a 16-color value; =build-theme.el= emits multi-spec only for those. Full per-face fallback is revisited only if console work becomes regular.
** TODO [#C] Consider consolidating/harmonizing the UI in all Message Clients
:PROPERTIES:
:LAST_REVIEWED: 2026-06-06
@@ -4063,6 +4374,75 @@ Restart the daemon, open a GUI frame, trigger an encrypted decrypt, confirm =pin
*** TODO [#C] Archive the original L3813 task :chore:
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 [#C] Google Contacts ↔ org-contacts sync investigation :feature:research:
+From the 2026-06-11 brainstorm. Goal: keep [[file:~/sync/org/contacts.org][contacts.org]] (real org-contacts: PROPERTIES drawers, mu4e completion, org-roam links) in sync with Google Contacts. Google side is solid — official People API (OAuth2, incremental syncToken) or CardDAV; no ToS risk. The hard parts are local: (1) identity — entries have no UID, so two-way needs a GOOGLE_ID property per entry plus a one-time fuzzy reconciliation of the two populated datasets (name/email/phone matching); (2) field mapping — space-separated multi-email in one property, free-text body notes, inconsistent phone formats (normalization decision); (3) conflict policy. First decision gates the rest: one-way Google→org read model (simple) vs true two-way. Candidate architectures: vdirsyncer (proven two-way engine w/ Google support; build only the vCard↔org translation, evaluate org-vcard fidelity) vs a direct People API script with sync state in org properties. Output: recommendation doc in docs/design/ naming direction + the normalization/conflict decisions for Craig. Not :solo: — the one-way-vs-two-way call and normalization policy are Craig's.
+
+** TODO [#C] Google Voice in Emacs — SMS + dialer investigation :feature:research:solo:
+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).
+
+** TODO [#C] latexmk workflow never activates (two breaks) :bug:solo:
+=modules/latex-config.el:66= — =:hook (TeX-mode-hook . ...)= gets use-package's =-hook= suffix appended (unbound symbol not ending in =-mode=), registering on nonexistent =TeX-mode-hook-hook=, so =TeX-command-default "latexmk"= is never set. Independently =:80= auctex-latexmk is =:defer t= with no trigger, so =auctex-latexmk-setup= never runs and "latexmk" isn't in TeX-command-list. Fix hook name to =TeX-mode=; change auctex-latexmk to =:after tex=. From the 2026-06 config audit.
+
+** TODO Manual testing and validation :verify:theme-studio:
+Exercised once the phases above land.
+*** 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
+- Read the L_max marker and the shaded unsafe band on the lightness slider
+- Drag lightness up toward and past the marker
+Expected: the marker is visible and correctly placed, the band above it reads as "unsafe," and crossing it is obvious; an out-of-scope face shows no marker.
+*** TODO Safe tint actually reads in real Emacs
+What we're verifying: a background tint the tool calls safe really keeps every token readable behind real syntax-colored text — the whole point of the worst-case floor.
+- In the tool, set a covered face (e.g. region) to a tint at or just below its L_max with the worst-case readout showing PASS
+- 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 [#D] theme-studio per-tier reseed controls :feature:theme-studio:
+Deferred from the seeding-engine spec (vNext). V1 reseeds all three guide-owned tiers at once; later consider separate "reseed syntax", "reseed UI", and "reseed package/org" controls if all-at-once proves too blunt. Spec: [[file:docs/design/theme-studio-seeding-engine-spec.org][spec]] (vNext; review folded in 2026-06-08).
+** TODO [#D] theme-studio low-contrast preset/mask mode :feature:theme-studio:
+Deferred from the perceptual color metrics spec (vNext). After raw OKLCH/APCA/DeltaE readouts exist, decide whether to add a named low-contrast workflow: APCA Lc bands, a contrast ceiling/floor mask, or a "soft" sibling to the existing any/AA+/AAA picker mask. Spec: [[file:docs/design/theme-studio-perceptual-color-metrics-spec.org][spec]] (vNext candidates; review folded in 2026-06-08).
+** TODO [#D] theme-studio CIEDE2000 DeltaE option :feature:theme-studio:
+Deferred from the perceptual color metrics spec (vNext). v1 uses DeltaE-OK on its native scale with a 0.02 threshold (decided); revisit CIEDE2000 only if the native OKLab scale proves too unfamiliar or poorly calibrated for palette distinguishability. Spec: [[file:docs/design/theme-studio-perceptual-color-metrics-spec.org][spec]] (vNext candidates; review folded in 2026-06-08).
** TODO [#D] Treesitter grammar offline cache :feature:offline:localrepo:
Treesitter grammars are downloaded by =treesit-auto= on first use and live outside the localrepo. For true offline reproducibility, cache the grammars next to the localrepo (a =.localrepo/treesitter/= tier, or a separate mirror script). Cross-linked from =docs/design/localrepo.org=.
@@ -4111,11 +4491,6 @@ Three small reveal.js improvements; collected into one task because each on its
2. *Default font sizing for slide elements.* Configure reveal.js font sizes for headings, body text, code blocks, etc. — better defaults via =org-reveal-head-preamble= CSS or a custom theme.
3. *Custom dupre reveal.js theme.* CSS theme using the colors from =themes/dupre-palette.el=. Install into =reveal.js/css/theme/= for use with =#+REVEAL_THEME: dupre=.
-** DOING Project-aware bug capture via C-c c t :feature:capture:
-Relocated from the global capture inbox 2026-06-06. When inside a projectile project, C-c c t (Task) files into that project's root todo.org under the "<Project> Open Work" header. If the project has no todo.org, fall back to the global inbox-file and warn naming the project.
-
-Implemented 2026-06-06 in =modules/org-capture-config.el=: a shared project-aware =function= capture target (=cj/--org-capture-project-location=) used by =C-c c t= (Task, =* TODO=) and a new =C-c c b= (Bug, =* TODO [#C]=). Matches an existing top-level "... Open Work" heading (so ~/.emacs.d hits "Emacs Open Work") and creates "<Capitalized project> Open Work" only when absent. Outside a project / no todo.org -> global inbox under "Inbox" (with a warning in the no-todo.org case). 15 ERT tests in =tests/test-org-capture-config-project-target.el=; daemon e2e confirmed a real capture lands "** TODO [#C] ..." prepended under Open Work. Awaiting Craig's interactive manual verify (see the Manual Testing task) before close. NOTE: the matching "<Project> Resolved Work" header for the wrap-up workflow is a separate concern, not handled here.
-
** TODO "? = curated help menu" convention across modes :feature:ux:discoverability:
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).
@@ -4124,389 +4499,14 @@ Task: survey the modes/modules Craig works in and identify where a =?= -> curate
** TODO the preview splits an already split window into 3 temporarily.
looks strange. potentially problematic for ai-terms.
-** TODO [#C] Google Contacts ↔ org-contacts sync investigation :feature:research:
-From the 2026-06-11 brainstorm. Goal: keep [[file:~/sync/org/contacts.org][contacts.org]] (real org-contacts: PROPERTIES drawers, mu4e completion, org-roam links) in sync with Google Contacts. Google side is solid — official People API (OAuth2, incremental syncToken) or CardDAV; no ToS risk. The hard parts are local: (1) identity — entries have no UID, so two-way needs a GOOGLE_ID property per entry plus a one-time fuzzy reconciliation of the two populated datasets (name/email/phone matching); (2) field mapping — space-separated multi-email in one property, free-text body notes, inconsistent phone formats (normalization decision); (3) conflict policy. First decision gates the rest: one-way Google→org read model (simple) vs true two-way. Candidate architectures: vdirsyncer (proven two-way engine w/ Google support; build only the vCard↔org translation, evaluate org-vcard fidelity) vs a direct People API script with sync state in org properties. Output: recommendation doc in docs/design/ naming direction + the normalization/conflict decisions for Craig. Not :solo: — the one-way-vs-two-way call and normalization policy are Craig's.
-
-** TODO [#C] Google Voice in Emacs — SMS + dialer investigation :feature:research:solo:
-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).
-
-** TODO [#B] Messenger window/key unification :feature:ux:
-Spec: [[file:docs/design/messenger-unification-spec.org][messenger-unification-spec.org]] (Draft, 2026-06-11). One library (=cj-messenger-lib.el=) gives every messenger the same shape: chat windows rise from the bottom (the signel rule, generalized), C-c C-c confirms, C-c C-k cancels, C-c C-a attaches — dispatched per backend through a registry + minor mode. Signel already conforms (reference backend); telega and slack join in phases 2-3; ERC later. All eight decisions settled 2026-06-11 (cancel closes an idle window; telega's filter-cancel shadow accepted; slack rooms join the bottom rule). Spec held open — Craig has more ideas to fold in before it's marked Ready.
-
-** TODO [#A] Lock screen silently fails — slock is X11-only :bug:
-=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.
-
-** TODO [#B] cj/undo-kill-buffer off-by-one on plain invocation :bug:quick:solo:
-=modules/ui-navigation.el:181= — =(interactive "p")= makes arg always ≥1, and the body does =(if arg (nth arg list) (car list))=, so the nth branch always runs and plain M-S-z reopens the SECOND-most-recently-killed file. The existing test passes 0 explicitly, masking it. Fix the indexing (=(interactive "P")= + =prefix-numeric-value=, or =nth (1- arg)=) and fix the test to cover the no-prefix path. From the 2026-06 config audit.
-
-** TODO [#B] reconcile-open-repos skips any repo with a dot in its name :bug:solo:
-=modules/reconcile-open-repos.el:174= — discovery regexp ="^[^.]+$"= matches only dot-free names, so =~/code/mcp.el=, =capture.el=, =google-contacts.el=, =auto-dim-other-buffers.el= etc. are never reconciled while M-P still reports "Complete." Replace with =directory-files-no-dot-files-regexp= + a hidden-dir check; add a regression test with a dotted repo name. From the 2026-06 config audit.
-
-** TODO [#B] jumper: register collisions and dead-marker errors :bug:solo:
-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] C-s C-s vertico-repeat path never works :bug:quick:solo:
-=modules/selection-framework.el:263= — =cj/consult-line-or-repeat= calls =vertico-repeat= on the second consecutive C-s, but nothing adds =vertico-repeat-save= to =minibuffer-setup-hook= (grep: zero hits config-wide), so it always signals "No Vertico session". Add the hook next to the vertico use-package block. From the 2026-06 config audit.
-
-** TODO [#B] dashboard-config setq wipes recentf-exclude list :bug:quick:solo:
-=modules/dashboard-config.el:199= =(setq recentf-exclude '("/emms/history"))= discards the five exclusions system-defaults.el:239-243 added earlier in init order (bookmarks, elpa, recentf, ElfeedDB, airootfs). Change to =add-to-list=. From the 2026-06 config audit.
-
-** TODO [#B] auth-config: unguarded gpg-connect-agent call + compile-time require :bug:quick:solo:
-From the 2026-06 config audit. =modules/auth-config.el:88= — bare =(call-process "gpg-connect-agent" ...)= in a =:demand t= :config signals file-missing and aborts init on machines without the binary; guard with =cj/executable-find-or-warn=. =auth-config.el:36= — =user-constants= is required only =eval-when-compile= but =authinfo-file= is read at load time; works from .el source, fails from standalone .elc. Use a runtime require (system-defaults.el:32-35 documents this exact trap).
-
-** TODO [#B] system-defaults: top-level server-start unguarded in batch :bug:quick:solo:
-=modules/system-defaults.el:140= — raw module load under =--batch= (make validate-modules on a machine with no daemon socket) starts a server from a batch process; the suite only passes because the testutil stubs it. Wrap in =(unless noninteractive ...)= — the repo's established guard for this defect class; same guard stops the =custom-file= =make-temp-file= at line 104 littering temp files per batch load. From the 2026-06 config audit.
-
-** TODO [#B] markdown live preview clobbered by markdown-mode :bug:quick:solo:
-=modules/markdown-config.el:54= defines bare =markdown-preview=, which markdown-mode redefines the moment the first .md loads — the impatient-mode live preview is dead and F2 silently runs the package command (agent verified in the live daemon). Also =:61= guards on =(boundp 'httpd-process)=, a variable that doesn't exist in simple-httpd — use =(httpd-running-p)=. And the =:config= =(setq imp-set-user-filter 'markdown-html)= at line 41 is doubly dead (function-not-variable, symbol names nothing) — delete. Rename to =cj/markdown-preview=, rebind F2. From the 2026-06 config audit.
-
-** TODO [#B] org-roam dailies template writes FILETAGS and TITLE on one line :bug:quick:solo:
-=modules/org-roam-config.el:42= — the "d" dailies head is ="#+FILETAGS: Journal #+TITLE: %<%Y-%m-%d>"= with no newline, so every C-c n d daily is malformed: no parsed #+TITLE, FILETAGS value "Journal #+TITLE: ...". The journal-copy template (lines 213-216) has it right. Add the newline; consider a sweep of existing dailies for the malformed first line. From the 2026-06 config audit.
-
-** TODO [#B] agenda sources: roam Projects missing, no existence filtering :bug:solo:
-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] org-roam :config triggers the 15-20s refile scan synchronously at first idle :bug:solo:
-=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.
-
-** TODO [#B] heavy-box comment inserts non-comment lines :bug:solo:
-=modules/custom-comments.el:427= — =cj/--comment-heavy-box= interior/empty lines carry no comment prefix, so in line-comment languages (elisp, Python) C-; C h injects syntax-breaking bare =*...= lines. The existing test characterizes the broken output (asserts =^\*.*\*$=). Prefix interiors like =cj/--comment-box= does; add the missing min-length validation (negative width hits make-string with a raw error); fix the test to assert corrected output. From the 2026-06 config audit.
-
-** TODO [#C] latexmk workflow never activates (two breaks) :bug:solo:
-=modules/latex-config.el:66= — =:hook (TeX-mode-hook . ...)= gets use-package's =-hook= suffix appended (unbound symbol not ending in =-mode=), registering on nonexistent =TeX-mode-hook-hook=, so =TeX-command-default "latexmk"= is never set. Independently =:80= auctex-latexmk is =:defer t= with no trigger, so =auctex-latexmk-setup= never runs and "latexmk" isn't in TeX-command-list. Fix hook name to =TeX-mode=; change auctex-latexmk to =:after tex=. From the 2026-06 config audit.
-
-** TODO [#A] mu4e: cmail can't trash, no account can refile :bug:
-=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.
-
-** TODO [#A] calendar-sync drops final occurrences and resurrects cancelled meetings :bug:solo:
-RFC 5545 conformance holes in =modules/calendar-sync.el=, all agenda-visible (from the 2026-06 config audit):
-- =:973,1015,1024= — UNTIL treated as exclusive (strict =calendar-sync--before-date-p=); RFC and Google make it inclusive, so the LAST instance of every UNTIL-bounded series vanishes. Tests assert loose count ranges, so it's unpinned. Allow equality.
-- =:578= — comma-separated EXDATE lists (Google emits them) never parse; the exclusion drops silently and cancelled occurrences reappear on the agenda. Split on "," before parsing; no comma-case test exists.
-- =:902= — timed events without DTEND render as all-day (time lost); multi-day all-day spans collapse to one day (end date unused, exclusive-DTEND unhandled). Emit start-time-only stamps and org date ranges.
-
-** TODO [#B] calendar-sync robustness: atomic writes, curl --fail, zero-event false errors :bug:solo:
-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.
-
-** TODO [#B] drill-refile clobbers global org-refile-targets with an invalid spec :bug:quick:solo:
-=modules/org-drill-config.el:95-98= — =setq org-refile-targets= replaces the session-wide value, so after one drill refile every org-refile everywhere offers only drill targets until restart; and the =(drill-dir :maxlevel . 1)= spec names a directory-path variable where org expects files, so the drill side yields nothing usable. Let-bind around the call with =((directory-files drill-dir t "\\.org$") :maxlevel . 1)=. From the 2026-06 config audit.
-
-** TODO [#B] ERC: double mention notifications + tautological server list :bug:quick:solo:
-From the 2026-06 config audit, =modules/erc-config.el=:
-- =:281= — =erc-modules= includes the built-in =notifications= module AND :config adds =cj/erc-notify-on-mention= to the same hook — every mention fires two desktop notifications. Pick one path (keep the custom one, slated for messenger unification).
-- =:100= — =cj/erc-connected-servers=: inside =with-current-buffer=, the free =erc-server-process= is the buffer's own local value, so the eq test is tautologically true — returns ALL ERC buffers (channels, dead connections). Use =erc-server-buffer-p= + =erc-server-process-alive=.
-- =:238= — =user-whole-name= read at load but =user-constants= only required at compile time (same trap as auth-config/keyboard-macros).
-
-** TODO [#B] slack-config lifecycle gaps :bug:quick:solo:
-From the 2026-06 config audit, =modules/slack-config.el=:
-- =:265= — w / @ / # bound to commands neither autoloaded nor in :commands — void-function before slack loads. Add to :commands.
-- =:246= — =cj/slack-close-all-buffers= reads =slack-current-buffer= (declared but unbound) without the boundp guard its sibling has — void-variable on C-; S Q before slack loads.
-- =:259= — raw =global-set-key= for C-; S bypasses =cj/register-prefix-map= (signal/erc use it); invisible to the keybindings registry and the planned unification enumeration.
-
-** TODO [#B] erc-yank silently publishes >5-line pastes as public gists :bug:
-=modules/erc-config.el:345= — C-y in any ERC buffer auto-creates a public gist for anything over 5 lines: clipboard content goes to a public URL with no confirmation, and no executable-find guard for =gist= (errors mid-send if absent). Privacy trap. Add a =yes-or-no-p= gate or drop the package for plain C-y. From the 2026-06 config audit.
-
-** TODO [#B] F7 diff-aware coverage classifies every changed file "not tracked" :bug:solo:
-=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 [#B] eshell: visual-commands nested-list + xterm-color dead hook :bug:quick:solo:
-=modules/eshell-config.el:104= — =add-to-list= pushes one LIST into the flat string list =eshell-visual-commands=, so lf/ranger/htop/top never get a visual terminal (and the r→ranger alias garbles). dolist the strings. =:166= — =:hook (eshell-before-prompt-hook . ...)= gets "-hook" appended → registers on nonexistent =eshell-before-prompt-hook-hook=; and =xterm-color-filter= is never added to =eshell-preoutput-filter-functions= anyway while TERM advertises xterm-256color. Wire xterm-color fully per its README or drop it + the TERM override. From the 2026-06 config audit.
-
-** TODO [#B] dirvish M (mark all files) marks every other file :bug:quick:solo:
-=modules/dirvish-config.el:218= — =dired-mark= advances point to the next line itself; the loop's extra =forward-line 1= then skips it, so consecutive files are marked alternately. Live mis-marking on a key that feeds batch operations (delete/copy on marked files) — data-loss adjacent. Drop the manual forward-line when a mark was made (or =dired-unmark-all-marks= + mark dirs + =dired-toggle-marks=). The trivial line-predicate helper is tested; the loop isn't — add the marked-count test. From the 2026-06 config audit.
-
-** TODO [#B] dwim-shell: zip overwrites its own name, backup timestamp never expands, dired menu key dead :bug:quick:solo:
-From the 2026-06 config audit, =modules/dwim-shell-config.el=:
-- =:338= — single-file zip is =zip -r '<<fne>>.<<e>>' '<<f>>'= — reconstructs the input filename as the archive ("Zip file structure invalid"; directories produce =foo.=). Should be ='<<fne>>.zip'= like the tar-gzip sibling.
-- =:549= — backup destination single-quotes =$(date ...)= so the substitution is literal: =foo.txt.$(date +%Y%m%d_%H%M%S).bak=. Move it outside the quotes or format-time-string in Elisp.
-- =:932= — dired-mode binding "M-S-d" is unreachable (Meta+Shift+d generates M-D); the dirvish binding two lines down is correctly "M-D". Fix + the stale commentary at dirvish-config.el:30.
-
-** TODO [#B] Go: format key void-functions, go-mode :config never runs :bug:quick:solo:
-=modules/prog-go.el:99,113-118= — .go maps to go-ts-mode so the go-mode package never loads, and =gofmt= isn't autoloaded in go-mode 1.6.0 — C-; f signals void-function, and the :config (exec-path += ~/go/bin, =gofmt-command "goimports"=) never executes. Wrapper that requires go-mode first (or autoload gofmt), move the setup to top level. From the 2026-06 config audit.
-
-** TODO [#B] prog hooks mutate global state per buffer :bug:quick:solo:
-From the 2026-06 config audit: =prog-go.el:64=, =prog-c.el:73=, =prog-shell.el:77= call global =(electric-pair-mode t)= from buffer setup hooks — one Go/C/shell buffer turns on pairing in org/text everywhere (python/webdev correctly use =electric-pair-local-mode=). =prog-general.el:79-80= — =display-line-numbers-type 'relative= setq/setq-default run from the hook AFTER the mode is enabled, so the first prog buffer of a session gets absolute numbers. Local-mode for the three; move the line-number setqs to top level.
-
-** TODO [#B] M-S- launcher keys dead: eww, elfeed, calibredb unreachable :bug:quick:solo:
-=eww-config.el:70= (M-S-e), =elfeed-config.el:36= (M-S-r), =calibredb-epub-config.el:115= (M-S-b) — Meta+Shift+letter generates the uppercase event (M-E/M-R/M-B), which never matches an explicit S- spec on a lowercase letter; verified dead in the live daemon (chord falls through to M-r move-to-window-line etc.). Same class as the text-config M-S-i finding. Write them as "M-E"/"M-R"/"M-B". Weather's M-S-w works only via the keyboard-compat translation layer — audit that layer's coverage while here. From the 2026-06 config audit.
-
-** TODO [#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 [#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).
-
-** TODO [#B] cj/gptel-switch-backend reintroduces the string-model crash :bug:quick:solo:
-=modules/ai-config.el:272= — =(setq gptel-model model)= with the raw completing-read STRING — the documented wrong-type-argument-symbolp modeline hang (CLAUDE.md gotcha), reachable from C-; a B today. =cj/gptel-change-model= (C-; a m) already does backend+model switching and interns correctly. Intern here, or delete switch-backend and keep one command. From the 2026-06 config audit.
-
-** TODO [#B] transcription: stderr never reaches the log, video transcripts stranded in /tmp :bug:solo:
-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.
-
-** TODO [#B] ledger-config is orphaned — ledger-mode never configured :bug:quick:
-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 [#B] eww quick-add bookmarks split the store and break the default file :bug:quick:solo:
-=modules/eww-config.el:116-126= — quick-add let-binds =eww-bookmarks-directory= to ~/.emacs.d/eww-bookmarks/ (creating a DIRECTORY at the path where the daemon's default store expects a FILE ~/.emacs.d/eww-bookmarks). After one quick-add, B reads an unreadable path and quick-added bookmarks are invisible post-restart. Drop the let-binding or setq the directory once in :config so both commands share one store. From the 2026-06 config audit.
-
-** TODO [#B] help-config: three defects in one small file :bug:quick:solo:
-From the 2026-06 config audit, =modules/help-config.el=:
-- =:67= — =cl-return-from= inside a plain =defun= (no cl-block): declining the save prompt signals "No catch for tag" instead of canceling. =cl-defun= or restructure.
-- =:108= — =:hook (info-mode . info-persist-history-mode)= is dead twice: Info's hook is =Info-mode-hook= (capital I), and =info-persist-history-mode= doesn't exist anywhere. Implement the intent or delete.
-- =:111= — auto-mode-alist maps .info to an interactive command that KILLS the buffer mid find-file — programmatic =find-file-noselect= of any .info destroys buffers and pops Info windows. Drop the entry; keep the explicit command. Zero test coverage on this module (the two broken paths are exactly the untested ones).
-
-** TODO [#A] Native compilation disabled config-wide; GC at stock 800KB :bug:
-From the 2026-06 config audit (verified against the live daemon). =early-init.el:69= =(setq native-comp-deferred-compilation nil)= — the obsolete alias of =native-comp-jit-compilation= — turns JIT native compilation OFF entirely, not "synchronous" as the comment claims: 19 .eln files exist for 184 packages, ~100 of 121 modules run interpreted for the daemon's lifetime, and system-defaults.el:42-44's speed-3/8-jobs/always-compile settings are dead. Plus =early-init.el:113-116= restores =gc-cons-threshold= to the captured STOCK default (800000, verified) post-startup — frequent small GC pauses forever. Together these plausibly feed the filed org-capture 15-20s task more than anything in the capture path itself. Actions: retest the old "Selecting deleted buffer" race on 30.2 and re-enable JIT (or AOT sweep); set a deliberate 16-64MB threshold (or gcmh). Check both before burning time on the capture-perf debug task.
-
-** TODO [#B] modeline runs synchronous git on the redisplay path, unguarded :bug:solo:
-=modules/modeline-config.el:173,154,145= — the mode-line :eval calls vc-backend/vc-state/vc-working-revision (synchronous git) on TTL expiry; a slow or unmounted filesystem stalls ALL redisplay. The cache key computes =file-truename= on every render (the "one stat per refresh" comment is wrong), and nothing is condition-case-wrapped, so a signal lands inside the mode-line eval. Defer the truename behind the TTL check; wrap the fetch in condition-case caching nil. From the 2026-06 config audit.
-
-** TODO [#A] Global yes-or-no-p fset defeats every strong confirmation :bug:quick:
-=modules/system-defaults.el:203= =(fset 'yes-or-no-p 'y-or-n-p)= — verified live. Several modules deliberately chose yes-or-no-p as the strong tier for irreversible actions: shutdown/reboot (=system-commands.el:74=, whose comment explicitly says "so a stray RET/space can't trigger them"), "permanently destroy files" (=dwim-shell-config.el:804=), file overwrites (=custom-buffer-file.el:159,199=, =music-config.el:374=). The fset makes all of them single-keystroke — the two-tier design is dead. Drop the fset, or provide a real =cj/confirm-strong= (typed "yes") for the irreversible set. From the 2026-06 config audit.
-
-** TODO [#B] Stale elpa gptel shadows the local fork — likely the gptel-magit root :bug:quick:solo:
-=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.
-
-** TODO [#B] vertico-prescient clobbers orderless filtering :bug:quick:solo:
-=modules/selection-framework.el:250= — =vertico-prescient-mode= defaults =vertico-prescient-enable-filtering t=, overriding =completion-styles= to prescient inside vertico sessions; the orderless config at :151 is dead exactly where it matters. Set =vertico-prescient-enable-filtering nil= — orderless matches, prescient sorts (and this resolves the dead =vertico-sort-function= finding in the buffer/window-libs child the other way around). From the 2026-06 config audit.
-
-** TODO [#B] 2026-06 full config audit — findings backlog :refactor:
-Module-by-module review of all 121 modules + init/early-init, holistic passes (startup/perf, stability, UX consistency, package strategy), and spin-offs into pearl, chime, emacs-wttrin. Method: parallel read-only review agents per module group; key claims spot-verified (incl. against the live daemon) before filing. Run 2026-06-11/12, COMPLETE. Tally: ~165 module findings + ~40 holistic + 30 spin-off ≈ 235 total; 40 high-impact bugs filed as standalone tasks above this parent; the rest live in the group children below. Spin-off findings delivered as inbox handoffs to pearl, chime, and emacs-wttrin (2026-06-12-0057). Start with the synthesis child below for the recommended attack order.
-
-*** Synthesis: the overall picture and attack order
-Six cross-cutting themes, then the order I'd work them.
-
-Themes:
-1. Performance has one systemic lever, not many small ones: native-comp is accidentally OFF config-wide and GC sits at the stock 800KB ([#A] task). Daemon init itself is healthy (1.11s measured). Fix the lever before any micro-deferral work, and before burning time on the org-capture-perf debug.
-2. A "dangerous defaults" safety cluster: yes-or-no-p fset (single-keystroke shutdown/file-destruction), the silently-failing Wayland lock screen, erc-yank's public gists, mu4e's broken trash/refile on the primary account. All four are [#A]/[#B] standalones; do these first — they're where the config can actually hurt you.
-3. Calendar/agenda data correctness: calendar-sync's RFC trio (vanishing final occurrences, resurrected cancelled meetings, collapsed multi-day events) + agenda sources missing roam Projects. Meetings are missed over this.
-4. Recurring mechanical defect classes worth sweeping as one commit each, config-wide: use-package :hook "-hook" suffix trap (org-babel, eshell, latex); eval-when-compile-only requires read at runtime (auth-config, keyboard-macros, erc-config); M-S-<letter> bindings vs uppercase events (4 dead keys + 1 asymmetry); raw C-; entries bypassing cj/register-prefix-map (8 modules); unreachable modules (prog-lsp, ledger-config, show-kill-ring, mu4e-org-contacts-setup); config for package versions long gone (mu4e 1.7 block, dashboard override, org timeline, checkdoc-arguments).
-5. The test suite has a blind-spot class: characterization tests asserting BROKEN output (reverse-lines, heavy-box, undo-kill's explicit 0), unit tests hand-building data that hides integration mismatches (F7 coverage paths), and an integration gate that prints green over "Ran 0 tests" (chime). When fixing any standalone bug above, fix its test to assert correct behavior — and consider extending the architecture smoke test to mechanically pin the class-4 sweeps (hooks must be bound after load, no raw C-; binds, no M-S-<letter> specs, no eval-when-compile requires of runtime vars).
-6. Consistency wants conventions, not patches: one notification facade (cj/notify — messenger spec addendum already covers the messenger half), one confirmation tier (the fset fix), one prefix-registration mechanism with labels, one buffer-naming shape. The messenger-unification registry mindset generalizes.
-
-Attack order: (a) the three [#A]s + gptel-shadow (it's blocking the filed gptel-magit investigation); (b) the daily-data pair — mail trash/refile + calendar RFC trio; (c) the :quick:solo: standalone sweep — roughly 20 one-to-five-line fixes, a satisfying solo batch; (d) the class-4 mechanical sweeps, one commit per class, each with its smoke-test guard; (e) the consistency conventions, opportunistically as those modules get touched.
-
-*** TODO Findings: foundation/system group
-From agents 2026-06-11; spot-verified sample. Remaining findings beyond the standalone bug tasks:
-- [BUG] =keyboard-compat.el:121= — terminal arrow-key fix runs once on emacs-startup-hook; =input-decode-map= is terminal-local, so =emacsclient -t= frames under the daemon never get it. Register on =tty-setup-hook= (GUI half already uses =server-after-make-frame-hook=).
-- [BUG] =config-utilities.el:142= — =cj/recompile-emacs-home=: =(boundp 'native-compile-async)= is always nil (it's a function — needs =fboundp=), so native compilation is never selected; and the helper deletes =<dir>/eln= when the real cache is =eln-cache/= (derive from =native-comp-eln-load-path=). Extend the existing test.
-- [BUG] =system-utils.el:94= — success message args swapped: prints "Running notes.txt on mpv...". Trivial; wired into dirvish (O) and calibredb so it shows regularly.
-- [REMOVE] =local-repository.el:51= — =localrepo-initialize=, its three defcustoms, and unprefixed =car-member= are dead; early-init owns archive setup with its own divergent path constant. Shrink to =cj/update-localrepo-repository= pointed at early-init's =localrepo-location=.
-- [REMOVE] =keybindings.el:146-147= — C-x C-f unset/reset is a no-op (already find-file); comment wrong. Delete or retarget.
-- [COVERAGE] =local-repository.el= — only module in the group with no test file.
-
-*** TODO Findings: UI core group
-From agents 2026-06-11; spot-verified sample. Remaining findings beyond the standalone bug tasks:
-- [BUG] =font-config.el:262= — emojify =:defer 1= means :config runs before any daemon GUI frame exists; =env-gui-p= picks ='unicode= permanently, GUI frames never get image emojis. Compute per-frame (=server-after-make-frame-hook=) or test =(daemonp)=.
-- [BUG] =font-config.el:283= — =cj/display-available-fonts= errors on second invocation: first call's =special-mode= sets read-only; next call's erase/insert signals. Wrap in =inhibit-read-only=. (Also [COVERAGE]: untested — a call-twice test catches it.)
-- [UX] =undead-buffers.el:82= — =cj/kill-other-window= in a single-window frame kills the buffer you're looking at (other-window no-ops; only delete-window is guarded). Add the sibling's =(user-error "No other window")= guard.
-- [UX] =undead-buffers.el:48= — C-u C-x k silently marks a buffer undead (then it refuses to die with no explanation later). Undocumented mode-switch inside a core-command remap; document or split into its own command.
-- [ENHANCE] =ui-theme.el:87= — theme persistence silently fails on a fresh machine until =persist/= exists; =make-directory= before the writability check.
-- [REMOVE] =dashboard-config.el:32-58= — =dashboard-insert-bookmarks= override is dead code: the :demand t require lets upstream dashboard-widgets.el redefine it; behavior survives only because upstream natively honors the settings now. Delete.
-- [REMOVE] =font-config.el:199-220= — all-the-icons stack (2 =:demand t= packages + unprompted network font install on fresh machines) likely redundant with nerd-icons everywhere; verify keyboard-compat's reference then drop.
-- [REMOVE] =ui-config.el:185= — duplicate =(use-package nerd-icons :defer t)= stanza; nerd-icons-config owns it. Delete stanza + stale Commentary bullet.
-
-*** TODO Findings: buffer/window libs group
-From agents 2026-06-11; spot-verified sample. Remaining findings beyond the standalone bug tasks:
-- [REMOVE] =show-kill-ring.el= — loaded by nothing (init require deliberately removed in b785a19d), so its M-S-k binding is dead; =keyboard-compat.el:177= still installs the M-K → M-S-k translation whose only purpose was this module. Re-add or delete module + stale translation/comment (consult-yank-pop largely supersedes it).
-- [UX] =selection-framework.el:38= — =vertico-sort-function= custom is dead config: =vertico-prescient-mode= (line 250) replaces sorting when it activates. Pick one policy (drop the custom, or =vertico-prescient-enable-sorting nil=).
-- [BUG] =custom-buffer-file.el:486= — =cj/view-email-in-buffer= leaks MIME handles when no displayable part: =user-error= fires before =mm-destroy-parts=. unwind-protect.
-- [ENHANCE] =custom-buffer-file.el:49= — eager =(require 'mm-decode)= at startup only for macro expansion; runtime require already exists at line 481. Make it =eval-when-compile=.
-- [UX] =custom-buffer-file.el:221= — =cj/copy-link-to-buffer-file= is a silent no-op in non-file buffers while siblings signal =user-error=. Match them.
-
-*** TODO Findings: editing helpers group
-From agents 2026-06-11; spot-verified sample (jump-paren, sortable-time confirmed). Beyond the standalone heavy-box task:
-- [BUG] =custom-misc.el:48= — jump-to-matching-paren with point ON a closer lands at the last inner sexp, not the opener (batch-verified). =(forward-char)= before =(backward-sexp)= in the char-after-closer case; the test only covers the after-closer position.
-- [BUG] =custom-datetime.el:71= — "sortable" time format is 12-hour ="%I:%M:%S %p %Z"= — "01:00:00 PM" sorts before "09:00:00 AM". Should be ="%H:%M:%S"=.
-- [BUG] =custom-comments.el:82= — =cj/comment-reformat= prints "No region was selected" even on success (message outside the if-else), and the fill-column shrink/restore isn't unwind-protected — an error leaves fill-column permanently -3. Use let-binding + =user-error=; also =mark-active= vs the config's usual =use-region-p=.
-- [BUG] =custom-line-paragraph.el:52= — join-line-or-region without region inserts a spurious blank line mid-buffer (verified); only insert the newline at eobp.
-- [BUG] =custom-line-paragraph.el:77= — duplicate-line-or-region splits a mid-line-ending region via open-line and duplicates an extra empty line when the region ends at bol. Normalize bounds to whole lines.
-- [BUG] =custom-ordering.el:158= — reverse-lines and number-lines mishandle the trailing newline ("a\nb\n" → "\nb\na"); the trailing-newline test asserts the broken output. =cj/--arrayify= (line 43) has the correct pattern — apply it; fix the characterization test.
-- [BUG] =custom-comments.el:152= — inline-border lines come out 2 chars short for even-length or empty text (parity computed from text length instead of remaining width); stacked dividers misalign.
-- [UX] =custom-text-enclose.el:216= — indent-lines =(interactive "p\nP")= couples COUNT and USE-TABS to one prefix arg — multi-column space indent is impossible interactively; docstrings claim "default 4" but "p" defaults to 1 (same in dedent :256).
-- [REMOVE] =custom-ordering.el:90= — =cj/arrayify-python= is byte-identical to =cj/arrayify-json= (two bindings, same output). Delete one or differentiate (single quotes for Python).
-- [UX] =custom-case.el:66= — title-case contradicts its docstring: "is" is in word-skip despite "linking verbs are major words"; no sentence-restart capitalization after periods; no capitalize-last-word rule. Align list + docstring.
-
-*** TODO Findings: text/prose tools group
-From agents 2026-06-11. Beyond the standalone markdown/latex tasks:
-- [BUG] =text-config.el:72= — "M-S-i" for edit-indirect-region is unreachable: Meta+Shift+i generates the event M-I, not M-S-i, so the keypress falls back to M-i tab-to-tab-stop. Rebind as "M-I" (the "was M-I" comment thought the rename was a no-op; it wasn't).
-- [BUG] =keyboard-macros.el:46= — user-constants required only =eval-when-compile= but =macros-file= is read at runtime; works only because init.el loads user-constants first. Plain require (same trap as auth-config).
-- [BUG] =keyboard-macros.el:137= — kill-emacs-hook fires =y-or-n-p= + an interactive name prompt whenever any last-kbd-macro exists — hazardous for daemon/systemd shutdown (no one to answer) and noisy for throwaway macros. Guard =(and last-kbd-macro (not noninteractive))= minimum; consider dropping the prompt (M-F3 already persists named macros).
-- [BUG] =lorem-optimum.el:221= — empty Markov chain (missing assets/liber-primus.txt) makes =cj/lipsum-insert= do =(insert nil)= — cryptic wrong-type error far from cause. Signal =user-error= naming the fix; also Commentary advertises "M-x cj/lipsum" but it has no interactive spec.
-- [UX] =flyspell-and-abbrev.el:230= — every C-' press re-runs =flyspell-buffer= over the whole buffer while flyspell-mode is off (the documented word-by-word workflow = O(buffer) per keypress in large files). Call =cj/flyspell-on-for-buffer-type= so the mode sticks and the scan runs once.
-- [ENHANCE] =text-config.el:121= — accent is wired to the company backend (=accent-company=); the filed Company→Corfu migration task doesn't list it, so C-` breaks silently post-migration. Add to the migration scope or switch to =accent-menu= now.
-
-*** TODO Findings: org core group
-From agents 2026-06-11; spot-verified sample (dailies head, babel hook, void bindings confirmed). Beyond the standalone tasks:
-- [BUG] =org-babel-config.el:27= — =:hook (org-babel-after-execute-hook . org-redisplay-inline-images)= gets a second "-hook" appended (symbol unbound at expansion, doesn't end in -mode) → registers on nonexistent =org-babel-after-execute-hook-hook=; inline dot-graph images never refresh after C-c C-c. Write =(org-babel-after-execute . ...)= or add-hook in :config.
-- [BUG] =org-roam-config.el:67,71= — C-c n p / C-c n w bound (and which-key-labeled) to =cj/org-roam-find-node-project= / =-webclip=, defined nowhere — keypress errors "autoloading failed to define function". Define via =cj/org-roam-find-node= (a project template exists) or drop bindings + labels.
-- [BUG] =org-export-config.el:74-81= — ox-texinfo block can never run (=:defer t=, no trigger, excluded from line-47 dolist and =org-export-backends=); commentary still advertises Texinfo. Add to the dolist or delete; also commentary says "subtree default scope" vs actual ='buffer= (line 61).
-- [UX] =org-roam-config.el:50-63= — two parallel template dirs drift: :custom templates read =~/.emacs.d/org-roam-templates/= while find-node-topic/recipe read =roam-dir/templates/= — overlapping recipe/topic/v2mom files, edits don't propagate. Pick one canonical dir.
-- [REMOVE] =org-agenda-config.el:84= — dead =timeline= entry in org-agenda-prefix-format (removed in org 9.1). Also =org-config.el:47-48= — the TASK note claiming =org-indent-indentation-per-level= "doesn't exist" is wrong (real org-indent defcustom); restore the setq or fix the comment.
-- [REMOVE] =org-babel-config.el:161= — =org-html-footnote-separator= is an ox-html setting parked in the babel module with a wrong comment; =org-roam-config.el:76= similarly hides =org-agenda-timegrid-use-ampm= in roam's :config (only takes effect after roam loads). Move both to their owning modules.
-- [REMOVE] =org-roam-config.el:363-390= — 28-line commented consult-org-roam block on a TASK comment; its proposed C-c n l / C-c n r now collide with live bindings, so it can't ship as written. Decide + delete (git keeps the draft).
-- [COVERAGE] =org-agenda-config.el:423= cj/add-timestamp-to-org-entry (defvar-inside-defun smell), =org-roam-config.el:115,185= node-insert-immediate + finalize-hook — untested.
-
-*** TODO Findings: org apps + calendar-sync group
-From agents 2026-06-11/12; spot-verified sample (UNTIL comparisons, EXDATE regex, drill setq confirmed). Beyond the standalone tasks:
-- [BUG] =org-reveal-config.el:241= — seven raw =global-set-key= "C-; p ..." calls carry a hidden load-order dependency on keybindings.el (signals "non-prefix key" otherwise); every sibling uses =defvar-keymap= + =cj/register-prefix-map=. Convert.
-- [BUG] =org-drill-config.el:131= — =:load-path "~/code/org-drill"= dev checkout breaks drill on machines without it (velox already diverges per the gptel-magit task). Guard with =file-directory-p= fallback to :vc.
-- [UX] =org-contacts-config.el:146= — =cj/org-contacts-find= visits the file BEFORE prompting (C-g strands you at point-min) and plain =search-forward= can match body text in another entry. Collect heading positions in org-map-entries, goto after prompt.
-- [REMOVE] =calendar-sync.el:1240= — =calendar-sync--fetch-ics= (buffer-string variant) is dead; the sync path uses the temp-file variant exclusively. 30 lines of duplicate curl/sentinel logic that will drift.
-- [REMOVE] =org-webclipper.el:216-241= dead commented keymap blocks; =org-contacts-config.el:118-124= commented duplicate capture template flagged "TASK: duplicate?!?". Delete both (git keeps drafts).
-- [COVERAGE] =calendar-sync.el:1274= — fetch sentinel branches (curl failure, temp-file cleanup, signal exit) untested; dispatch tests stub above this layer.
-
-*** TODO Findings: mail group
-From agents 2026-06-12; spot-verified sample (cmail trash gap, no refile folders, gmail-first contexts confirmed). Beyond the standalone [#A] task:
-- [BUG] =mail-config.el:392-407= — C-; e account nav lambdas call =mu4e-search=, not autoloaded — void-function before first mu4e launch. Add to :commands or require first.
-- [BUG] =mail-config.el:481-484= — unconditional =org-msg-edit-mode= :after advice on replies defeats the =(reply-to-text . (text))= alternative at :459 and re-runs a major mode org-msg already set up. Gate or remove.
-- [BUG] =mu4e-attachments.el:222= — the *mu4e attachments* selection buffer saves through stale MIME handles if the view changed before s — errors or saves the wrong message's parts. Check =buffer-live-p= per handle at save.
-- [BUG] =mail-config.el:329= — "save attachment" in =mu4e-headers-actions= can't work from headers (MIME vars are view-buffer-local, nil in headers-mode). Drop it there.
-- [BUG] =mail-config.el:282-305= — HTML view block sets variables obsolete since mu4e 1.7 (installed 1.14.1): =mu4e-view-prefer-html=, =mu4e-html2text-command= (also set twice: 186, 285), =mu4e-view-show-images=, =mu4e-view-image-max-width=. The pandoc/w3m selection never runs; shr renders regardless. Delete the dead block (image/privacy reconciliation already filed separately).
-- [BUG] =mail-config.el:45-49,80-89= — top-level =(defvar message-send-mail-function nil)= pre-empts message.el's defcustom default; with msmtp absent the fallback leaves it nil → "invalid function: nil" on first send. Explicit =smtpmail-send-it= fallback or descriptive user-error.
-- [UX] =mail-config.el:171,196-199= — =pick-first= + gmail listed first makes gmail the startup context though cmail reads as primary everywhere else — quiet wrong-account hazard for the first compose. Reorder contexts.
-- [REMOVE] =mu4e-org-contacts-setup.el= — unreachable (nothing requires it; mail-config calls activation directly) and its featurep gate would be nil at init anyway. Delete or fold its two setqs into mail-config.
-- [REMOVE] =mail-config.el:208,232= — =mu4e-starred-folder= isn't a mu4e variable (invented, no effect); =:174= =mu4e-maildir= is the obsolete alias of root-maildir set on the previous line. Drop all three.
-- [REMOVE] =mu4e-org-contacts-integration.el:158,171-172= — hook surgery on =mu4e--compose-setup-completion= is a no-op on mu4e 1.14 (called directly, not via hook; already gated by the var activation sets). Delete both hook calls.
-- [COVERAGE] =mu4e-attachments.el:101-105= — mid-batch save-failure path and stale-handle scenario untested.
-
-*** TODO Findings: messengers group
-From agents 2026-06-12. Beyond the standalone tasks; several feed the messenger-unification spec:
-- [BUG] =signal-config.el:201= — contact cache docstring claims "cleared on signel-stop/restart"; nothing clears it (grep: fork never references it). Stale list after relink/reconnect. Advise =signel-stop= or clear on start.
-- [BUG] =signal-config.el:298= — fetched-and-empty contact list is indistinguishable from cold cache (nil), so a zero-contact account re-runs the blocking fetch (up to fetch-timeout) on every C-; M m. Cache a sentinel.
-- [UX] =slack-config.el:208= — =cj/slack-notify= lacks signel's hardening: no truncation (giant toasts), no sound gating, no notifications-notify fallback when the script is absent. Unification-relevant: extract a shared =cj/messenger-notify= (title prefix, truncation, sound flag, script-with-fallback) — noted in the unification spec.
-- [ENHANCE] =telega-config.el:52= — telega has NO notification path (=telega-notifications-mode= not enabled); incoming Telegram messages invisible unless the buffer is on screen. Enable, or route through the shared notifier. Unification-relevant.
-- [COVERAGE] — =cj/erc-join-channel-with-completion= (erc:148, four-way reconnect branching), =cj/erc-connected-servers= (would have caught the tautology), =cj/slack-notify= predicates, =cj/signel--ensure-started= branches — all untested.
-
-*** TODO Findings: programming group
-From agents 2026-06-12; spot-verified sample (prog-lsp unreachable confirmed by grep). Beyond the standalone tasks:
-- [BUG→FOLD] =prog-lsp.el= — the module is UNREACHABLE: nothing requires it, so its entire LSP policy (TRAMP guard, file-watch ignores, read-process-output-max, idle-delay 0.5) is dead while prog-general.el:388-416's older conflicting block wins (idle 0.1, lsp-ui-doc on). Fold this fact into the filed "Make prog-lsp.el the single owner of generic LSP policy" task — it doesn't currently record that prog-lsp never loads.
-- [BUG] =flycheck-config.el:68-70= — =checkdoc-arguments= isn't a real variable (invented name + invented format); the intended checkdoc suppression has never worked. Use =flycheck-emacs-lisp-checkdoc-variables= or drop.
-- [BUG] =prog-json.el:87-90= — C-c C-q → jq-interactively binding defers to eval-after-load of jq-mode, which nothing loads — dead key. Bind in =cj/json-setup= via local-set-key (jq-interactively IS autoloaded).
-- [BUG] =prog-python.el:129-132= — lsp-pyright's :hook lambda calls =lsp-deferred= unguarded on the same hook as the guarded =cj/python-setup= — pyright-absent machines still get the LSP attach prompt the guard exists to prevent. Move the require into the guarded branch; delete the hook.
-- [BUG] =prog-lisp.el:122-125= — =:after (flycheck package-lint)= waits for a manual M-x to load package-lint, so =flycheck-package-setup= effectively never runs. Hook on flycheck load + require inside.
-- [UX] =prog-python.el:111-115=, =prog-go.el:111-114=, =prog-webdev.el:128-147= — setup hooks attach to ts-modes only (C/shell hook both variants); grammar-unavailable fallback to classic modes silently loses indent/keys/formatter/LSP. Add classic-mode hooks.
-- [UX] =prog-webdev.el:165-173= — web-mode gets the format key but none of the promised setup (no company/flyspell/LSP in HTML buffers). Add to the setup hook or fix the Commentary.
-- [ENHANCE] gopls, clangd, bash-language-server, shfmt, shellcheck lack the =cj/executable-find-or-warn= load-time warnings pyright/prettier have; prog-shell's =:if (executable-find ...)= evaluates once at startup and silently disables shfmt/flycheck setup forever.
-- [REMOVE] =prog-training.el:36-37= — =(url-debug t)= turns on GLOBAL url.el debug logging once leetcode loads. Debugging leftover; delete.
-- [REMOVE] =prog-webdev.el:85=, =prog-json.el:44=, =prog-yaml.el:39= — three byte-identical format-region helpers. Extract one shared tested helper (system-lib).
-
-*** TODO Findings: dev tooling group
-From agents 2026-06-12; spot-verified sample. Beyond the standalone F7 task:
-- [BUG] =vc-config.el:138-144= — =cj/goto-git-gutter-diff-hunks= (C-; v d) never did what it claims: consult-line over "^[+\\-]" matches source text, not gutter hunks. Build candidates from =git-gutter:diffinfos= or drop the binding (C-; v n/p covers it).
-- [BUG] =dev-fkeys.el:116-122= — F4 compile+run one-shot hook installs on GLOBAL =compilation-finish-functions= before the prompt; C-g leaves it armed and the next unrelated compile triggers projectile-run-project. Use the buffer-local pattern the module already uses for cache-revert (same in =--f4-clean-rebuild-impl=:143).
-- [BUG] =test-runner.el:84,222= — documented ~/.emacs.d/tests fallback doesn't exist (=cj/test-global-directory= defvar'd nil, never set); outside a project =(file-directory-p nil)= crashes in three commands. Initialize the defvar or guard with user-error. (Adds specifics to the open "Fix up test runner" task — fold.)
-- [BUG] =test-runner.el:288= — focus-add prefix check lacks the trailing slash so =tests-scratch/= passes the "inside tests/" check; the correct helper =cj/test--file-in-directory-p= exists at :168 — use it.
-- [BUG] =vc-config.el:217-219= — difftastic blame map binds D and S to the same command (show); D should be diff per the transient four lines down.
-- [UX] =diff-config.el:37= — =ediff-diff-options "-w"= ignores ALL whitespace in every ediff session — indentation-only Python changes compare as identical. Drop the default; toggle per-session.
-- [UX] =restclient-config.el:64-65= — raw global-set-key "C-; R n" hides a load-order dependency (header claims "Runtime requires: none"); use defvar-keymap + =cj/register-prefix-map= like siblings (same class as org-reveal, slack).
-- [UX] =vc-config.el:196= — clipboard clone via synchronous =call-process= freezes every emacsclient frame for the whole clone. make-process + sentinel.
-- [REMOVE] =vc-config.el:80-82= — phantom autoload =git-timemachine-show-selected-revision= (no such function in the package) appears in M-x and errors. Drop from :commands.
-- [REMOVE] =httpd-config.el:19-30= — pointless =:defer 1= (impatient-mode loads simple-httpd on demand) + unprefixed eager globals =wwwdir=/=check-or-create-wwwdir= creating www/ on every startup. =:defer t=, prefix, or fold into markdown-config.
-- [COVERAGE] — intersect/parse unit tests hand-build matching keys (the F7 bug's escape route); =--coverage-elisp-run='s compilation-finish wiring, goto-git-gutter-diff-hunks, timemachine candidate round-trip untested. F-key sweep clean: no collisions; F5 free for the debug-backend task.
-
-*** TODO Findings: shell/term/files group
-From agents 2026-06-12; spot-verified sample (eshell nested list confirmed). Beyond the standalone tasks:
-- [BUG] =dirvish-config.el:37= — =cj/xdg-open= attributed to system-utils in the require-comment but defined in external-open.el; neither dirvish-config nor dwim-shell-config (caller at :876) requires it — "Direct test load: yes" headers are false. Require external-open (or move the fn into external-open-lib) + fix comment.
-- [UX] =tramp-config.el:73= — =revert-without-query '(".*")= kills revert confirmation for EVERY file in Emacs, buried in the TRAMP module. Scope to =tramp-file-name-regexp= or move deliberately to an editing module.
-- [UX] =dirvish-config.el:403= — quick-access entries lx (~/archive/lectures), phl (~/projects/homelab), pn (~/projects/nextjob) point at directories that don't exist on this machine. Prune or create.
-- [REMOVE] =dwim-shell-config.el:474,507= — open-externally (raw xdg-open) and open-file-manager (thunar/nautilus probe chain) duplicate cj/xdg-open (dirvish o) and cj/dirvish-open-file-manager-here (f); ascii-art references jp2a, the module's only absent binary. Delete the two duplicates; install jp2a or drop ascii-art.
-- [REMOVE] =tramp-config.el:115= — custom =sshfast= method referenced nowhere (everything uses sshx); =tramp-own-remote-path= added twice (:39,:128); =dirtrack-list= and =magit-git-executable "/usr/bin/git"= are unrelated globals hiding here. Prune/relocate.
-- [COVERAGE] — eshell visual-commands/xterm-color wiring and the dirvish mark-all loop had no load-and-assert tests (both standalone bugs above); TRAMP perf settings look sound for the DUET latency concern (attr caching, no remote VC, direct-async + controlmaster).
-
-*** TODO Findings: AI group
-From agents 2026-06-12; spot-verified sample (string-model setq confirmed). Beyond the standalone tasks:
-- [BUG] =ai-term.el:875= — close derives the tmux session name from =default-directory=, which ghostel retargets via OSC 7; after a cd the kill-session misses (orphaned agent session) or name-collides with a different aiv- session. Derive from the buffer name's immutable basename.
-- [UX] =ai-term.el:827= — multi-window F9 toggle-off unconditionally delete-windows, never restoring the displaced edge-window buffer the Commentary (:24) and reuse-edge docstring (:521) promise. Restore when quit-restore still matches, or fix the docs to describe delete-window reality.
-- [UX] =ai-conversations-browser.el:191= — browser load stubs =y-or-n-p= to nil, silently discarding an unsaved in-progress conversation (the direct C-; a l path offers to save). Give ai-conversations a file-arg internal instead of puppeting the interactive command via cl-letf; also the =(caar cands)= fallback loads the newest conversation on a filename mismatch — fail loudly.
-- [ENHANCE] =ai-quick-ask.el:103= — dismiss mid-stream kills the buffer without =gptel-abort= — request keeps streaming to a dead buffer (wasted tokens).
-- [NOTE] =ai-mcp.el= — unreachable from init, consistent with the paused Phase 1.5; add a one-line Commentary note ("not wired until Phase 2") so future audits don't re-flag, and revisit =cj/mcp-enabled-servers= defaulting to all nine servers before wiring.
-- [COVERAGE] — load/autosave lifecycle untested (fresh-session load, timer self-cancel, close-buffer session-name derivation).
-
-*** TODO Findings: media/reading group
-From agents 2026-06-12; spot-verified sample (M-S- bindings, eww store split confirmed). Beyond the standalone tasks:
-- [BUG] =music-config.el:585= — =cj/music-add-dired-selection= gates =dired-get-marked-files= on =(use-region-p)= — but dired marks aren't a region; marked files are ignored, + adds only file-at-point. Drop the conditional (the function already falls back correctly). Note for the EMMS-free rewrite: dirvish + shadows =dired-create-directory= — deliberate decision needed before carrying it over.
-- [UX] =media-utils.el:195-204= — =cj/yt-dl-it= watches tsp (which enqueues and exits), so "Finished downloading" fires immediately while yt-dlp may fail later, silently; also affects elfeed d. Message "queued" honestly or watch the real job (tsp -f).
-- [UX] =browser-config.el:34-47,171= — first-run fallback picks EWW (first, "always available") over installed real browsers; fresh machines get org links in a text browser until cj/choose-browser runs. Prefer the first external match.
-- [REMOVE] =video-audio-recording.el:442-488= — =cj/recording-group-devices-by-hardware= is dead code (nothing calls it) carrying a hardcoded "Jabra SPEAK 510 USB" branch. Delete + its test file.
-- [REMOVE] =calibredb-epub-config.el:198-212= — =set-auto-mode= :around advice for .epub is redundant with nov's :mode registration (auto-mode-alist wins before magic-fallback); overhead + failure surface on every file visit. Remove and verify.
-- [COVERAGE] — eww interactive commands (switch-search-engine, bookmark-quick-add, copy-url) and =cj/nov-center-images= untested.
-
-*** TODO Findings: apps/misc group
-From agents 2026-06-12. Beyond the standalone tasks:
-- [BUG] =hugo-config.el:49= — =cj/hugo-new-post= void-functions on =org-hugo-slug= in a fresh session (ox-hugo is :after ox, which loads on first export); =cj/hugo-export-post= already requires ox-hugo — do the same here.
-- [BUG] =help-utils.el:73= — arch-wiki search signals raw file-missing when the docs dir is absent; the friendly install hint at :81 is unreachable. Guard with =file-directory-p= + user-error up front.
-- [UX] =hugo-config.el:244= — eight raw global-set-key C-; h calls + hand-rolled which-key mutate cj/custom-keymap directly, against keybindings.el's own instruction. Convert to defvar-keymap + =cj/register-prefix-map= (same class as org-reveal, restclient, slack).
-- [ENHANCE] =games-config.el:25= — =:defer 1= pulls malyon + 2048 into every session for nothing; use =:commands=. Also :config references =org-dir= without requiring user-constants (free-variable warning at byte-compile).
-- [REMOVE] =wrap-up.el:29= — =elisp-compile-mode= doesn't exist (real mode emacs-lisp-compilation-mode derives from compilation-mode, already covered at :27); dead line. (The prior unguarded-timer fix is intact.)
-- [REMOVE] =help-config.el:99-106= — stray empty :preface + dead commented Info-directory-list block. Delete.
-- [NOTE] =duet-config.el= — orphaned BY DESIGN (Commentary documents pre-alpha staging; Stage 1 is the wire-in trigger). Audit record only.
-- [COVERAGE] — help-config and help-utils have zero test files; the two broken paths above are exactly the untested branches.
-
-*** TODO Findings: holistic — startup & performance
-From the 2026-06-12 holistic pass; daemon init measured at 1.11s (healthy). Beyond the standalone [#A] native-comp/GC task:
-- [BUG→FOLD] the eager-org chain: =org-config.el:352= org-appear has no defer trigger (only :custom) → requires all of org at init; org-agenda (=:after org :demand t=) cascades; chime's =:demand t= pulls it anyway. org-config is the most expensive require (0.229s of 1.11s). Decide fully-eager vs fully-deferred — and =init.el:146='s "calendar-sync must come after org-agenda" contract exists only as a comment (three uncoordinated writers of =org-agenda-files=). Both facts belong in the filed defer-modules task before that refactor starts.
-- [PERF] =dirvish-config.el:385-387= — =:defer 0.5= defeated by :init calling autoloaded =dirvish-override-dired-mode= → dirvish fully loads at init (0.072s, third most expensive; trace-confirmed). Own the eager load or defer the override to a dired-mode-hook shim.
-- [PERF] timed =:defer N= loads unused packages into every start: simple-httpd (:1s + startup mkdir despite the defer), malyon, 2048-game, emojify (may hit network), ligature. Convert to :commands/mode hooks.
-- [UX] =early-init.el:235-256= — synchronous =package-refresh-contents= on the startup path when any archive cache is >7 days old (MELPA ~6MB) — multi-second network-bound start, fires in batch too. Make async post-startup or push into the localrepo update script (distinct from the filed bootstrap-relocation task).
-- [PERF] =early-init.el:228= — no =package-quickstart= with 184 packages; activation walks every package dir each start (~0.3s of early-init). Free win; regenerate after package ops.
-- [PERF] =prog-general.el:298= — =yas-reload-all= immediately before =yas-global-mode= scans snippet dirs twice per start (doubled "[yas] Prepared..." message). Delete the line.
-- [REMOVE] cross-ref: the all-the-icons stack (already in UI core findings) is 2 of the :demand t packages plus a per-frame install-check hook.
-
-*** TODO Findings: holistic — daemon stability
-From the 2026-06-12 holistic pass. Architecture-level verdict good (timers cancelled/guarded, calendar-sync async well-contained, advice mostly named + guarded). Residual:
-- [STABILITY] =transcription-config.el:293= — sentinel chain has no unwind-protect; =--append-to-log='s =insert-file-contents= signals if the log is missing → process buffer leaks, entry stuck 'running in the modeline forever, no notification. Extends the filed transcription standalone — fix together.
-- [STABILITY] =calendar-sync.el:1646= — hourly timer body: fetch/parse guarded but the timezone check and =--require-calendars= run bare — any signal repeats hourly forever (the exact class fixed in four modules once). Condition-case the body; demote the hourly echo-area message to the silent log.
-- [STABILITY] =music-config.el:865= — four anonymous-lambda advice in :config stack per live reload (verified: lambdas don't dedupe) and can't be advice-removed. Name the function.
-- [STABILITY] =system-defaults.el:69= — =display-warning= advice appends to comp-warnings-log with no condition-case (unwritable path → every async comp warning signals from inside display-warning) and the log grows unbounded. Guard + cap.
-- [STABILITY] =media-utils.el:164= — playback sentinel assumes the process buffer is alive (user killed *player:...* → sentinel error, diagnostics lost); sibling yt-dl sentinel shares the kill-buffer gap. buffer-live-p guards.
-- [ENHANCE] =system-commands.el:86= — =#'ignore= sentinel + output to /dev/null makes failing lock/suspend indistinguishable from success — the user walks away from an unlocked machine. Message on nonzero exit. (Compounds the [#A] slock task: the broken lock currently fails through exactly this silent path.)
-- [ENHANCE] =ui-config.el:153= — post-command cursor hook unguarded: any future signal self-removes it silently (cursor stops signaling modified/read-only until restart); frame-hook lambda also accumulates per reload. with-demoted-errors + name it.
-- (kill-emacs-hook y-or-n-p prompt independently re-found here — already filed in the text/prose child; convergence noted.)
-
-*** TODO Findings: holistic — UX consistency
-From the 2026-06-12 holistic pass. Verdict: more coherent than most 120-module configs (~85% prefix-helper adoption, M-S translation fully covers its 18 bindings, F-keys collision-free, DEF-arg prompts dominate). Beyond the standalone [#A] fset task:
-- [BUG] notification env gate: =transcription-config.el:169-171= gates desktop notifications on =(getenv "DISPLAY")= — an X11 predicate that works only because XWayland exports it. Use =env-gui-p= (host-environment.el provides it).
-- [UX] four notification stacks beyond the messenger split (notify script ± fallback, alert.el, raw notifications-notify, echo-only for calendar-sync/recording completions). Proposed: one cj/notify facade (transcription's =cj/--notify= is the right shape) — config-wide companion to the messenger-notify addendum in the unification spec.
-- [UX] five more C-; entries bypass the register helpers or lack labels: =browser-config.el:182= (C-; B, no label), =org-babel-config.el:51= (C-; k, no label), =flycheck-config.el:62-64= (:bind into cj/custom-keymap), =pearl-config.el:43= (:bind-keymap C-; L, no label), =dev-fkeys.el:533= (helper but no label). Sweep onto cj/register-prefix-map with labels.
-- [UX] user-error vs message inconsistent for "nothing to act on" config-wide (examples: =custom-whitespace.el:190=, =jumper.el:202=, =chrono-tools.el:99= message; =mu4e-attachments.el:112=, =ai-rewrite.el:79= user-error; =test-runner.el:392/394= mixes both 2 lines apart). Convention: user-error when the command can't proceed; message when it ran and found nothing.
-- [ENHANCE] M-S translation layer: complete for GUI (18/18) but installs only on env-gui-p paths — terminal frames have no M-uppercase route; and =dwim-shell-config.el:932/934= binds M-S-d (dired) vs raw M-D (dirvish) asymmetrically. Feeds the filed M-S review task with the concrete map.
-- [ENHANCE] which-key labels: register-helper's LABEL arg used by exactly 1 of 23 registrants (rest use separate with-eval-after-load blocks); label style drifts ("X menu" vs bare nouns). Adopt LABEL arg + one style.
-- [ENHANCE] "?" curated-menu candidates (for the filed convention task): elfeed search/show, dirvish, signel chat/dashboard, music playlist, ai-conversations-browser, mu4e-attachments, transcription status, pearl. calibredb remains the model.
-- [UX] ="(Cancel)"= pseudo-candidate in =music-config.el:253-256= vs C-g everywhere else (90+ prompts). Drop it.
-- [UX] buffer-naming drifts across three conventions (*AI-Assistant* / *Kill Ring* / *dashboard*); pick Title Case + "*Name: param*", lowercase for process logs.
-- [ENHANCE] C-; f formatter shadowing implemented 3 ways (:bind :map vs local-set-key in hooks); unify on :bind. Also =keybindings.el:21= commentary still says "C-c j" for the jump prefix the code binds at C-; j.
-- [ENHANCE] initial-input anti-pattern at =dwim-shell-config.el:661,680= and =erc-config.el:176-177= against the config's DEF-arg norm.
+** DOING Project-aware bug capture via C-c c t :feature:capture:
+Relocated from the global capture inbox 2026-06-06. When inside a projectile project, C-c c t (Task) files into that project's root todo.org under the "<Project> Open Work" header. If the project has no todo.org, fall back to the global inbox-file and warn naming the project.
-*** TODO Findings: holistic — package strategy
-From the 2026-06-12 holistic pass (184 elpa dirs). Core stack modern (vertico/consult/embark/orderless, treesit-auto, built-in which-key, current magit/forge/telega/slack). Beyond the standalone gptel/prescient tasks:
-- [REMOVE] true orphans, nothing references them: js2-mode, tide, json-mode (pre-treesit JS stack). package-delete + drop from .localrepo.
-- [REMOVE] emojify: 2021 snapshot, dormant upstream, crashes in lui (slack disabled it), Emacs 30 renders emoji natively. Drop the use-package + hooks (=font-config.el:253=, =erc-config.el:211=); it stays on disk only as slack's declared dep.
-- [BUG] legacy-mode hooks miss the ts modes: =prog-general.el:91-92= hooks =yaml-mode-hook=/=toml-mode-hook= but the config runs yaml-ts/toml-ts — general prog settings silently don't apply in YAML/TOML buffers. Rehook; delete toml-mode + eldoc-toml + yaml-mode packages (superseded by treesit).
-- [RISK→FOLD] localrepo priority 200 is absolute, so package-upgrade silently no-ops on everything mirrored — the engine that fossilized emojify@2021/toml-mode@2016/js2@2023. The filed refresh-script task at [#D] deserves [#B] + a quarterly cadence, else every orphan finding regrows.
-- [RISK] fork fleet sync-back stories: org-drill flip back to :vc when done (filed dev-checkout finding); auto-dim-other-buffers local checkout with :vc commented — decide its home; org-msg pins =:rev :newest= (unpinned moving target) — pin a known rev. signel/duet/pearl/wttrin/gloss/chime self-owned remotes are fine.
-- [UPGRADE] wiki-summary (2018, dead upstream, predates Wikipedia's REST API; sole caller help-utils) — the audit's one write-your-own: ~30-line url-retrieve against the REST summary endpoint. Delete the package, inline the helper.
-- [UPGRADE] xterm-color droppable in eshell on Emacs 30's native ansi-color (its only use; also doubly-broken per the eshell standalone task — fixing by deletion is an option).
-- [ENHANCE] Python tier: poetry.el (sluggish) + pyvenv (2021) keep only if Poetry projects are still real; blacken fine until ruff-format (reformatter.el already installed). lsp-pyright current.
-- [DECIDED] projectile, lsp-mode, dirvish: keep (wired into 10/7/many modules, maintained, migration cost > benefit). On the record so future audits don't relitigate.
+Implemented 2026-06-06 in =modules/org-capture-config.el=: a shared project-aware =function= capture target (=cj/--org-capture-project-location=) used by =C-c c t= (Task, =* TODO=) and a new =C-c c b= (Bug, =* TODO [#C]=). Matches an existing top-level "... Open Work" heading (so ~/.emacs.d hits "Emacs Open Work") and creates "<Capitalized project> Open Work" only when absent. Outside a project / no todo.org -> global inbox under "Inbox" (with a warning in the no-todo.org case). 15 ERT tests in =tests/test-org-capture-config-project-target.el=; daemon e2e confirmed a real capture lands "** TODO [#C] ..." prepended under Open Work. Awaiting Craig's interactive manual verify (see the Manual Testing task) before close. NOTE: the matching "<Project> Resolved Work" header for the wrap-up workflow is a separate concern, not handled here.
-*** TODO Findings: spin-off repos (pearl, chime, emacs-wttrin)
-Full findings delivered as handoffs to each repo's inbox/ (2026-06-12-0057-from-.emacs.d-handoff-*.org); each repo's next session files them through its own value gate. Highlights:
-- pearl (10 findings; suite green, 66 ERT files): auth-source negative-cache trap in pearl-clear-cache (the 2026-06-01 incident class, unfixed); sync wrapper ignores pearl-request-timeout + async has no timeout; mutation errors discard Linear's GraphQL reason; no RATELIMITED handling; dead legacy API layer (~150 lines).
-- chime (10 findings; suite green; the 2026-06-11 watchdog handoff VERIFIED landed in full): lookahead vars never injected into the async child (documented feature silently capped at 8 days — one-line fix); days-until-event nil crash on mixed timed/all-day events; stale-callback race after watchdog interrupt (generation counter needed); default test run prints green integration banner over "Ran 0 tests".
-- emacs-wttrin (10 findings; ~56 ERT files, CI; the face-flood reminder VERIFIED resolved — test 8f3c770 + fix c5e5e1d, reminder cleared from notes.org): no network timeouts (wttr.in stalls hang the loading buffer); error-path response-buffer leak; non-favorite cache never expires; 17 unreleased commits incl. two features — tag v0.4.0.
+** VERIFY Palette-columns spec review :theme-studio:
+SCHEDULED: <2026-06-12 Fri>
+Read [[file:docs/theme-studio-palette-columns-spec.org][docs/theme-studio-palette-columns-spec.org]] (Draft, from the 2026-06-10 design discussion) and bless or amend. Decisions 9 and 10 are the two session calls awaiting your word: strips flip to lightest→darkest top→bottom to match the dropdown, and each dropdown column run places the base at its natural lightness position (vs bg/fg bases leading before any steps). On "spec's good": mark Ready, file the phase breakdown, cancel the [#C] hint-override task, start Phase 1.
* Emacs Resolved
** DONE [#B] Fix likely =elpa-mirror-location= path bug :bug:quick: