aboutsummaryrefslogtreecommitdiff
path: root/todo.org
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-13 14:33:51 -0500
committerCraig Jennings <c@cjennings.net>2026-05-13 14:33:51 -0500
commit66dd09aea621bfb202914c15b9be2529529bb871 (patch)
tree2758ea2b72942047c46f89dbb1abde2c093decfa /todo.org
parente7ab89b109881231bc5465b09122966191b43d8c (diff)
downloaddotemacs-66dd09aea621bfb202914c15b9be2529529bb871.tar.gz
dotemacs-66dd09aea621bfb202914c15b9be2529529bb871.zip
chore: start tracking todo.org for cross-machine sync
Diffstat (limited to 'todo.org')
-rw-r--r--todo.org3689
1 files changed, 3689 insertions, 0 deletions
diff --git a/todo.org b/todo.org
new file mode 100644
index 00000000..bd015800
--- /dev/null
+++ b/todo.org
@@ -0,0 +1,3689 @@
+#+TITLE: Emacs Config Tasks
+#+AUTHOR: Craig Jennings
+#+ARCHIVE: %s::* Emacs Resolved
+
+* Emacs Priority Scheme
+
+Use priority to express impact and urgency, not task type. Bugs, refactors,
+tests, chores, and features can all be high or low priority.
+
+- =[#A]= Urgent risk or current workflow blocker. Use for credential exposure,
+ security/privacy leaks, data loss, destructive behavior, startup breakage,
+ failing tests that block work, or a feature/refactor that unblocks a core
+ daily workflow.
+- =[#B]= Important planned work. Use for concrete bugs, high-leverage
+ architecture cleanup, brittle load-order/test gaps, dependency failures, or
+ feature work with a clear design and expected near-term use.
+- =[#C]= Useful but optional. Use for low-risk cleanup, ergonomics, smoke tests,
+ investigations with limited current impact, or feature work that would improve
+ the setup but is not yet a committed workflow.
+- =[#D]= Someday/maybe or watchlist. Use for speculative features, tiny polish,
+ upstream/package tracking, optimizations without current pain, or deferred
+ ideas that should not compete with active maintenance.
+
+For =PROJECT= headings, use the highest priority of the meaningful child work
+inside the project. If a project only contains exploration or review, assign the
+priority by the expected decision value rather than the number of files touched.
+
+Use tags to describe the work shape:
+- =:bug:= means the current behavior is wrong or likely broken.
+- =:feature:= means the task adds a new user-visible capability or workflow.
+- =:refactor:= means the task changes structure/ownership without primarily
+ changing behavior.
+- =:quick:= means the task appears low effort and localized. It is a planning
+ hint, not a promise; remove it if the task grows during implementation.
+
+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 [#A] Add Telegram Messaging
+https://github.com/zevlg/telega.el
+Make sure there is a setup script to run, so that the docker container can be installed post emacs dotfiles repository clone on a fresh install
+** DOING [#A] Org Agenda fixes :bug:
+*** 2026-05-13 Wed @ 13:05:21 -0500 Skip CANCELLED entries from main agenda SCHEDULE
+see the following screenshot
+/home/cjennings/pictures/screenshots/2026-05-13_071428.png
+
+Fix shipped on main: commit =8e57950=. Added an org-agenda-skip-function
+to the SCHEDULE block of the "d" command in =org-agenda-custom-commands=
+that filters entries with TODO state CANCELLED. Scope is deliberately
+narrow -- DONE and FAILED scheduled tasks still render.
+
+Tests in =tests/test-org-agenda-config-skip-functions.el= (Normal +
+Boundary) lock in the configuration form on the agenda block and
+verify the other blocks aren't accidentally carrying the same skip.
+**** TODO [#C] Refactor: extract org-agenda-prefix-format literal :refactor:
+=modules/org-agenda-config.el= currently inlines =" %i %-15:c%?-15t% s"=
+across four blocks of =org-agenda-custom-commands= (overdue, hi-pri,
+schedule, priority-B). Extract into a defvar (e.g.
+=cj/--main-agenda-prefix-format=) and reference it from each block.
+Surfaced during the audit for the CANCELLED-schedule fix.
+*** 2026-05-13 Wed @ 13:27:39 -0500 Clear dedicated before toggling window split
+Reproduction steps
+- open an org file (I was using this projects's todo.org file
+- hit the f8 button to open the agenda view. it opens fine.
+- toggle-window-split
+>>> The agenda displays on both panes after the toggle.
+see snapshot below
+/home/cjennings/pictures/screenshots/2026-05-13_071603.png
+
+Fix shipped on main: commit =97f0f8e=. Root cause was the dedicated
+=*Org Agenda*= window (set via =display-buffer-alist= rule) rejecting
+the internal =set-window-buffer= swap. The non-dedicated buffer never
+crossed and both panes ended up showing the agenda.
+
+=modules/ui-navigation.el= now clears dedicated on both windows at the
+top of =toggle-window-split= before the swap. The toggle is an
+explicit layout change, so preserving per-window dedicated through it
+would just re-trigger the same wedge on the next invocation.
+
+Tests in =tests/test-ui-navigation--toggle-window-split.el= (5 tests,
+Normal + Boundary) cover the no-dedicated baseline, the bug-trigger,
+post-toggle cleared state, and the 1-window / 3-window no-op cases.
+Verified red against the unfixed code (the bug-trigger test errored
+=Window is dedicated to '*test-toggle-b*'=) before applying the fix.
+
+Live verification pending: =M-x load-file modules/ui-navigation.el=,
+then walk through F8 + M-S-t in a fresh session.
+*** TODO [#A] Enhancement: replace todo indicators with project name
+In the overdue section, the high priority section, and the priority B section, each of the entries has a todo: indicator, which is the name of the file.
+This is not useful. Based on how I'm using emacs, every entry is likely to come from a file named todo.org.
+A much preferrable option would be to have the project's name there instead.
+so, for instance this todo.org file would show "emacs.d" as the project name in place of todo.
+
+see snapshot below for an example of the current state.
+/home/cjennings/pictures/screenshots/2026-05-13_071840.png
+
+** TODO [#B] Add ERT coverage for modules below 70% :tests:
+
+Coverage snapshot from =make coverage-summary= on 2026-05-12:
+=4405/6735= executable lines covered (=65.4%=) across 72 tracked module files.
+
+Add focused ERT tests for each module currently below 70% coverage, then rerun
+=make coverage= and update or close the child tasks as their module coverage
+crosses the threshold.
+
+*** TODO [#B] Add ERT tests for =modules/prog-python.el= (0/20, 0.0%) :tests:
+*** TODO [#B] Add ERT tests for =modules/selection-framework.el= (0/3, 0.0%) :tests:
+*** TODO [#B] Add ERT tests for =modules/keyboard-compat.el= (1/29, 3.4%) :tests:
+*** TODO [#B] Add ERT tests for =modules/prog-webdev.el= (1/21, 4.8%) :tests:
+*** TODO [#B] Add ERT tests for =modules/calibredb-epub-config.el= (7/104, 6.7%) :tests:
+*** TODO [#B] Add ERT tests for =modules/system-defaults.el= (1/12, 8.3%) :tests:
+*** TODO [#B] Add ERT tests for =modules/ui-navigation.el= (4/46, 8.7%) :tests:
+*** TODO [#B] Add ERT tests for =modules/prog-go.el= (3/27, 11.1%) :tests:
+*** TODO [#B] Add ERT tests for =modules/system-commands.el= (6/49, 12.2%) :tests:
+*** TODO [#B] Add ERT tests for =modules/external-open.el= (5/33, 15.2%) :tests:
+*** TODO [#B] Add ERT tests for =modules/org-webclipper.el= (10/59, 16.9%) :tests:
+*** TODO [#B] Add ERT tests for =modules/system-utils.el= (5/26, 19.2%) :tests:
+*** TODO [#B] Add ERT tests for =modules/org-reveal-config.el= (9/45, 20.0%) :tests:
+*** TODO [#B] Add ERT tests for =modules/coverage-elisp.el= (5/19, 26.3%) :tests:
+*** TODO [#B] Add ERT tests for =modules/org-noter-config.el= (27/99, 27.3%) :tests:
+*** TODO [#B] Add ERT tests for =modules/ai-config.el= (53/191, 27.7%) :tests:
+*** TODO [#B] Add ERT tests for =modules/slack-config.el= (23/74, 31.1%) :tests:
+*** TODO [#B] Add ERT tests for =modules/org-roam-config.el= (26/80, 32.5%) :tests:
+*** TODO [#B] Add ERT tests for =modules/custom-text-enclose.el= (51/145, 35.2%) :tests:
+*** TODO [#B] Add ERT tests for =modules/dirvish-config.el= (68/180, 37.8%) :tests:
+*** TODO [#B] Add ERT tests for =modules/hugo-config.el= (38/96, 39.6%) :tests:
+*** TODO [#B] Add ERT tests for =modules/org-refile-config.el= (21/51, 41.2%) :tests:
+*** TODO [#B] Add ERT tests for =modules/org-contacts-config.el= (36/79, 45.6%) :tests:
+*** TODO [#B] Add ERT tests for =modules/transcription-config.el= (75/162, 46.3%) :tests:
+*** TODO [#B] Add ERT tests for =modules/music-config.el= (130/278, 46.8%) :tests:
+*** TODO [#B] Add ERT tests for =modules/mail-config.el= (9/19, 47.4%) :tests:
+*** TODO [#B] Add ERT tests for =modules/custom-buffer-file.el= (123/212, 58.0%) :tests:
+*** TODO [#B] Add ERT tests for =modules/org-agenda-config.el= (49/83, 59.0%) :tests:
+*** TODO [#B] Add ERT tests for =modules/host-environment.el= (34/57, 59.6%) :tests:
+*** TODO [#B] Add ERT tests for =modules/custom-ordering.el= (61/101, 60.4%) :tests:
+*** TODO [#B] Add ERT tests for =modules/ui-theme.el= (25/40, 62.5%) :tests:
+*** TODO [#B] Add ERT tests for =modules/custom-comments.el= (230/358, 64.2%) :tests:
+*** TODO [#B] Add ERT tests for =modules/ui-config.el= (20/31, 64.5%) :tests:
+*** TODO [#B] Add ERT tests for =modules/custom-whitespace.el= (53/82, 64.6%) :tests:
+*** TODO [#B] Add ERT tests for =modules/jumper.el= (64/99, 64.6%) :tests:
+*** TODO [#B] Add ERT tests for =modules/test-runner.el= (146/222, 65.8%) :tests:
+*** TODO [#B] Add ERT tests for =modules/browser-config.el= (53/76, 69.7%) :tests:
+
+** TODO [#A] Fix Python tree-sitter font-lock query syntax error :bug:
+SCHEDULED: <2026-04-27 Mon>
+
+Diagnosed 2026-04-26 — paused at /start-work Gate 2. Root cause is system-level, not in =.emacs.d=: Emacs 30.2 + tree-sitter library 0.26.x predicate-syntax mismatch. Emacs sends =#match= (no =?= suffix), tree-sitter 0.26 rejects anything but =#match?=. Affects every =:match=, =:equal=, =:pred= predicate in every treesit-aware mode, not just Python.
+
+Full investigation, reproduction, and fix-option analysis in:
+
+[[file:inbox/python-treesit-predicate-mismatch.txt][inbox/python-treesit-predicate-mismatch.txt]]
+
+Fix surfaces are upstream (Emacs source or tree-sitter library) — local options are workarounds. Recommended next-session path: check whether Emacs 30.3 / master has a fix; if not, override =python--treesit-settings= in =modules/prog-python.el= to strip the loudest predicate-using queries (loses some highlighting, kills the redisplay flood).
+
+** STALLED [#C] EPUB text is slightly left-of-center (shr word-wrap shortfall) :bug:
+[2026-05-12] Visual review of the reading-width rework is done -- it's good. Not sure I actually need this nit fixed; the left-of-center bias is minor and the `+'/`-' keys let me nudge it. Parking here until I decide it bothers me enough.
+
+After =b7c6b2c=, the EPUB text block is centered with `set-window-margins' at `(natural - nov-text-width) / 2' each side -- but the *rendered* text is a bit narrower than `nov-text-width' columns, because `shr' wraps at word boundaries, so the typical line ends a few columns short of the fill width. The text is left-aligned within its `nov-text-width'-wide fill region, so the unused tail of that region adds to the right margin -- the block reads as shifted left of center. Adjusting `cj/nov-margin-percent' (the `+'/`-' keys) re-flows and happens to look better at some widths (probably the line-ending pattern lands tighter), which is the same effect, not a real difference.
+
+Plan: in `cj/nov-update-layout', after the render, measure the actual widest line (`(save-excursion (goto-char (point-min)) (let ((m 0)) (while (not (eobp)) (end-of-line) (setq m (max m (current-column))) (forward-line 1)) m))') and center on *that* instead of on `nov-text-width'. Or, cheaper but coarser: bias the left margin by a small fudge (a column or two). The measure-the-text approach is correct; do it if it's not too slow on big chapters (it scans the buffer once per render -- the buffer's already in memory, so likely fine). =modules/calibredb-epub-config.el=, =tests/test-calibredb-epub-config.el=.
+
+cj: this is now confirmed. mark it DONE.
+
+** TODO [#B] Rework dev F-keys: compile+run (F4), test (F6), coverage (F7) :feature:
+
+Consolidate the developer F-key block into a coherent sequence. F5 reserved for debug (separate ticket). Format bindings move off F6 to C-; f.
+
+Menu mechanism: =completing-read= everywhere (consistent with F7 coverage scope prompt and with the vertico/consult workflow in the rest of the config). No transient definitions.
+
+**F4 — compile + run**
+
+- F4 (no modifier): completing-read with candidates filtered by project type. Detection via projectile-project-compilation-cmd and heuristic fallbacks (go.mod, Makefile, Eask, package.json, pyproject.toml, docker-compose.yml).
+ - Compiled project candidates: "Compile", "Run", "Compile + Run" (default), "Clean + Rebuild"
+ - Interpreted project candidates: "Run" only
+- C-F4: fast path = Compile only. On interpreted projects, shows "not a compiled language" and no-ops.
+- M-F4: fast path = Clean + Rebuild. Same "not applicable" behavior on interpreted projects.
+
+The dispatcher reads projectile's per-project compile/run/test commands. No Docker-specific logic in the command itself. Container workflows are configured via projectile's prompt-and-cache (or .dir-locals.el from the dev-project-setup helper).
+
+**F6 — run tests**
+
+- F6 (no modifier): completing-read top-level:
+ - "All tests"
+ - "Current file's tests"
+ - "Run a test..." (nested completing-read with individual tests)
+- C-F6: fast path = "Current file's tests"
+- M-F6: fast path = "Run a test..."
+
+"Current file's tests": if current buffer is a test file, run it directly. If source file, find matching test file(s) via language conventions (elisp: tests/test-<module>*.el; python: tests/test_<module>.py; etc.) and run them aggregated.
+
+"Run a test...": build a candidate list of individual tests, pre-select the last-chosen test for this buffer (buffer-local cj/--last-test-run), present via completing-read. Pressing RET re-runs last. Memory is buffer-local so different source files remember their own last-test.
+
+Candidate set for "Run a test...":
+- If buffer is a test file: parse the file, return its test definitions.
+- If buffer is a source file: find matching test file(s) and aggregate their test definitions.
+- No matches: error out with "No tests found for <buffer>". Don't silently fall through.
+
+Per-language test discovery:
+- Python, Go, TypeScript/JavaScript: tree-sitter queries (treesit-auto already configured, grammars auto-install)
+ - Python: (function_definition name: (identifier) @name (:match "^test_" @name))
+ - Go: (function_declaration name: (identifier) @name (:match "^Test" @name))
+ - TS/JS: (call_expression function: (identifier) @fn arguments: (arguments (string) @name) (:match "^\\(test\\|it\\)$" @fn))
+ - Parsing unopened test files: use with-temp-buffer + insert-file-contents + python-ts-mode (etc.) + treesit-query-capture
+- Elisp: built-in sexp navigation; scan for (ert-deftest <name> ...) forms. No tree-sitter needed.
+
+**F7 — coverage** (already designed in docs/design/coverage.org)
+
+**Required moves:**
+- Move blacken-buffer (python), shfmt-buffer (sh), clang-format-buffer (c) off F6 to C-; f prefix (already the format-buffer prefix).
+- Move projectile-run-project off F6 (folds into the new F4 completing-read).
+
+**Ordering:**
+Do this after the coverage-config work ships. No churn mid-flight.
+
+** TODO [#B] Review and rebind M-S- keybindings :refactor:
+
+Changed from M-uppercase to M-S-lowercase for terminal compatibility.
+These may override useful defaults - review and pick better bindings:
+- M-S-b calibredb (was overriding backward-word)
+- M-S-c time-zones (was overriding capitalize-word)
+- M-S-d dwim-shell-menu (was overriding kill-word)
+- M-S-e eww (was overriding forward-sentence)
+- M-S-f fontaine (was overriding forward-word)
+- M-S-h split-below
+- M-S-i edit-indirect
+- M-S-k show-kill-ring (was overriding kill-sentence)
+- M-S-l switch-themes (was overriding downcase-word)
+- M-S-m kill-all-buffers
+- M-S-o kill-other-window
+- M-S-r elfeed
+- M-S-s window-swap
+- M-S-t toggle-split (was overriding transpose-words)
+- M-S-u winner-undo (was overriding upcase-word)
+- M-S-v split-right (was overriding scroll-down)
+- M-S-w wttrin (was overriding kill-ring-save)
+- M-S-y yank-media (was overriding yank-pop)
+- M-S-z undo-kill-buffer (was overriding zap-to-char)
+
+** TODO [#B] Build cj/dev-setup-project helper (per docs/design/dev-setup-project.org) :feature:
+
+Interactive command that opens a review buffer with proposed per-subdirectory .dir-locals.el contents (projectile compile/run/test + cj/coverage-backend), optional starter Makefile when none exists, and gitignore updates. User edits inline, C-c C-c writes all files.
+
+Design: [[file:../docs/design/dev-setup-project.org][docs/design/dev-setup-project.org]]
+
+Scope of v1:
+- modules/dev-setup-config.el (command + review-buffer major mode)
+- Three-tier detection: existing Makefile, existing package.json/pyproject.toml scripts, fall-back starter Makefile generation.
+- Project shapes supported: pure Elisp, pure Go, pure Python, pure Node/TS, Docker Compose polyglot.
+- Re-run semantics: status banners (UNCHANGED / WILL UPDATE / WILL CREATE), idempotent gitignore append, never modifies an existing Makefile.
+- ERT tests for the pure helpers (Makefile parser, package.json parser, shape detection, target-to-role mapping, review-buffer parser).
+
+Deferred:
+- Rust (Cargo.toml), Java (pom.xml), other language shapes.
+- Project-wide override config file.
+- Auto-detecting external run scripts in conventional locations.
+
+Do this after the F-key rework ticket ships; don't want to churn project configs before the keys are stable.
+
+** TODO [#B] Pick and wire a debug backend for F5 :feature:
+
+Bind F5 globally to a debug entry point. Backend choice is the hard part:
+
+- dape (Debug Adapter Protocol for Emacs) — modern, multi-language via DAP. Single UX across Python, Go, TS, Rust, etc. Less mature than DAP clients in other editors.
+- realgud — wraps multiple debuggers (pdb, gdb, node --inspect, etc.). More mature; UX varies by backend.
+- Language-specific stacks — dap-mode (python-mode + dap), delve for go, ts-node --inspect, etc. Best per-language UX; most config work.
+
+F5 itself will be simple (start/resume debug). Likely modifier variants once the backend is picked:
+- C-F5 toggle breakpoint at point
+- M-F5 eval expression in debug context (or step-over shortcut)
+
+Evaluate against these projects' languages: elisp (edebug already works), Python, Go, TS, shell. Shell debug is usually print-based; skip.
+
+Do this after the F-key rework ticket ships so F5 is the only hole left.
+
+** TODO [#B] Build debug-profiling.el module :feature:
+
+Reusable profiling infrastructure for targeted slow-command investigation. Consolidates scattered profiler bindings (currently in =modules/config-utilities.el=) and adds two pure-helper-backed entry points: "profile next command" and "time region or sexp." Designed via =/brainstorm= 2026-04-26.
+
+Design: [[file:../docs/design/debug-profiling.org][docs/design/debug-profiling.org]]
+
+Implement via =/start-work= against the design — branch =feat/debug-profiling=, commits decomposed along the test-first split-for-testability boundary. Once shipped, use it as the v1 exercise on the queued [#B] org-capture target-building investigation.
+
+** TODO [#C] Review and implement flycheck modeline customization spec :feature:
+
+Add flycheck status (error/warning counts) to custom modeline to make it visible again.
+
+**Spec Document:**
+[[file:docs/flycheck-modeline-customization-spec.org][flycheck-modeline-customization-spec.org]]
+
+**Summary:**
+Current custom modeline excludes minor-mode-alist where flycheck displays its status.
+Need to either:
+1. Add flycheck's lighter directly to mode-line-format (simplest)
+2. Customize flycheck prefix/indicator and add to modeline
+3. Create custom flycheck segment with full control
+
+**Files to Modify:**
+- modules/flycheck-config.el (customize prefix/indicator)
+- modules/modeline-config.el (add to mode-line-format)
+
+**Recommended Approach:**
+Option 4 (Hybrid) from spec - customize flycheck variables + add to modeline.
+Simple, maintainable, respects flycheck's built-in logic.
+
+** TODO [#C] Migrate from Company to Corfu (with prescient integration) :feature:
+:PROPERTIES:
+:COMPLETE_CONFIG: [[file:.ai/SOMEDAY-MAYBE.org::1611][todo.org:1611-1639]]
+:END:
+
+Complete config already exists in someday-maybe.org — just needs to be executed.
+
+While migrating, also extend prescient (already wired into vertico) to Corfu for smart sorting. The prescient piece is a small follow-up to the migration, so handled in the same task.
+
+** TODO [#C] Consider removing gptel and the C-; a AI-assistant keymap :refactor:cleanup:
+
+Claude Code (via the F9 ai-vterm launcher) has fully replaced the gptel
+side-chat workflow. The =C-; a= prefix and the gptel use-package block
+in =modules/ai-config.el= no longer get used.
+
+Decide whether to:
+
+1. *Remove entirely.* Drop =modules/ai-config.el= +
+ =modules/ai-conversations.el=, the =C-; a= keymap registration,
+ the gptel/anthropic/openai package installs, and the saved-
+ conversations directory. Update =init.el= to stop requiring the
+ module. Net code reduction is large.
+2. *Keep but mothball.* Move the module to =modules/archived/= so the
+ bindings disappear but the code stays available for reference if
+ the workflow ever comes back.
+3. *Trim to the part that's still useful.* The rewrite-region command
+ (=C-; a r=) is the one piece Claude Code in a separate vterm can't
+ do as smoothly -- it edits the current buffer in place against a
+ prompt. If that's worth keeping, narrow =ai-config.el= to just
+ that command + its backend setup and drop everything else.
+
+Scope notes for whichever path:
+
+- =C-; a= keymap is registered in =ai-config.el='s tail; if removed,
+ the prefix becomes free for repurposing or stays unbound.
+- gptel pulls in =anthropic= / =openai= backends; both keys live in
+ =auth-config.el= but aren't referenced elsewhere -- safe to leave
+ the auth entries even if gptel goes.
+- =ai-conversations.el= is autoloaded via =ai-config.el= and stores
+ saved conversations in a designated dir; the dir + content go too
+ if removing entirely.
+- which-key registrations under =C-; a= disappear automatically when
+ the keymap goes.
+
+Companion to the F9 ai-vterm work shipped 2026-05-08. Filed because
+the C-F9 binding was already pulled from gptel during that work.
+
+** TODO [#C] Extend F2 "preview" convention across modes :feature:
+
+F2 is the universal preview key. Currently bound in markdown-mode (markdown-preview) and org-mode (org-reveal, moved from F5). Extend to other modes where a "preview" action is natural:
+
+- Hugo blog (hugo-config.el) — preview the post in browser
+- HTML / web-mode — open in browser
+- Any other mode with a natural "preview this" action
+
+Keep the binding mode-local so F2 stays available as a global candidate where no preview makes sense.
+
+** TODO [#C] Build localrepo and document limitations :feature:
+
+Repeatable installs and safe rollbacks.
+
+.localrepo only contains packages from package.el archives.
+Treesitter grammars are downloaded separately by treesit-auto on first use.
+For true offline reproducibility, need to cache treesitter grammars separately.
+
+** TODO [#C] Investigate sqlite finalizer error on init :bug:
+
+=*Messages*= shows =finalizer failed: (wrong-type-argument sqlitep nil)= during init. A package is finalizing an sqlite handle that's already nil — indicates a teardown bug somewhere. Likely culprits: forge, magit-todos, or any package using the sqlite backend.
+
+Investigation order:
+1. =M-x toggle-debug-on-message= with a regex matching =finalizer failed=.
+2. Restart Emacs to capture the backtrace.
+3. Check =modules/git-config.el= (forge) and any other sqlite-using module.
+
+Single occurrence per session, no visible impact yet. Track in case it grows.
+
+Discovered 2026-04-26 in =*Messages*=.
+
+** TODO [#C] Investigate TRAMP/dirvish showing question marks for file dates :bug:
+
+Remote directories in dirvish show "?" instead of actual modification dates.
+Tried several approaches without success - needs deeper investigation.
+
+**Attempted fixes (all reverted):**
+1. Connection-local dired-listing-switches with -alh (didn't help)
+2. Disabling tramp-direct-async-process (reported to cause this, but disabling didn't fix it)
+3. Hook to set different listing switches for remote vs local (didn't help)
+
+**Possible causes to investigate:**
+- dirvish may be using its own attribute fetching that bypasses dired-listing-switches
+- May need dirvish-specific configuration for remote file attributes
+- Could be an Emacs 29/30 + TRAMP + dirvish interaction issue
+- May require changes to how dirvish renders the file-size attribute on remote
+
+**Files involved:**
+- modules/tramp-config.el
+- modules/dirvish-config.el
+
+** TODO [#C] Finish terminal GPG pinentry configuration :feature:
+
+Continue work on terminal-mode GPG passphrase prompts (loopback mode).
+Branch: terminal-pinentry
+
+Changes in progress (modules/auth-config.el):
+- Use epa-pinentry-mode 'loopback in terminal
+- Use external pinentry (pinentry-dmenu) in GUI
+- Requires env-terminal-p from host-environment module
+
+** TODO [#D] Polish reveal.js presentation setup :feature:
+
+Three small reveal.js improvements; collected into one task because each on its own is too small to track separately.
+
+1. *Image insertion helper.* Function to insert images with proper org-reveal attributes (sizing, background images, etc.) without having to remember the syntax.
+2. *Default font sizing for slide elements.* Configure reveal.js font sizes for headings, body text, code blocks, etc. — better defaults via =org-reveal-head-preamble= CSS or a custom theme.
+3. *Custom dupre reveal.js theme.* CSS theme using the colors from =themes/dupre-palette.el=. Install into =reveal.js/css/theme/= for use with =#+REVEAL_THEME: dupre=.
+
+** TODO [#D] Evaluate and integrate Buttercup for behavior-driven integration tests :tests:
+
+Complex workflow testing capability.
+
+** TODO [#D] Dedup the doubly-defined functions in calibredb-epub-config.el :cleanup:
+=make compile= flags =calibredb-epub-config.el= for defining =cj/calibredb-clear-filters= (line ~79) and =cj/nov-jump-to-calibredb= (line ~277) twice each — the later definition silently shadows the earlier. Find which copy is current, delete the stale one. Pre-existing; noticed 2026-05-12 while fixing the Nov text-width loop.
+
+** TODO [#D] Track ELPA upstream byte-compile warnings (esxml, poetry) :chore:
+
+Two ELPA packages emit byte-compile warnings on =make compile= that aren't fixable in this repo:
+
+1. =elpa/esxml-20250421.1632/esxml.el= — =Warning: Unknown type: attrs= and =Unknown type: stringp= (a defcustom =:type= spec).
+2. =elpa/poetry-20240329.1103/poetry.el= — =Warning: Case 'X will match 'quote'= for four cases (=post-command=, =projectile=, =project=, =switch-buffer=). Quoted symbols inside =pcase= clauses — should be unquoted upstream.
+
+No action in this repo. Revisit when packages update. File upstream issues if warnings linger past a few months.
+
+Discovered 2026-04-26 in =*Messages*= during compile.
+
+** TODO [#D] Add status dashboard for dwim-shell-command processes :feature:
+
+Create a command to show all running dwim-shell-command processes with their status.
+Currently, there's no unified view of multiple running extractions/conversions.
+
+**Current behavior:**
+- Each command shows spinner in minibuffer while running
+- Process buffers created: `*Extract audio*`, etc.
+- On completion: buffer renamed to `*Extract audio done*` or `*Extract audio error*`
+- No way to see all running processes at once
+
+**Recommended approach:**
+Custom status buffer that reads `dwim-shell-command--commands`.
+Can add mode-line indicator later as enhancement.
+** PROJECT [#A] Architecture review follow-up from 2026-05-03 :refactor:
+
+High-level pass over =init.el=, =early-init.el=, and all 104 files in
+=modules/=. The main theme: the config works, but load order, startup side
+effects, credentials, and test measurement are more implicit than they should
+be. Use this project as the parent tracker; each child below should land as a
+small, reviewable change.
+
+Review snapshot:
+- =modules/= has 104 files and about 24k lines including =init.el= and
+ =early-init.el=.
+- =init.el= eagerly =require=s nearly every module.
+- =make coverage= passed when allowed to write the test scratch directory.
+- Coverage report: =3240/4952= executable lines, =65.43%=, across 49 module
+ files. Caveat: 55 module files do not appear in the report at all, so the
+ real project confidence is lower than the raw percentage suggests.
+
+*** PROJECT [#A] Untangle the eager =init.el= load graph :architecture:refactor:
+
+=init.el= currently functions as the dependency graph by eagerly requiring
+almost every module in a fixed order. That makes modules harder to test in
+isolation and hides real dependencies behind "loaded earlier in init.el"
+assumptions.
+
+Spec: [[file:docs/design/init-load-graph.org][docs/design/init-load-graph.org]]
+
+**** VERIFY [#B] Write full design spec for the =init.el= load-graph refactor :architecture:refactor:
+
+Create a design document that defines the target architecture, module
+categories, migration phases, test strategy, acceptance criteria, and risk
+controls for untangling the eager =init.el= load graph.
+
+Review incorporation:
+- Treat helper consolidation as adjacent architecture work, not a direct
+ acceptance criterion for the load-graph refactor.
+- Mention utility extraction guardrails in the spec so Phase 2 dependency work
+ has a clear rule for duplicated helpers found along the way.
+
+Verify 2026-05-04:
+- Added [[file:docs/design/init-load-graph.org][docs/design/init-load-graph.org]].
+- Incorporated review feedback by making utility consolidation an explicit
+ sibling project with guardrails and candidate helper families.
+- Parsed the spec and =todo.org= with =org-element=.
+- Committed the tracked spec as =0528475=.
+
+**** TODO [#B] Classify modules by role and startup requirement :refactor:
+
+Create a simple inventory, probably in =docs/design/= or an org note linked
+from this task:
+- Pure library modules: should have explicit =require=s, no top-level keybinds,
+ no timers, no package install/load side effects.
+- Package configuration modules: mostly =use-package=, hooks, mode bindings.
+- Startup side-effect modules: server startup, timers, dashboard, weather,
+ calendar auto-sync, quick-video setup, etc.
+- User command modules: expose interactive commands but defer heavy package
+ loading until the command runs.
+
+Acceptance criteria:
+- Every module has an assigned category.
+- Any module that must be eager has a documented reason.
+- Obvious "modules in test" or "WIP need to fix" comments in =init.el= are
+ either retired or turned into actual tasks.
+
+**** TODO [#B] Add explicit module dependencies before changing load order :refactor:
+
+Several modules assume things like =cj/custom-keymap=, path constants, or
+environment predicates already exist. Before deferring load, make each module
+declare what it uses.
+
+Guidance:
+- Prefer runtime =(require 'foo)= for actual runtime dependencies.
+- Use =eval-when-compile= only for macros or compile-time declarations.
+- Avoid shims like "define this keymap if it does not exist" except in tests.
+- If a module only needs a command from another module, consider =autoload=.
+
+Acceptance criteria:
+- Loading a module directly in batch mode either succeeds or gives a clear
+ missing-package error.
+- =make validate-modules= still passes.
+- New tests cover any extracted pure dependency helpers.
+
+**** TODO [#B] Defer feature modules behind autoloads, hooks, and commands :refactor:
+
+Once dependencies are explicit, reduce the number of modules required at
+startup. Start with lower-risk feature modules:
+- Entertainment and optional integrations: =games-config=, =music-config=,
+ =weather-config=, =slack-config=, =erc-config=.
+- Heavy document/media modules: =pdf-config=, =calibredb-epub-config=,
+ =video-audio-recording=, =transcription-config=.
+- AI/rest tooling: =ai-config=, =restclient-config=, =ai-conversations=.
+
+Do this incrementally. After each batch:
+- Restart Emacs interactively.
+- Run =make test= or at least targeted tests.
+- Check that keybindings still resolve and which-key labels still appear.
+
+**** TODO [#B] Centralize custom keymap registration :refactor:
+
+Many modules mutate =cj/custom-keymap= or global keys at top level. This is a
+real architectural boundary because it forces load order and makes standalone
+module loading brittle.
+
+Expected outcome:
+- Define a small helper or convention for registering prefix maps.
+- Modules can expose their keymaps without assuming =keybindings.el= has already
+ loaded.
+- =keybindings.el= remains the owner of global prefixes like =C-;=.
+- Existing keymaps continue to work.
+
+Related existing task: [#B] "Review and rebind M-S- keybindings".
+
+*** PROJECT [#B] Consolidate shared utility helpers :architecture:refactor:
+
+Helpers are scattered across feature modules where they were first needed.
+Some are duplicated, and some private helpers are generic enough to belong in a
+shared foundation library. This is adjacent to the load-graph refactor because
+central helper ownership reduces hidden inter-module dependencies, but it
+should remain a sibling project so load-order batches stay small and
+reviewable.
+
+Guidance:
+- Do not extract a helper until at least two callers are clearly the same
+ shape.
+- Prefer growing =system-lib.el= first; split into topic libraries only if it
+ becomes too broad or starts pulling coarse dependencies into foundation
+ startup.
+- Keep one helper extraction per commit.
+- Move unit tests with the helper. Consumers should keep behavior/integration
+ coverage.
+- Do not add heavy package dependencies to foundation helpers.
+
+**** VERIFY [#B] Write full utility consolidation design spec :architecture:refactor:
+
+Create a design document that inventories candidate helper extractions,
+recommends grouping and naming, explains how the helpers fit into existing
+library modules, defines migration phases, and identifies testing/rollback
+rules.
+
+Spec: [[file:docs/design/utility-consolidation.org][docs/design/utility-consolidation.org]]
+
+Verify 2026-05-04:
+- Added [[file:docs/design/utility-consolidation.org][docs/design/utility-consolidation.org]].
+- Spec includes framing questions, existing library fit, proposed grouping,
+ concrete pull/rename table, migration phases, test strategy, acceptance
+ criteria, risks, open questions, and recommended first commits.
+- Parsed the spec and =todo.org= with =org-element=.
+- Committed the tracked spec as =3ea4707=.
+- Incorporated complete review feedback in =dd77ebd=, including API behavior
+ contracts, speculative-extraction rules, =system-lib= dependency budget,
+ inventory/audit artifacts, test relocation policy, commit type guidance,
+ =use-package :if= load-order policy, and Phase 5 cache-design addendum
+ requirement.
+
+**** VERIFY [#B] Inventory private helpers across modules :refactor:
+
+Walk every module and tag private helpers as genuinely module-specific,
+generic-but-trapped, or duplicated. Capture likely consumers and any dependency
+cost before extracting.
+
+Candidate families:
+- shell argument formatting,
+- executable lookup with user-visible warnings,
+- argv-based process runners,
+- path containment/safe-base predicates,
+- Org-safe heading/property/body text sanitizers,
+- cache-with-TTL plus invalidation hooks,
+- warning/message wrappers.
+
+Verify 2026-05-10:
+- Added [[file:docs/design/utility-inventory.org][docs/design/utility-inventory.org]] covering the 30 entries in the spec's
+ Candidate Extraction Table grouped by family (executable discovery, shell
+ quoting, process runner, file/path, external-open, Org-safe text, cache,
+ logging, macros/debug, theme I/O, string).
+- For each helper recorded: visibility, dependencies, side effects, callers
+ (production + test), test files, priority, decision (Migrate / Leave / Defer)
+ with rationale.
+- Decisions Summary: 11 Migrate, 3 Leave, 13 Defer.
+- Concrete next-action list groups Migrate items by Phase (2 = foundation
+ helpers, 3 = Org-safe text, 4 = external-open consolidation) for the order
+ the spec recommends.
+- Discoveries: =cj/log-silently= has 10 production callers (more than the
+ spec's table suggested -- defer is the right call); =cj/--file-manager-program-for=
+ shipped today in =dirvish-config.el= is the new form of OS-dispatch
+ consolidation and should fold into =cj/external-open-command= during Phase 4.
+
+**** TODO [#B] Extract executable lookup with warning helper :refactor:
+
+Create a generic helper such as =cj/find-executable-or-warn= from the useful
+=mail-config= pattern. It should return the executable path or nil and produce
+a clear warning when the executable is missing.
+
+**** TODO [#B] Extract argv-based process runner helper :refactor:
+
+Generalize the =coverage-core= process pattern into a dependency-light helper
+that captures output and signals a clear =user-error= with command/status/output
+on failure. Consider a small git wrapper only after the generic runner exists.
+
+**** TODO [#B] Extract Org-safe text sanitizers :refactor:
+
+Move heading/property/body sanitization into a shared helper once at least one
+non-calendar consumer is ready. Keep behavior explicit so external text cannot
+accidentally create headings or malformed properties.
+
+*** PROJECT [#A] Move package bootstrap out of =early-init.el= where possible :startup:refactor:
+
+=early-init.el= currently handles package archives, package refresh, installing
+=use-package=, and =use-package-always-ensure=. That is more than early startup
+needs and can make startup network-sensitive.
+
+**** TODO [#B] Split early startup from package bootstrap :refactor:
+
+Keep =early-init.el= focused on things that must happen before package and UI
+startup:
+- GC/file-name-handler startup tuning.
+- =load-prefer-newer=.
+- frame/UI suppression.
+- minimal debug behavior.
+
+Move package archive setup and =use-package= installation to a normal module or
+bootstrap command, unless there is a specific reason it must run in
+=early-init.el=.
+
+Acceptance criteria:
+- Fresh install/bootstrap still works from a documented command or script.
+- Normal startup does not refresh archives or install packages unexpectedly.
+- Offline startup remains quiet and predictable.
+
+**** TODO [#A] Revisit package signature policy
+
+=package-check-signature= is disabled. Decide whether that is still necessary
+for the localrepo/mirror workflow.
+
+Expected outcome:
+- Prefer signatures on by default.
+- If signatures must be disabled for local mirrors, scope that exception and
+ document why.
+- Add a note to the local repository docs so future package failures do not
+ lead to permanent insecure defaults.
+
+*** PROJECT [#B] Make coverage reporting account for untracked modules :tests:
+
+The current coverage result is useful but easy to overread. =make coverage=
+reported =65.43%= for files that undercover saw, but only 49 of 104 module
+files appeared in =.coverage/simplecov.json=.
+
+**** TODO [#B] Teach the coverage report to list modules missing from SimpleCov
+
+Expected outcome:
+- Compare =modules/*.el= against paths present in =.coverage/simplecov.json=.
+- Show a separate "not in report" section.
+- Do not silently fold those files into the percentage until we decide the
+ semantics. A visible missing-file count is enough for v1.
+
+**** TODO [#B] Decide whether unreported modules count as 0% coverage
+
+This is a policy decision:
+- Counting missing modules as 0% gives a more honest project-level number.
+- Keeping the current number is useful for "instrumented executable lines only".
+
+Recommendation: display both:
+- Instrumented coverage: current SimpleCov percentage.
+- Project module coverage: includes unreported module files as 0% or reports
+ them separately with an explicit caveat.
+
+Related existing task: [#B] "Coverage audit: untested and lightly-tested
+modules".
+
+*** TODO [#B] Add a lightweight architecture smoke test for startup contracts :tests:
+
+After the above refactors start, add one or two smoke tests that protect the
+architecture instead of individual functions.
+
+Candidate checks:
+- All modules can be loaded directly with only =modules/= on =load-path=, or
+ skipped with a clear external package reason.
+- No module other than =keybindings.el= binds =C-;= itself.
+- Startup-only modules do not run timers in batch test mode.
+
+Keep this small. The goal is to catch accidental return to hidden load-order
+coupling, not to build a full static analyzer.
+
+** PROJECT [#B] Module-by-module review and hardening :review:
+
+Review every file in =modules/= and capture concrete bugs, tests, refactors,
+and design improvements as child tasks. This is intentionally separate from the
+top-level architecture review: the architecture project tracks cross-cutting
+load/startup/test structure, while this project tracks module-specific work.
+
+Review protocol for each module:
+- Read the module directly, not just the test names.
+- Check runtime dependencies, top-level side effects, keybindings, timers,
+ external executable assumptions, secrets, host-specific paths, and user-data
+ writes.
+- Check existing test coverage and whether tests protect the highest-risk
+ behavior.
+- Promote larger findings into child =PROJECT= tasks with phases. Keep small
+ fixes as plain =TODO= tasks.
+
+Priority scheme: use the top-level =Priority Scheme= section in this file.
+
+Suggested review order:
+1. Foundation: =system-lib=, =user-constants=, =host-environment=,
+ =system-defaults=, =keybindings=, =config-utilities=, =early-init=,
+ =init=.
+2. Custom editing utilities: =custom-*=, =external-open=, =media-utils=.
+3. UI and navigation: =ui-*=, =font-config=, =modeline-config=,
+ =selection-framework=, =mousetrap-mode=, =popper-config=.
+4. Org workflow: =org-*=, =calendar-sync=, =hugo-config=, =gloss-config=.
+5. Programming workflow: =prog-*=, =dev-fkeys=, =test-runner=,
+ =coverage-*=, =vc-config=.
+6. Integrations and applications: mail, Slack, ERC, Elfeed, EWW, Dirvish,
+ PDF, Calibre, music, recording/transcription, AI/rest tooling.
+
+*** TODO [#B] Review foundation modules :review:
+
+Scope:
+- =system-lib.el=
+- =user-constants.el=
+- =host-environment.el=
+- =system-defaults.el=
+- =keybindings.el=
+- =config-utilities.el=
+- =early-init.el=
+- =init.el=
+
+Expected output:
+- Add one child task for each actionable finding.
+- Note "no action" only when the module has been reviewed and no task is
+ needed.
+- Cross-reference existing architecture tasks instead of duplicating them.
+
+Review progress:
+- =system-lib.el=: reviewed 2026-05-03. No immediate action beyond the existing
+ [#B] system-lib extraction task.
+- =host-environment.el=: reviewed 2026-05-03. See child tasks below.
+- =user-constants.el=: reviewed 2026-05-03. See child tasks below.
+- =system-defaults.el=: reviewed 2026-05-03. See child tasks below.
+- =keybindings.el=: reviewed during architecture pass. No new module-specific
+ action beyond the load-order/keymap architecture tasks.
+- =config-utilities.el=: reviewed 2026-05-03. No new module-specific action;
+ profiling extraction is already tracked by [#B] "Build debug-profiling.el
+ module".
+- =early-init.el=: reviewed 2026-05-10. See child tasks below and the existing
+ [#B] "Split early startup from package bootstrap" task.
+- =init.el=: reviewed 2026-05-10. See child tasks below and the existing
+ eager load-graph architecture tasks.
+
+**** PROJECT [#B] Split path constants from filesystem initialization in =user-constants.el= :refactor:
+
+=user-constants.el= defines paths and immediately creates directories/files at
+module load time. That makes a simple =(require 'user-constants)= write to the
+filesystem, including org files and calendar placeholder files. This is useful
+for interactive startup but brittle for tests, batch tools, and future
+autoloading.
+
+***** TODO [#B] Extract pure path definitions from startup writes :refactor:
+
+Expected outcome:
+- Loading path constants should not create files by default.
+- Put filesystem creation behind an explicit command/hook, e.g.
+ =cj/initialize-user-directories-and-files= called from startup/wrap-up, not
+ from the constant module's top level.
+- Keep startup behavior equivalent in normal interactive Emacs.
+
+Pitfalls:
+- Some modules may assume =gcal-file=, =pcal-file=, =dcal-file=, agenda files,
+ or org inbox files already exist. Handle those call sites deliberately.
+- Calendar placeholder creation may belong in =calendar-sync= or
+ =org-agenda-config=, not in generic constants.
+
+***** TODO [#B] Make initialization failures actionable :refactor:
+
+=cj/verify-or-create-dir= and =cj/verify-or-create-file= currently catch errors
+and only =message= them. That can hide a broken environment until a later module
+fails less clearly.
+
+Expected outcome:
+- Decide which paths are required vs optional.
+- Required path failures should signal a clear =user-error= or startup warning
+ that is hard to miss.
+- Optional path failures should be logged but not block startup.
+- Add tests around success, optional failure, and required failure behavior.
+
+**** TODO [#C] Clean up host environment predicates and timezone detection :cleanup:refactor:
+
+Small module-specific cleanup in =host-environment.el=:
+- =env-desktop-p= has a docstring that says "host is a laptop"; it should say
+ desktop / no battery.
+- =env-x-p= uses =(string= (window-system) "x")= while =env-x11-p= uses symbol
+ comparison. Existing tests pass, but the two predicates should use one style
+ and document the difference between "X display" and "X11 not Wayland".
+- =cj/match-localtime-to-zoneinfo= reads every zoneinfo file and compares
+ contents. That is fine as a fallback, but it is expensive enough to consider
+ caching or preferring symlink/env methods first if this ever runs during
+ startup.
+
+Acceptance criteria:
+- Fix the docstring.
+- Normalize or document =env-x-p= vs =env-x11-p= semantics.
+- Add or adjust tests only if behavior changes.
+
+**** TODO [#C] Add minimal =system-defaults.el= setting smoke tests :tests:
+
+=system-defaults.el= has no direct test file, despite holding high-impact
+defaults: server startup, backup behavior, custom-file behavior, symlink
+prompting, minibuffer GC hooks, backup directory, and mouse/key disabling.
+
+Keep this narrow; do not test Emacs itself. Good smoke assertions:
+- =vc-follow-symlinks= has the intended explicit value.
+- =custom-file= points at a temp file and is not loaded from the repo.
+- =backup-directory-alist= points inside =user-emacs-directory/backups=.
+- Minibuffer GC hooks are registered.
+
+This should be done after the =vc-follow-symlinks= fix so the test captures the
+correct behavior.
+
+**** TODO [#B] Move package bootstrap policy out of =early-init.el= :startup:refactor:
+
+=early-init.el= currently handles performance/debug setup, package archive
+construction, archive refresh policy, =use-package= installation, package
+signature policy, and Unicode defaults. That makes early startup do network- and
+package-manager-adjacent work before the regular module system exists.
+
+This overlaps with the existing [#B] "Split early startup from package
+bootstrap" task; keep the implementation there if that task is already active.
+This foundation review finding is the module-level acceptance detail.
+
+Expected outcome:
+- =early-init.el= keeps only settings that must happen before normal init:
+ startup GC/file-handler tuning, debug flag setup, native-comp workaround,
+ =load-prefer-newer=, site-start suppression, and package startup suppression.
+- Package archive setup, refresh/install policy, and =use-package= bootstrap
+ live in a normal module or bootstrap helper that can be tested directly.
+- Offline and missing-package states produce actionable errors without doing an
+ unexpected package refresh during early startup.
+- Existing local repo and ELPA mirror behavior is preserved.
+
+Pitfalls:
+- Do not break first-run bootstrap on a clean machine.
+- Keep local repositories higher priority than online archives.
+- Avoid prompting or refreshing archives during batch tests.
+
+**** TODO [#C] Decide and test package signature policy :security:startup:
+
+=early-init.el= sets =package-check-signature= to =nil= after package setup, with
+an earlier commented emergency toggle for expired signatures. That may be
+intentional for local mirrors, but it is security-sensitive enough to make the
+policy explicit.
+
+Expected outcome:
+- Document when signatures should be disabled, if ever.
+- Prefer signatures on for online archives unless a local-mirror workflow
+ requires otherwise.
+- If signatures stay disabled, add a clear comment explaining the trust model.
+- Add a small test or validation helper around the computed package policy if
+ package bootstrap is extracted.
+
+*** TODO [#B] Review custom editing utility modules :review:
+
+Scope:
+- =custom-buffer-file.el=
+- =custom-case.el=
+- =custom-comments.el=
+- =custom-datetime.el=
+- =custom-line-paragraph.el=
+- =custom-misc.el=
+- =custom-ordering.el=
+- =custom-text-enclose.el=
+- =custom-whitespace.el=
+- =external-open.el=
+- =media-utils.el=
+
+Review progress:
+- Core =custom-*= text modules reviewed 2026-05-03. They have unusually strong
+ direct ERT coverage compared with the rest of the config.
+- =external-open.el= and =media-utils.el= reviewed 2026-05-03. See child tasks.
+- =custom-buffer-file.el= reviewed 2026-05-03. See child tasks.
+
+**** TODO [#B] Harden external process launching in =external-open.el= and =media-utils.el= :security:refactor:
+
+=external-open.el= and =media-utils.el= use shell command strings for launching
+external applications:
+- =cj/open-this-file-with= interpolates the user-supplied command into
+ =call-process-shell-command=.
+- =cj/media-play-it= builds a shell command for players and optional =yt-dlp=
+ stream extraction.
+
+This is mostly controlled local input, but it is still brittle: command paths
+with spaces can fail, arguments are hard to reason about, and future URL/source
+changes could create shell quoting bugs.
+
+Expected outcome:
+- Prefer =start-process= / =call-process= with argv lists where possible.
+- If shell is required for command substitution, isolate and quote every
+ untrusted value.
+- Add tests around command construction for:
+ - file paths with spaces and shell metacharacters,
+ - URL strings with shell metacharacters,
+ - configured player args,
+ - missing executable errors.
+
+Pitfalls:
+- =cj/open-this-file-with= may intentionally accept "program plus args". If so,
+ split the command deliberately or introduce separate program/args prompts.
+- Some media players need different URL handling; preserve the existing
+ =:needs-stream-url= behavior.
+
+**** TODO [#C] Add coverage for =external-open.el= and =media-utils.el= :tests:
+
+The core custom editing modules are covered, but these integration helpers have
+little or no direct test coverage despite owning shell/process boundaries.
+
+Useful test seams:
+- Pure command-builder helpers for external open and media play.
+- Player availability selection from =cj/media-players=.
+- Error behavior when =yt-dlp=, =tsp=, or the selected player is missing.
+- Advice behavior for externally opened file extensions should not leave
+ surprising buffers behind.
+
+This pairs naturally with the process-launch hardening task above.
+
+**** TODO [#C] Audit destructive buffer/file keybindings for confirmation policy :ux:
+
+=cj/buffer-and-file-map= includes destructive operations under =C-; b=,
+including delete file, erase buffer, clear top, clear bottom, and revert. Some
+are intentionally fast, but this module is high blast radius.
+
+Expected outcome:
+- Decide which operations need confirmation when the buffer is modified or
+ visiting a file.
+- At minimum, document the intended policy in =custom-buffer-file.el=.
+- Consider safer wrappers for =erase-buffer= and =revert-buffer= under the
+ personal keymap.
+
+**** TODO [#C] Add explicit autoloads/requires for cross-module command keybindings :cleanup:refactor:
+
+Several custom utility keymaps bind symbols owned by other modules without
+declaring the relationship:
+- =custom-ordering.el= binds =cj/org-sort-by-todo-and-priority=.
+- =custom-text-enclose.el= binds =change-inner= and =change-outer=.
+- =custom-buffer-file.el= binds =cj/kill-buffer-and-window= and external-open
+ commands.
+
+These work in the current eager =init.el= load order, but standalone module
+loading and future deferral will be cleaner if the dependencies are explicit.
+
+Expected outcome:
+- Use =autoload= for commands that should remain lazy.
+- Use =declare-function= for byte-compiler clarity when only the symbol is
+ needed.
+- Add a simple module-load smoke test if this becomes part of the load-graph
+ refactor.
+
+*** TODO [#B] Review UI and navigation modules :review:
+
+Scope:
+- =ui-config.el=
+- =ui-navigation.el=
+- =ui-theme.el=
+- =font-config.el=
+- =modeline-config.el=
+- =selection-framework.el=
+- =mousetrap-mode.el=
+- =popper-config.el=
+
+Review progress:
+- Reviewed 2026-05-03.
+- =mousetrap-mode.el= has strong focused and integration tests.
+- =modeline-config.el= has pure string-helper coverage, but not VC/runtime
+ segment behavior.
+- =font-config.el=, =ui-theme.el=, =selection-framework.el=, =ui-navigation.el=,
+ and =popper-config.el= have little direct test coverage.
+
+**** TODO [#C] Decide whether =popper-config.el= should exist while disabled :cleanup:
+
+=popper-config.el= is required by =init.el=, but the only =use-package popper=
+form is =:disabled t=. That makes the module a no-op while still participating
+in the load graph.
+
+Expected outcome:
+- Either remove it from =init.el= until Popper is wanted, or re-enable and test
+ the popup behavior.
+- If kept disabled, add a clear task/comment explaining why it remains.
+
+This is low priority, but it is a good example of load graph noise to clean up
+during the =init.el= deferral work.
+
+*** TODO [#B] Review Org workflow modules :review:
+
+Scope:
+- =org-config.el=
+- =org-agenda-config.el=
+- =org-babel-config.el=
+- =org-capture-config.el=
+- =org-contacts-config.el=
+- =org-drill-config.el=
+- =org-export-config.el=
+- =org-noter-config.el=
+- =org-refile-config.el=
+- =org-reveal-config.el=
+- =org-roam-config.el=
+- =org-webclipper.el=
+- =calendar-sync.el=
+- =hugo-config.el=
+- =gloss-config.el=
+
+Review progress:
+- Reviewed 2026-05-03 at high level.
+- =calendar-sync.el= has substantial focused coverage for parsing, recurrence,
+ timezone conversion, event conversion, and regressions. The largest remaining
+ risks are configuration/secrets, startup side effects, and process/network
+ boundaries.
+- =org-agenda-config.el= and =org-refile-config.el= now have useful cache tests,
+ but the cache lifecycle and startup idle timers still deserve a design pass.
+- =org-noter-config.el= already has an older [#B] workflow VERIFY task. Do not
+ duplicate that work here.
+- =hugo-config.el= and =org-reveal-config.el= have focused helper coverage.
+- =gloss-config.el= is a thin package wrapper; no local unit-test target unless
+ custom glue is added.
+- Deeper pass 2026-05-10 added follow-up tasks for org-roam done hooks, drill
+ file selection/package loading, Org export defaults, Babel templates, and
+ contact/Mu4e boundaries.
+
+**** PROJECT [#A] Split personal calendar configuration from =calendar-sync.el= :security:refactor:
+
+=calendar-sync.el= is a reusable sync engine, but it also defines the personal
+=calendar-sync-calendars= value at top level. The concrete URLs are private feed
+tokens, so they should be rotated and moved out of source. This overlaps the
+top-level architecture/security item; this module task tracks the implementation
+shape.
+
+***** TODO [#A] Load calendar definitions from a private source :refactor:
+
+Expected outcome:
+- =calendar-sync.el= should default =calendar-sync-calendars= to nil or a safe
+ placeholder.
+- Put real calendar plists in private machine config, auth-source, env-backed
+ elisp, or an ignored file loaded from =custom-file= / host config.
+- =calendar-sync-status= and =calendar-sync-start= should explain missing config
+ clearly without erroring.
+
+Pitfalls:
+- =org-agenda-config.el= expects =gcal-file=, =pcal-file=, and =dcal-file= in
+ agenda file lists. Missing calendar config should not break agenda startup.
+- Avoid logging URLs on fetch failures.
+
+**** PROJECT [#B] Normalize Org agenda/refile cache lifecycle :perf:refactor:
+
+=org-agenda-config.el= and =org-refile-config.el= both solve the same startup
+problem with hand-rolled globals: cache value, cache time, TTL, "building" flag,
+idle timer, force-refresh command, and a synchronous fallback. The behavior is
+useful, but the implementation is duplicated and has edge cases.
+
+***** TODO [#B] Extract a small reusable cache helper or shared pattern :refactor:
+
+Expected outcome:
+- Keep the agenda and refile public commands unchanged.
+- Share the common "valid cache or rebuild" control flow, or at least document
+ why the two implementations intentionally differ.
+- Make "build in progress" semantics real. Today the message says "waiting",
+ but the code continues into a rebuild path rather than waiting or using the
+ old cache.
+
+***** TODO [#B] Make directory scan failures visible but non-fatal
+
+=org-refile-config.el= silently ignores =permission-denied= while scanning
+directories, and =org-agenda-config.el= assumes =projects-dir= exists and is
+readable. These are acceptable interactive defaults only if the resulting
+agenda/refile target list tells the user what was skipped.
+
+Expected outcome:
+- Missing optional roots should log a concise warning once per refresh.
+- Required roots should produce an actionable error.
+- Tests should cover missing =projects-dir= and permission/error cases by
+ stubbing directory functions.
+
+***** TODO [#C] Suppress startup idle timers in batch/test contexts
+
+Both modules start cache builders with =run-with-idle-timer= at top level. That
+is fine for interactive startup, but awkward for tests and batch commands.
+
+Expected outcome:
+- Gate the idle timers behind =(not noninteractive)= or an explicit startup
+ function.
+- Preserve normal interactive behavior.
+- Add a smoke test that requiring the modules in batch does not schedule cache
+ builders.
+
+**** TODO [#A] Revisit =org-confirm-babel-evaluate= default :security:
+
+=org-babel-config.el= sets =org-confirm-babel-evaluate= to nil globally. That
+means every source block in every Org file can execute without confirmation,
+including files from cloned repos, downloaded notes, or web clips.
+
+Expected outcome:
+- Decide whether the global default should be safe (=t=) with a fast toggle for
+ trusted buffers/projects, or whether only selected languages should skip
+ confirmation.
+- Keep =babel-confirm= or replace it with a clearer command that reports and
+ toggles the current policy.
+- Add a test/smoke assertion for the chosen default.
+
+**** TODO [#B] Add guardrails to =cj/move-org-branch-to-roam= :ux:
+
+=org-roam-config.el= implements =cj/move-org-branch-to-roam= by copying the
+subtree, cutting it from the source buffer, writing a new roam file, and syncing
+the database. There is no confirmation, rollback, or save behavior around the
+destructive step.
+
+Expected outcome:
+- Confirm before cutting large subtrees or when the source buffer is modified.
+- Write the new file before deleting source content, and avoid losing the
+ subtree if file creation or =org-roam-db-sync= fails.
+- Decide whether the source buffer should be saved automatically or left dirty.
+- Add tests around the pure slug/demotion/format helpers are already present;
+ add one integration-style test around failure ordering if feasible.
+
+**** TODO [#C] Make =org-webclipper.el= initialization less global-state-heavy :cleanup:refactor:
+
+=org-webclipper.el= stores protocol URL/title in global variables, registers
+capture templates lazily, and clears those globals during template expansion.
+That is workable for one-at-a-time org-protocol calls, but brittle if a capture
+is interrupted or nested.
+
+Expected outcome:
+- Prefer passing URL/title through the capture plist or a lexical wrapper rather
+ than global temp vars where possible.
+- Ensure aborted captures clear temp state.
+- Keep the existing browser bookmarklet workflow unchanged.
+
+**** TODO [#C] Review external executable assumptions in Org export/publishing modules :cleanup:
+
+=org-export-config.el= assumes =zathura= for one Pandoc PDF path, =hugo-config.el=
+assumes =hugo= and a browser/file-manager opener, =org-reveal-config.el= assumes
+a local =reveal.js= checkout, and =org-webclipper.el= assumes Pandoc through
+=org-web-tools=.
+
+Expected outcome:
+- Add explicit executable/directory checks before commands run.
+- Error messages should name the missing tool and the command/setup needed.
+- Keep startup quiet; only check expensive/external requirements when the
+ relevant command runs.
+
+**** TODO [#B] Guard the org-roam completed-task hook around non-file buffers :bug:refactor:
+
+=org-roam-config.el= adds a global =org-after-todo-state-change-hook= that
+copies newly completed tasks to today's org-roam journal. The hook assumes the
+current Org buffer is visiting a file:
+
+- It calls =(buffer-file-name)= and passes the result to =string=.
+- =cj/org-roam-copy-todo-to-today= later compares =file-truename= of the daily
+ file and the current buffer file.
+
+That can error in capture buffers, indirect buffers, temporary Org buffers, or
+other fileless Org workflows.
+
+Expected outcome:
+- Extract a predicate for "should copy this completed task to today's journal".
+- Skip fileless buffers, calendar sync files, aborted capture buffers, and tasks
+ already in the target daily file.
+- Keep the normal completed-task journal workflow unchanged.
+- Add tests for fileless buffers, =gcal-file=, already-daily buffers, and a
+ normal project/todo buffer.
+
+**** TODO [#B] Make Org drill file selection robust and shared :bug:refactor:
+
+=org-capture-config.el= and =org-drill-config.el= both scan =drill-dir= for
+candidate =.org= files with inline =directory-files= calls. If =drill-dir= is
+missing, empty, or unreadable, the user gets a low-level error from whichever
+command happened to run.
+
+Expected outcome:
+- Extract one helper that returns valid drill files or signals a clear
+ =user-error=.
+- Use it from drill capture templates, =cj/drill-start=, and =cj/drill-edit=.
+- Preserve the current completing-read workflow when files exist.
+- Add tests for missing directory, empty directory, and normal selection list.
+
+**** TODO [#C] Clarify contradictory Org export task defaults :cleanup:tests:
+
+=org-export-config.el= sets =org-export-with-tasks= twice in a row: first to
+=("TODO")= and then to =nil=. The final behavior is "export no tasks", but the
+adjacent comments describe both policies.
+
+Expected outcome:
+- Pick the intended default and remove the contradictory assignment/comment.
+- Add a narrow smoke test for the chosen =org-export-with-tasks= value after
+ =ox= config loads.
+- If task export should vary by workflow, expose an explicit command or local
+ export option instead of relying on the global default.
+
+**** TODO [#C] Fix and cover Org Babel structure templates :bug:tests:
+
+=org-babel-config.el= adds a Java structure template as =("java" . "src javas")=,
+which appears to expand to the wrong language name. The template list is useful
+but currently untested, so small typos can persist unnoticed.
+
+Expected outcome:
+- Correct the Java template or remove it if Java blocks are not used.
+- Add a focused test that loads the Org Tempo config and asserts key templates
+ expand to the intended language names for common aliases: =bash=, =zsh=,
+ =el=, =py=, =json=, =yaml=, and =java=.
+
+**** TODO [#C] Make org-contacts/Mu4e boundaries explicit :cleanup:refactor:
+
+=org-contacts-config.el= defines helpers that call Mu4e functions when the
+current major mode is a Mu4e mode, and the =use-package org-contacts= block is
+=:after (org mu4e)= while also requiring =mu4e= inside =:config=. This works in
+the current eager setup, but the ownership boundary is unclear now that
+=mu4e-org-contacts-integration.el= exists.
+
+Expected outcome:
+- Decide whether contact capture-from-email behavior belongs in
+ =org-contacts-config.el= or the Mu4e integration modules.
+- Add =declare-function= / autoloads or move Mu4e-specific code behind
+ =with-eval-after-load 'mu4e=.
+- Keep plain Org contact commands usable on systems without Mu4e loaded.
+- Add a smoke test for loading =org-contacts-config.el= without Mu4e stubs if
+ practical.
+
+**** TODO [#C] Add an Org workflow health check command :feature:ux:
+
+Several Org workflow modules depend on personal paths, optional external tools,
+and local package checkouts. Failures currently show up at command time in
+different ways, depending on which module hits the missing dependency first.
+
+Recommended improvement:
+- Add a lightweight =cj/org-workflow-doctor= command that checks the main Org
+ workflow prerequisites without mutating user data.
+- Report status for core files/directories: =org-dir=, =roam-dir=, =drill-dir=,
+ =contacts-file=, =webclipped-file=, =cj/hugo-content-org-dir=, and
+ =cj/reveal-root=.
+- Report optional executable/package availability for Pandoc/org-web-tools,
+ Hugo, reveal.js, org-drill, org-roam, and org-noter.
+- Keep startup quiet; run this only on demand.
+- Make the checker return structured data so it can be unit-tested and displayed
+ either in Messages or a buffer.
+
+**** TODO [#C] Add capture-template key collision and target smoke tests :tests:
+
+Org capture templates are assembled across =org-capture-config.el=,
+=org-contacts-config.el=, =org-webclipper.el=, and other feature modules. The
+current setup works, but template ownership is implicit and duplicate keys or
+missing target files would be easy to miss.
+
+Recommended improvement:
+- Add a test helper that loads the Org capture-related modules with temp path
+ bindings.
+- Assert template keys are unique or intentionally overridden.
+- Assert templates that write to files point at non-empty path variables.
+- Cover lazy additions for contact and webclipper templates without requiring a
+ browser/org-protocol round trip.
+
+**** TODO [#C] Document Org workflow module ownership and load boundaries :docs:refactor:
+
+The Org workflow is spread across many modules with overlapping responsibilities:
+capture templates, keymaps, org-protocol handlers, refile/agenda target
+construction, roam notes, publishing, and document annotation. The code is
+usable, but future load-order work will be easier with explicit ownership notes.
+
+Recommended improvement:
+- Add a short design note under =docs/design/= that maps each Org module to the
+ behavior it owns.
+- Call out which modules may mutate global Org variables, capture templates,
+ keymaps, and protocol handlers.
+- Define which modules should be safe to load in batch mode and which are
+ allowed to start timers or require interactive packages.
+- Link this note from the Org workflow review task and the broader load-graph
+ refactor.
+
+*** TODO [#B] Review programming workflow modules :review:
+
+Scope:
+- =prog-c.el=
+- =prog-general.el=
+- =prog-go.el=
+- =prog-json.el=
+- =prog-lisp.el=
+- =prog-lsp.el=
+- =prog-python.el=
+- =prog-shell.el=
+- =prog-training.el=
+- =prog-webdev.el=
+- =prog-yaml.el=
+- =coverage-core.el=
+- =coverage-elisp.el=
+- =test-runner.el=
+- =vc-config.el=
+- =keyboard-compat.el=
+- =dev-fkeys.el=
+
+Review progress:
+- Reviewed 2026-05-03 at high level.
+- =dev-fkeys.el= reviewed 2026-05-03 after local edits settled. The focused
+ dev-fkeys test set passed: 22 test files, 163 ERT tests.
+- =coverage-core.el= / =coverage-elisp.el= have strong pure-helper tests.
+- Language formatter wiring is covered for Python, Go, shell, webdev, JSON, and
+ YAML.
+- =test-runner.el= has direct tests, but project-scoping is still a design gap.
+
+**** TODO [#C] Revisit F4 project classification vs actual project capabilities :ux:
+
+=dev-fkeys.el= classifies a project as =interpreted= if it has
+=pyproject.toml=, =requirements.txt=, =Pipfile=, or =package.json=, even when it
+also has a =Makefile=. That intentionally keeps Python/Node projects on a
+Run-only F4 menu, but it also hides useful Compile/Clean options for projects
+where =Makefile=, =package.json= scripts, or Projectile cached commands provide
+real build/test tasks.
+
+Expected outcome:
+- Decide whether F4 should classify by language family or by available
+ capabilities.
+- Consider deriving candidates from Projectile's known compile/run/test commands
+ first, then falling back to markers.
+- Keep the current "interpreted markers win" behavior only if that remains the
+ intentional UX after trying it in mixed Python/Node projects.
+
+**** PROJECT [#B] Consolidate LSP ownership across programming modules :architecture:refactor:
+
+LSP setup is currently split across =prog-general.el=, =prog-lsp.el=, and each
+language module. There are multiple =use-package lsp-mode= forms and some
+conflicting defaults:
+- =prog-general.el= enables snippets/UI doc/sideline behavior.
+- =prog-lsp.el= disables snippets/UI doc/sideline-heavy behavior.
+- Python, Go, shell, C, and webdev modules both call =lsp-deferred= from local
+ setup functions and add package hooks that call =lsp-deferred= again.
+
+This probably works because lsp-mode is defensive, but it makes the final
+runtime policy hard to predict.
+
+***** TODO [#B] Make =prog-lsp.el= the single owner of generic LSP policy :refactor:
+
+Expected outcome:
+- Move generic =lsp-mode= and =lsp-ui= defaults out of =prog-general.el=.
+- Keep language-specific server variables in language modules.
+- Keep one hook path per language for starting LSP.
+- Preserve the remote-file guard.
+
+Pitfalls:
+- =lsp-pyright= may still need a language-specific hook to load before LSP
+ starts.
+- Do not accidentally re-enable UI/doc/sideline behavior that was explicitly
+ disabled for performance.
+
+***** TODO [#B] Add a startup smoke test for LSP config resolution
+
+Keep this narrow. A useful test can require the LSP-related modules with mocked
+=use-package= side effects and assert that:
+- generic defaults are set in one place,
+- no duplicate hook entries are installed for the same mode,
+- =lsp-enable-remote= remains nil.
+
+**** TODO [#B] Gate tree-sitter grammar auto-install behind an explicit policy :startup:
+
+=prog-general.el= sets =treesit-auto-install= to =t=. That means opening a file
+can trigger grammar download/build/install behavior. This is convenient on a
+fresh machine, but it is a startup/network/build side effect in normal editing
+and batch contexts.
+
+Expected outcome:
+- Prefer ='prompt= or a custom command such as =cj/install-treesit-grammars=.
+- Batch/test startup should never auto-install grammars.
+- Document the intentional bootstrap path for a new machine.
+
+This should be coordinated with the existing [#A] Python tree-sitter predicate
+syntax issue, since both touch tree-sitter reliability.
+
+**** TODO [#C] Harden git clone from clipboard in =vc-config.el= :robustness:refactor:
+
+=cj/git-clone-clipboard-url= shells out to =git clone= from clipboard text and
+derives the clone directory with =file-name-nondirectory=. The URL is quoted, so
+this is not an immediate shell-injection bug, but process handling and path
+derivation are still brittle.
+
+Expected outcome:
+- Use =start-process= or =call-process= with =("git" "clone" url)=.
+- Validate that the target directory exists and is writable before cloning.
+- Derive the expected repository directory robustly for HTTPS, SSH, and local
+ clone URLs.
+- Report clone failures from the process exit status instead of assuming the
+ directory appears.
+
+**** TODO [#C] Decide whether auto-executable shell scripts should be opt-in :ux:
+
+=prog-shell.el= adds a global =after-save-hook= that sets executable bits on any
+saved file with a shebang. This is convenient, but it silently changes file
+modes for every buffer in the session.
+
+Expected outcome:
+- Decide whether this should remain global, be limited to shell/script modes, or
+ prompt the first time per file.
+- Preserve the fast path for real scripts.
+- Keep the existing =cj/make-script-executable= tests updated for the chosen
+ policy.
+
+**** TODO [#C] Review language formatter process boundaries :cleanup:
+
+JSON, YAML, and webdev formatters use =shell-command-on-region= with command
+strings. Most inputs are fixed or shell-quoted, but formatter code is a good
+place to standardize process handling.
+
+Expected outcome:
+- Prefer process APIs with argv lists where practical.
+- Keep point preservation behavior.
+- Keep existing formatter wiring tests and add command-construction tests if a
+ helper is extracted.
+
+*** TODO [#B] Review integrations and application modules :review:
+
+Scope:
+- AI/rest: =ai-config.el=, =ai-conversations.el=, =restclient-config.el=
+- Mail/chat/social: =mail-config.el=, =mu4e-*.el=, =slack-config.el=,
+ =erc-config.el=, =elfeed-config.el=, =eww-config.el=
+- File/media/apps: =dirvish-config.el=, =dwim-shell-config.el=, =pdf-config.el=,
+ =calibredb-epub-config.el=, =music-config.el=, =quick-video-capture.el=,
+ =video-audio-recording.el=, =transcription-config.el=
+- Utilities/apps: =auth-config.el=, =browser-config.el=, =dashboard-config.el=,
+ =help-config.el=, =help-utils.el=, =jumper.el=, =keyboard-macros.el=,
+ =local-repository.el=, =lorem-optimum.el=, =reconcile-open-repos.el=,
+ =show-kill-ring.el=, =system-commands.el=, =system-utils.el=,
+ =tramp-config.el=, =undead-buffers.el=, =weather-config.el=, =wrap-up.el=
+
+Review progress:
+- Reviewed 2026-05-03 at high level by direct reads plus risky-pattern search.
+- Recording/transcription and music modules have much stronger coverage than
+ most application wrappers.
+- Existing coverage audit already tracks =ai-conversations=, =quick-video-capture=,
+ =dashboard-config=, =mail-config=, =show-kill-ring=, =system-commands=, and
+ =wrap-up= as high-value test targets.
+
+**** TODO [#B] Make system restart/shutdown commands more defensive :safety:
+
+=system-commands.el= exposes high-impact shell commands through a convenience
+menu. The restart-Emacs path starts a shell command that restarts the user
+service and reconnects, then schedules =kill-emacs= after one second. If the
+service command is unavailable or fails, the current session can still be killed.
+
+Expected outcome:
+- Check whether the Emacs daemon service exists before offering the service
+ restart command.
+- Start restart/reconnect work as a process with an exit sentinel.
+- Kill the current Emacs only after the replacement path has clearly started,
+ or keep a non-daemon fallback that does not kill the session on failure.
+- Consider requiring a stronger confirmation for shutdown/reboot than a single
+ RET/space confirmation.
+- Add smoke tests around key resolution and command selection without invoking
+ real system commands.
+
+**** TODO [#A] Prevent REST API keys from being saved into template files :security:bug:
+
+=restclient-config.el= opens =data/skyfi-api.rest= and replaces the
+=:skyfi-key= line in that file-visiting buffer with the real key from
+=authinfo.gpg=. Even if the function does not write to disk itself, an
+accidental save can persist the key.
+
+Expected outcome:
+- Open SkyFi requests in a scratch/indirect buffer, or mark the injected buffer
+ read-only with a save guard that restores =PLACEHOLDER= before writing.
+- Make the buffer visibly modified state sane after injection.
+- Keep the existing tests that assert the template file remains unchanged, and
+ add a test for accidental save behavior.
+
+**** TODO [#B] Reconcile mail image/privacy settings :privacy:
+
+=mail-config.el= documents blocked remote images and sets
+=gnus-blocked-images=, but later enables both =mu4e-show-images= and
+=mu4e-view-show-images=. The interactive toggle changes =gnus-blocked-images=
+buffer-locally, so the final privacy behavior is hard to reason about without
+manual testing against real HTML messages.
+
+Expected outcome:
+- Decide the default policy for embedded images versus remote HTTP images.
+- Make the toggle report the effective state in the current mu4e view buffer.
+- Add a short manual checklist or mocked test for the variables that control
+ remote image display.
+
+**** TODO [#C] Clean up mail compose buffer lifecycle conflicts :cleanup:quick:
+
+=mail-config.el= first sets =message-kill-buffer-on-exit= to =t= in the mu4e
+configuration, then =org-msg= later sets it to nil. That may be intentional for
+org-msg editing, but the ownership is unclear.
+
+Expected outcome:
+- Decide whether compose buffers should be killed on send/exit for plain mu4e,
+ org-msg, or both.
+- Move the final policy next to the owner module.
+- Add a short note in the config explaining the choice.
+
+**** TODO [#B] Remove automatic startup timers from =quick-video-capture.el= :startup:refactor:
+
+=quick-video-capture.el= schedules both an =after-init-hook= idle timer and a
+fallback =run-with-timer= to initialize org-protocol/capture glue shortly after
+startup. This is a small side effect, but it loads Org capture/protocol plumbing
+even if the video workflow is never used.
+
+Expected outcome:
+- Register the protocol lazily through autoloadable setup, or initialize only
+ when Org/protocol support is already active.
+- Batch/test startup should not schedule timers.
+- Keep manual bookmarklet usage working when an org-protocol URL arrives before
+ the rest of Org has been used.
+
+**** TODO [#B] Avoid global temp state in =quick-video-capture.el= :cleanup:refactor:
+
+Like =org-webclipper.el=, quick video capture passes URL state through a global
+=cj/video-download-current-url=. Interrupted captures or nested capture flows can
+leave stale state.
+
+Expected outcome:
+- Pass the URL through capture/protocol state where possible.
+- Ensure aborted captures clear the temp URL.
+- Add coverage for manual URL prompt, protocol URL, and aborted capture cleanup.
+
+**** TODO [#B] Audit shell-command-heavy recording and dwim-shell workflows :security:refactor:
+
+=video-audio-recording.el= and =dwim-shell-config.el= are intentionally close to
+the shell: pactl/ffmpeg/qpdf/7z/tesseract/media conversion commands are the
+point. They also have the highest process and quoting surface in the config.
+
+Expected outcome:
+- Keep the current workflows, but catalog which commands accept filenames,
+ URLs, passwords, or free-form user input.
+- Prefer argv process APIs for commands that do not require a shell.
+- For commands that must use shell templates, document which placeholders are
+ safely quoted by =dwim-shell-command= and add focused tests around password
+ temp-file cleanup.
+
+***** TODO [#A] Fix async password temp-file lifetime in dwim-shell commands :bug:
+
+Several password commands create a temp file, call
+=dwim-shell-command-on-marked-files=, and delete the temp file in
+=unwind-protect= immediately after the command is launched. Because these
+commands are normally asynchronous, =qpdf= or =7z= may start after the password
+file is already gone.
+
+Affected workflows:
+- PDF password protect and unprotect.
+- Remove ZIP encryption.
+- Create encrypted ZIP.
+
+Expected outcome:
+- Keep password material out of command-line arguments.
+- Delete password files only after the spawned process exits.
+- Add tests or a small harness that proves cleanup happens on success, failure,
+ and user cancellation.
+
+***** TODO [#A] Quote or argv-ify user-controlled dwim-shell inputs :security:bug:
+
+Several commands interpolate clipboard text, archive names, prefixes,
+recipients, timestamps, and output paths into shell templates. Some are quoted
+by dwim-shell placeholders, but several explicit =format= calls are not robust
+against spaces, quotes, newlines, or shell metacharacters.
+
+Specific cases to check first:
+- =cj/dwim-shell-commands-git-clone-clipboard-url= uses =git clone <<cb>>=
+ rather than an argv process call or a quoted URL.
+- Encrypted archive names and GPG recipients are interpolated into single-quoted
+ shell fragments.
+- Sequential rename prefixes are interpolated into =mv= destinations.
+- Video thumbnail timestamps come from =read-string= and are inserted into
+ =ffmpeg -ss=.
+- Video concatenation builds a concat list with =echo= / =tr= / =sed=, which is
+ fragile for filenames with spaces or quotes.
+
+Expected outcome:
+- Replace high-risk commands with process helpers where practical.
+- Where dwim-shell templates remain, add focused command-construction tests.
+- Validate user strings as domain values when possible, e.g. ffmpeg timestamps.
+
+***** TODO [#B] Clarify broad or misleading file-operation commands :safety:bug:
+
+Two dwim-shell commands look broader or weaker than their names suggest:
+- =cj/dwim-shell-commands-remove-empty-directories= runs
+ =find . -type d -empty -delete= from the current directory, not from the marked
+ files.
+- =cj/dwim-shell-commands-secure-delete= calls =shred= without =-u=, so it may
+ overwrite file contents but leave the directory entry in place.
+
+Expected outcome:
+- Scope empty-directory cleanup to an explicit selected root and show that root
+ in the confirmation prompt.
+- Decide whether secure delete should actually remove files with =shred -u= or
+ be renamed to describe overwrite-only behavior.
+- Add tests around command strings or extract small pure builders.
+
+***** TODO [#B] Quote X11 and audio recording command paths :bug:
+
+=video-audio-recording.el= quotes devices and filenames in the Wayland
+=wf-recorder= command path, but the X11 =ffmpeg= path and audio-only =ffmpeg=
+path interpolate device names and output filenames without shell quoting. This
+will break on output directories with spaces and can mishandle unusual device
+names.
+
+Expected outcome:
+- Shell-quote mic device, system device, and output file consistently in every
+ shell command path.
+- Prefer argv process APIs for ffmpeg where possible.
+- Add regression tests for recording directories with spaces.
+
+***** TODO [#B] Track recorder processes instead of killing by program name :safety:bug:
+
+The Wayland recording path stops recording with =pkill -INT wf-recorder=. That
+can interrupt unrelated =wf-recorder= processes outside Emacs.
+
+Expected outcome:
+- Store the process object or PID for the recorder launched by this module.
+- Stop only that process or process group.
+- Preserve existing toggle behavior and tests for already-running recordings.
+
+***** TODO [#C] Ensure chosen recording directories are created directly :bug:
+
+The recording toggles accept a directory via prefix argument, then derive parent
+directories in a way that can create the parent but not necessarily the selected
+recording directory itself.
+
+Expected outcome:
+- Normalize the selected destination as either an explicit file or explicit
+ directory.
+- Ensure the actual target directory exists before launching ffmpeg/wf-recorder.
+- Add tests for new directories and paths containing spaces.
+
+**** TODO [#C] Make AI conversation persistence path-safe and project-aware :cleanup:refactor:
+
+=ai-conversations.el= has good pure helper seams but is currently untested in
+this repo. The path slugging is simple and the save/load/delete commands operate
+directly in a single global directory.
+
+Expected outcome:
+- Add tests for candidate sorting, topic slug collisions, autosave path setup,
+ and delete confirmation behavior.
+- Consider whether conversations should remain global or support project-scoped
+ subdirectories.
+- Confirm autosave never writes partial prompt/response state to an unexpected
+ file after loading a different conversation.
+
+**** TODO [#B] Harden calendar sync operational behavior around the parser :data:refactor:
+
+=calendar-sync.el= has broad parser/recurrence coverage, but the operational
+path around it still has startup, persistence, and fetch risks.
+
+Expected outcome:
+- Move private calendar URLs out of source and rotate the exposed feed URLs
+ before doing further cleanup.
+- Avoid immediate network fetches at module load unless explicitly enabled for
+ interactive sessions.
+- Add a per-calendar in-flight guard so a timer tick cannot launch overlapping
+ syncs for the same calendar.
+- Use =curl --fail= or equivalent status handling so HTTP error pages are not
+ treated as successful ICS downloads.
+- Write generated Org files atomically via a temp file and rename.
+- Read the local state file with =read-eval= disabled.
+
+**** TODO [#B] Add first coverage for AI conversation persistence :tests:
+
+=ai-conversations.el= is not currently represented in =.coverage/simplecov.json=.
+The module has several pure helper seams and a few file operations that can be
+tested without loading GPTel.
+
+Expected outcome:
+- Test slug generation, timestamp parsing, candidate sorting, and latest-file
+ selection.
+- Test save/load header stripping against a temp conversations directory.
+- Test autosave path setup and delete confirmation with stubbed prompts.
+- Keep GPTel itself mocked or avoided unless a later integration test needs it.
+
+**** TODO [#C] Add first coverage for Dirvish utility helpers :tests:
+
+=dirvish-config.el= is not currently represented in =.coverage/simplecov.json=.
+The pure-ish Dired helpers have a few sharp edges that are easy to characterize
+with mocked =dired-get-filename= / =dired-get-marked-files= calls.
+
+Expected outcome:
+- Test playlist path construction and reject playlist names that escape
+ =music-dir=.
+- Test duplicate/copy-path/wallpaper helpers when there is no file at point.
+- Test project-relative, home-relative, absolute, and Org-link path copying.
+- Keep Dirvish package loading mocked; these tests should not require the full
+ UI package.
+
+**** TODO [#B] Require runtime constants explicitly in =dirvish-config.el= :startup:bug:
+
+=dirvish-config.el= uses =eval-when-compile= for =user-constants= and
+=system-utils=, but runtime configuration constructs quick-access entries from
+constants such as =code-dir=, =music-dir=, =pix-dir=, and recording directories.
+This depends on load order rather than the module declaring its runtime inputs.
+
+Expected outcome:
+- Require runtime dependencies normally or add clear =defvar= declarations for
+ values owned elsewhere.
+- Keep byte compilation clean without making standalone module loads depend on
+ accidental init order.
+- Add a module-load smoke test with required constants stubbed.
+
+**** TODO [#B] Harden Dirvish path helpers around nil files and path traversal :bug:
+
+Several Dirvish helpers derive path components before checking whether Dired has
+a file at point. Playlist creation also accepts a raw playlist name and expands
+it under =music-dir= without rejecting =../= style input.
+
+Expected outcome:
+- In duplicate, copy-path, and wallpaper helpers, check for a file before
+ calling path functions.
+- Reject playlist names that contain directory separators or resolve outside
+ =music-dir=.
+- Add regression tests for no-file-at-point and traversal-like playlist names.
+
+**** TODO [#C] Add first smoke coverage for mail and system command modules :tests:
+
+=mail-config.el= and =system-commands.el= are not currently represented in the
+coverage report. Both can get useful coverage without sending mail or invoking
+real system commands.
+
+Expected outcome:
+- For mail, stub executable discovery and assert the resulting config either
+ assigns valid commands or reports missing dependencies clearly.
+- For system commands, test keymap shape, menu candidates, confirmation routing,
+ and command-string construction with =shell-command= stubbed.
+- Keep all tests batch-safe and non-destructive.
+
+**** TODO [#C] Harden EWW/Elfeed synchronous network helpers :cleanup:refactor:
+
+=elfeed-config.el= includes synchronous URL retrieval helpers for converting
+YouTube channel/playlist URLs into feed entries, and =eww-config.el= advises URL
+retrieval to inject a user agent only from EWW buffers.
+
+Expected outcome:
+- Add timeouts/error handling to synchronous feed-conversion requests.
+- Kill temporary URL buffers after parsing.
+- Add a small test or manual checklist for the EWW user-agent advice so it does
+ not affect package.el or non-EWW URL callers.
+
+**** TODO [#C] Move Slack which-key registration behind =with-eval-after-load= :cleanup:quick:
+
+=slack-config.el= calls =which-key-add-keymap-based-replacements= at top level,
+while most modules defer which-key registration. If which-key is not loaded or
+autoloaded as expected, Slack config can fail during require.
+
+Expected outcome:
+- Wrap the registration in =with-eval-after-load 'which-key=.
+- Add a module-load smoke test or byte-compile check if easy.
+
+** VERIFY [#B] Move lsp-file-watch-ignored-directories to global .emacs.d config :chore:refactor:
+SCHEDULED: <2026-04-27 Mon>
+
+Shipped 2026-04-26 in commit 781b46e. Implementation: =cj/lsp-file-watch-ignored-extras= (thirteen patterns) and =cj/lsp--add-file-watch-ignored-extras= in =modules/prog-lsp.el=, called from the lsp-mode use-package =:config=. Seven ERT tests in =tests/test-prog-lsp--add-file-watch-ignored-extras.el=, all green.
+
+Manual verify (tomorrow): restart Emacs, open =~/code/deepsat/orchestration_dashboard_mvp/backend/test_mission_image_api.py=, watch for the file-watch prompt. Expected: no prompt, or count well below the previous 1905. If still prompting at ~1905, iterate on the pattern list.
+
+After verification: drop the redundant =lsp-file-watch-ignored-directories= entry from the deepsat MVP's =.dir-locals.el= here and on velox.
+
+Setting =lsp-file-watch-ignored-directories= via the project's =.dir-locals.el= doesn't apply at the buffer level. Confirmed via =M-: lsp-file-watch-ignored-directories= in a Python buffer — value is the lsp-mode default, not the 7 patterns we wrote in dir-locals. The safety prompt was answered with =!= and the dir-locals are otherwise live (the projectile commands take effect).
+
+Fix: move the seven patterns into the lsp config module as a global default with =setq-default= or per-pattern =add-to-list=. The patterns are project-agnostic build/cache directories — safe as defaults for any project.
+
+Patterns to add:
+- =[/\\\\]node_modules\\'=
+- =[/\\\\]\\.ruff_cache\\'=
+- =[/\\\\]dist\\'=
+- =[/\\\\]coverage\\'=
+- =[/\\\\]test-results\\'=
+- =[/\\\\]playwright-report\\'=
+- =[/\\\\]tf[/\\\\]\\.terraform\\'=
+
+After landing: =M-x lsp-workspace-shutdown=, reopen a Python file, confirm the directory count drops well below the default threshold of 1000 (currently 1905). Then remove the redundant entries from the deepsat MVP's =.dir-locals.el= here and on velox.
+
+Discovered 2026-04-26 testing dashboard MVP F-key setup.
+
+** VERIFY [#B] Continue org-noter custom workflow implementation (IN PROGRESS) :feature:bug:
+
+Continue debugging and testing the custom org-noter workflow from 2025-11-21 session.
+This is partially implemented but has known issues that need fixing before it's usable.
+
+**Last worked on:** 2025-11-21
+**Current status:** Implementation complete but has bugs, needs testing
+
+**Known Issues to Fix:**
+
+1. **Double notes buffer appearing when pressing 'i' to insert note**
+ - When user presses 'i' in document to insert a note, two notes buffers appear
+ - Expected: single notes buffer appears
+ - Need to debug why the insert-note function is creating duplicate buffers
+
+2. **Toggle behavior refinement needed**
+ - The toggle between document and notes needs refinement
+ - May have edge cases with window management
+ - Need to test various scenarios
+
+**Testing Needed:**
+
+1. **EPUB files** - Test with EPUB documents (primary use case)
+2. **Reopening existing notes** - Verify it works when notes file already exists
+3. **Starting from notes file** - Test opening document from an existing notes file
+4. **PDF files** - Verify compatibility with PDF workflow
+5. **Edge cases:**
+ - Multiple windows open
+ - Splitting behavior
+ - Window focus after operations
+
+**Implementation Files:**
+- modules/org-noter-config.el - Custom workflow implementation
+- Contains custom functions for document/notes toggling and insertion
+
+**Context:**
+This custom workflow is designed to make org-noter more ergonomic for Craig's reading/annotation
+workflow. It simplifies the toggle between document and notes, and streamlines note insertion.
+The core functionality is implemented but needs debugging before it's production-ready.
+
+**Next Steps:**
+1. Debug the double buffer issue when pressing 'i'
+2. Test all scenarios listed above
+3. Refine toggle behavior based on testing
+4. Document the final keybindings and workflow
+
+** VERIFY [#B] Test and review restclient.el implementation :tests:
+
+Test the new REST API client integration in a running Emacs session.
+
+**Keybindings to test:**
+- C-; R n — new scratch *restclient* buffer (should open in restclient-mode)
+- C-; R o — open .rest file (should default to data/ directory)
+- C-; R s — open SkyFi template (should auto-inject API key from authinfo)
+
+**Functional tests:**
+1. Open tutorial-api.rest, run JSONPlaceholder GET (C-c C-c) — verify response inline
+2. Run POST example — verify 201 response with fake ID
+3. Run httpbin header echo — verify custom headers echoed back
+4. Navigate between requests with C-c C-n / C-c C-p
+5. Test jq filtering (requires jq installed): restclient-jq loaded?
+6. Open scratch buffer (C-; R n), type a request manually, execute
+7. which-key shows "REST client" menu under C-; R
+
+**SkyFi key injection (if authinfo entry exists):**
+- C-; R s should replace :skyfi-key = PLACEHOLDER with real key
+- Key should NOT be written to disk (verify file still shows PLACEHOLDER)
+
+* Emacs Resolved
+** DONE [#B] Fix likely =elpa-mirror-location= path bug :bug:quick:
+CLOSED: [2026-05-03 Sun]
+
+=early-init.el= builds =elpa-mirror-location= with:
+
+#+begin_src emacs-lisp
+(concat user-home-dir ".elpa-mirrors/")
+#+end_src
+
+That likely expands to =~/..= incorrectly, e.g. =/home/cjennings.elpa-mirrors/=
+instead of =/home/cjennings/.elpa-mirrors/=. Use =expand-file-name= instead.
+
+Acceptance criteria:
+- Local mirror paths resolve under the home directory as intended.
+- Add a small testable helper if this logic moves out of =early-init.el=.
+
+Done 2026-05-03:
+- Replaced =concat= path construction with =expand-file-name= for
+ =elpa-mirror-location=, =localrepo-location=, and local mirror archive paths.
+- Added =tests/test-early-init-paths.el= to load =early-init.el= with package
+ side effects stubbed and assert local archive paths.
+
+** DONE [#B] Fix =vc-follow-symlinks= setting in =system-defaults.el= :bug:quick:
+CLOSED: [2026-05-03 Sun]
+
+=modules/system-defaults.el= has:
+
+#+begin_src emacs-lisp
+(setq-default vc-follow-symlinks)
+#+end_src
+
+The comment says "don't ask to follow symlinks if target is version
+controlled", but evaluating this leaves =vc-follow-symlinks= as =nil=. That
+means the intended prompt suppression is not actually configured. The likely
+fix is =t=, but verify the exact Emacs semantics first.
+
+Acceptance criteria:
+- Set =vc-follow-symlinks= to the intended value explicitly.
+- Add a small regression test or startup smoke assertion for this setting.
+- Confirm opening a symlinked, version-controlled file no longer prompts.
+
+Done 2026-05-03:
+- Confirmed from Emacs docs that =t= follows version-controlled symlinks without
+ prompting.
+- Set =vc-follow-symlinks= explicitly to =t=.
+- Added =tests/test-system-defaults-vc-follow-symlinks.el=.
+
+** DONE [#B] Fix overwritten =C-; != system command prefix :bug:quick:
+CLOSED: [2026-05-03 Sun]
+
+=system-commands.el= first binds =cj/system-command-map= under =C-; !=, then
+later replaces the same prefix with =cj/system-command-menu=:
+
+#+begin_src emacs-lisp
+(keymap-set cj/custom-keymap "!" cj/system-command-map)
+...
+(keymap-set cj/custom-keymap "!" #'cj/system-command-menu)
+#+end_src
+
+That likely makes the documented subkeys such as =C-; ! r= and =C-; ! s=
+unreachable.
+
+Expected outcome:
+- Decide whether =C-; != is a prefix map or a direct menu command.
+- If keeping both, bind the menu inside the prefix, e.g. =C-; ! != or =C-; ! m=.
+- Add a key-resolution smoke test for the chosen bindings.
+
+Done 2026-05-03:
+- Kept =C-; != as the prefix map.
+- Moved the completing-read menu to =C-; ! !=.
+- Added which-key labels for the documented subkeys.
+- Added =tests/test-system-commands-keymap.el=.
+
+** DONE [#B] Ensure formatters for TS, Python, Go, Shell with automated tests :tests:
+CLOSED: [2026-04-30 Thu]
+
+Audit showed the four formatters were already consistently bound to =C-; f=
+across the relevant mode-maps. No production change needed — this branch
+shipped the regression net only.
+
+5 new files (1 testutil + 4 per-language test files), 17 tests total, all
+passing.
+
+Per-language wiring inventory (locked in):
+- Python: =blacken-buffer= in =python-ts-mode-map= (use-package =:bind=)
+- Shell: =shfmt-buffer= in =sh-mode-map= and =bash-ts-mode-map=
+ (use-package =:bind=, gated on =:if (executable-find shfmt-path)=)
+- Go: =gofmt= via =cj/go-mode-keybindings= hook + =local-set-key=
+- TS / JS / Web: =cj/webdev-format-buffer= via =cj/webdev-keybindings= hook
+
+Each test file checks: prog module requires without error, formatter package
+is in =features=, format command is fboundp, C-; f binding resolves, and the
+underlying executable is on PATH (skipped via =ert-skip= if not installed).
+
+Real-formatting tests (run formatter on misformatted input, assert output)
+were deferred — wiring tests catch the highest-frequency regressions cheaply
+without crossing the boundary into testing the upstream formatter tools.
+
+** DONE [#A] Continue coverage push on low-coverage modules :tests:
+CLOSED: [2026-04-30 Thu]
+
+The four scoped low-coverage modules — =keybindings.el=, =config-utilities.el=,
+=org-noter-config.el=, =host-environment.el= — are now covered. 121 new tests
+across 18 test files. Plus one production bug fixed in
+=cj/validate-org-agenda-timestamps= (property-check branch was dead since the
+function was written: =(intern (downcase prop))= built plain symbols where
+=org-element-property= expects keywords).
+
+Modules covered (per-function test files, Normal/Boundary/Error categories):
+
+- =keybindings.el= — =cj/jump-open-var=, the auto-generated jump commands.
+- =host-environment.el= — laptop/desktop predicates, platform predicates,
+ display predicates, system-timezone detection. Folded a docstring fix on
+ =cj/detect-system-timezone= along the way.
+- =config-utilities.el= — =with-timer=, =cj/compile-this-elisp-buffer=,
+ =cj/emacs-build--summary-string=, info-commands smoke. Plus refactor pass
+ to extract testable internals from the four heavyweight interactives:
+ =cj/--delete-compiled-files-in-dir=, =cj/--benchmark-method=,
+ =cj/--recompile-emacs-home=, =cj/--validate-timestamps-in-buffer= +
+ =cj/--format-validation-report-section=.
+- =org-noter-config.el= — preferred-split, title-to-slug,
+ generate-notes-template, the predicate cluster.
+
+Broader scope (the 11 high-value untested modules, 7 lightly-tested ones, and
+~28 use-package wrappers to triage) is tracked under the [#B] "Coverage audit:
+untested and lightly-tested modules" entry.
+
+** DONE [#B] Test Slack mark-as-read and bury buffer (C-; S q) :tests:
+CLOSED: [2026-04-30 Thu]
+
+Verified working. =cj/slack-mark-read-and-bury= is bound to =C-; S q= in
+=modules/slack-config.el=, replacing the previous binding that referenced a
+non-existent =slack-buffer-mark-as-read-and-bury=.
+
+** DONE [#A] Fix calendar-sync UNTIL boundary regression :bug:
+CLOSED: [2026-05-03 Sun]
+
+Real cause was a Saturday-only flake in the test, not a stale =.elc= as
+earlier triage suggested. =test-calendar-sync--expand-weekly-boundary-single-week-5-element-until=
+built its byday string from a 0-indexed Sunday-first array
+=("SU" "MO" "TU" "WE" "TH" "FR" "SA")= while the production code uses
+Monday=1, Sunday=7 throughout. When start-date landed on a Sunday
+(start-weekday=7), =(nth 7 array)= returned nil, and inside =expand-weekly=
+the =(mod (- nil current-weekday) 7)= form raised
+=wrong-type-argument number-or-marker-p nil=. The test failed every
+Saturday (when "tomorrow" is Sunday) and passed the other six days.
+
+Fixed in commit =8ec668d= by switching the lookup to
+=(nth (1- start-weekday) '("MO" "TU" "WE" "TH" "FR" "SA" "SU"))= — the
+same convention as every other weekday-mapping in the codebase. Verified
+across all 7 weekdays via faked =current-time=.
+
+Production code was internally consistent; no production change needed.
+
+** DONE [#B] Investigate missing yasnippet configuration
+CLOSED: [2026-02-16 Mon]
+
+Resolved: snippets were in ~/sync/org/snippets/ but directory was empty after
+machine migration. Restored 28 snippets from backup, relocated snippets-dir
+to ~/.emacs.d/snippets/ for source control.
+
+** DONE [#B] Write Complete ERT Tests for This Config [13/13]
+CLOSED: [2026-02-16 Mon]
+
+All 13 modules covered: custom-case (43), custom-datetime (10), hugo-config (41),
+org-capture-config (22), modeline-config (26), config-utilities (11),
+org-agenda-config (31), org-contacts-config (40), ui-config (27),
+org-refile-config (16), org-webclipper (31), org-noter-config (30),
+browser-config (20). 172 test files, all passing.
+
+** DONE [#B] Validate recording startup
+CLOSED: [2026-02-15 Sun 15:40]
+
+Check process status after starting.
+Parse ffmpeg output for errors.
+Show actual ffmpeg command for debugging.
+
+** DONE [#C] Fix EMMS keybinding inconsistency with other buffers
+CLOSED: [2026-02-15 Sun 15:40]
+
+EMMS keybindings conflict with standard buffer keybindings, causing mistypes.
+Results in accidental destructive actions (clearing buffers), requires undo + context switch.
+Violates Intuitive value - muscle memory should help, not hurt.
+
+** DONE [#B] Update stale model list in ai-config.el
+CLOSED: [2026-03-06 Fri]
+
+Model IDs were outdated. Updated to current models (claude-opus-4-6, claude-sonnet-4-6, etc.).
+See cleanup task in ai-config.el for full list of related improvements.
+
+** DONE [#C] Graduate easy-hugo into hugo-config.el, retire wip.el, uninstall pomm
+CLOSED: [2026-04-22 Wed]
+
+Move the easy-hugo use-package block from modules/wip.el into modules/hugo-config.el so the full Hugo pipeline (new post → ox-hugo export → preview server → SSH deploy) lives in one place and is actually reachable at runtime. wip.el is currently not required in init.el, so its only live block (pomm) never ran anyway.
+
+Scope:
+- Verify easy-hugo is usable against current Hugo CLI and the paths in the existing config (~/code/cjennings-net/, /var/www/cjennings/).
+- If easy-hugo is healthy: graduate it and propose keybindings under the C-; h hugo prefix.
+- If easy-hugo is unmaintained or broken: document the issues, assess whether fixing or forking is viable.
+- Delete modules/wip.el entirely and remove the commented-out (require 'wip) line at init.el:156.
+- Uninstall pomm (remove from elpa/).
+- Confirm make compile no longer warns about wip or pomm.
+
+** DONE [#C] Consider Recording Enhancement via post-processing hooks
+CLOSED: [2026-04-04 Sat 12:00]
+
+Auto-compress after recording.
+Move to cloud sync directory.
+Generate transcript (once transcription workflow exists).
+
+** DONE [#B] Implement coverage reporting (per docs/design/coverage.org)
+CLOSED: [2026-04-23]
+
+Diff-aware coverage report with pluggable backends. Shipped v1 on 2026-04-23.
+
+Design: [[file:../docs/design/coverage.org][docs/design/coverage.org]]
+
+What shipped:
+- modules/coverage-core.el (engine, backend registry, cj/coverage-report, cj/coverage-report-mode)
+- modules/coverage-elisp.el (undercover.el backend, auto-registered on load)
+- make coverage Makefile target (simplecov JSON output, per-file isolation, .elc cleanup, exclusion list)
+- tests/run-coverage-file.el (undercover driver for the Makefile)
+- ERT tests for all pure helpers (parse-simplecov, parse-diff, intersect, format-report, backend registry, scope lookup) plus one smoke test for the command
+- F7 global binding
+- docs/design/coverage.org (design doc with historical LCOV→simplecov pivot note)
+
+Notable pivots during implementation:
+- Switched collection format from LCOV to simplecov (undercover's :merge-report t only supports simplecov).
+- `make coverage` must delete modules/*.elc first so undercover's source-level instrumentation actually fires.
+- Excluded tests/test-all-comp-errors.el from coverage runs (byte-compiles modules, which fails under undercover instrumentation).
+
+Deferred to future tickets:
+- Python, TypeScript, Go backends
+- Fringe-overlay coverage display (parked over perf concerns)
+- Historical coverage tracking
+
+** DONE [#A] Fix "Invalid face attribute :foreground nil" flood :bug:
+CLOSED: [2026-04-26 Sun 20:15]
+
+Diagnosed 2026-04-26 — paused at /start-work Gate 2. Full diagnostic, root cause, proposed fix, test plan, and verification path saved to wttrin's inbox (the fix lives in that repo, so the diagnostic does too):
+
+[[file:~/code/emacs-wttrin/inbox/wttrin-face-flood-diagnosis.txt][~/code/emacs-wttrin/inbox/wttrin-face-flood-diagnosis.txt]]
+
+Summary: root cause is =wttrin--make-emoji-icon= in =/home/cjennings/code/emacs-wttrin/wttrin.el:598-608=. Builds a face spec with =:foreground foreground= unconditionally when =wttrin-mode-line-emoji-font= is set; the caller passes nil when the cache is fresh, producing =(:family ... :foreground nil)= which Emacs treats as invalid on every redisplay.
+
+Fix lives in the wttrin repo (cross-repo), not =.emacs.d=. Two-commit scope: regression test + fix.
+
+** DONE [#B] Test Slack desktop notifications (DM and @mention) :tests:
+CLOSED: [2026-04-26 Sun]
+
+Notifications were silently failing due to two bugs in =cj/slack-notify=:
+1. =slack-room-im-p= (nonexistent) → =slack-im-p= (correct EIEIO predicate)
+2. =slack-message-to-string= (propertized) → =slack-message-body= (plain text)
+
+Verified in actual Slack use: desktop notifications fire correctly for DMs and @mentions, with the title and message body rendering as expected.
+
+**File:** modules/slack-config.el (cj/slack-notify function)
+
+** DONE [#C] Clean up ai-config.el
+CLOSED: [2026-03-06 Fri]
+
+Cleaned up assorted issues in =modules/ai-config.el=:
+- Stale model list updated to current IDs (=claude-opus-4-6=, =claude-sonnet-4-6=, etc.).
+- Removed duplicate =gptel-backend= setq (lines 284 and 295 both did the same set).
+- Deleted the unused =cj/gptel-backends= defvar (duplicated =cj/gptel--available-backends=).
+- Moved helpers (=cj/gptel--fresh-org-prefix=, =cj/gptel--refresh-org-prefix=, =cj/gptel-backend-and-model=, =cj/gptel-insert-model-heading=) outside the use-package =:config= block for visibility and byte-compilation.
+- Changed =gptel-include-reasoning= from ='ignore= to a buffer name (=*AI-Reasoning*=) so reasoning lands in a separate buffer, isn't re-sent as context, and can be toggled per-session via the gptel menu (=C-; a M=).
+- Switched =gptel-magit= loading from a hook to lazy autoloads via =with-eval-after-load 'magit= so it only loads on key press.
+- Moved Rewrite from =&= to =C-; a r= and Clear context to =C-; a c= for clearer mnemonics.
+
+** DONE [#B] Use file basename, not buffer name, when moving buffer files :review:bug:quick:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 19:24
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review custom editing utility modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=cj/--move-buffer-and-file= builds the destination as =(concat dir "/" name)=,
+where =name= is =(buffer-name)=. If the buffer has been renamed, uniquified
+(e.g. =foo.txt<2>=), or otherwise differs from the file basename, the move can
+write an unexpected destination filename.
+
+Expected outcome:
+- Use =(file-name-nondirectory filename)= for the destination basename unless
+ the interactive command explicitly asks for a new name.
+- Add regression tests for:
+ - renamed buffer visiting =original.txt=,
+ - duplicate buffer names / uniquified names,
+ - target directory with and without trailing slash.
+
+Done 2026-05-03:
+- =cj/--move-buffer-and-file= now derives the destination basename from
+ =buffer-file-name=.
+- =cj/move-buffer-and-file= uses the same basename when checking/prompting for
+ overwrite.
+- Added regression coverage for renamed and uniquified buffer names.
+
+** DONE [#B] Fix malformed drill capture template in =org-capture-config.el= :review:bug:quick:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 19:28
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review Org workflow modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+The ="d"= drill capture template appears to have a malformed source link:
+
+#+begin_src org
+Source: [[%:link][%:description]
+nCaptured On: %U
+#+end_src
+
+It is missing the closing =]]= and has a literal leading =n= before "Captured".
+
+Expected outcome:
+- Fix the template string.
+- Add a narrow test that expands or inspects the template and confirms the
+ source link plus "Captured On" line are well-formed.
+
+Done 2026-05-03:
+- Closed the source link in the regular drill capture template.
+- Removed the stray literal =n= before =Captured On=.
+- Added =tests/test-org-capture-config-drill-template.el=.
+
+** DONE [#B] Disable auth-source debug logging by default :review:security:quick:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 19:40
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review integrations and application modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=auth-config.el= sets =auth-source-debug= to =t=. Debug output is helpful while
+fixing GPG/auth-source issues, but credential lookup debug logging should not be
+the steady-state default for a config that handles Slack, AI, REST, mail, and
+transcription credentials.
+
+Expected outcome:
+- Default =auth-source-debug= to nil.
+- Add an explicit troubleshooting command or variable to enable auth debugging
+ temporarily.
+- Confirm no module logs secret values directly on auth failure.
+
+Done 2026-05-03:
+- Defaulted =auth-source-debug= to nil via =cj/auth-source-debug-enabled=.
+- Added =cj/set-auth-source-debug= and =cj/toggle-auth-source-debug= for
+ temporary troubleshooting.
+- Added =tests/test-auth-config-debug.el=.
+- Scanned nearby auth callers; obvious failure messages name hosts/logins but
+ do not print secret values directly.
+
+** DONE [#B] Quote F6 current-file test commands in =dev-fkeys.el= :review:bug:quick:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 19:44
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review programming workflow modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=cj/--f6-test-runner-cmd-for= builds shell command strings from relative paths,
+directories, and source stems:
+
+#+begin_src emacs-lisp
+(format "pytest %s" rel-path)
+(format "make test-name TEST=^test-%s-" stem)
+(format "go test ./%s" rel-dir)
+#+end_src
+
+This is fine for the current repo's simple filenames, but it will break or
+misbehave for paths with spaces or shell metacharacters. Since these commands
+feed =compile=, either quote each dynamic argument or move to a command-builder
+that returns argv plus a shell-rendering function.
+
+Expected outcome:
+- Quote =rel-path=, =stem= / test regex, and =rel-dir= appropriately.
+- Add regression tests for:
+ - Python test file under a directory with spaces,
+ - Elisp module stem containing shell-significant characters,
+ - Go package directory with spaces.
+- Keep existing command strings unchanged for ordinary paths.
+
+Done 2026-05-03:
+- Added =cj/--f6-shell-quote-argument= so F6 command strings escape dynamic
+ paths and test regexes only when needed.
+- Quoted Python rel-paths, generated Python test paths, Elisp =FILE= /
+ =TEST= values, and Go package paths.
+- Added regression coverage for Python paths with spaces, Elisp stems with
+ shell metacharacters, and Go package directories with spaces.
+- Confirmed ordinary command strings remain unchanged.
+
+** DONE [#B] Disable mail transport debug logging and validate dependencies :review:security:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 19:57
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review integrations and application modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=mail-config.el= sets =smtpmail-debug-info= to =t= and builds mail commands from
+=executable-find= results at config time. If =msmtp= or =mbsync= is missing, the
+configuration can silently produce unusable command values. Mail debug output is
+also too sensitive to leave enabled by default.
+
+Expected outcome:
+- Default =smtpmail-debug-info= to nil.
+- Add an explicit mail troubleshooting variable/command for temporary SMTP
+ debug logging.
+- Validate =msmtp= and =mbsync= before assigning send/sync commands, and show a
+ clear warning or disable the dependent feature when missing.
+- Add a module-load test with stubbed =executable-find= results.
+
+Done 2026-05-03:
+- Defaulted =smtpmail-debug-info= to nil via =cj/smtpmail-debug-enabled=.
+- Added =cj/set-smtpmail-debug= and =cj/toggle-smtpmail-debug= for temporary
+ troubleshooting.
+- Added validation helpers for =msmtp= and =mbsync= so missing executables
+ warn and do not produce unusable command values.
+- Added =tests/test-mail-config-transport.el= with stubbed executable lookup.
+
+** DONE [#B] Make test scratch paths sandbox- and CI-friendly :refactor:tests:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 19:59
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#A] Architecture review follow-up from 2026-05-03
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: refactor
+:END:
+
+=tests/testutil-general.el= hardcodes =~/.temp-emacs-tests/=. That caused the
+first =make coverage= run to fail under the default workspace sandbox because
+tests attempted to write outside the repo and =/tmp=.
+
+Confirmed again 2026-05-03 with =make test=: the default sandbox run reported
+32 failing test files across custom-buffer, custom-line, music, Org, undead
+buffers, and recording cleanup tests. Rerunning the same command with approval
+outside the sandbox passed all 311 test files. This is a test-environment
+contract problem, not a regression in those modules.
+
+Expected outcome:
+- Let tests honor an env var, for example =CJ_EMACS_TEST_DIR=.
+- Default to =(make-temp-file ... t)= or a stable directory under
+ =temporary-file-directory=.
+- Keep an option for a stable local directory when debugging manually.
+- Ensure cleanup is robust and guarded against deleting outside the selected
+ test root.
+
+Acceptance criteria:
+- =make test-file FILE=test-custom-line-paragraph-join-line-or-region.el=
+ works without special home-directory write permission.
+- =make coverage= works in a clean sandbox/CI environment.
+- Update any docs or Makefile notes that assume =~/.temp-emacs-tests/=.
+
+Done 2026-05-03:
+- Changed =cj/test-base-dir= to honor =CJ_EMACS_TEST_DIR= for stable local
+ debugging and otherwise create a unique directory under
+ =temporary-file-directory=.
+- Replaced prefix-string path checks with =file-in-directory-p= and added a
+ deletion guard that refuses broad roots such as =temporary-file-directory=.
+- Updated =make clean-tests= to clean the new temp-root pattern and the legacy
+ =~/.temp-emacs-tests= directory.
+- Added =tests/test-testutil-general.el=.
+- Confirmed default sandbox =make test= passes: 312 test files.
+
+** DONE [#B] Fix C single-file compile command path handling :review:bug:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 20:11
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review programming workflow modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=prog-c.el= builds the fallback single-file compile command from =(buffer-name)=:
+
+#+begin_src emacs-lisp
+(format "gcc -Wall -Wextra -g -o %s %s"
+ (file-name-sans-extension (buffer-name))
+ (buffer-name))
+#+end_src
+
+This breaks for renamed buffers, duplicate buffer names, paths with spaces, and
+files outside =default-directory=.
+
+Expected outcome:
+- Use =buffer-file-name= for source path and derive output from the file path.
+- Shell-quote both paths.
+- If the buffer is not visiting a file, show a clear message or use a safe temp
+ target.
+- Add regression tests for filenames with spaces and renamed buffers.
+
+Done 2026-05-03:
+- Added =cj/c--single-file-compile-command= and changed the fallback path to
+ use =buffer-file-name= instead of =(buffer-name)=.
+- Shell-quoted source and output paths.
+- Made non-file buffers signal a clear =user-error=.
+- Added =tests/test-prog-c-compile-command.el= for spaces, shell
+ metacharacters, renamed buffers, and non-file buffers.
+
+** DONE [#B] Replace shell-based coverage git diff calls with argv process calls :review:robustness:refactor:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 20:11
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review programming workflow modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=coverage-core.el= uses =shell-command-to-string= for git diff scopes, including
+forms with command substitution:
+
+#+begin_src emacs-lisp
+git diff $(git merge-base HEAD main)..HEAD --unified=0
+#+end_src
+
+The inputs are mostly fixed, but this code is central tooling and should avoid
+shell parsing entirely.
+
+Expected outcome:
+- Use =process-file= / =call-process= with argv lists.
+- Compute merge bases with a separate git invocation.
+- Surface git failures as clear =user-error= messages.
+- Preserve the existing parser and report formatting tests.
+
+Done 2026-05-03:
+- Added argv-boundary tests before the implementation change.
+- Replaced =shell-command-to-string= with =process-file= based git helpers.
+- Compute merge-base with a separate =git merge-base HEAD <base>= invocation
+ before running =git diff <merge-base>..HEAD --unified=0=.
+- Surface non-zero git exits as =user-error= messages that include the git
+ argv, exit status, and command output.
+- Updated the interactive coverage report smoke test to stub =process-file=.
+
+** DONE [#B] Cache or cheapen VC work in the custom modeline :review:perf:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 20:24
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review UI and navigation modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=modeline-config.el= computes VC branch/state in a mode-line =:eval= form using
+=vc-backend=, =vc-working-revision=, =vc-git--symbolic-ref=, and =vc-state=.
+Even though it only displays for the selected window, this can become expensive
+in large repositories, remote/TRAMP buffers, or slow filesystems.
+
+Expected outcome:
+- Measure the current cost in normal git repos and a TRAMP/remote-like case if
+ available.
+- Cache branch/state per buffer and invalidate on buffer/file save, VC refresh,
+ or timer.
+- Avoid VC calls for remote files unless explicitly enabled.
+- Add tests around any pure formatting/cache invalidation helpers.
+
+Pitfalls:
+- Mode-line code runs often; avoid anything that can block redisplay.
+- Do not lose the useful active-window-only behavior.
+
+Done 2026-05-03:
+- Added buffer-local VC modeline caching with a short TTL, plus cache clearing
+ on save and revert.
+- Kept the active-window-only modeline rendering behavior.
+- Skipped VC work for remote files by default, with a custom option to opt in.
+- Added focused tests for cache reuse, TTL refresh, remote-file bypass, cache
+ clearing, and VC rendering metadata.
+- Measured on this repo after the change: uncached reads were about 2.4 ms
+ each, cached reads were about 0.0025 ms each, and remote-skipped reads avoid
+ VC calls while still paying the cheap =file-remote-p= check.
+
+** DONE [#B] Make Projectile command-cache revert state compilation-local :review:robustness:bug:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 21:11
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review programming workflow modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=dev-fkeys.el= protects Projectile's compile/test/run command caches by
+capturing prior state in the global =cj/--projectile-revert-state= and reverting
+on failed compile when the cached command changed. The idea is useful and well
+covered, but the state is global while compilation processes are asynchronous.
+
+Risk:
+- Starting another Projectile compile/test/run before the first finish hook
+ fires can overwrite the state.
+- The finish hook is installed even when no project/cache state was captured.
+- A failure from one compilation buffer could theoretically act on state from a
+ later command.
+
+Expected outcome:
+- Store revert metadata on the compilation buffer/process where possible, or
+ close over immutable state in a one-shot hook instead of using one global
+ variable.
+- Only install the revert hook when state was captured.
+- Add a test that simulates two overlapping compile processes finishing out of
+ order.
+
+Done 2026-05-03:
+- Changed Projectile command-cache revert capture to return immutable state
+ instead of storing live compile metadata in one global variable.
+- Installed one-shot buffer-local compilation finish hooks on the compilation
+ buffer returned by Projectile, so overlapping compiles keep separate revert
+ metadata.
+- Avoided installing revert hooks when no project/cache state was captured.
+- Added regression coverage for two overlapping compiles finishing out of order.
+
+** DONE [#C] Tighten =dev-fkeys.el= load-order contract with Projectile :review:cleanup:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 21:17
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review programming workflow modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=init.el= loads =dev-fkeys.el= before =prog-general.el=, while =prog-general.el=
+owns the =projectile= setup. =dev-fkeys.el= currently works through autoloads,
+=fboundp= checks, and top-level advice, but the dependency is implicit.
+
+Expected outcome:
+- Either require/load Projectile before installing advice, or move the
+ =dev-fkeys= require after Projectile setup.
+- Keep direct batch requiring of =dev-fkeys.el= test-friendly.
+- Add a module-load smoke test for "Projectile not loaded yet" and "Projectile
+ loaded after dev-fkeys".
+
+Done 2026-05-03:
+- Replaced raw top-level Projectile =advice-add= calls with named advice
+ wrappers and an explicit idempotent installer.
+- Registered advice immediately when Projectile is already loaded, otherwise
+ delayed installation with =eval-after-load=.
+- Kept direct batch requiring of =dev-fkeys.el= from forcing Projectile to load.
+- Added smoke tests for deferred registration, already-loaded registration, and
+ bounded installation behavior when Projectile functions are unavailable.
+
+** DONE [#B] Retire legacy =cj/--projectile-revert-on-fail= and global revert state :review:chore:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 21:32
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review programming workflow modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+After scoping the projectile cache-revert state to each compile (commit
+=31edc86=), =cj/--projectile-revert-on-fail= and the global
+=cj/--projectile-revert-state= are production-dead. They survive only so
+=tests/test-dev-fkeys--projectile-revert-on-fail.el= keeps exercising the
+inner decision logic via the legacy wrapper.
+
+Expected outcome:
+- Delete =cj/--projectile-revert-on-fail= and =cj/--projectile-revert-state=
+ from =modules/dev-fkeys.el=.
+- Re-point the existing =test-dev-fkeys--projectile-revert-on-fail.el= cases
+ at =cj/--projectile-revert-state-on-fail= (or rename the file to match
+ the new target).
+- Confirm the broader dev-fkeys test set still passes after the rename.
+
+Done 2026-05-03:
+- Removed the production-dead legacy wrapper and global revert state from
+ =dev-fkeys.el=.
+- Repointed the existing revert tests at =cj/--projectile-revert-state-on-fail=.
+- Removed stale test bindings/assertions that only existed for the legacy global
+ state.
+
+** DONE [#C] Review duplicate or competing search/keybinding setup in =selection-framework.el= :review:cleanup:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 23:27
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review UI and navigation modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=selection-framework.el= binds =C-s= to =consult-line= and later rebinds it to
+=cj/consult-line-or-repeat=. The final behavior is probably intended, but the
+earlier binding is dead configuration and makes the file harder to reason about.
+
+Expected outcome:
+- Remove the intermediate =C-s= binding or explain it.
+- Add a small test or smoke check that =C-s= resolves to
+ =cj/consult-line-or-repeat= after the module loads.
+
+Verify 2026-05-03:
+- Removed the intermediate global =C-s= binding to =consult-line=.
+- Kept the final =C-s= binding to =cj/consult-line-or-repeat=.
+- Added a smoke test that loads =selection-framework.el= with package setup
+ stubbed and asserts =C-s= resolves to =cj/consult-line-or-repeat=.
+
+** DONE [#C] Move and test theme persistence behavior :review:tests:refactor:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 23:46
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review UI and navigation modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=ui-theme.el= persists theme names to =theme-file= and loads fallback themes
+when the file is absent or invalid. The current default path is built from
+=org-dir= as =emacs-theme.persist=, which makes UI theme persistence depend on
+Org-directory configuration and keeps an Emacs preference outside the Emacs
+home directory.
+
+Desired direction:
+- Make the persisted theme file a dotfile inside =user-emacs-directory=, e.g.
+ =.emacs-theme= or another clear dotfile name.
+- Remove the runtime need for =org-dir= from theme persistence.
+- Keep the theme persistence code self-contained in =ui-theme.el= unless an
+ existing constants helper is a better local fit.
+- Preserve the current user-facing behavior: chosen themes persist, unreadable
+ or invalid saved themes fall back, and literal ="nil"= means no enabled theme.
+- Refactor the current large theme-load function into smaller helpers for:
+ reading persisted theme names, disabling enabled themes, loading one named
+ theme, applying a persisted theme value, and loading fallback themes.
+- Prefer =defcustom= for user-facing persistence/fallback settings.
+- Replace generic =cj/read-file-contents= / =cj/write-file-contents= names with
+ theme-specific helpers or move generic helpers elsewhere.
+- Prefer =write-region= over visiting the file with =write-file= for persistence.
+- Decide whether the top-level =(cj/load-theme-from-file)= side effect should
+ remain in the module or become an explicit init call; preserve startup behavior
+ either way.
+
+Useful tests:
+- The default =theme-file= expands under =user-emacs-directory= and does not
+ depend on =org-dir=.
+- Reading a missing/unreadable theme file returns nil.
+- Writing to a writable temp theme file succeeds.
+- Invalid theme name triggers fallback path without leaving multiple themes
+ enabled.
+- The literal ="nil"= disables themes.
+- Loading a valid persisted theme uses that theme and does not also load the
+ fallback.
+- Theme application disables existing themes before loading a valid or fallback
+ theme, so themes do not stack.
+- Theme writes use the configured =theme-file= and do not visit that file in a
+ temp buffer.
+
+Keep tests isolated by binding =theme-file= to a temp file and mocking
+=load-theme= / =disable-theme= where appropriate. Avoid mutating the real
+=custom-enabled-themes= state in tests.
+
+Pitfalls:
+- =ui-theme.el= currently calls =cj/load-theme-from-file= at module load time,
+ so tests should either bind =theme-file= before loading or mock file/theme
+ effects carefully.
+- If changing the persisted filename, consider whether a migration path from
+ the old =org-dir/emacs-theme.persist= location is worth doing now or should
+ be a separate compatibility task.
+
+Verify 2026-05-03:
+- Moved the default =theme-file= to =(expand-file-name ".emacs-theme"
+ user-emacs-directory)=.
+- Removed the =org-dir= / =user-constants= dependency from =ui-theme.el= theme
+ persistence.
+- Split theme persistence into theme-specific helpers for read/write,
+ disabling themes, named theme loading, fallback loading, and applying a
+ persisted value.
+- Switched persistence writes to =write-region=.
+- Moved startup theme loading out of module load side effects and into
+ =init.el= immediately after requiring =ui-theme=.
+- Added focused tests for the default path, missing reads, writes,
+ =write-region= use, valid persisted themes, invalid fallback, missing
+ fallback, and literal ="nil"=.
+- Verified with =make test-file FILE=test-ui-theme-persistence.el=,
+ =make test-file FILE=test-all-comp-errors.el=,
+ =make test-file FILE=test-dupre-theme.el=, and full =make test=.
+
+** DONE [#B] Make test-runner focus state project-scoped :review:tests:bug:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 23:46
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review programming workflow modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=test-runner.el= stores =cj/test-focused-files= and =cj/test-mode= globally.
+When switching between projects, focused test filenames and mode can bleed into
+the next project.
+
+Expected outcome:
+- Scope focused files and mode by project root.
+- Keep the current UI commands unchanged.
+- Coordinate with the existing [#B] "Add project-aware ERT test isolation when
+ switching projects" task so test registration and focus state follow the same
+ project boundary.
+
+Verify 2026-05-03:
+- Added per-project test-runner state keyed by Projectile project root, with
+ focused files and all/focused mode tracked independently per project.
+- Kept the existing interactive commands and legacy public variables mirrored
+ to the current project state.
+- Removed the hard test-time dependency on requiring Projectile before project
+ root calls can be mocked.
+- Added regression tests proving focused files and mode do not bleed across
+ projects.
+- Verified with =make test-file FILE=test-test-runner.el=,
+ =make test-file FILE=test-all-comp-errors.el=,
+ =make test-file FILE=test-dev-fkeys--f6-test-runner.el=,
+ =make test-file FILE=test-dev-fkeys--f6-current-file-tests-impl.el=, and
+ full =make test=.
+
+** DONE [#B] Add project-aware ERT test isolation when switching projects :tests:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 23:46
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:END:
+
+When switching between elisp projects (e.g., emacs.d to Chime), previously loaded
+ERT tests remain in memory causing confusion and wrong tests to run.
+
+**Problem:**
+- ERT tests globally registered in Emacs session
+- `M-x ert RET t RET` runs ALL loaded tests from ALL projects
+- Can accidentally run emacs.d tests when working on Chime
+- Current workaround: restart Emacs (loses session state)
+
+**Solution:**
+Create `cj/ert-clear-tests` and `cj/ert-run-current-project-tests`:
+- Clear tests when switching projects (hook into project-switch)
+- Use test name prefixes to selectively clear (cj/ vs chime-)
+- Only run current project's tests
+
+**Success Criteria:**
+- Switch projects -> old tests cleared
+- Only current project's tests run with `M-x ert`
+- Works with both interactive and batch runs
+
+Verify 2026-05-03:
+- Added =cj/ert-clear-tests= to delete ERT tests loaded by this runner from
+ other known project roots while keeping the current project's tests.
+- Added =cj/ert-run-current-project-tests= and routed =cj/test-run-all= through
+ a current-project selector, so the test runner's "all" path runs all tests
+ for the current project rather than every loaded ERT test in the session.
+- Hooked =cj/test-project-switch-reset= into
+ =projectile-after-switch-project-hook= after Projectile loads.
+- Added regression tests for clearing other-project ERT tests and selecting
+ only current-project test names.
+- Verified with =make test-file FILE=test-test-runner.el=,
+ =make test-file FILE=test-all-comp-errors.el=,
+ =make test-file FILE=test-dev-fkeys--f6-test-runner.el=,
+ =make test-file FILE=test-dev-fkeys--f6-current-file-tests-impl.el=, and
+ full =make test=.
+
+** DONE [#B] Sanitize calendar-generated Org headings and properties :review:bug:
+CLOSED: [2026-05-03 Sun]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-03 Sun 23:52
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review integrations and application modules
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review
+:END:
+
+=calendar-sync--event-to-org= sanitizes description body text against accidental
+Org headings, but event summaries, locations, organizers, statuses, and URLs are
+inserted into headings/property drawers directly. Calendar text containing
+newlines, leading stars, or property drawer markers can corrupt the generated
+Org structure.
+
+Expected outcome:
+- Add separate sanitizers for Org heading text and property values.
+- Preserve readable event text while escaping or flattening structural
+ characters.
+- Add tests for summaries with newlines/stars and locations with property-like
+ lines.
+
+Verify 2026-05-03:
+- Added separate sanitizers for Org heading text and Org property values.
+- Event summaries now flatten newlines and convert leading heading stars to
+ dashes before being inserted as Org heading text.
+- Location, organizer, status, and URL values now collapse structural
+ whitespace into single-line property values before insertion into the
+ property drawer.
+- Added regression tests for summaries with newlines/stars and property values
+ containing =:END:=, property-looking text, and heading-looking text.
+- Verified with =make test-file FILE=test-calendar-sync--event-to-org.el=,
+ =make test-file FILE=test-calendar-sync--sanitize-org-body.el=,
+ =make test-file FILE=test-all-comp-errors.el=,
+ =make test-file FILE=test-calendar-sync.el=,
+ =make test-file FILE=test-calendar-sync--parse-event.el=,
+ =make test-file FILE=test-calendar-sync--event-start-time.el=, and full
+ =make test=.
+
+** DONE [#B] Add a no-config startup test for =calendar-sync.el= :review:security:refactor:
+CLOSED: [2026-05-04 Mon]
+:PROPERTIES:
+:ARCHIVE_TIME: 2026-05-04 Mon 00:05
+:ARCHIVE_FILE: ~/.emacs.d/todo.org
+:ARCHIVE_OLPATH: Emacs Open Work/PROJECT [#B] Module-by-module review and hardening/Review Org workflow modules/PROJECT [#A] Split personal calendar configuration from =calendar-sync.el=
+:ARCHIVE_CATEGORY: todo
+:ARCHIVE_TODO: DONE
+:ARCHIVE_ITAGS: review security refactor
+:END:
+
+Bind =calendar-sync-calendars= to nil and verify:
+- requiring the module does not start a timer,
+- =calendar-sync-status= reports the missing configuration cleanly,
+- no network process is started.
+
+Verify 2026-05-03:
+- Removed the tracked top-level personal calendar plist from =calendar-sync.el=,
+ leaving =calendar-sync-calendars= nil by default.
+- Added an ignored private config path, =calendar-sync.local.el=, loaded when
+ readable so local calendar definitions can stay outside git.
+- Added =calendar-sync.local.el= to =.gitignore= and moved the current local
+ calendar plist into that ignored file to preserve this machine's workflow.
+- Gated top-level auto-start behind =(not noninteractive)= so batch/test loads
+ do not start timers or network fetches, even when private config exists.
+- Added startup tests for no-config loads, missing-config status reporting,
+ private config loading, and private config not auto-starting in batch.
+- Verified with =make test-file FILE=test-calendar-sync-no-config-startup.el=,
+ =make test-file FILE=test-calendar-sync.el=,
+ =make test-file FILE=test-all-comp-errors.el=, and full =make test=.
+
+** DONE [#C] Add focused tests for early startup archive construction :tests:
+CLOSED: [2026-05-10 Sun]
+
+=tests/test-early-init-paths.el= covers path constants, but not archive
+selection, archive priorities, refresh decisions, or the offline/localrepo
+branches that make startup reproducible.
+
+Useful assertions after package bootstrap is extracted:
+- Local repo and local mirrors are added only when their directories exist.
+- Local archives keep higher priority than online archives.
+- =cj/use-online-repos= disables online archives and refresh attempts.
+- Stale or missing online archive caches request refresh only through the
+ extracted bootstrap path, not by loading unrelated modules.
+
+Verify 2026-05-10:
+- Extended =tests/test-early-init-paths.el= to cover local archive presence,
+ local-vs-online priority, offline archive omission, fresh-cache no-refresh,
+ and missing-cache refresh behavior.
+- Ran =make test-file FILE=test-early-init-paths.el=.
+
+** DONE [#C] Move inline GPT tool wiring out of =init.el= :startup:refactor:
+CLOSED: [2026-05-10 Sun]
+
+=init.el= contains a =with-eval-after-load 'gptel= block that mutates
+=load-path= and requires local files from =~/.emacs.d/gptel-tools=. This is
+feature-specific integration code inside the top-level load graph, and it will
+be hard to test or defer cleanly while it stays inline.
+
+Expected outcome:
+- Move the tool registration into =ai-config.el= or a small dedicated module.
+- Guard the local tool directory and individual tool files so missing optional
+ files produce a clear message rather than breaking startup after =gptel= loads.
+- Keep =init.el= limited to coarse module loading until the load-graph refactor
+ removes most eager =require=s.
+- Add a smoke test for the missing-directory path if the helper is pure enough.
+
+Verify 2026-05-10:
+- Moved optional GPTel tool loading into =ai-config.el= via
+ =cj/gptel-load-local-tools=.
+- Removed the inline =with-eval-after-load 'gptel= tool block from =init.el=.
+- Added =tests/test-ai-config-gptel-local-tools.el= for missing-directory,
+ present-tool, and missing-file behavior.
+- Ran focused AI config tests and checked parens for =init.el= and
+ =modules/ai-config.el=.
+
+** DONE [#C] Clean up Org keymap ownership and duplicate maps :cleanup:refactor:
+CLOSED: [2026-05-10 Sun]
+
+=org-config.el= creates =cj/org-map= under =cj/custom-keymap=, then later
+creates a separate =cj/org-keymap= under =C-; O=. Other Org modules bind their
+own global prefixes directly. This works with the current eager load order, but
+it makes the intended owner of Org commands less clear.
+
+Expected outcome:
+- Pick one owner for the Org command prefix.
+- Move module-specific menus under that owner or document why they remain
+ separate (=C-c n= for org-roam may be worth keeping).
+- Avoid duplicate definitions for =C-; O= and =cj/org-map=.
+- Coordinate with the broader custom keymap/load-order architecture task.
+
+Verify 2026-05-10:
+- Removed the duplicate =cj/org-keymap= and kept =cj/org-map= as the single
+ owner of =C-; O= through =cj/custom-keymap=.
+- Kept =C-; O c= bound to =cj/org-clear-element-cache=, which handles all Org
+ buffers by default and only the current Org buffer with a prefix argument.
+- Added =tests/test-org-config-keymap-ownership.el= and updated the existing
+ Org sort test to load newer source in the presence of ignored =.elc= files.
+- Ran =make test-file FILE=test-org-config-keymap-ownership.el= and
+ =make test-file FILE=test-org-sort-by-todo-and-priority.el=.
+
+** DONE [#A] Make repo reconciliation non-destructive by default :data:refactor:
+CLOSED: [2026-05-10 Sun]
+
+Before this refactor, =reconcile-open-repos.el= recursively scanned repos and,
+for dirty repos, ran
+=git stash --quiet=, =git pull --rebase --quiet=, and =git stash pop --quiet=
+before opening Magit. That is high blast radius for a convenience command: stash
+pop conflicts, untracked files, submodules, and worktrees can all create messy
+states.
+
+Verify 2026-05-10:
+- Dirty repos now open Magit for review without running stash, pull, or stash
+ pop.
+- Clean repos still pull with =git pull --rebase --quiet= via =process-file=.
+- Git calls now use argv lists through =cj/reconcile--git=.
+- Reconcile results distinguish =pulled=, =needs-review=, =skipped=,
+ =pull-failed=, and =status-failed=.
+- Repo discovery prunes heavy/generated directories and stops at repo roots by
+ default.
+- HTTP/HTTPS remote skipping is explicit and configurable via
+ =cj/reconcile-skipped-remote-regexp=.
+- Ran all reconcile ERT files and byte-compiled =reconcile-open-repos.el=.
+
+*** DONE [#A] Change dirty repo handling to review-first :bug:
+
+Expected outcome:
+- Clean repos may still pull automatically if desired.
+- Dirty repos should open Magit or a review buffer before any stash/pull/pop.
+- If an auto-reconcile mode is kept, require an explicit prefix argument or
+ separate command name.
+- Update the current dirty-repo tests so they assert the new review-first
+ behavior instead of encoding =git stash= / =git pull= / =git stash pop= as the
+ desired path.
+
+*** DONE [#B] Replace shell git calls with process helpers and parse statuses :refactor:
+
+Expected outcome:
+- Use =process-file= / =call-process= with argv lists.
+- Capture stdout/stderr per repo for a final report.
+- Distinguish clean, dirty, skipped, pull-failed, and needs-review states.
+- Add tests with stubbed git command results.
+
+*** DONE [#B] Prune expensive directories while discovering repos :refactor:
+
+=cj/find-git-repos= recursively walks the configured project/code roots and
+checks every directory for a nested =.git=. That can wander through
+=node_modules=, =.venv=, =target=, vendored source, build output, and nested
+dependency checkouts.
+
+Expected outcome:
+- Add a configurable prune list for heavy/generated directories.
+- Stop descending once a repo root has been found unless explicitly requested.
+- Add tests for nested repos and ignored heavy directories.
+
+*** DONE [#C] Make remote skip policy explicit and configurable
+
+=cj/reconcile--reference-clone-p= treats HTTP/HTTPS remotes as reference clones
+and skips them. That may be right for this machine, but the behavior is encoded
+as a naming mismatch and can skip ordinary repos.
+
+Expected outcome:
+- Rename the predicate to reflect the actual policy, or make the policy
+ configurable.
+- Report skipped repos with the reason in the final reconcile output.
+- Keep tests for SSH remotes, HTTP remotes, and local/file remotes.
+
+** DONE [#B] ai-vterm: occasional wrong-edge replay after buffer-move dance :bug:
+CLOSED: [2026-05-10 Sun]
+
+Shipped 2026-05-09 in commit =26e9763= "fix(ai-vterm): harden F9 toggle across multi-window and buffer-move". The fix maps cardinal directions to frame-edge variants on replay (=right= → =rightmost=, =below= → =bottom=), switches captured units from frame-fractions to absolute body-cols / body-lines, wraps replay sizes in =(body-columns . N)= / =(body-lines . N)= cons forms so dividers don't shift the body, and uses =delete-window= (with =one-window-p= guard) instead of =quit-window= so buffer-moved windows don't leak. 7 regression tests added covering each scenario; 80 ai-vterm tests pass.
+
+Surfaced 2026-05-09. After an extended sequence with both vterm and
+ai-vterm visible, switching orientations, buffer-moving claude
+between positions, and toggling each independently, claude
+eventually replayed at the right side when its captured direction
+should have been =below= (it had just been buffer-moved to the
+bottom and toggled there).
+
+The full sequence that hit it:
+
+1. F9 to open claude (right side).
+2. F12 to open vterm. M-S-t to flip vterm to right-side -- gives
+ dashboard | vterm | claude. Toggle each off/on; both behave.
+3. F12 vterm off. M-S-t flips claude to left half. Toggle both
+ off/on; both behave.
+4. Toggle both off. F12 vterm on. M-S-t flips vterm to bottom.
+ Toggle both on/off; both behave.
+5. Buffer-move claude to the bottom.
+6. Toggle claude there -- claude pops up at the right instead of
+ at the bottom.
+
+** DONE [#B] Scope F12 (vterm-toggle) to non-claude vterm buffers, preserve user orientation :refactor:bug:
+CLOSED: [2026-05-10 Sun]
+
+Shipped 2026-05-09 in commit =554b32d= "feat(vterm): F12 toggle that excludes claude and preserves geometry". F12 now binds =cj/vterm-toggle= (replaces the =vterm-toggle= package binding). =cj/--vterm-toggle-buffer-p= excludes =claude [= prefixed buffers from the candidate set; =cj/--vterm-toggle-capture-state= records direction + body size at toggle-off; =cj/--vterm-toggle-display-saved= replays via =(body-columns . N)= / =(body-lines . N)= cons forms with cardinal direction mapped to frame-edge variant. Toggle-off uses =delete-window= (with =one-window-p= guard) so buffer-move scenarios don't leak ghost windows. The hard-coded =(window-height . 0.7)= override is gone — user-resized geometry persists. 19 new tests across buffer-filter, dispatch, and display.
+
+F12 previously ran =vterm-toggle=, which picked the most-recent vterm buffer
+as the toggle target. When that target was a =claude [<repo>]= buffer (which
+has its own F9/C-F9/M-F9 dispatch via =modules/ai-vterm.el=), F12 ended up
+toggling Claude. The display-buffer rule in =modules/eshell-vterm-config.el=
+already excluded =claude [= names from the bottom-window placement, but the
+exclusion only governed /where/ a buffer landed once vterm-toggle had chosen
+it -- not which buffer got chosen.
+
+Two changes shipped:
+
+1. *Filter claude buffers from vterm-toggle's target set* via
+ =cj/--vterm-toggle-buffer-p=, which ignores buffers whose names start with
+ "claude [".
+2. *Respect user-modified window orientation.* Captured direction + body size
+ at toggle-off; replayed via =(body-columns . N)= / =(body-lines . N)= cons
+ forms. The hard-coded =(window-height . 0.7)= override is gone.
+
+** DONE [#C] Move vterm-copy-mode binding off C-c C-t to the personal keymap :chore:quick:
+CLOSED: [2026-05-10 Sun 02:02]
+
+Default vterm binding is =C-c C-t=, which collides with the =C-c= space many modes
+reach for and is awkward to hit when the terminal is the active buffer. Move it
+to the personal keymap (=C-;= prefix) — pick a mnemonic letter (e.g. =C-; V c=
+for "vterm copy") and unbind the default in =vterm-mode-map=. Update
+=modules/eshell-vterm-config.el= alongside any related vterm bindings.
+
+Implemented with a broader =C-; V= vterm menu, clickable URLs in vterm buffers,
+=C-; V c= for raw =vterm-copy-mode=, and =C-; V C= for tmux-pane history
+capture into a temporary Emacs buffer.
+** DONE [#A] AI-Term-Related Improvements
+*** DONE Check for widen/shorten buffer keys.
+The keybinding you "thought you had" is =windsize= on =C-s-<arrow>= (Ctrl+Super) — a tiling WM eats Ctrl+Super, which is why it didn't seem to exist. Shipped 2026-05-11 in commit =f837e5f= "feat(window): resize the split with C-; b <arrow>": =C-; b <left>/<right>/<up>/<down>= moves the active window's divider that way (via =windsize=), then keeps =cj/window-resize-map= active so bare arrows keep nudging until any other key (or =C-g= / =<escape>=); =C-u N C-; b <right>= resizes by N. The old =C-s-<arrow>= bindings were dropped; =windsize= is now =:commands=-deferred with =windsize-cols=/=windsize-rows= at 2. =cj/window-resize-sticky= (in ui-navigation.el) dispatches on the arrow that triggered it and arms the loop. New ERT tests; all green.
+*** DONE Evaluate this buffer should be in personal keybindings also.
+=eval-buffer= is now on =C-; b e= (it already had =C-c b=). =e= had been =cj/view-email-in-buffer= and the requested fallback =C-; b m= is =cj/move-buffer-and-file=, so email-view moved to =C-; b E= (docstring + which-key updated too). All in =modules/custom-buffer-file.el=.
+*** DONE Last ai-project used should be topmost in completing-read.
+Shipped 2026-05-11 in commit =c14d6c8= "feat(ai-vterm): order the project picker by most-recently-used". The picker's active group (projects with a live tmux session) now leads with projects opened this session, most-recent first (=cj/--ai-vterm-mru=, pushed by =cj/--ai-vterm-show-or-create=), then the rest of the active group alpha, then the no-session group alpha. Bundled fix: =cj/--ai-vterm-tmux-session-name= now sanitizes =.= / =:= → =_= the way tmux does, so =.emacs.d= (real session =aiv-_emacs_d=) is correctly matched to its session and shows up in the active group (and crash-recovery reattaches instead of spawning a duplicate). New tests + updated tests; all green.
+
+*** DONE Kill other window that leaves the split where it is.
+Didn't exist (the closest, =cj/kill-other-window= on =M-S-o=, *deletes* the other window). Shipped 2026-05-11 in commit =0ddbcde= "feat(window): kill the other window's buffer with C-; b K": =cj/kill-other-window-buffer= (in undead-buffers.el, on =C-; b K=) kills or buries the buffer shown in the other window and leaves that window and the split alone — the window then shows whatever bury/kill surfaces next. Reuses =cj/kill-buffer-or-bury-alive= so =cj/undead-buffer-list= buffers (=*scratch*= etc.) are buried; with 3+ windows it acts on =next-window=; errors with "No other window" if there's only one. =M-S-o= / =cj/kill-other-window= kept as-is (different op). 4 new ERT tests; all green.
+*** DONE Kill this buffer/window that leaves the split.
+Yes — the command is =cj/kill-buffer-and-window= (in =modules/undead-buffers.el=), bound to =C-; b k= (keymap entry in =modules/custom-buffer-file.el=, under the "buffer and file menu"). It does =(delete-window)= on the current window unless it's the only one, then kills (or buries, for "undead" buffers like =*scratch*=) the buffer — so in a 3-column split, =C-; b k= in column 2 leaves columns 1 and 3 as a normal 2-column split. No code change needed.
+
+Footnote on the =M-S-c= memory: =M-S-c= was Emacs's default =capitalize-word= and is now =time-zones= (=modules/chrono-tools.el=) — it was never this command. The =M-S-= window-killing family is =M-S-o= → =cj/kill-other-window= and =M-S-m= → =cj/kill-all-other-buffers-and-windows=.
+*** DONE M-w shouldn't close the buffer or copy-mode
+Shipped 2026-05-11 in commit =949bdeb= "feat(vterm): unify the keys in vterm copy-mode and tmux history". Both scrollback surfaces (=vterm-copy-mode= and the tmux-history buffer) now share one key story: =M-w= copies the active region and stays put (copy several things in a row); =C-g=, =<escape>=, or =q= leaves without copying; =RET= is unbound (no "copy and exit" — vterm's default =RET → vterm-copy-mode-done= binding removed). Dropped the now-dead =cj/vterm-tmux-history-copy-and-quit= (=M-w= then =q= is the equivalent). Also moved =cj/vterm-tmux-history= from =C-; x C= to =C-; x h= (unshifted, frees =C=) and refreshed the file's stale commentary header. Tests updated.
+*** DONE cursor still orange after hitting return.
+Shipped 2026-05-11 in commit =a70bb98= "fix(ui-config): use the writeable cursor color in a live vterm". Root cause: =vterm-mode= sets =buffer-read-only=, so the post-command cursor-color hook painted the cursor the read-only color (orange) any time point was in a vterm — copy-mode and the live terminal alike. Fix: a live vterm (=vterm-mode= and not =vterm-copy-mode=) now reports =unmodified= (white); =vterm-copy-mode= still reports =read-only= (orange), which Craig confirmed he wants. Extracted =cj/--buffer-cursor-state= for testability; 7 new ERT tests.
+
+*** DONE open in other window question/issue
+Shipped 2026-05-11 in commit =071fb5e= "feat(ai-vterm): keep emacsclient files out of the agent window". =server-start= left =server-window= nil, so =emacsclient -n= opened files in the selected window — which is the agent window when you're typing in it. Fix in ai-vterm.el: =server-window= now points at =cj/--ai-vterm-server-display=, which routes the file to a non-agent window (splitting one off the agent when it's the only window); emacsclient from anywhere else still goes through =pop-to-buffer=. Helper =cj/--ai-vterm-non-agent-window= picks the target (skips the minibuffer, dedicated windows, agent windows). 7 new ERT tests. Confirmed working — direction-agnostic, picks the "other" window whichever side the agent is on.
+** DONE [#A] Optimize org-capture target building performance :perf:
+CLOSED: [2026-05-11 Mon 13:05]
+
+15-20 seconds every time capturing a task (12+ times/day).
+Major daily bottleneck - minutes lost waiting, plus context switching cost.
+
+Implemented 2026-05-11: cache validated =file+headline= target markers in
+=org-capture-config.el= so repeated task captures into =Inbox= skip Org's
+full-file headline scan. Added regression coverage in
+=tests/test-org-capture-config-target-cache.el=.
+** DONE [#A] Fix Slack reaction workflow (C-; S !) :bug:
+CLOSED: [2026-05-11 Mon 14:08]
+
+Reactions via ~C-; S !~ (~slack-message-add-reaction~) have two problems:
+
+1. *Emoji picker only shows GitHub-style names* — without the ~emojify~ package,
+ ~slack-select-emoji~ falls back to a flat ~completing-read~ over 1600+ names
+ fetched from GitHub's iamcal/emoji-data. Common names like ~thumbsup~ and ~pray~
+ are buried. A curated shortlist of common reactions would fix the UX.
+
+2. *CRITICAL: post-command-hook bug traps user in Slack buffer* —
+ ~slack-reaction-echo-description~ is added to ~post-command-hook~ (buffer-local)
+ in all Slack buffers. When the cursor lands on a reaction widget, it reads the
+ ~reaction~ text property and calls ~slack-reaction-help-text~. If the reaction
+ EIEIO object is malformed, the error fires on *every keystroke*, making it
+ impossible to switch buffers, run M-x, or even C-g. The only escape is killing
+ Emacs externally (~pkill emacs~).
+
+ The fix must address this hook FIRST before any other reaction work.
+ Approach: advise ~slack-reaction-echo-description~ with ~condition-case~ to
+ silently catch errors, or remove it from ~post-command-hook~ entirely.
+
+ Relevant code in emacs-slack:
+ - ~slack-buffer.el:399~ — adds hook
+ - ~slack-buffer.el:374~ — ~slack-reaction-echo-description~ definition
+ - ~slack-reaction.el:72~ — ~slack-reaction-help-text~ method
+
+Implemented 2026-05-11:
+- Added a safe advice around ~slack-reaction-echo-description~. If malformed
+ reaction data errors from the buffer-local ~post-command-hook~, the hook is
+ removed for that buffer and a single message is shown instead of trapping
+ every keystroke.
+- Rebound ~C-; S !~ to ~cj/slack-message-add-reaction~, which presents a short
+ common reaction list first and keeps an ~Other...~ fallback to upstream
+ ~slack-message-reaction-input~.
+- Added regression coverage in =tests/test-slack-config-reactions.el=.
+
+**Discovered:** 2026-03-06
+** DONE [#B] Coverage audit: untested and lightly-tested modules :tests:
+CLOSED: [2026-05-11 Mon 14:38]
+
+Snapshot of test-coverage gaps as of 2026-04-26. The existing [#A] "Continue coverage push" task already targets =keybindings.el=, =config-utilities.el=, =org-noter-config.el=, and =host-environment.el=; this entry catalogs the rest so future sessions have a working list.
+
+**Methodology.** 102 modules in =modules/=, cross-referenced against =tests/= using fuzzy name matching (full module name, drop =-config=/=-setup= suffix, first hyphen segment). Categorized by likely test value.
+
+**High-value untested (substantial logic, real test value):**
+- =ai-conversations= — gptel persistence + autosave; 13 functions
+- =quick-video-capture= — yt-dlp queue, org-protocol; 5 functions
+- =dashboard-config= — custom commands (=cj/dashboard-only=, etc.)
+- =external-open= — partially refactored; helpers covered, commands still bare
+- =keyboard-compat= — terminal vs GUI Meta+Shift translation
+- =help-config= and =help-utils= — interactive help and lookup commands
+- =mail-config= — helpers (some covered via transcription tests; rest bare)
+- =show-kill-ring= — kill-ring UI logic
+- =system-commands= — shell command wrappers
+- =ui-navigation= and =ui-theme= — navigation + theme switching
+- =wrap-up= — init-finalize helpers
+
+**Lightly covered (1–2 tests, likely many uncovered functions):**
+- =modeline-config= (2 tests)
+- =org-agenda-config= (2)
+- =org-capture-config= (2)
+- =org-reveal-config= (2)
+- =transcription-config= (1) — helpers tested, start/stop loop bare
+- =jumper= (1)
+- =keyboard-macros= (1)
+
+**Likely low-value (mostly use-package wrappers):**
+About 28 modules are dominated by use-package + hooks + keybinds — testing them would mostly test Emacs/use-package itself. Examples: =auth-config=, =diff-config=, =dirvish-config=, =elfeed-config=, =erc-config=, =eww-config=, the =prog-*= language modules, etc. For each, review whether the file has any helper functions beyond use-package. If yes, write characterization tests. If not, document as "no unit tests appropriate" so the next audit skips it.
+
+**Approach.** Pick 2–3 modules per session from the high-value list. Refactor-first if needed (split interactive wrapper from pure helper per =.claude/rules/elisp-testing.md=), then write Normal/Boundary/Error coverage. Re-run =cj/coverage-report= (F7, project scope) after each batch so progress is measurable.
+
+**Cross-references:**
+- [[file:.ai/sessions/2026-04-22-09-49-coverage-v1-shipped-system-utils-tested.org][2026-04-22 session]] — coverage v1 shipped, 59.6% baseline
+- [[file:.claude/rules/elisp-testing.md][.claude/rules/elisp-testing.md]] — per-function test files, refactor-first, three required categories
+
+**2026-05-11 refresh.** Re-ran =make coverage= after excluding timing-sensitive
+=tests/test-lorem-optimum-benchmark.el= from coverage instrumentation. The
+benchmark file still runs in normal test-file/unit flows, but Undercover slows
+timing assertions enough to make it unsuitable for coverage. Low-coverage means
+instrumented modules below 50% executable-line coverage, plus modules missing
+from SimpleCov entirely. For missing modules, first decide whether the file has
+testable project logic; if it is just use-package/keybinding glue, document it
+as intentionally low-value instead of forcing brittle tests.
+
+*** TODO [#B] Add coverage for =prog-python.el= (0.0%, 0/20) :tests:
+
+Focus on formatter/setup helpers and any Python command-building logic. Avoid
+testing Emacs package glue directly; split pure helpers first if needed.
+
+*** TODO [#C] Add coverage for =selection-framework.el= (0.0%, 0/3) :tests:
+
+Add a smoke test for the final binding/setup behavior or document why the three
+instrumented lines are configuration-only.
+
+*** TODO [#B] Add coverage for =keyboard-compat.el= (3.4%, 1/29) :tests:
+
+Cover terminal-vs-GUI translation predicates and key normalization paths. Keep
+tests table-driven so future terminal quirks are easy to add.
+
+*** TODO [#B] Add coverage for =prog-webdev.el= (4.8%, 1/21) :tests:
+
+Cover formatter wiring and web-mode helper behavior without invoking external
+formatters.
+
+*** TODO [#C] Add coverage for =system-defaults.el= (8.3%, 1/12) :tests:
+
+Characterize the platform/default-setting helpers with stubs around system
+calls. Do not assert machine-specific defaults directly.
+
+*** TODO [#B] Add coverage for =ui-navigation.el= (8.7%, 4/46) :tests:
+
+Extend the existing window-resize tests to cover navigation commands,
+window-selection behavior, and boundary cases around missing/side windows.
+
+*** TODO [#B] Add coverage for =prog-go.el= (11.1%, 3/27) :tests:
+
+Cover Go formatting/test command wiring and any path/build helper behavior with
+external commands stubbed.
+
+*** TODO [#B] Add coverage for =system-commands.el= (12.2%, 6/49) :tests:
+
+Cover shell command wrapper construction, missing executable behavior, and
+interactive command smoke paths with process execution stubbed.
+
+*** TODO [#B] Add coverage for =external-open.el= (15.2%, 5/33) :tests:
+
+The library helpers already have some coverage; add smoke/characterization tests
+for user-facing open commands and error paths while stubbing launcher calls.
+
+*** TODO [#B] Add coverage for =org-webclipper.el= (16.9%, 10/59) :tests:
+
+Cover URL/content parsing, manual URL prompt behavior, protocol URL handling,
+and aborted capture cleanup.
+
+*** TODO [#C] Add coverage for =system-utils.el= (19.2%, 5/26) :tests:
+
+Review remaining utilities not covered by =eval-buffer= tests. Add focused
+Normal/Boundary/Error tests for pure helpers; stub process/system calls.
+
+*** TODO [#C] Add coverage for =org-reveal-config.el= (20.0%, 9/45) :tests:
+
+Extend existing header-template/title tests to cover export command setup and
+option-building helpers.
+
+*** TODO [#C] Add coverage for =coverage-elisp.el= (26.3%, 5/19) :tests:
+
+The detector is covered; add tests for report-path/project-root behavior and a
+stubbed run callback path if it can be tested without launching compilation.
+
+*** TODO [#C] Add coverage for =org-noter-config.el= (27.3%, 27/99) :tests:
+
+Build on the existing predicate/template tests. Target note-file placement,
+preferred split behavior, and interactive wrapper smoke tests.
+
+*** TODO [#B] Add coverage for =ai-config.el= (27.7%, 53/191) :tests:
+
+Prioritize model/backend selection edge cases, gptel local-tool registration,
+and command helpers. Stub network/model calls.
+
+*** TODO [#C] Add coverage for =dirvish-config.el= (30.4%, 48/158) :tests:
+
+Existing utility tests cover several helpers. Add focused coverage for remaining
+playlist/quick-access/display-path branches and document config-only areas.
+
+*** TODO [#B] Add coverage for =slack-config.el= (32.0%, 24/75) :tests:
+
+Extend reaction workflow coverage to message lookup, workspace/account
+configuration, and command error handling with Slack APIs stubbed.
+
+*** TODO [#B] Add coverage for =org-roam-config.el= (32.5%, 26/80) :tests:
+
+Extend existing slug/demote/link tests to cover TODO copy behavior, capture
+helpers, and file/path boundary cases.
+
+*** TODO [#C] Add coverage for =custom-text-enclose.el= (35.2%, 51/145) :tests:
+
+Many wrappers are tested, but coverage is still low. Identify untested commands
+and add table-driven region/buffer boundary cases.
+
+*** TODO [#C] Add coverage for =hugo-config.el= (39.6%, 38/96) :tests:
+
+Extend metadata/template tests to draft collection, path derivation, and command
+wrapper behavior with file/process calls stubbed.
+
+*** TODO [#C] Add coverage for =org-refile-config.el= (41.2%, 21/51) :tests:
+
+Build on target-building tests. Cover org-mode enforcement, missing files, and
+refile target edge cases.
+
+*** TODO [#C] Add coverage for =org-contacts-config.el= (45.6%, 36/79) :tests:
+
+Extend email parsing/finalize tests to contact lookup, capture field handling,
+and malformed contact data.
+
+*** TODO [#C] Add coverage for =transcription-config.el= (46.3%, 75/162) :tests:
+
+Existing helper tests are strong; add start/stop/process lifecycle tests with
+process creation and sentinels stubbed.
+
+*** TODO [#C] Add coverage for =music-config.el= (46.8%, 130/278) :tests:
+
+Coverage is broad but below 50%. Target remaining playlist mutation,
+navigation, and MPD side-effect paths with process calls stubbed.
+
+*** TODO [#B] Add coverage for =mail-config.el= (47.4%, 9/19) :tests:
+
+Transport helpers are covered. Add smoke tests for account context data,
+maildir shortcuts, bookmarks, and safe command setup without sending mail.
+
+*** TODO [#B] Add or triage first coverage for =ai-conversations.el= (missing from SimpleCov) :tests:
+
+High-value missing module. Cover gptel persistence, autosave path selection, and
+load/save error behavior with filesystem calls isolated.
+
+*** TODO [#C] Add or triage first coverage for =auth-config.el= (missing from SimpleCov) :tests:
+
+Review for testable cache/debug helpers versus package glue. Add tests for
+helpers; document any config-only surface as intentionally untested.
+
+*** TODO [#C] Add or triage first coverage for =calibredb-epub-config.el= (missing from SimpleCov) :tests:
+
+Review EPUB preference helpers and calibredb/nov hooks. Add tests around pure
+helpers; avoid brittle package-load assertions.
+
+*** TODO [#C] Add or triage first coverage for =chrono-tools.el= (missing from SimpleCov) :tests:
+
+Identify date/time helpers and command formatting logic. Add deterministic tests
+with current time stubbed.
+
+*** TODO [#B] Add or triage first coverage for =dashboard-config.el= (missing from SimpleCov) :tests:
+
+Cover custom dashboard commands such as single-window/dashboard-only behavior
+with buffer/window operations isolated.
+
+*** TODO [#D] Triage =diff-config.el= as missing from SimpleCov :tests:
+
+Mostly likely package/keybinding glue. Add tests only for local helper logic; if
+none exists, document as no unit tests appropriate.
+
+*** TODO [#C] Add or triage first coverage for =dwim-shell-config.el= (missing from SimpleCov) :tests:
+
+Review DWIM shell command selection and buffer/process helpers. Stub shell
+launches and cover fallback/error cases.
+
+*** TODO [#D] Triage =elfeed-config.el= as missing from SimpleCov :tests:
+
+Check for project-owned feed/search helpers. If it is only elfeed setup,
+document as low-value for unit coverage.
+
+*** TODO [#D] Triage =erc-config.el= as missing from SimpleCov :tests:
+
+Check for local IRC command helpers. If the file is package setup only, document
+as intentionally untested.
+
+*** TODO [#C] Add or triage first coverage for =eshell-config.el= (missing from SimpleCov) :tests:
+
+Cover local eshell helper functions and command aliases where possible. Avoid
+tests that depend on a live shell session.
+
+*** TODO [#D] Triage =eww-config.el= as missing from SimpleCov :tests:
+
+Review for local URL/browser helpers. If configuration-only, document as
+low-value for unit coverage.
+
+*** TODO [#D] Triage =flycheck-config.el= as missing from SimpleCov :tests:
+
+Prefer testing only project-owned predicate/setup helpers. Do not test flycheck
+package internals.
+
+*** TODO [#D] Triage =flyspell-and-abbrev.el= as missing from SimpleCov :tests:
+
+Look for local dictionary/abbrev helpers. If the file only wires modes/hooks,
+document that no unit tests are appropriate.
+
+*** TODO [#D] Triage =font-config.el= as missing from SimpleCov :tests:
+
+Check for font-selection helpers; otherwise classify as environment-specific UI
+configuration.
+
+*** TODO [#D] Triage =games-config.el= as missing from SimpleCov :tests:
+
+Check for project-owned game command wrappers. If it is only package setup,
+document as low-value.
+
+*** TODO [#C] Add or triage first coverage for =gloss-config.el= (missing from SimpleCov) :tests:
+
+Review glossary lookup/parsing helpers. Add pure tests where possible and smoke
+test interactive commands with completion stubbed.
+
+*** TODO [#B] Add or triage first coverage for =help-config.el= (missing from SimpleCov) :tests:
+
+High-value missing module from the original audit. Cover interactive help lookup
+commands and buffer-selection behavior with display functions stubbed.
+
+*** TODO [#B] Add or triage first coverage for =help-utils.el= (missing from SimpleCov) :tests:
+
+High-value missing module. Cover lookup/formatting helpers and error behavior
+for unknown symbols/topics.
+
+*** TODO [#D] Triage =httpd-config.el= as missing from SimpleCov :tests:
+
+Check for local server helpers. If it only configures simple-httpd, document as
+configuration-only.
+
+*** TODO [#D] Triage =latex-config.el= as missing from SimpleCov :tests:
+
+Review for local compile/view helper logic. Avoid asserting package setup unless
+there are project-owned predicates.
+
+*** TODO [#D] Triage =ledger-config.el= as missing from SimpleCov :tests:
+
+Check for project-owned ledger helpers; otherwise document as mode setup.
+
+*** TODO [#C] Add or triage first coverage for =local-repository.el= (missing from SimpleCov) :tests:
+
+Review repository path/discovery helpers and add filesystem-backed tempdir tests
+for meaningful local logic.
+
+*** TODO [#D] Triage =markdown-config.el= as missing from SimpleCov :tests:
+
+Check for local markdown command helpers. If it is only mode configuration,
+document as no unit tests appropriate.
+
+*** TODO [#C] Add or triage first coverage for =media-utils.el= (missing from SimpleCov) :tests:
+
+Original audit called this out with process boundaries. Cover launcher/command
+selection helpers with external commands stubbed.
+
+*** TODO [#C] Add or triage first coverage for =mu4e-org-contacts-integration.el= (missing from SimpleCov) :tests:
+
+Cover contact lookup/insert integration with mu4e and org-contacts calls
+stubbed.
+
+*** TODO [#C] Add or triage first coverage for =mu4e-org-contacts-setup.el= (missing from SimpleCov) :tests:
+
+Cover setup helpers and contact field defaults where project-owned logic exists.
+
+*** TODO [#D] Triage =org-agenda-config-debug.el= as missing from SimpleCov :tests:
+
+Check for debug helper functions worth testing. If it is ad-hoc diagnostics,
+document as low-value.
+
+*** TODO [#D] Triage =org-babel-config.el= as missing from SimpleCov :tests:
+
+Review for project-owned org-babel helper logic. Avoid tests that only assert
+language registration.
+
+*** TODO [#D] Triage =org-drill-config.el= as missing from SimpleCov :tests:
+
+Check for local drill helpers beyond package setup. Add characterization tests
+only for those helpers.
+
+*** TODO [#D] Triage =org-export-config.el= as missing from SimpleCov :tests:
+
+Review export option/path helpers. If configuration-only, document as no unit
+tests appropriate.
+
+*** TODO [#D] Triage =pdf-config.el= as missing from SimpleCov :tests:
+
+Check for local PDF helper commands. Avoid tests tied to external PDF tools
+unless command construction can be isolated.
+
+*** TODO [#D] Triage =popper-config.el= as missing from SimpleCov :tests:
+
+Review popup classification helpers. Add tests for predicates; document pure
+package setup as intentionally untested.
+
+*** TODO [#D] Triage =prog-general.el= as missing from SimpleCov :tests:
+
+Check for local development command helpers beyond global key setup. Add tests
+only for project-owned logic.
+
+*** TODO [#D] Triage =prog-lisp.el= as missing from SimpleCov :tests:
+
+Review Lisp-mode helper behavior. If it is only hooks/mode setup, document as
+low-value.
+
+*** TODO [#D] Triage =prog-training.el= as missing from SimpleCov :tests:
+
+Check for exercise/training command helpers and add characterization tests if
+present.
+
+*** TODO [#B] Add or triage first coverage for =quick-video-capture.el= (missing from SimpleCov) :tests:
+
+High-value missing module. Cover yt-dlp queue behavior, org-protocol handling,
+URL parsing, and process error paths with external commands stubbed.
+
+*** TODO [#B] Add or triage first coverage for =show-kill-ring.el= (missing from SimpleCov) :tests:
+
+High-value missing module. Cover kill-ring formatting, selection behavior, and
+empty-ring boundaries.
+
+*** TODO [#D] Triage =text-config.el= as missing from SimpleCov :tests:
+
+Review for local text helper commands. If it only configures packages/hooks,
+document as no unit tests appropriate.
+
+*** TODO [#D] Triage =tramp-config.el= as missing from SimpleCov :tests:
+
+Check for local TRAMP path helpers. Avoid tests requiring remote connections.
+
+*** TODO [#C] Add or triage first coverage for =vc-config.el= (missing from SimpleCov) :tests:
+
+Review project-owned VC helpers and command wrappers. Stub git/process calls.
+
+*** TODO [#D] Triage =weather-config.el= as missing from SimpleCov :tests:
+
+Check for request/format helpers. Avoid live network tests; stub weather calls.
+
+*** TODO [#B] Add or triage first coverage for =wrap-up.el= (missing from SimpleCov) :tests:
+
+Cover init-finalize helpers and post-startup behavior with hooks/timers stubbed.
+** DONE [#B] Review all config and pull library functions into system-lib file :refactor:
+Superseded by =PROJECT [#B] Consolidate shared utility helpers= (the structured version of this, with =docs/design/utility-consolidation.org= as the spec and =docs/design/utility-inventory.org= as the config-wide audit -- 30 candidate helpers across all modules, decided 11 Migrate / 3 Leave / 13 Defer). The system-lib extractions shipped 2026-05-10: =c75e36f= (=cj/executable-find-or-warn= from mail-config), =f1e8f08= (=cj/shell-quote-argument-readable= from dev-fkeys), =57e558c= (=cj/process-output-or-error= + =cj/git-output-or-error= from coverage-core), =aa72245= (=cj/file-from-context= from system-utils), plus the earlier =8e8152e= (=cj/log-silently=) -- each with its own test file. The rest of the 11 Migrate items landed as new =-lib.el= modules in the same marathon (=cj-cache-lib.el=, =cj-org-text-lib.el=, =external-open-lib.el=, =cj-window-geometry-lib.el=, =cj-window-toggle-lib.el=). The 13 deferred candidates remain tracked under the Consolidate-shared-utility-helpers PROJECT, not here.
+** DONE [#C] Clean up calibredb-epub-config.el :refactor:bug:
+CLOSED: [2026-05-11 Mon 14:55]
+
+1. *Remove ~:defer 1~ from calibredb use-package* — loads calibredb 1 second after
+ startup even though ~:commands~ and ~:bind~ already handle lazy loading. Free
+ startup time.
+
+2. *Double rendering on EPUB open* — ~cj/nov-apply-preferences~ calls
+ ~(nov-render-document)~ explicitly, but it runs as a ~nov-mode~ hook which fires
+ after nov already renders. Every EPUB open renders twice.
+
+3. *visual-fill-column-width doesn't adapt on resize* — calculated once at open
+ time based on window size. Resizing or splitting the window won't recalculate
+ text width. Consider hooking ~window-size-change-functions~ or
+ ~window-configuration-change-hook~.
+
+4. *~calibredb-search-page-max-rows 20000~* — effectively disables pagination.
+ Could slow down the search buffer if library grows large. Monitor or lower.
+
+5. *Anonymous lambda for zathura keybinding* — ~("z" . (lambda ...))~ won't show
+ a name in which-key or describe-key. Replace with a named function.
+
+**File:** modules/calibredb-epub-config.el
+
+Implemented 2026-05-11: removed the timed calibredb load, removed the explicit
+=nov-render-document= call from the =nov-mode= hook to avoid double rendering,
+made Nov text width recalculate after window configuration changes, lowered
+=calibredb-search-page-max-rows= from 20000 to 500, and replaced the anonymous
+zathura binding with =cj/nov-open-external=. Added focused helper coverage in
+=tests/test-calibredb-epub-config.el= for the adaptive width calculation and
+named external-open command.
+** DONE [#C] Update email setup script for the work account :chore:
+CLOSED: [2026-05-11 Mon 14:21]
+
+Follow-up to the deepsat mu4e work shipped 2026-04-27. The mu4e config (=modules/mail-config.el=), =.mbsyncrc=, =.msmtprc=, and the encrypted password file (=.config/.dmailpass.gpg=) all gained a third account. There is an "email setup script" (per Craig's mention while wrapping up that work) that needs the equivalent updates so a fresh machine bootstraps with all three accounts. Craig will name the specific script when picking this up.
+
+Likely shape:
+- Wherever the script writes / templates =.mbsyncrc=, add the dmail block (5-channel layout, mirroring the gmail block).
+- Wherever it writes =.msmtprc=, add the dmail SMTP account (passwordeval against =~/.config/.dmailpass.gpg=).
+- Ensure the encrypted password file exists or is sourced correctly during setup.
+
+Implemented 2026-05-11: updated =scripts/setup-email.sh= so the setup flow
+handles the deepsat/dmail account alongside gmail and cmail. The script now
+creates =~/.mail/dmail=, passes =craig.jennings@deepsat.com= to =mu init=,
+and installs/validates =~/.config/.dmailpass.gpg= using the same encrypted-file
+pattern as gmail. While there, the credential bootstrap was made explicit:
+gmail and dmail keep encrypted =.gpg= files because mbsync/msmtp decrypt them at
+use time, while cmail is decrypted to the plaintext ProtonBridge password file.
+** DONE [#C] Stand up packaging CI for personal Elisp packages :ci:feature:
+CLOSED: [2026-05-11 Mon 14:08]
+
+Get =chime=, =org-msg=, and =wttrin= covered by automated package-quality checks. Three pieces, all aimed at the same set of repos, so tracked together:
+
+1. *melpazoid* — MELPA-submission validator. Run against each package; gives a pre-submission checklist so packages don't bounce on basics.
+2. *package-lint* — elisp-specific package linter. Catches header issues, autoload problems, version-spec drift. Can be run locally as part of =make lint= and in CI.
+3. *elisp-check GitHub Action* — zero-config CI workflow that wraps the above plus byte-compile and basic tests. One =.github/workflows/elisp.yml= per package.
+
+Order of execution: package-lint first (most actionable, fastest feedback), then elisp-check (CI wiring), then melpazoid (heavier; only matters if/when submitting to MELPA).
+** DONE [#D] Optimize lorem-optimum performance and liber-primus.txt size :perf:
+CLOSED: [2026-05-11 Mon 14:17]
+
+Lorem-optimum text generation is generally slow but doesn't completely break workflow.
+Two benchmark tests were disabled (marked :slow) because they take MINUTES instead of seconds.
+
+**Current State:**
+- Tests disabled to unblock test suite (DONE 2025-11-09)
+- Performance is acceptable for daily use, but could be better
+- liber-primus.txt may be too large for optimal performance
+
+**Investigation:**
+1. Profile lorem-optimum to find bottlenecks
+2. Check if liber-primus.txt size needs optimization
+3. Optimize performance to get tests under 5 seconds
+4. Re-enable benchmark tests once performance is acceptable
+
+**Related Files:**
+- modules/lorem-optimum.el (needs profiling and optimization)
+- tests/test-lorem-optimum-benchmark.el (tests disabled with :tags '(:slow))
+- liber-primus.txt (corpus file, may need size optimization)
+
+Implemented 2026-05-11: optimized generation hotspots in =modules/lorem-optimum.el=
+by avoiding repeated string/list appends, caching random Markov keys as a vector,
+and hardening title generation while preserving empty-chain behavior. Re-enabled
+the benchmark tests in =tests/test-lorem-optimum-benchmark.el= by removing their
+=:slow= tags and added a title-generation regression test in
+=tests/test-lorem-optimum.el=. Checked =assets/liber-primus.txt= directly; it is
+36,475 bytes / 5,374 words, so no corpus shrink was needed. The benchmark file
+now runs all 10 tests in under one second, with 100K-word learning measured under
+200 ms on this machine.
+** DONE [#D] Migrate lsp-eldoc-hook to eldoc-documentation-functions :chore:quick:
+CLOSED: [2026-05-11 Mon 14:12]
+
+=modules/prog-lsp.el:68= sets =lsp-eldoc-hook= to nil. Byte-compile flags it as obsolete since lsp-mode 9.0.0; replacement is =eldoc-documentation-functions=. Find the lsp-mode-supplied entry there and remove it (or set the variable buffer-locally per the new API). Discovered 2026-04-26 during refactor audit on the file-watch-ignored-extras change.
+
+Implemented 2026-05-11: replaced the obsolete =lsp-eldoc-hook= assignment with
+=cj/lsp--disable-eldoc-hover=, installed from =lsp-managed-mode-hook=. The helper
+removes =lsp-eldoc-function= from buffer-local
+=eldoc-documentation-functions= after lsp-mode adds it. Covered in
+=tests/test-prog-lsp--add-file-watch-ignored-extras.el=.
+** DONE [#B] Simplify mail attachment save workflow :feature:
+
+Saving attachments out of mu4e is currently a multi-step dance via =mu4e-view-save-attachments= + embark + vertico. The flow documented in =modules/mail-config.el:10-17= goes:
+
+#+begin_quote
+After running =mu4e-view-save-attachments=:
+- invoke =embark-act-all= in the completion menu, then RET to save all
+- OR TAB (=vertico-insert=), comma each file to mark, RET to save selected
+#+end_quote
+
+That's four keystrokes for "save all to default dir" and N+2 for "save the one I want." Both common cases should be one keystroke.
+
+Proposed shape:
+- =cj/mu4e-save-all-attachments= → save every attachment in current message to a sensible default dir (=~/Downloads/= or per-thread). One keystroke.
+- =cj/mu4e-save-attachment-here= → completing-read on attachment names; save selected one. One keystroke + selection.
+- Bind both under =C-; e= (the existing email map already has =a= and =d= for attach/delete in compose).
+
+Open question: should the "save all" target be a fixed dir, prompt every time, or use the directory of an associated org-noter / project context? Flagged for design decision when this lands.
+
+Decision: save all should prompt every time.
+
+**Files:** =modules/mail-config.el= (add helpers, wire into mu4e-view-actions and the =C-; e= keymap).
+
+Implemented 2026-05-11: added direct mu4e view attachment save commands in
+=modules/mail-config.el=. =cj/mu4e-save-all-attachments= prompts once for a
+directory and saves every attachment-like MIME part. =cj/mu4e-save-attachment-here=
+prompts for a directory, then uses =completing-read= to save one attachment.
+Both reuse mu4e's MIME part metadata, uniquify hook, path joiner, and
+=mm-save-part-to-file= save primitive instead of driving the existing
+multi-select completion UI. Duplicate filenames are disambiguated by part index.
+Bound under =C-; e S= and =C-; e s= with which-key labels. Covered by
+=tests/test-mail-config-attachments.el=.
+
+Extended 2026-05-11: added =cj/mu4e-save-some-attachments= on =C-; e m=. It
+prompts for the destination directory, opens a dedicated =*mu4e attachments*=
+selection buffer, and lets the user mark rows with RET, mark all with =a=,
+unmark all with =u=, save marked with =s=, and quit with =q=. The selection
+buffer shows labels, MIME types, and approximate sizes while reusing the same
+attachment save helpers.
+
+Committed and pushed 2026-05-11 as =1aa8d0f= "feat(mu4e): simpler attachment-save commands on C-; e S/s/m"; 18 ERT tests in =tests/test-mail-config-attachments.el=. (Two small UX follow-ups — `entry-at-point' user-error outside a row, and clearing marks / auto-quit after save — are tracked under "Post-batch review follow-ups (2026-05-11)".)
+** DONE [#B] EPUB text renders full-width: visual-fill-column margins not applied in nov-mode :bug:
+*** Resolved 2026-05-12
+Fixed in =b7c6b2c= "fix(nov): center the EPUB text by setting window margins directly" -- took the "preferred" plan below: `nov-text-width' is now a column count (~80% of the window's natural width) so nov's `shr' fills the text itself, and `cj/nov-update-layout' centers the block with `set-window-margins' directly (plus `set-window-fringes ... t' to push the fringes off the reading area). `visual-fill-column' is dropped from nov entirely -- its margin-setting still mysteriously never applied, but that's moot now. `+'/`=' / `-'/`_' re-flow and re-center; a buffer-local `kill-buffer-hook' resets the margins/fringes. The text-width math factored into `cj/nov--natural-window-width' + `cj/nov--text-width'. Remaining nit: see "EPUB text is slightly left-of-center" below.
+*** Problem
+Opening an EPUB renders the text filling 100% of the window width, not the configured ~80%. =modules/calibredb-epub-config.el=, =cj/nov-apply-preferences= (on =nov-mode-hook=).
+
+Before the 2026-05-12 work it was the opposite — a too-narrow ~third-of-the-window strip — caused by a feedback loop (=cj/nov--text-width-for-window= computed from =window-body-width=, which is post-margin, so each =cj/nov-update-layout= pass shaved the column by another margin fraction, bottoming out at =cj/nov-min-text-width= = 40). Commit =1c5c8bd= "fix(nov): rework the EPUB reading-width layout" fixed the loop (width now computed from the window's *natural* column count, idempotent) and split out a pure =cj/nov--text-width= helper with a regression test; =4d9a206= set the default =cj/nov-margin-percent= to 10 (= 80% text); both also re-added =b3b537f='s =(nov-render-document)= for the cold open, made =cj/nov-update-layout= a command, and bound =+= / === / =-= / =_= in =nov-mode-map= to adjust the width live (clamp 0..25, i.e. text 50%..100%). The *width computation* and the *loop* are fixed. The *margin application* is not — hence 100%.
+
+*** Why 100% specifically
+=cj/nov-apply-preferences= sets =(setq-local nov-text-width t)=. With =nov-text-width= = t, =nov-render-html= renders the text *unfilled* — it swaps =shr-fill-line= for =nov-fill-line=, which only indents, never wraps — so the buffer holds one long logical line per paragraph, and =visual-line-mode= is relied on to wrap it visually at the window's *text-area* width. =cj/nov-update-layout= is supposed to narrow that text area by turning on =visual-fill-column-mode= with =visual-fill-column-width= set to ~80% of the window's columns and letting =visual-fill-column--adjust-window= set the left/right *window display margins*. The margins never get set, so the text area stays the full window width → text wraps at 100%.
+
+*** Diagnostics captured (Craig's running Emacs, in the EPUB buffer, 2026-05-12)
+=M-: (list :margin cj/nov-margin-percent :body-w (window-body-width) :vfc-w visual-fill-column-width :vfc-mode (bound-and-true-p visual-fill-column-mode) :vfc-feat (featurep 'visual-fill-column) :wmargins (window-margins) :ntw nov-text-width)= →
+ =(:margin 10 :body-w 152 :vfc-w 121 :vfc-mode t :vfc-feat t :wmargins (nil . nil) :ntw t)=
+So: the new code IS loaded (=cj/nov-margin-percent= 10), =visual-fill-column= IS loaded, =visual-fill-column-mode= IS on, =visual-fill-column-width= IS correct (121 = 80% of 152) — but =(window-margins)= is =(nil . nil)=. =M-x cj/nov-update-layout= (which calls =visual-fill-column--adjust-window=) does NOT change it; =M-: (condition-case e (progn (cj/nov-update-layout) (window-margins)) (error e))= → =(nil . nil)= (no error caught, margins still nil). So =visual-fill-column='s margin-setting path (=visual-fill-column--adjust-window= → =visual-fill-column--set-margins= → =set-window-margins=) is not landing in nov-mode buffers.
+
+*** Why it doesn't apply — UNKNOWN
+Code-reading =visual-fill-column-2.7.0= didn't pin it down. =visual-fill-column--adjust-window= does =(with-selected-window window (visual-fill-column--reset-window window) (when visual-fill-column-mode (... (visual-fill-column--set-margins window))))=. For the result to be =(nil . nil)=, either =--set-margins= isn't reached (the =(when visual-fill-column-mode ...)= check is false in whatever buffer =with-selected-window= makes current) or it computed left=right=0 (=set-window-margins window 0 0= → =(window-margins)= = =(nil . nil)=). =--set-margins= computes 0/0 only when =total-width= (≈ window-width) ≤ =visual-fill-column-width= (121) — and window-width is 152, so it shouldn't. Candidate causes not yet ruled out: (a) the =default= face is remapped to "Merriweather" :height 180 in nov buffers (via =face-remap-add-relative= in =cj/nov-apply-preferences=), and =set-window-margins='s units (canonical frame-font columns) vs. the remapped 18pt buffer columns may be confusing the column math; (b) =--adjust-window= being invoked on the wrong window (it defaults to =(selected-window)=, not the EPUB's window — relevant when nov-mode-hook runs before =find-file= switches the window, and possibly later); (c) a =visual-fill-column= 2.7.0 / Emacs 30 regression with =nov-fill-line=-style rendering; (d) something resetting the margins after they're set.
+
+*** Plan forward — preferred: stop delegating the width to visual-fill-column
+Set =nov-text-width= to a *computed integer* instead of =t=, so nov's =shr= fills the rendered text to that width itself — no dependence on =visual-fill-column='s window margins working at all. =visual-fill-column= then only *centers* the already-narrow block (if it works; if it still doesn't, the text is left-aligned at ~80%, which is acceptable). Specifically:
+- =cj/nov-apply-preferences=: =(setq-local nov-text-width (cj/nov--text-width-for-window))= (integer) instead of =t=.
+- =cj/nov-update-layout=: recompute and =setq-local= both =nov-text-width= and =visual-fill-column-width=, then call =(nov-render-document)= so =shr= re-flows the text at the new width (currently it only re-sets the vfc width). Still keep the =visual-fill-column-mode= + =--adjust-window= calls for centering.
+- =+= / =-= keep working: they adjust =cj/nov-margin-percent= then call =cj/nov-update-layout=, which now re-renders.
+- =cj/nov-min-text-width= (40) stays the absolute column floor.
+TDD test-first. Touches =modules/calibredb-epub-config.el= + =tests/test-calibredb-epub-config.el=. ~25 lines.
+
+*** Alternatives considered
+(1) Keep =nov-text-width= = t + =visual-fill-column= and keep poking at *why* the margins don't apply — needs more in-Emacs diagnostics (e.g. trace =visual-fill-column--set-margins=, check =(window-margins)= right after =(set-window-margins win 15 16)=, check whether a stray window param clamps it). Higher uncertainty.
+(2) Left-align at the computed width with no centering at all (drop =visual-fill-column= from nov entirely) — simpler, but loses the centered look Craig wanted.
+Preferred is the =nov-text-width=-as-integer approach because it's robust regardless of what =visual-fill-column= does.
+** DONE [#B] Post-batch review follow-ups (2026-05-11) :refactor:tests:
+Minor items found while reviewing the 2026-05-11 commit batch (a70bb98..2b88c6a). The major fix (org-capture cache-key consistency) and the coverage gaps were already handled in commits =fc94e5b= / =e0e0ecd= / =2b88c6a=; these are the leftovers.
+
+*** DONE [#B] Give the benchmarks a real home (`make benchmark' or `:tags '(:perf)') :tests:perf:
+The 2026-05-11 lorem-optimum perf work (=7f353e9=) dropped the `:slow' tags from the benchmark tests so they run in every `make test', and one (`benchmark-learn-100k-words') gained an absolute wall-clock threshold (`(should (< time 5000.0))'). Then =1f4c692= excluded `test-lorem-optimum-benchmark.el' from `make coverage' because undercover's instrumentation breaks those thresholds. That's a fragmented policy and the thresholds are machine-dependent (a slower CI runner or older laptop could blow 5s). Pick one: (a) restore `:tags '(:perf)' on the benchmark tests and add a `make benchmark' target that runs them, or (b) replace the absolute thresholds with relative checks ("100K is no more than ~20x slower than 10K") that catch O(N^2) regressions without depending on the machine. Either way `make test' should stop running absolute-time benchmarks by default.
+
+*** DONE [#B] Verify Nov EPUB renders at the right width on first open :bug:
+There WAS a regression, deeper than =b3b537f=: `cj/nov--text-width-for-window' computed the column from `window-body-width' (post-margin), so `cj/nov-update-layout' (on `window-configuration-change-hook') shrank the column on every pass — a feedback loop bottoming out at `cj/nov-min-text-width' (40 cols) regardless of `cj/nov-margin-percent'. Fixed 2026-05-12 in =1c5c8bd= "fix(nov): rework the EPUB reading-width layout": width now from the window's natural column count (idempotent), pure `cj/nov--text-width' helper + regression test, `cj/nov-margin-percent' default 12 (~76% text), `b3b537f's `(nov-render-document)' re-added for the cold open, `cj/nov-update-layout' made a command, and `+'/`='/`-'/`_' added in `nov-mode-map' to adjust the width live (50%..100%). Visual confirm in real Emacs still pending Craig's restart.
+
+*** DONE [#C] Surface `cj/slack-message-add-reaction' errors outside a Slack buffer :ux:
+`cj/slack-message-add-reaction' (C-; S !, added in =bbd1b73=) silently no-ops when `slack-current-buffer' is nil — e.g. if the binding fires outside a Slack buffer. The `when-let*' chain just bails with no feedback. Add a `user-error "Not in a Slack buffer"' (and the same for `slack-buffer-team' returning nil) so the misuse surfaces instead of being swallowed.
+
+*** DONE [#C] Rename `cj/lsp--disable-eldoc-hover' to `cj/lsp--remove-eldoc-provider' :refactor:
+The function (added in =96d5d6a=) removes one specific provider — `lsp-eldoc-function' — from the buffer-local `eldoc-documentation-functions'. If lsp-mode ever adds another eldoc provider, the current function wouldn't catch it; the name promises more than it does. Rename to match what it actually does and update the `add-hook' callsite + the regression test.
+
+*** DONE [#C] Add `bats' test infra and cover `scripts/setup-email.sh' helpers :tests:
+The 2026-05-11 email-setup work (=eddc103=) added `install_encrypted_password' and `decrypt_password' — cleanly factored (filenames in, file-or-`exit 1' out) but untested, since the repo has no shell-test infrastructure. With a temp `$PASSWORD_DEST_DIR' and mocked `gpg'/`cp', they'd test cleanly. Add `bats' (or pick an alternative), wire a `make test-shell' target, and cover the two helpers plus the dest-exists-skip and missing-source-fails paths.
+
+*** DONE [#C] Split the mu4e attachment workflow out of `mail-config.el' :refactor:
+The 2026-05-11 mu4e attachment commit (=1aa8d0f=) added ~247 lines to `mail-config.el' for a self-contained attachment-save UI (helpers + three commands + a `special-mode'-derived selection buffer). None of it depends on the rest of `mail-config'. As that file grows, moving this into `modules/mu4e-attachments.el' (or `mail-attachments-lib.el', matching the `-lib.el' convention) would keep both files easier to read. The seam is clean.
+
+*** DONE [#C] Clear marks (or auto-quit) after `cj/mu4e-attachment-selection-save-marked' :ux:
+After saving the marked attachments, the `*mu4e attachments*' buffer (=1aa8d0f=) stays open with the same marks intact — pressing `s' again re-saves the same set silently. Decide what the workflow wants: auto-`quit-window' after a successful save, or clear the marks and stay so the user can save another batch. Right now it does neither.
+** DONE [#D] Create print function for dirvish bound to uppercase P :feature:
+
+Add a print function that works on printable files (PDF, txt, org, etc.) and bind it to uppercase P in dirvish-mode. Should detect file type and use appropriate print command (lpr for text files, print dialog for PDFs, etc.).
+** DONE [#D] Collapse the duplicated per-file test loop in the Makefile :chore:
+=test-unit=, =test-integration=, and =coverage= each carry a near-identical ~40-line shell loop (run each file in its own Emacs, count passes, collect failures, print a summary box). The three drifted once already (the =:perf= tag filter had to be added in three places). Extract a single =define=d shell function or a helper recipe parametrized by test list + extra =-l= args + label, and have the three targets call it. Cosmetic — the Makefile works — so low priority. Noticed 2026-05-12 while adding =make benchmark=.