aboutsummaryrefslogtreecommitdiff
path: root/todo.org
diff options
context:
space:
mode:
Diffstat (limited to 'todo.org')
-rw-r--r--todo.org743
1 files changed, 599 insertions, 144 deletions
diff --git a/todo.org b/todo.org
index c598582f..d2478ad0 100644
--- a/todo.org
+++ b/todo.org
@@ -44,13 +44,45 @@ Tags are additive. For example, a small wrong-behavior fix can be
=:bug:quick:=, and a feature that requires internal restructuring can be
=:feature:refactor:=.
* Emacs Open Work
+** TODO [#B] Unified popup placement and dismissal rules :feature:
+All transient popups should follow one set of principles. Placement: when the Emacs frame is wider than tall, the popup rises from the right; when square or taller, from the bottom — settle the aspect-ratio threshold and the pop-out percentage. Dismissal: C-c C-c when there's an accept action, C-c C-k when there's a cancel, otherwise =q= closes the window. This generalizes two existing tasks — ai-term adaptive placement (the aspect-ratio docking) and the messenger window/key unification spec (the C-c C-c / C-c C-k dismissal) — into one config-wide policy. From the roam inbox.
+
+** TODO [#C] Pull a fullscreen terminal window away with C-; b + arrow :feature:
+When a terminal fills the frame, =C-; b= then a right or down arrow should shrink the window from that edge, reducing its width or height so another buffer can share the screen without leaving the terminal. Relates to the ai-term adaptive placement and unified-popup tasks. From the roam inbox.
+
+** TODO [#C] Remove unused system-power keybindings :refactor:quick:
+=modules/system-commands.el= binds shutdown (=C-; ! s=), reboot (=C-; ! r=), restart-Emacs (=C-; ! e=) and friends under the =C-; != prefix. Craig rarely uses them and wants the key real-estate back. Drop the bindings he doesn't use; the completing-read menu can still reach the rare ones. Confirm the exact set to keep before unbinding. From the roam inbox.
+
+** TODO [#C] Dirvish: free D for hard-delete, move duplicate :feature:quick:
+In dirvish, keep =d= = delete (=dired-do-delete=), move duplicate (=cj/dirvish-duplicate-file=, currently =D=) to another key, and bind =D= = =sudo rm -rf= for a forced hard delete — capital for the more destructive op. Craig's note says "duplicate on 2"; confirm that's the intended key, and guard the sudo path carefully before wiring. From the roam inbox.
+
+** DONE [#C] Swap buffer delete/diff keys — destructive on capital :refactor:quick:solo:
+CLOSED: [2026-06-13 Sat]
+=modules/custom-buffer-file.el:515= binds =d= = =cj/delete-buffer-and-file= and =D= = =cj/diff-buffer-with-file=. Destructive commands should be the capital, and diff is the one hit often (when saving a buffer changed on disk). Swap them: =D= = delete, =d= = diff. From the roam inbox.
+Swapped 2026-06-13: =cj/buffer-and-file-map= now binds =d= = =cj/diff-buffer-with-file=, =D= = =cj/delete-buffer-and-file=. keymap-lookup test added; live daemon re-bound by hand (defvar-keymap won't reassign a bound var on reload). Keypress check is a VERIFY under Manual testing and validation.
+
+** TODO [#B] ai-term adaptive side/bottom window placement :feature:quick:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+The ai-term window should dock from whichever edge conserves more screen space, chosen at display time from the frame's aspect ratio: when the frame is wider than it is tall, dock from the right; when it is square or taller than wide, dock from the bottom. Compare the frame's pixel width against its height in the display-buffer rule to pick the edge.
+
+** TODO [#B] Keymap consolidation — resolve decisions, run Phase 1-2 :feature:refactor:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+Spec: [[file:docs/design/keybinding-console-safety-spec.org][keybinding-console-safety-spec.org]]. Phase 0 (revert 4a1ecf64) is done and pushed. Decisions D1-D5 are open TODOs in the spec; D2/D4/D5 gate the primary work (Phase 1 prune via Appendix D, Phase 2 consolidate + retire the translation block), while D1/D3 (the console-safe prefix) gate only the optional Phase 3 and can stay open indefinitely. Resolve D2/D4/D5, then run Phase 1-2. Appendix D is the keybinding pruning checklist. Add a =#+TODO: TODO | DONE SUPERSEDED CANCELLED= header line to the spec if adopting those decision keywords (rulesets convention update, 2026-06-12).
+
+** TODO [#D] Desktop quick-capture: Note + Recipe types :feature:solo:
+Deferred 2026-06-13 — build when the need triggers, not ahead of use. Add generic Note (timestamped datetree) and Recipe (skeleton with Ingredients/Instructions + :SOURCE:) capture types to =cj/quick-capture= in =modules/org-capture-config.el=: one template each with an absolute target plus its key in the desktop subset; reuse the existing frame-cleanup. Full design in the archsetup handoff (2026-06-13 note in the inbox/sessions).
+
** TODO [#A] Calibre Open Work
: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:
+*** 2026-06-12 Fri @ 07:34:05 -0500 Calibre bookmark naming ships "Author, Title" from the filename
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
@@ -65,9 +97,9 @@ Implemented 2026-06-06. Source decision: parse the *filename*, not the embedded
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.
+Manual verify filed under the Manual testing and validation parent.
-*** DOING [#A] Reconsider Calibre keybindings :feature:
+*** 2026-06-12 Fri @ 07:34:05 -0500 Curated Calibre keybinding menu + docked description shipped
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:
@@ -81,30 +113,49 @@ Survey finding 2026-06-06: calibredb already binds almost all of this in calibre
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).
+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. Manual verify filed under the Manual testing and validation parent.
*** TODO Embed Calibre DB metadata into the EPUB files
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:quick:
+** DOING [#A] Lock screen silently fails — slock is X11-only :bug:quick:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
=modules/system-commands.el:105= binds the lockscreen command to =slock=, which can't grab a Wayland session; =cj/system-cmd= launches it detached with output silenced, so C-; ! l does nothing and the screen never locks. Security issue: Craig believes the screen locks when it doesn't. Fix: =hyprlock= (or =swaylock=), ideally resolved per session type via =env-wayland-p= so an X11 fallback survives for other machines. From the 2026-06 config audit.
+Fixed 2026-06-13: lockscreen-cmd resolves to =loginctl lock-session= on Wayland (logind Lock → hypridle → hyprlock, the path idle/sleep locking already uses), =slock= on X11; also added the missing =(require 'host-environment)=. Live in the daemon; manual lock test under the Manual testing parent.
-** TODO [#A] mu4e: cmail can't trash, no account can refile :bug:quick:solo:
+** DOING [#A] mu4e: cmail can't trash, no account can refile :bug:quick:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
=modules/mail-config.el:217-220= — the cmail context (primary account) sets only drafts/sent, so D falls back to default "/trash" which doesn't exist under ~/.mail (=/cmail/Trash= does); and NO context sets =mu4e-refile-folder=, so r targets nonexistent "/archive" everywhere. Accepting mu4e's offer to create the maildir strands mail in a directory mbsync never syncs — messages silently vanish from the server's view. Add =mu4e-trash-folder= to cmail + per-context =mu4e-refile-folder=. From the 2026-06 config audit.
+Fixed 2026-06-13: cmail gets =mu4e-trash-folder= "/cmail/Trash"; refile is a per-message function (=cj/mu4e--refile-folder=) instead of a per-context string — mu4e context :vars are sticky, so a per-context refile leaks one account's archive folder into another. cmail → "/cmail/Archive"; gmail/dmail signal a =user-error= rather than move mail into an unsynced phantom folder (Craig chose the fail-safe over syncing [Gmail]/All Mail — the All Mail option means a multi-GB pull + cross-folder duplicates; revisit if local Gmail archiving is wanted). Applies on next mu4e open; pure dispatch helper covered by tests.
** TODO [#A] calendar-sync drops final occurrences and resurrects cancelled meetings :bug:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
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:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
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:
+** DOING [#A] Global yes-or-no-p fset defeats every strong confirmation :bug:quick:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
=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.
+Fixed 2026-06-13 (Craig chose the surgical option): added =cj/confirm-strong= to system-lib.el (binds =use-short-answers= nil for one =yes-or-no-p= call → typed "yes"); removed the redundant fset (kept =use-short-answers t= so benign prompts stay single-key); routed the 6 irreversible sites through it (shutdown/reboot, permanent-destroy, file overwrites). Note: the fset is baked into the running daemon and can't be cleared from Lisp, so the typed-"yes" tier goes live only after a daemon restart — manual confirm under the Manual testing parent. TDD; tests green.
-** TODO [#B] theme-studio preview face mislinks (org, erc, flycheck) :bug:quick:solo:
+** DONE [#B] theme-studio preview face mislinks (org, erc, flycheck) :bug:quick:solo:
+CLOSED: [2026-06-13 Sat]
:PROPERTIES:
:LAST_REVIEWED: 2026-06-11
:END:
@@ -115,6 +166,70 @@ Found by Craig 2026-06-11 during the manual-test walk (org case), then a full au
3. app.js:720 (flycheck) — swapped: brackets carry flycheck-delimited-error and the content flycheck-error-delimiter. In flycheck's delimiters highlighting style the delimiter strings get error-delimiter and the enclosed text gets delimited-error; swap them.
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).
+Fixed 2026-06-13: org heading three now has an =org-todo= keyword before =org-headline-todo=, flycheck delimiters/content are mapped to =flycheck-error-delimiter= / =flycheck-delimited-error= correctly, and ERC own/remote message text use =erc-input-face= / =erc-default-face=. Added =#previewlinktest= to pin all three mappings.
+
+** TODO [#B] theme-studio UI face inheritance needs a spec :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+Package faces model =inherit= explicitly, but UI faces currently expose only fg/bg/style fields in the table and generated theme output. Before implementing UI-face inheritance, write and review a small spec that defines: which UI faces get an inherit selector, how own defaults from =emacs-default-faces.json= appear versus effective inherited values, how export/import stores cleared vs inherited vs explicit values, how preview resolution follows UI inherit chains, and what browser gates prove the behavior. This touches the UI model, generated defaults, export format, preview rendering, and reset semantics, so it should not be slipped in as a refactor.
+
+** TODO [#B] theme-studio import organization workflow needs a spec :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+Design import handling for unstructured color sources such as Emacs themes, CSS palettes, screenshots, and generic palette files. Principles from the 2026-06-13 Theme Studio discussion:
+- Preserve declared structure whenever an imported entry has a =columnId=.
+- For unstructured legacy imports with no =columnId=, avoid silent hue clustering and avoid treating arbitrary =color-N= names as one long ramp; each =color-N= should become its own base column.
+- Keep meaningful generated ramp-name inference for names like =blue-1= / =blue= / =blue+1=.
+- Group external numeric color-name variants for compact display: =blue1= / =blue2= / =blue3= infer column =blue=; =grey80= / =grey81= infer column =grey=; =orchid3= infers =orchid=. This is display organization, not proof that the colors are an authored Theme Studio span.
+- If a numeric external base is spanned later, generate from the actual base name, e.g. =blue1-1= / =blue1= / =blue1+1=, while keeping those generated tiles in the inferred =blue= column.
+- Add explicit organization tools rather than hidden inference: group selected colors into a column, suggest hue groups as a preview/action, sort imported colors for inspection, and promote a color from an import bucket into a normal column.
+- Consider a compact imported/captured bucket UI for large unstructured imports while preserving per-color column ids internally.
+
+** TODO [#B] theme-studio palette generator :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+Spec draft: [[file:docs/theme-studio-palette-generator-spec.org][theme-studio-palette-generator-spec.org]].
+
+Build a constraint-first palette generator for Theme Studio: start from bg/fg, generate editable palette columns in OKLCH, preview candidate columns before applying them, and apply proposals by append/replace/regenerate modes while preserving stable column ids and existing assignments where possible. V1 is palette-only, not automatic face assignment; generator output becomes normal editable columns after apply.
+
+** TODO [#B] theme-studio save button does not overwrite the current theme file :bug:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+From the roam inbox: the =save= button currently behaves too much like =export=. The intended workflow is: after a theme file has been opened or saved once, pressing =save= or using the save keybinding overwrites that same file instead of downloading a fresh export. If browser security prevents overwriting a previously exported download without a file handle, make that limitation explicit in the UI and reconsider whether the separate save button should remain. This needs a small product decision around export-vs-save semantics before implementation.
+
+** TODO [#C] theme-studio reconsider the JSON show button :feature:quick:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+From the roam inbox: the =show= button for the raw JSON export does not fit the main theme-design workflow, but it may still be useful for debugging. Decide whether to hide it behind a debugging affordance, rename it, or remove it. Quick UI cleanup once the desired debugging surface is chosen; not marked solo because it is a workflow preference call.
+
+** DONE [#C] theme-studio separate tile selection from name editing :feature:quick:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+From the roam inbox: clicking a palette tile should have predictable intent. Single-click anywhere on a tile should select the whole color tile for editing/assignment. Double-clicking the name should enter name-edit mode with the cursor at the beginning of the name. Add browser-gate coverage for both paths so accidental single-click name edits do not regress.
+
+Shipped 2026-06-13: tile names are read-only until double-clicked; single-click selects the tile, and =#columntest= pins single-click vs double-click behavior.
+
+** TODO [#B] theme-studio spans should stop at bg and fg bounds :bug:quick:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+From the roam inbox: spanning a color should not generate colors beyond the current ground endpoints. No generated color should be darker than =bg= or lighter than =fg=. If the darker side hits =#000000=, do not create duplicate black tiles; if the lighter side hits =#ffffff=, do not create duplicate white tiles. Apply this to normal column spans and ground spans as appropriate, then pin with Node tests for the ramp/column plan and a browser gate for the palette UI.
+
+** DONE [#B] theme-studio delete entire color column :feature:quick:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+Add a column-level delete affordance to the palette. Deleting a normal color column removes the base color and every generated/imported tile in that column from the screen and from =PALETTE=. Ground remains pinned and non-deletable. Existing assignments that referenced removed tiles should follow the current deleted-color behavior: they remain on the old hex and appear as recoverable =(gone)= values rather than silently repointing.
+
+Acceptance notes: add a browser gate proving a column delete removes all entries with that stable =columnId=, leaves other columns alone, leaves ground non-deletable, and preserves any references to deleted hexes as gone values.
+
+Shipped 2026-06-13 in commit =2cf730d5=: normal column headers have a delete button, ground has no delete affordance, deleted names feed =lastGone= for recovery, and =#columntest= pins the behavior.
** TODO [#B] Split window opens the dashboard in the other window :feature:quick:solo:
:PROPERTIES:
@@ -122,13 +237,27 @@ Pin with a browser-gate assertion that these preview elements link the right fac
: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 [#B] theme-studio palette ramps + contrast safety v1 :feature:
+** TODO [#B] rulesets page-me notifications should name the source project :feature:quick:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+From the roam inbox: the page-me workflow should always identify which project emitted the notification. Add or request a project/source label in the rulesets notification path so a page is actionable when multiple agents/projects are active. Not marked solo here because the implementation likely belongs in the rulesets project rather than this Emacs config.
+
+** TODO [#C] archsetup Waybar Wi-Fi module should show no-internet state :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+From the roam inbox: the Waybar Wi-Fi module should distinguish "connected to an access point" from "connected and has internet." Add a no-internet state or indicator to the archsetup Waybar configuration. Not marked quick/solo because it needs the archsetup environment and live network-state verification.
+
+** DONE [#B] theme-studio palette ramps + contrast safety v1 :feature:
+CLOSED: [2026-06-13 Sat]
:PROPERTIES:
:LAST_REVIEWED: 2026-06-10
:END:
The v1 build from [[file:docs/theme-studio-palette-ramps-spec.org][theme-studio-palette-ramps-spec.org]] (Ready, Codex-reviewed). Two coupled features: a ramp generator (one base color → harmonized tonal ramp) and background-contrast safety (worst-case floor over a face's foreground set + safe-lightness guidance).
All five phases + the README close-out landed 2026-06-09 (commits 1d51a332, 9da6c663, e7021bfe, 1d8b9f9e, 843bbf08, 23926837); =make theme-studio-test= green (78 node tests, 12 browser gates). Code-complete and self-verified. Remaining: the aesthetic and real-Emacs-fidelity sign-off under the Manual testing parent below (does a ramp harmonize, does the safe band read clearly, does a "safe" tint actually read behind real syntax). Mark this DONE once that passes.
+Retired 2026-06-13: this parent is no longer active planning work. The useful pieces are already in the current tool, and later structural-column work replaced the standalone ramp workflow.
*** 2026-06-09 Tue @ 18:40:20 -0500 Ramp generator core landed
Phase 1 (commit =1d51a332=). =ramp(baseHex, {n, stepL, chromaEase})= in app-core.js → ={steps: [{hex, clamped, offset}], adjusted}= or ={steps: [], error: 'bad-hex'}=. Holds the OKLCH hue, steps lightness by =stepL=, quadratic chroma-ease toward the extremes, gamut-clamps each step; knobs clamp to range with the clamped knob named in =adjusted= (n=2/stepL=0.08/chromaEase=0.5 defaults). 10 node tests (mid/near-white/near-black bases, hue-hold, chroma ease, knob clamping, malformed hex), suite 55→65, =make theme-studio-test= green. The app-core integrity stripper now drops =import= lines too.
*** 2026-06-09 Tue @ 19:06:46 -0500 Ramp UI in palette landed
@@ -142,13 +271,15 @@ Phase 5 (commit =843bbf08=). The OKLCH picker gets a "safe for" selector over th
*** 2026-06-09 Tue @ 19:06:46 -0500 README + test-surface close-out landed
Commit =23926837=. README documents the ramp controls and defaults, the worst-case floor / limiting foreground, the five covered faces, the safe-lightness guidance, and WCAG-drives-PASS-FAIL with APCA as a diagnostic; the browser-gate list is updated. =make theme-studio-test= carries all new node tests and the #ramptest/#contrasttest/#safetest gates. All acceptance criteria met.
-** TODO [#B] theme-studio color families :feature:
+** DONE [#B] theme-studio color columns :feature:
+CLOSED: [2026-06-13 Sat]
:PROPERTIES:
:LAST_REVIEWED: 2026-06-11
:END:
Show the palette as hue-grouped strips (dark→light) over the existing flat, individually-editable palette. Grouping is by OKLCH hue from the hex, so renaming a color never moves it. A per-strip count control generates a symmetric ramp (N → base ±N) from the strip's most-saturated color; regenerate is authoritative, repointing surviving-step references by lightness rank and leaving removed-step references a visible "(gone)". The ground strip is synthesized from the bg/fg assignments and pinned first; the standalone ramp panel is removed. Designed in [[file:docs/theme-studio-color-families-spec.org][docs/theme-studio-color-families-spec.org]]. Codex-reviewed Ready 2026-06-10 after response folded: pivoted from name-derived families to hex-derived families over a flat palette, which designs out the name-grammar/import-inference and chip-ownership blockers. All review findings dispositioned; both open decisions resolved. Builds on and supersedes the palette-ramps v1 ramp UI.
All six phases landed 2026-06-10 (commits ebe18d51, 74db9a52, 111687b0, e7ae18c4, 77783126, f6ab0001, 9daeff15, and the Phase 6 commit); =make theme-studio-test= green (98 node tests, 16 browser gates). Code-complete and self-verified. The hue-adjacent warm-color grouping limitation is filed as a separate research task (=~/color-sorting.org=). Remaining: the manual aesthetic/fidelity sign-off under the Manual testing parent (hue grouping reads right, regenerate-replace reads as deliberate, removed-step "(gone)" is clear). Mark this DONE once that passes.
+Retired 2026-06-13: the current implementation no longer uses color-derived hue families. Palette entries carry stable structural column ids, generated colors stay in their originating column, renames do not move tiles, and =#columntest= / =#roundtriptest= pin the behavior.
*** 2026-06-10 Wed @ 01:17:45 -0500 Family model core landed
Phase 1 (commit =ebe18d51=, grouping reworked in =77783126=). =familiesFromPalette=, =regenFamily=, =rankByLightness=, =stepRepointPlan= in app-core.js, pure and hex-derived. Grouping started as gap-clustering + flat neutral threshold; after the design discussion it became nearest-hue-anchor bucketing (no single-linkage chaining) + a lightness-scaled neutral threshold (pale tints keep their hue, mid grays go neutral). regenFamily handles n=0 without ramp()'s clamp; stepRepointPlan maps survivors / lists removed by signed lightness rank. 20 node tests including the green/yellow split and the no-chaining case. Open: hue-adjacent warm colors still merge — research task above (=~/color-sorting.org=).
*** 2026-06-10 Wed @ 01:17:45 -0500 Family sort core landed
@@ -189,12 +320,18 @@ 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 [#B] theme-studio guide-support features :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
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
[[file:docs/design/theme-studio-seeding-engine-spec.org][theme-studio-seeding-engine-spec.org]] — role table + face→role maps for syntax/UI/org, OKLCH shade generation, reseed dupre-revised to the compact mapping. Codex-reviewed, Ready. Implementation tracked under the seeding-engine parent below.
*** TODO Guide-support views and advisories spec
Five optional surfaces, all dismissible and non-blocking, in one collapsible panel where they advise: (1) CVD-simulation toggle on previews (deuteranopia/protanopia/tritanopia); (2) squint/blur preview toggle; (3) lightness-ramp view + palette advisories (accent count over 6-8, roles separated only by red/green) — depends on the OKLCH/ΔE core; (4) definition-vs-call / weight advisories; (5) state-over-syntax preview (region/search/diff tint over real syntax-colored text). Sequence: rewritten guide reviewed → seeding-engine spec → this. Advisories (3, 4) layer on the perceptual-metrics feature.
** TODO [#B] theme-studio seeding engine :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
Spec (Ready): [[file:docs/design/theme-studio-seeding-engine-spec.org][spec]]. Role table → guide-correct defaults for syntax/UI/org; reseed dupre-revised.json to the compact mapping; opens seeded with an all-tier reseed button. Depends on the perceptual-metrics colormath.js core for OKLCH shade generation, so it runs after that feature's Phase 1.
*** TODO Seed model + seed() + #seedtest :solo:
Phase 1. Palette anchors + OKLCH shade generation (reusing colormath.js), the ROLES table, and the three face→role maps as data; pure seed(). Gate: #seedtest asserts representative syntax/UI/org faces resolve correctly (bi→blue-grey, fnd→gold+bold, region bg-only, link underlined, org-level-1 strongest, org-code literal lane) and a non-org bespoke package (magit) keeps its curated seed.
@@ -206,7 +343,7 @@ Keep #seedtest, #selftest, the default-on-open check, the dupre-revised round-tr
:PROPERTIES:
:LAST_REVIEWED: 2026-06-06
:END:
-pressing g has should refresh. find another binding for Telegram.
+On the dashboard, =g= should refresh the buffer (=dashboard-refresh-buffer=); =g= currently opens Telegram (=cj/telega=, dashboard-config.el:88), so move Telegram to another key. F1 (=cj/dashboard-only=) should also run a refresh at the end, so re-showing the dashboard always lands on fresh content. F1-refresh folded in from the roam inbox 2026-06-13.
** TODO [#B] TTY-accessible personal C-; keymap :feature:solo:quick:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-05
@@ -759,25 +896,41 @@ Tie this into the existing coverage work:
:LAST_REVIEWED: 2026-06-01
:END:
** TODO [#B] Messenger window/key unification :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
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 [#C] cj/undo-kill-buffer skip-visited uses delq (eq) on path strings :bug:quick:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
+=modules/ui-navigation.el= — the visited-file filter calls =(delq buf-file recently-killed-list)= where =buf-file= is a fresh string from =expand-file-name=, never =eq= to the =recentf-list= entries, so already-open files are never skipped (the skip logic is dead). Use =delete= (equal-based). Found 2026-06-12 while fixing the off-by-one above; the two bugs cancel exactly when one file is open, which is why it went unnoticed.
-** TODO [#B] reconcile-open-repos skips any repo with a dot in its name :bug:solo:
+** DOING [#B] reconcile-open-repos skips any repo with a dot in its name :bug:quick:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
=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.
+*** 2026-06-13 Sat @ 11:01:44 -0500 Fixed: regexp swapped + hidden-dir check
+=cj/find-git-repos= now iterates =directory-files-no-dot-files-regexp= (keeps dotted names, drops =.=/=..=) plus a =string-prefix-p "."= guard for hidden dirs. Regression test =test-find-git-repos-boundary-dotted-repo-name-found= (mcp.el/capture.el/plain-repo → 3 found); existing hidden-dirs-skipped test stays green; 11/11. Live daemon confirmed all six dotted repos under ~/code now discovered (mcp.el, gptel-mcp.el, capture.el, google-contacts.el, google-maps.el, auto-dim-other-buffers.el). Awaiting Craig's confirm → DONE.
** TODO [#B] jumper: register collisions and dead-marker errors :bug:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
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:
+** DOING [#B] C-s C-s vertico-repeat path never works :bug:quick:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-13
+:END:
=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.
+*** 2026-06-13 Sat @ 10:59:52 -0500 Fixed: vertico-repeat-save hooked
+Added top-level =(add-hook 'minibuffer-setup-hook #'vertico-repeat-save)= after the vertico use-package block (placed top-level, not inside use-package, so the stub-use-package test exercises it; =vertico-repeat-save= is autoloaded, deferring the load to first minibuffer). New test asserts hook membership; 5/5 green; evaled into the live daemon (=:on-hook= now t). Awaiting Craig's confirm → DONE.
** 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).
@@ -785,11 +938,10 @@ From the 2026-06 config audit. =modules/auth-config.el:88= — bare =(call-proce
** 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:
+** DONE [#B] markdown live preview clobbered by markdown-mode :bug:quick:solo:
+CLOSED: [2026-06-13 Sat]
=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.
+Fixed 2026-06-13: renamed the defun to =cj/markdown-preview= and rebound =<f2>=; guard is now =(httpd-running-p)=; deleted the dead =(setq imp-set-user-filter 'markdown-html)= (impatient-mode use-package is now =:defer t= only). Added a guard test (server-down → user-error); 4/4 green; live daemon confirms =cj/markdown-preview= defined and guarding. Browser-render check is a VERIFY.
** TODO [#B] agenda sources: roam Projects missing, no existence filtering :bug:solo:
From the 2026-06 config audit, =modules/org-agenda-config.el=:
@@ -808,9 +960,6 @@ From the 2026-06 config audit, =modules/calendar-sync.el=:
- =: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).
@@ -835,20 +984,20 @@ From the 2026-06 config audit, =modules/slack-config.el=:
** 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:
+** DONE [#B] dwim-shell: zip overwrites its own name, backup timestamp never expands, dired menu key dead :bug:quick:solo:
+CLOSED: [2026-06-13 Sat]
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.
+Fixed 2026-06-13: zip single-file template now ='<<fne>>.zip'=; backup uses =format-time-string= in Elisp (real =YYYYMMDD_HHMMSS= stamp, dropped the now-unneeded =date= util); dired key M-S-d→M-D + dirvish-config.el:30 doc corrected. Both command strings extracted into top-level builders (=cj/dwim-shell--zip-single-file-command=, =cj/dwim-shell--dated-backup-command=) so they're unit-testable without the dwim-shell-command package — the command defuns live in its use-package :config, which the batch harness doesn't load. 2 builder tests green in make; live daemon confirms all three (backup stamp, .zip, dired M-D). Real backup/zip run + the dired keypress are a VERIFY.
** 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.
+The global electric-pair this turns on also paired "<" in org, stranding a ">" after "<"-key snippets (=#+end_src>=, broke cj-scan). That symptom is fixed separately (=d9c90e83=, an =electric-pair-inhibit-predicate= for "<"). This task remains the root fix: pairing should not be global at all.
** 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.
@@ -874,11 +1023,13 @@ Nothing requires =modules/ledger-config.el= (verified by grep), so .dat/.ledger/
** 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:
+** DONE [#B] help-config: three defects in one small file :bug:quick:solo:
+CLOSED: [2026-06-13 Sat]
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).
+Fixed 2026-06-13: (1) extracted the save/cancel/open decision into a pure =cj/--info-open-plan= and routed =cj/open-with-info-mode= through it — no more =cl-return-from=, declining cancels cleanly; (2) deleted the dead =:hook= and the empty =:preface=; (3) dropped the destructive =auto-mode-alist= .info entry (kept =cj/open-with-info-mode= as an M-x command and =cj/browse-info-files= on C-h i). New test-help-config.el covers the planner (open / save-then-open / cancel) — 3 green; module loads clean. Stale daemon state (the .info auto-mode entry + the bogus info-mode-hook entry) cleared by hand so the running session is correct without a restart. Interactive Info open + find-file-no-longer-destructive are a VERIFY.
** 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.
@@ -886,8 +1037,10 @@ From the 2026-06 config audit, =modules/help-config.el=:
** 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:
+** DONE [#B] vertico-prescient clobbers orderless filtering :bug:quick:solo:
+CLOSED: [2026-06-13 Sat]
=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.
+Fixed 2026-06-13: added =:custom (vertico-prescient-enable-filtering nil)= to the vertico-prescient use-package. Live daemon confirmed filtering nil + =completion-styles (orderless basic)= with the mode re-enabled — orderless matches, prescient sorts. No ERT test (framework defcustom, unexercisable in the stubbed-use-package harness); the in-session matching check is a VERIFY under Manual testing and validation.
** 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.
@@ -1115,55 +1268,6 @@ Full findings delivered as handoffs to each repo's inbox/ (2026-06-12-0057-from-
- 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:
-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
-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
-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
-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
-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
-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 :test:
-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
-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
-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
-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 :test:
-=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
-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)
-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
-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
-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 :test:
-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:
High-level pass over =init.el=, =early-init.el=, and all 104 files in
@@ -1493,64 +1597,6 @@ Expected outcome:
- 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:quick:solo:
-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:
-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
:PROPERTIES:
:LAST_REVIEWED: 2026-06-04
@@ -2955,12 +3001,14 @@ configuration (=text-config=, =diff-config=, =ledger-config=,
: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:
+** DONE [#C] theme-studio Rust + Zig language previews :feature:
: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.
+Shipped 2026-06-13: Rust and Zig were added to =samples.py= and =generate.py=, with generator tests pinning the language-specific category coverage.
+
** TODO [#C] theme-studio face-consistency check :feature:
:PROPERTIES:
:LAST_REVIEWED: 2026-06-10
@@ -4381,6 +4429,65 @@ From the 2026-06-11 messenger-unification brainstorm. Google Voice has no offici
** TODO Manual testing and validation
Exercised once the phases above land.
+*** VERIFY info-mode open is non-destructive and cancels cleanly
+What we're verifying: opening a .info file no longer auto-kills the buffer, and the explicit cj/open-with-info-mode prompt cancels cleanly on decline. Fixed in modules/help-config.el; stale daemon state already cleared, so this also survives a fresh restart.
+- find-file a .info file (e.g. one under elpa) — it should open as an ordinary buffer, not vanish into Info
+- In that buffer, edit something, then M-x cj/open-with-info-mode; at the save prompt answer no
+- Repeat M-x cj/open-with-info-mode on an unmodified .info buffer
+Expected: find-file leaves the buffer intact (no auto-kill); declining the save prompt prints "Operation canceled" with no "No catch for tag" error; on an unmodified buffer it opens the file in Info.
+*** VERIFY dwim-shell zip/backup/menu-key behave
+What we're verifying: single-file zip makes a valid <name>.zip, the dated backup gets a real timestamp, and the dwim-shell menu is reachable on M-D in plain dired. Fixed in modules/dwim-shell-config.el, reloaded into the daemon.
+- In dired, mark a single file, run the dwim-shell menu (M-D), pick Zip
+- Mark a file, run the menu, pick "Backup with date"
+- Open a plain dired buffer (not dirvish) and press M-D
+Expected: zip produces foo.zip (a valid archive, openable); backup produces foo.ext.YYYYMMDD_HHMMSS.bak with a real date; M-D opens the dwim-shell command menu in plain dired (before the fix it did nothing there).
+*** VERIFY markdown live preview renders in the browser
+What we're verifying: F2 in a markdown buffer runs the custom cj/markdown-preview (not markdown-mode's own command) and the impatient-mode strapdown preview actually renders. Fixed in modules/markdown-config.el, reloaded into the daemon.
+- Open a .md file with some markdown content
+- M-x cj/markdown-preview-server-start (starts simple-httpd on :8080)
+- Press F2 in the markdown buffer
+Expected: a browser opens http://localhost:8080/imp showing the rendered markdown, and edits to the buffer update the preview live. Pressing F2 before starting the server gives a user-error telling you to start it.
+*** VERIFY orderless matching works inside a vertico session
+What we're verifying: vertico-prescient no longer overrides completion-styles, so orderless's space-separated, out-of-order matching is live in the minibuffer (prescient still sorts). Fixed in modules/selection-framework.el, applied live in the daemon.
+- Run a command with a vertico minibuffer (e.g. M-x, or C-x b)
+- Type two space-separated fragments out of order, e.g. "mode buf" to match "switch-to-buffer-other-... mode" style candidates
+Expected: candidates match on both fragments regardless of order (orderless), and the ordering still reflects prescient frecency. Before the fix, space-separated out-of-order input would not match.
+*** VERIFY C-; b d diffs, C-; b D deletes
+What we're verifying: the buffer-and-file keymap now puts diff on the easy lowercase key and the destructive delete on the capital. Swapped in modules/custom-buffer-file.el and re-bound live in the daemon.
+- Open a file buffer and edit it without saving
+- Press C-; b d
+- Press C-; b D, then cancel at the delete confirmation
+Expected: C-; b d runs the diff (buffer vs saved file); C-; b D starts delete-buffer-and-file (offers to delete the file). Before the swap these were reversed.
+*** TODO C-s C-s repeats the last search
+What we're verifying: the second consecutive C-s repeats the previous consult-line search instead of erroring "No Vertico session". Fix in modules/selection-framework.el (vertico-repeat-save now on minibuffer-setup-hook), live in the daemon.
+- Press C-s, type a search term, RET to dismiss (or just narrow then exit)
+- Press C-s again, then C-s a second time without any command in between
+Expected: the second C-s reopens the last search (vertico-repeat) rather than signalling "No Vertico session".
+*** TODO reconcile-open-repos includes dot-named repos
+What we're verifying: M-P (reconcile open repos) now visits repos whose directory name has a dot (mcp.el, capture.el, etc.), which the old "^[^.]+$" filter silently skipped. Fix in modules/reconcile-open-repos.el, live in the daemon; live-daemon check already confirmed discovery, this is the through-the-command spot-check.
+- Run M-P (or M-x cj/reconcile-open-repos)
+- Watch the per-repo progress / final summary
+Expected: dot-named repos under ~/code (mcp.el, gptel-mcp.el, capture.el, google-contacts.el, …) appear in the reconciliation pass, not just dot-free ones.
+*** TODO org-capture quick-capture popup behaves correctly
+What we're verifying: the Hyprland Super+Shift+N popup is single-window, offers only the sensible templates, files to the inbox, never orphans its frame, and runs the capture in the popup even when launched from a focused main frame (archsetup request 2026-06-12; fixes in modules/org-capture-config.el incl. the frame-targeting focus-race fix, all pushed and live). archsetup verified the base case on ratio 2026-06-12; the focus-race fix landed after.
+- Press Super+Shift+N to open the quick-capture popup
+- The *Org Select* menu should fill the popup frame as one window (no top sliver of your last-visited buffer, one modeline) and list only Task / Bug / Event — and NOT split your main frame (the focus-race fix)
+- It should show no "C — Customize org-capture-templates" row
+- Pick Task (t): the CAPTURE buffer also fills the frame as one window; finishing with C-c C-c files it to the global inbox under "Inbox" (not a project's todo.org)
+- Re-open and pick Event (e): it prompts for a date and files to the schedule
+- Re-open and hit q (or C-g) at the menu: the popup frame closes (no orphan)
+Expected: single window at every step; menu limited to Task/Bug/Event; Task/Bug land in the inbox; aborting at the menu closes the frame; the frame still closes on normal finalize and C-c C-k.
+*** TODO Lock screen actually locks on Wayland
+What we're verifying: C-; ! l locks the screen on Wayland. slock (X11-only) never worked here; the locker now runs loginctl lock-session, which logind turns into a Lock signal that hypridle handles by running hyprlock — the same path idle/sleep locking already uses. Fix in modules/system-commands.el, live in the daemon.
+- Press C-; ! l (or run M-x cj/system-cmd-lock)
+- The screen should lock with hyprlock
+- Unlock with your password
+Expected: the screen locks immediately and unlocks with your password. (Before the fix it printed "Running lockscreen-cmd..." and nothing happened.)
+*** TODO Irreversible actions require a typed "yes" after a daemon restart
+What we're verifying: the strong-confirm tier is restored for irreversible actions. The global (fset 'yes-or-no-p 'y-or-n-p) was removed and those sites now call cj/confirm-strong, which forces a typed "yes"/"no". The fset is baked into the running daemon and can't be cleared from Lisp, so this only takes effect after a restart. Ordinary yes-or-no-p prompts stay single-key (use-short-answers t).
+- Restart the Emacs daemon (clean state)
+- Trigger an irreversible action, e.g. M-x cj/system-cmd-shutdown (then abort), or attempt to overwrite a file via the rename/move commands
+Expected: the irreversible prompt requires typing the full word "yes" (not a single y); a benign yes-or-no-p prompt elsewhere still accepts a single keystroke.
*** 2026-06-11 Thu @ 18:29:39 -0500 Verified UI-face preview and contrast survive a ground bg change
Craig walked the repro: mode-line with its own fg/bg kept its preview bg and ratio through a ground change; ground-dependent rows re-rated; package-faces contrast column updated. Pass. Closed the [#A] contrast-cell and [#B] preview-bg parents.
*** 2026-06-11 Thu @ 18:29:39 -0500 Verified seeded package-face defaults, with steel tuning
@@ -4433,6 +4540,38 @@ What we're verifying: lowering a family's count leaves a referencing face visibl
- Lower that family's count to 2 so blue+3 disappears
- Read the assignment's dropdown
Expected: the dropdown shows "(gone)" for the removed step, never a silent jump to a different color; re-pointing it is a deliberate choice.
+*** TODO Calibre bookmark default name is "Author, Title"
+What we're verifying: a new nov bookmark takes the "Author, Title" form parsed from the filename, not the raw EPUB filename.
+- Open an EPUB in Calibre (nov buffer).
+- Hit m to set a bookmark.
+Expected: the default bookmark name is "Author, Title" (underscores stripped, colon restored), e.g. "Agatha Christie, The A.B.C. Murders".
+
+*** TODO Calibre curated ? menu and docked description
+What we're verifying: the curated ? transient, the docked description, and the full dispatch all work in a live calibredb buffer.
+- In a calibredb search buffer, press ? and confirm the curated menu (library / filter / sort / open / describe) appears.
+- Press d or v to dock the selected book's description in a bottom-30% buffer; press q to dismiss it.
+- Press H and confirm calibredb's full dispatch opens.
+Expected: ? shows the curated menu, d/v dock the description (q dismisses), H opens the full calibredb dispatch.
+
+*** TODO 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.
+
+*** TODO 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.
+
+*** TODO Project-aware capture files into the right todo.org
+What we're verifying: C-c c t and C-c c b file into the current projectile project's todo.org under its "<Project> Open Work" header, and fall back to the global inbox outside a project.
+- Inside a projectile project that has a todo.org, run C-c c t (Task), capture a test entry, and confirm it lands under "<Project> Open Work".
+- Run C-c c b (Bug) similarly and confirm it lands as "* TODO [#C] ..." under the same header.
+- Run a capture from outside any project (or a project with no todo.org) and confirm the global-inbox fallback with a warning.
+Expected: in-project captures land in that project's Open Work; out-of-project captures fall back to the global inbox with a warning.
+
** TODO [#D] theme-studio per-tier reseed controls :feature:
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:
@@ -4495,15 +4634,270 @@ Task: survey the modes/modules Craig works in and identify where a =?= -> curate
** TODO [#C] the preview splits an already split window into 3 temporarily.
looks strange. potentially problematic for ai-terms.
-** DOING [#C] Project-aware bug capture via C-c c t :feature:
-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.
-
** VERIFY [#C] Palette-columns spec review
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.
+** VERIFY [#C] page-signal pager account deregistered — re-registration needs your hands
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-12
+:END:
+Reported by .emacs.d 2026-06-12 01:01: the dedicated pager number (+15045173983, the Claude Pager Google Voice number on signal-cli) returns "User ... is not registered" on every send — Signal appears to have deregistered it (GV numbers get periodically re-verified). Re-registration requires captcha/SMS, which only you can do. Until then every page-signal call fails; .emacs.d's config-audit page fell back to email. Wrapper lives at claude-templates/bin/page-signal.
+
+** Emacs Packages — Curl-Friendly Web Service Wrappers
+Ideas for new Emacs packages following the same pattern as wttrin: HTTP GET to a simple web service, render results in a buffer, optionally show summary in the mode-line. All of these share the async fetch + caching infrastructure already proven in wttrin.
+Captured On: [2026-04-04 Sat]
+*** TODO Stock Market / Finance Package (Finnhub or Alpha Vantage)
+Build a stock watchlist and quote viewer for Emacs. User defines a list of symbols; package fetches quotes and renders a formatted table in a dedicated buffer. Optional mode-line ticker showing one or more symbols rotating on a timer.
+
+**** Features
+- Customizable watchlist: user defines a list of stock symbols in a defcustom; package fetches and displays all of them in a single buffer
+- Formatted quote table: symbol, company name, current price, daily change (absolute and percent), volume — color-coded green/red for gains/losses
+- ASCII sparkline charts: inline mini-charts showing intraday or multi-day price movement using Unicode block characters (▁▂▃▅▇ style)
+- Mode-line ticker: rotating display of one or more symbols with price and change indicator, similar to wttrin's weather widget — click to open the full watchlist buffer
+- Detail view: press RET on a symbol to see extended data — open/high/low/close, 52-week range, market cap, P/E ratio (data availability depends on backend)
+- Auto-refresh with market awareness: background timer fetches new data during market hours; pauses on weekends and after-hours to conserve API calls
+- Unit/currency preference: display prices in local currency if the backend supports it
+- Cache layer: same pattern as wttrin — serve cached data instantly, refresh in background, show staleness indicator when data is old
+- Interactive symbol lookup: ~M-x stock-add-symbol~ with completion against a symbol database or search endpoint
+
+**** What you'd learn
+- JSON parsing in elisp (~json-parse-buffer~, ~json-read~) — these APIs return JSON, not plain text, so this is the main new skill vs. wttrin
+- ASCII chart rendering — drawing sparklines or simple price charts with Unicode block characters in a buffer
+- API key management in elisp — storing keys in ~auth-source~ or custom variables, passing them as query params
+
+**** Where the complexity lives
+- Rendering: No pre-formatted ASCII comes back from the API. You'd build the table layout and any chart visualization yourself. This is the bulk of the work.
+- Market hours awareness: Knowing when to fetch (pre-market, regular, after-hours, weekends) to avoid wasting API calls.
+- Rate limiting: Free tiers are tight. Finnhub gives 60 calls/min which is generous; Alpha Vantage gives only 25/day on the free tier. Caching strategy matters more here than in wttrin.
+
+**** Candidate backends
+- Finnhub (finnhub.io): Free API key, 60 calls/min, real-time US quotes. JSON only. Best rate limit of the free options.
+- Alpha Vantage (alphavantage.co): Free API key, 25 calls/day. Supports CSV output which is trivial to parse — no JSON needed. Good for daily summaries, bad for frequent polling.
+- Twelve Data (twelvedata.com): Free key, 800 calls/day, 8/min. Covers stocks, forex, crypto, ETFs. JSON and CSV.
+
+**** Downsides
+- API key requirement adds friction for users (signup, config). Not as frictionless as wttrin.
+- Rate limits mean you can't poll aggressively. Stale data is the norm on free tiers.
+- Financial APIs change or shut down. Yahoo Finance's unofficial API has broken repeatedly over the years. Even paid services deprecate endpoints. Expect maintenance.
+- Finnhub and Alpha Vantage are US-market-centric. International coverage varies.
+
+**** Effort: Medium-High
+The fetch/cache layer is straightforward (reuse wttrin patterns). The rendering layer (tables, charts, color-coding gains/losses) is where most of the time goes. Expect this to be a real project, not a weekend hack.
+
+**** Name candidates (backronyms)
+Pick one. All are recursive (self-referential) in the style of CHIME.
+- BULL — *BULL Updates Live Listings*
+- MINT — *MINT Indexes Noteworthy Tickers*
+- QUOTE — *QUOTE Updates Ongoing Ticker Estimates*
+- ASSET — *ASSET Surfaces Stock Exchange Tickers*
+- MOAT — *MOAT Monitors Active Tickers*
+- TRADE — *TRADE Reveals Active Daily Equities*
+- BELL — *BELL Exhibits Live Listings*
+- CHART — *CHART Highlights Asset Rate Tickers*
+- BOARD — *BOARD Oversees Asset Rate Data*
+- VAULT — *VAULT Aggregates Underlying Listing Tickers*
+
+*** TODO rate.sx Wrapper — Cryptocurrency Rates
+Wrap Igor Chubin's rate.sx service. This is the lowest-effort, highest-pattern-match option — rate.sx works exactly like wttr.in. Returns colored ASCII tables with sparkline charts. Same ~User-Agent: curl~ trick, same ANSI escape codes.
+
+**** Features
+- Full crypto dashboard: ~M-x rate-sx~ opens a buffer with a colored ASCII table of top cryptocurrencies — name, price, 24h change, market cap, and sparkline charts — all rendered by the service
+- Single coin lookup: ~M-x rate-sx-coin~ prompts for a coin name (e.g., ~eth~, ~btc~) and displays its detailed view
+- Plain price fetch: query ~rate.sx/1BTC~ to get a single numeric price — useful for mode-line display or programmatic use from other elisp
+- Mode-line widget: show the price of one or more coins in the mode-line with periodic background refresh, similar to wttrin's weather indicator
+- Customizable coin list: user picks which coins appear in the dashboard via a defcustom
+- Currency base selection: rate.sx supports displaying prices in different fiat currencies
+- ANSI color rendering: reuse wttrin's ~xterm-color~ pipeline to convert the service's colored ASCII output into Emacs faces
+- Cache with background refresh: same timer-based pattern as wttrin — data stays warm, buffer opens instantly
+
+**** What you'd learn
+- Very little new — this is almost a copy of wttrin with different URL construction. Good first project if you want to validate the pattern before tackling stocks.
+- Could explore sharing infrastructure between wttrin and this package (common async fetch, caching, ANSI rendering).
+
+**** Where the complexity lives
+- Minimal. The service does the formatting. Your job is URL construction, fetch, ANSI-to-faces conversion (already solved in wttrin via ~xterm-color~), and buffer display.
+- Coin selection UX: letting users pick which coins to show, custom vs. top-N, etc.
+
+**** Downsides
+- Single point of failure: rate.sx is one person's side project. If Chubin takes it down, the package is dead. No fallback.
+- Crypto-only. No traditional stocks, forex, or commodities.
+- Less useful than a stock package for most people.
+
+**** Effort: Low
+Could reuse 70-80% of wttrin's code. A weekend project if you're focused.
+
+*** TODO Frankfurter Currency Exchange Package
+Wrap the Frankfurter API (frankfurter.dev) for fiat currency conversion and historical rates. ECB data, open source, no API key.
+
+**** Features
+- Quick conversion: ~M-x currency-convert~ prompts for amount, base currency, and target currency — displays the result in the echo area (e.g., "100 USD = 91.34 EUR")
+- Multi-target conversion table: convert one amount against several currencies at once, rendered as an aligned table in a dedicated buffer
+- Historical rate lookup: query a specific date's exchange rate — useful for expense reports, invoicing, or curiosity
+- Rate trend view: fetch a date range and display a table or ASCII sparkline showing how a currency pair moved over days/weeks/months
+- Latest rates dashboard: ~M-x currency-latest~ shows today's rates for a user-defined set of currency pairs in a buffer
+- Interactive currency selection: completing-read over the ~30 supported currencies with full names (e.g., "USD — United States Dollar")
+- Mode-line rate display: optionally show one currency pair's rate in the mode-line with daily background refresh
+- Cache layer: rates only update once per business day, so caching is especially effective — fetch once, serve all day
+
+**** What you'd learn
+- JSON parsing in elisp (the API returns JSON, not formatted text)
+- Table rendering — building aligned currency tables with ~format~ and text properties
+- Historical data display — the API supports date ranges, so you could show rate trends over time
+
+**** Where the complexity lives
+- Rendering: You'd build the table and any trend visualization yourself.
+- Date handling in elisp for historical queries (~encode-time~, ~format-time-string~, etc.).
+- UX: interactive base/target currency selection with completion.
+
+**** Downsides
+- ECB data updates once per business day. No real-time rates — this is reference data, not trading data.
+- Covers ~30 currencies (major fiats). No crypto, no exotic currencies.
+- Frankfurter is open-source and self-hostable, which is good for longevity, but the public instance could still go away.
+
+**** Effort: Low-Medium
+JSON parsing adds a step vs. wttrin's plain text, but the API is clean and well-documented. Straightforward project.
+
+*** TODO ipinfo.io — IP and Geolocation Lookup
+~curl ipinfo.io~ returns JSON with your public IP, city, region, country, ISP, and timezone. No auth needed for basic lookups (1000 requests/day unauthenticated).
+
+**** Features
+- My IP: ~M-x ipinfo~ fetches your public IP and geolocation, displays a formatted summary in a buffer or the echo area — IP, city, region, country, ISP, timezone, coordinates
+- Arbitrary IP lookup: ~M-x ipinfo-lookup~ prompts for an IP address and shows the same geolocation detail
+- Copy IP to kill ring: one-keystroke convenience for grabbing your public IP
+- Open in browser map: command to open the returned lat/long coordinates in OpenStreetMap or Google Maps via ~browse-url~
+- Hostname resolution: the API also returns the reverse DNS hostname for an IP
+- Mode-line IP display: optionally show your current public IP in the mode-line (useful when switching between networks/VPNs)
+- Org-mode integration: insert IP/geo info as an org property block or table row at point
+
+**** What you'd learn
+- Minimal new skills — simple JSON response, single fetch, render in buffer or echo area.
+- Could add map integration (open coordinates in browser or an Emacs map package).
+
+**** Where the complexity lives
+- Almost nowhere. This is the simplest possible package. Fetch JSON, format it, display it.
+- If you want to look up arbitrary IPs (not just your own), add a prompt with completion history.
+
+**** Downsides
+- Very niche utility. You look up your IP occasionally, not daily.
+- Free tier is generous (1000/day) but authenticated lookups require a token for enriched data.
+- Privacy-conscious users may not want to send their IP to a third party (though they already do by virtue of connecting).
+
+**** Effort: Very Low
+An afternoon project. Good as a learning exercise for the fetch-parse-render pattern if you haven't done JSON APIs in elisp before.
+
+*** TODO icanhazdadjoke.com — Dad Jokes in Emacs
+~curl -H "Accept: text/plain" https://icanhazdadjoke.com~ returns a single plain-text joke. No auth, no key, no rate limit concerns for casual use.
+
+**** Features
+- Random joke: ~M-x dad-joke~ fetches a random joke and displays it in the echo area — minimal disruption, maximum groan
+- Joke buffer: ~M-x dad-joke-buffer~ opens a dedicated buffer with a joke, nicely formatted with a large font face. Press ~n~ for the next joke, ~q~ to quit
+- Search jokes: ~M-x dad-joke-search~ prompts for a term (e.g., "cat") and displays matching jokes in a buffer — the API supports ~?term=~ search
+- Startup joke: optional hook to display a dad joke in the echo area or scratch buffer on Emacs startup
+- Org-mode insertion: ~M-x dad-joke-insert~ inserts a joke at point — for lightening up documentation or commit messages
+- Kill ring: ~M-x dad-joke-yank~ fetches a joke and puts it directly in the kill ring for pasting elsewhere
+
+**** What you'd learn
+- Nothing technically new — this is the simplest possible HTTP-GET-to-buffer pattern.
+- Good excuse to experiment with fun presentation: display in echo area, dedicated buffer, or even as a startup message.
+
+**** Where the complexity lives
+- It doesn't. Fetch a string, display it. The API also supports search (~?term=dog~) if you want to add that.
+
+**** Downsides
+- Toy project. Zero practical utility beyond morale.
+- The joke quality is... dad jokes.
+
+**** Effort: Trivial
+An hour, maybe two if you add search and a nice buffer layout. Publishable on MELPA as a novelty package.
+
+*** TODO qrenco.de — QR Code Generator
+Chubin's QR code service. ~curl qrenco.de/hello~ returns a QR code rendered in Unicode block characters. Encodes arbitrary text, URLs, WiFi credentials, etc.
+
+**** Features
+- Encode text: ~M-x qr-encode~ prompts for a string and displays the QR code in a dedicated buffer using Unicode block characters
+- Encode region: ~M-x qr-encode-region~ encodes the currently selected text — quick way to QR-ify a URL, password, or snippet
+- Encode URL at point: ~M-x qr-encode-url~ detects the URL under point (via ~thing-at-point~) and generates a QR code for it
+- WiFi QR codes: ~M-x qr-wifi~ prompts for SSID, password, and encryption type, then generates the standard WiFi QR format (~WIFI:T:WPA;S:MyNetwork;P:password;;~) — scan to join a network
+- Buffer font management: automatically sets the buffer to a monospace font with consistent Unicode block rendering (same approach as wttrin's Liberation Mono override)
+- Copy as text: yank the QR code's block characters to the kill ring for pasting into emails, READMEs, or chat
+- Adjustable size: the service supports size parameters — expose this as a prefix argument or defcustom
+
+**** What you'd learn
+- Handling Unicode block character output (not ANSI colors this time, but character-level rendering)
+- Interactive input patterns — prompting for text to encode, or encoding the current region/URL at point
+
+**** Where the complexity lives
+- Font and character width: QR codes require a monospace font where the block characters render at consistent widths. Some Emacs font configurations break this. You'd need to set the buffer font explicitly (like wttrin does).
+- The service sometimes returns ANSI codes for inverted colors. May need ~xterm-color~ or manual processing.
+
+**** Downsides
+- Same single-point-of-failure risk as rate.sx — one person's service.
+- QR codes in a terminal/buffer are inherently lower resolution than image-based ones. Scanning reliability depends on terminal font size and screen.
+- Niche use case. Most people generate QR codes infrequently.
+
+**** Effort: Low
+Similar to rate.sx in scope. The fetch is trivial; font handling and display are the main considerations.
+
+*** TODO dns.toys — Multi-Tool Utility via DNS
+dns.toys answers queries over DNS instead of HTTP. ~dig 100USD-EUR.fx @dns.toys~ returns currency conversion, ~dig mumbai.time @dns.toys~ returns world time, ~dig 42km-mi.unit @dns.toys~ does unit conversion. Also supports base conversion, math constants, and more.
+
+**** Features
+- Currency conversion: ~M-x dns-toys-currency~ prompts for amount and currency pair (e.g., "100 USD to EUR"), displays result in echo area
+- World time: ~M-x dns-toys-time~ prompts for a city name and shows the current local time — faster than searching online, no browser needed
+- Unit conversion: ~M-x dns-toys-unit~ prompts for a value and unit pair (e.g., "42 km to mi"), returns the conversion
+- Base conversion: ~M-x dns-toys-base~ converts between decimal, hex, octal, and binary (e.g., "100 dec to hex")
+- Math constants: ~M-x dns-toys-constant~ looks up pi, e, tau, etc. — niche but handy in a calc session
+- Unified command: ~M-x dns-toys~ with a smart prompt that detects query type from input format, dispatching to the right DNS query automatically
+- Echo area results: all results display in the echo area by default for quick non-disruptive answers, with an optional dedicated buffer for history
+- Async queries: use ~start-process~ with sentinels so ~dig~ calls don't block Emacs
+
+**** What you'd learn
+- Calling external processes from elisp (~call-process~ or ~start-process~ to invoke ~dig~) instead of ~url-retrieve~. This is a meaningfully different integration pattern from wttrin.
+- Parsing DNS TXT record output — dig returns structured but noisy output; you'd extract the answer section.
+- Building a multi-function package — this one service covers currency, time, units, and base conversion, so the UX needs a dispatch mechanism (separate commands, or a unified prompt with type detection).
+
+**** Where the complexity lives
+- Output parsing: ~dig~ output is not designed for human consumption. You'd parse the ANSWER SECTION, strip TTL/class/type fields, and extract the payload string.
+- Latency: DNS queries are fast but ~call-process~ on ~dig~ has subprocess overhead. For interactive use this is fine; for mode-line updates you'd want async (~start-process~ with a sentinel).
+- Feature breadth: The temptation is to wrap every dns.toys feature. Scoping to a focused set (currency + time + units) keeps it manageable.
+
+**** Downsides
+- Requires ~dig~ installed (standard on Linux/macOS, not on Windows). Limits portability.
+- dns.toys is a single maintainer's project. Same fragility concern as rate.sx and qrenco.de.
+- DNS protocol means no rich formatting — just short text strings. The results are useful but visually plain.
+- Some networks/firewalls block non-standard DNS queries, which would silently break the package.
+
+**** Effort: Low-Medium
+The individual queries are trivial. The interesting work is building a clean multi-function UX and handling the process-based (vs. HTTP-based) integration pattern. Good project for learning elisp process management.
+
+*** TODO cheat.sh Integration — Programming Cheat Sheets
+~curl cheat.sh/tar~ returns a syntax-highlighted cheat sheet. Supports language-specific queries like ~cheat.sh/python/lambda~. Already has some Emacs integrations (cheat-sh.el exists) but could be worth a custom implementation if existing packages don't fit your workflow.
+
+**** Features
+- Quick lookup: ~M-x cheat-sh~ prompts for a topic (e.g., "tar", "git/stash") and displays a syntax-highlighted cheat sheet in a dedicated buffer
+- Language-scoped queries: ~M-x cheat-sh-lang~ prompts for language then topic (e.g., ~python/lambda~, ~go/goroutine~) with two-stage completion
+- Context-aware lookup: detect the current buffer's major mode and scope the query to that language automatically — in a Python buffer, querying "lambda" goes to ~cheat.sh/python/lambda~
+- ANSI-to-faces rendering: convert the service's syntax-highlighted ANSI output to proper Emacs font-lock faces using ~xterm-color~ (same pipeline as wttrin)
+- Navigation: browse related topics from within the buffer — follow-up queries without returning to the minibuffer. Previous/next topic history with ~p~ / ~n~
+- Completion against the topic list: fetch and cache ~cheat.sh/:list~ to provide completing-read over all available topics
+- Offline cache: optionally cache previously viewed cheat sheets for offline access or instant re-display
+- Region query: select a command or function name and look it up directly with ~M-x cheat-sh-region~
+
+**** What you'd learn
+- ANSI syntax highlighting → Emacs faces (same skill as wttrin)
+- Deep completion support: cheat.sh has a massive topic tree. Building good interactive completion for ~cheat.sh/{language}/{topic}~ is a UX challenge.
+
+**** Where the complexity lives
+- Completion and navigation: the value is in making it fast to find the right cheat sheet. ~cheat.sh/:list~ returns thousands of entries.
+- Existing packages: ~cheat-sh.el~ already exists on MELPA. You'd need a reason to build your own (better caching, offline support, integration with your workflow).
+
+**** Downsides
+- Overlaps with existing Emacs packages. Check ~cheat-sh.el~ before building.
+- The service aggregates from many sources. Quality is inconsistent across topics.
+
+**** Effort: Medium
+If building from scratch. Low if extending or wrapping an existing package. The completion UX is where the effort goes.
* Emacs Resolved
** DONE [#B] Fix likely =elpa-mirror-location= path bug :bug:quick:
CLOSED: [2026-05-03 Sun]
@@ -7828,3 +8222,64 @@ CLOSED: [2026-06-11 Thu]
In the UI faces table, the preview cell for a face with its own bg renders with the ground bg instead. Repro: set mode-line fg=black, bg=blue — the preview cell should be black text on blue, but shows black on black (the live buffer mode-line is fine). Root cause: =applyGround= (app.js:300) blankets EVERY =.ex= element's background to =MAP['bg']=, and the preview cell =cP= shares =className='ex'= (app.js:753), so it clobbers the per-face bg =paintUI= sets (app.js:739) — runs on load and on every ground change. Fix: stop applyGround from touching the UI-face preview cells (scope its =.ex= selector to the code/example cells, give the preview cell its own class, or re-run paintUI after). The contrast cell shares the same staleness, so confirm both.
*** 2026-06-10 Wed @ 14:40:22 -0500 Fixed by the applyGround scoping under the contrast-cell task
Same root cause as the [#A] contrast-cell task, fixed there in one change: =applyGround= scopes its blanket to =#legbody .ex= + the code panes and repaints UI faces through =paintUI=. #contrasttest pins the preview-bg survival. Awaiting the same repro check.
+** DONE [#B] cj/undo-kill-buffer off-by-one on plain invocation :bug:quick:solo:
+CLOSED: [2026-06-12 Fri]
+Fixed in =modules/ui-navigation.el=: indexing is now =(nth (1- arg) ...)=, so a numeric prefix is 1-based and plain M-S-z re-opens the most-recently-killed file (was opening the second). Rewrote the two undo-kill tests to exercise the real no-prefix path (arg=1 -> first) and a 1-based numeric prefix; both red against the bug, green after. Full suite: no new failures (the 4 pre-existing dupre-theme failures are the separate task below). Live-reloaded into the daemon.
+** DONE [#B] dashboard-config setq wipes recentf-exclude list :bug:quick:solo:
+CLOSED: [2026-06-12 Fri]
+Fixed in =modules/dashboard-config.el=: extracted the EMMS exclusion into =cj/--dashboard-exclude-emms-from-recentf= (the =:config= side-effect was not reachable for a test) and switched =setq= to =add-to-list=, so the five exclusions system-defaults adds earlier in init order survive. Two ERT tests in =tests/test-dashboard-config-recentf-exclude.el= (preserves prior entries / adds the pattern); the preservation test was red before, green after. Live-reloaded into the daemon and restored the five wiped entries in the running session.
+** DONE [#B] org-roam dailies template writes FILETAGS and TITLE on one line :bug:quick:solo:
+CLOSED: [2026-06-12 Fri]
+Fixed in =modules/org-roam-config.el=: extracted the dailies head into the =cj/--org-roam-dailies-head= defconst (so it is unit-testable, the value was unreachable inside the use-package =:custom= form) and gave it real newlines — =#+FILETAGS: Journal\n#+TITLE: %<%Y-%m-%d>\n=. Two ERT tests in =tests/test-org-roam-config-dailies-head.el= assert FILETAGS and TITLE sit on separate lines and the head ends in a newline (both red before, green after). Live-reloaded into the daemon. Open follow-up for Craig: existing malformed daily files (with the run-together first line) are data, not code — sweep them by hand if desired.
+** DONE [#B] drill-refile clobbers global org-refile-targets with an invalid spec :bug:quick:solo:
+CLOSED: [2026-06-12 Fri]
+Fixed in =modules/org-drill-config.el=: =cj/drill-refile= now =let=-binds =org-refile-targets= (the session-wide value survives) and supplies =(directory-files drill-dir t "\\.org$")= as the file list instead of the bound =drill-dir= symbol (org reads a bound symbol as a directory string, which yielded nothing). Rewrote the stale test (it asserted the buggy =(assoc 'drill-dir ...)=) into two: targets are a real .org file list, and the global is not clobbered. Both red before, green after. Live-reloaded into the daemon.
+
+Follow-up 2026-06-12 (Codex review): the first fix reinvented file-listing with a raw =directory-files= call, bypassing the shared validated entry point =cj/--drill-files-or-error= — no missing/unreadable-dir =user-error=, silent fall-through on an empty dir, and it included leading-dot =.org= files the rest of the module excludes. Re-routed through =cj/--drill-files-or-error= + =expand-file-name=; the test was rewritten into three (validated-helper targets, no global clobber, =user-error= on a missing dir).
+** CANCELLED [#B] M-S- launcher keys dead: eww, elfeed, calibredb unreachable :bug:quick:solo:
+CLOSED: [2026-06-13 Sat]
+Not a bug. The audit used =key-binding=, which ignores =key-translation-map=, so it read the M-S- launcher chords as dead. They work in GUI: =keyboard-compat.el= installs a =key-translation-map= entry (=M-E -> M-S-e=, etc.) in GUI frames, so Meta+Shift+letter reaches eww/elfeed/calibredb. The "fix" =4a1ecf64= bound =M-E= directly and broke them instead; reverted here. The real console-reachability problem (the chords are dead outside GUI) is the subject of [[file:docs/design/keybinding-console-safety-spec.org][the keybinding-console-safety spec]].
+** DONE [#B] Signel Client Open Work
+CLOSED: [2026-06-12 Fri]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-12
+:END:
+Parent task for the Emacs Signal client bring-up. 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]].
+
+Closed 2026-06-12: the bring-up shipped (dated history below). The signel project now has its own =.ai/= scope, so all open signel/signal-cli issues moved to [[file:~/code/signel/todo.org][the signel todo]] and are tracked there flat (the three open children here — handle-error leak, link-with-QR, groups in picker — moved in that pass). Work on =modules/signal-config.el= stays in this file.
+
+*** 2026-06-12 Fri @ 07:34:05 -0500 Signel notify-only-for-unviewed-conversation shipped
+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. The two manual checks moved to the Manual testing and validation parent.
+
+*** 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.
+** DONE [#C] Project-aware bug capture via C-c c t :feature:
+CLOSED: [2026-06-12 Fri]
+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. Manual verify filed under the Manual testing and validation parent. NOTE: the matching "<Project> Resolved Work" header for the wrap-up workflow is a separate concern, not handled here.