#+TITLE: Emacs Config #+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. - =:solo:= means Claude can do the task end to end with no input from Craig: bounded scope, no design or preference call, and verifiable in the local setup (tests, byte-compile, launch). Tasks needing a policy/preference decision, visual judgment, or a live remote do not get =:solo:=. Tags are additive. For example, a small wrong-behavior fix can be =:bug:quick:=, and a feature that requires internal restructuring can be =:feature:refactor:=. * Emacs Open Work ** TODO [#C] Manually verify cj/org-finalize-task journal copy :test: Confirm the live behavior the unit tests mock out. In a real Emacs (org-roam loaded), run =C-; O d= on a level-3 sub-task and on a level-2 task. Expect the sub-task to flip to a dated entry, the level-2 to keep its keyword and gain a date-only CLOSED line, and in both cases a copy to land in today's daily under "Completed Tasks". Triggered by: 2026-05-22 L56 finalize-task work. ** TODO [#C] Dashboard over-scroll: pin last line to window bottom :bug: :PROPERTIES: :LAST_REVIEWED: 2026-05-22 :END: Triggered by: 2026-05-20 Dashboard buffer too long follow-up. After the opens-at-top fix (=4ac1b81=), the dashboard can still be scrolled past its content: the banner image makes the buffer just over one screenful, so the wheel / =C-v= / =M->= pull the last line up and leave empty space below it. Craig wants scrolling to stop once the trailing line reaches the window bottom (no void) while still allowing scroll-down to reach content below the window. Findings from the 2026-05-20 investigation: - =pixel-scroll-precision-mode= is off, so this is standard line-based scroll overshoot (the tall banner image inflates the rendered height). - A =window-start= clamp does not work: =window-start= only lands on line boundaries, so it can't express a position partway into the banner image — it either blocks all scrolling or leaves the void. - A =recenter -1= pin on =post-command-hook= does not work: it fires on every command, so it fights item navigation (the cursor can't reach the projects / bookmarks / recents). - Right design: clamp only on actual scroll commands — advise =mwheel-scroll= / =scroll-up-command= / =scroll-down-command= / =end-of-buffer= to =recenter -1= when over-scrolled, never on navigation commands. - Live experiment scratch file: =~/dashboard-overscroll-experiment.el=. ** TODO [#C] Separate dashboard navigator color from list items :feature: The dashboard navigator (icons + labels) and the recentf/project/bookmark list items are both painted by =dashboard-items-face=: the navigator gets a =dashboard-items-face= overlay, and overlays beat text properties, so the per-button =dashboard-navigator= face is inert. To color the navigator independently of the items, override where that overlay is applied — advise or redefine =dashboard-insert-navigator=, or strip/replace the overlay's face. Triggered by: 2026-05-22 dashboard color work (L105). ** PROJECT [#B] Architecture review follow-up from 2026-05-03 :refactor:no-sync: High-level pass over =init.el=, =early-init.el=, and all 104 files in =modules/=. The main theme: the config works, but load order, startup side effects, credentials, and test measurement are more implicit than they should be. Use this project as the parent tracker; each child below should land as a small, reviewable change. Review snapshot: - =modules/= has 104 files and about 24k lines including =init.el= and =early-init.el=. - =init.el= eagerly =require=s nearly every module. - =make coverage= passed when allowed to write the test scratch directory. - Coverage report: =3240/4952= executable lines, =65.43%=, across 49 module files. Caveat: 55 module files do not appear in the report at all, so the real project confidence is lower than the raw percentage suggests. *** 2026-05-15 Fri Consolidate shared utility helpers :architecture:refactor: CLOSED: [2026-05-15 Fri] Helpers are scattered across feature modules where they were first needed. Some are duplicated, and some private helpers are generic enough to belong in a shared foundation library. This is adjacent to the load-graph refactor because central helper ownership reduces hidden inter-module dependencies, but it should remain a sibling project so load-order batches stay small and reviewable. Guidance: - Do not extract a helper until at least two callers are clearly the same shape. - Prefer growing =system-lib.el= first; split into topic libraries only if it becomes too broad or starts pulling coarse dependencies into foundation startup. - Keep one helper extraction per commit. - Move unit tests with the helper. Consumers should keep behavior/integration coverage. - Do not add heavy package dependencies to foundation helpers. **** DONE [#B] Write full utility consolidation design spec :architecture:refactor: CLOSED: [2026-05-04 Mon] Create a design document that inventories candidate helper extractions, recommends grouping and naming, explains how the helpers fit into existing library modules, defines migration phases, and identifies testing/rollback rules. Spec: [[file:docs/design/utility-consolidation.org][docs/design/utility-consolidation.org]] Verify 2026-05-04: - Added [[file:docs/design/utility-consolidation.org][docs/design/utility-consolidation.org]]. - Spec includes framing questions, existing library fit, proposed grouping, concrete pull/rename table, migration phases, test strategy, acceptance criteria, risks, open questions, and recommended first commits. - Parsed the spec and =todo.org= with =org-element=. - Committed the tracked spec as =3ea4707=. - Incorporated complete review feedback in =dd77ebd=, including API behavior contracts, speculative-extraction rules, =system-lib= dependency budget, inventory/audit artifacts, test relocation policy, commit type guidance, =use-package :if= load-order policy, and Phase 5 cache-design addendum requirement. **** DONE [#B] Inventory private helpers across modules :refactor: CLOSED: [2026-05-10 Sun] Walk every module and tag private helpers as genuinely module-specific, generic-but-trapped, or duplicated. Capture likely consumers and any dependency cost before extracting. Candidate families: - shell argument formatting, - executable lookup with user-visible warnings, - argv-based process runners, - path containment/safe-base predicates, - Org-safe heading/property/body text sanitizers, - cache-with-TTL plus invalidation hooks, - warning/message wrappers. Verify 2026-05-10: - Added [[file:docs/design/utility-inventory.org][docs/design/utility-inventory.org]] covering the 30 entries in the spec's Candidate Extraction Table grouped by family (executable discovery, shell quoting, process runner, file/path, external-open, Org-safe text, cache, logging, macros/debug, theme I/O, string). - For each helper recorded: visibility, dependencies, side effects, callers (production + test), test files, priority, decision (Migrate / Leave / Defer) with rationale. - Decisions Summary: 11 Migrate, 3 Leave, 13 Defer. - Concrete next-action list groups Migrate items by Phase (2 = foundation helpers, 3 = Org-safe text, 4 = external-open consolidation) for the order the spec recommends. - Discoveries: =cj/log-silently= has 10 production callers (more than the spec's table suggested -- defer is the right call); =cj/--file-manager-program-for= shipped today in =dirvish-config.el= is the new form of OS-dispatch consolidation and should fold into =cj/external-open-command= during Phase 4. **** DONE [#B] Extract executable lookup with warning helper :refactor: CLOSED: [2026-05-10 Sun] Create a generic helper such as =cj/find-executable-or-warn= from the useful =mail-config= pattern. It should return the executable path or nil and produce a clear warning when the executable is missing. Done 2026-05-10: - Shipped as =cj/executable-find-or-warn= in =modules/system-lib.el= (commit =c75e36f4=, extracted from =mail-config=). - First consumer rewired in =12c2cb14= (=cj/set-wallpaper= in =dirvish-config.el=). **** DONE [#B] Extract argv-based process runner helper :refactor: CLOSED: [2026-05-10 Sun] Generalize the =coverage-core= process pattern into a dependency-light helper that captures output and signals a clear =user-error= with command/status/output on failure. Consider a small git wrapper only after the generic runner exists. Done 2026-05-10: - Shipped =cj/process-output-or-error= plus the =cj/git-output-or-error= wrapper in =modules/system-lib.el= (commit =57e558ce=, extracted from =coverage-core=). **** DONE [#B] Extract Org-safe text sanitizers :refactor: CLOSED: [2026-05-10 Sun] Move heading/property/body sanitization into a shared helper once at least one non-calendar consumer is ready. Keep behavior explicit so external text cannot accidentally create headings or malformed properties. Done 2026-05-10: - Shipped =modules/cj-org-text-lib.el= (renamed to its final =-lib= form in commit =0f9e3087=) with three sanitizers: =cj/org-sanitize-body-text=, =cj/org-sanitize-property-value=, =cj/org-sanitize-heading=. *** 2026-05-15 Fri Make coverage reporting account for untracked modules :tests: CLOSED: [2026-05-15 Fri] The current coverage result is useful but easy to overread. =make coverage= reported =65.43%= for files that undercover saw, but only 49 of 104 module files appeared in =.coverage/simplecov.json=. Definition: in this task, "untracked modules" means repository-owned =modules/*.el= files that should be part of the Emacs configuration coverage universe but have no entry in =.coverage/simplecov.json= after =make coverage= runs. These files may be missing because no test required them, because loading was skipped due to package/environment guards, or because instrumentation did not see them. They are distinct from tracked modules with 0% covered lines, which already appear in SimpleCov and can be scored directly. Completed 2026-05-15: - Both child tasks are done. - =make coverage-summary= reports missing modules explicitly and also reports a separate project-module score where missing modules count as 0%. - Focused summary tests and byte-compilation of the summary helper passed. **** 2026-05-15 Fri Teach the coverage report to list modules missing from SimpleCov CLOSED: [2026-05-15 Fri] Expected outcome: - Compare =modules/*.el= against paths present in =.coverage/simplecov.json=. - Show a separate "not in report" section. - Do not silently fold those files into the percentage until we decide the semantics. A visible missing-file count is enough for v1. Done 2026-05-15: - =make coverage-summary= now compares direct =modules/*.el= files on disk against the module paths present in =.coverage/simplecov.json=. - The terminal report appends a =Not in SimpleCov report= section with a count and the missing module paths. - Missing modules are explicitly excluded from the displayed percentage for now; the policy question below remains open. - Added focused tests in =tests/test-coverage-summary.el= for missing-module reporting and for ignoring =.elc= files and nested paths outside direct =modules/*.el= ownership. **** 2026-05-15 Fri Decide whether unreported modules count as 0% coverage CLOSED: [2026-05-15 Fri] This is a policy decision: - Counting missing modules as 0% gives a more honest project-level number. - Keeping the current number is useful for "instrumented executable lines only". Recommendation: display both: - Instrumented coverage: current SimpleCov percentage. - Project module coverage: includes unreported module files as 0% or reports them separately with an explicit caveat. Decision 2026-05-15: - Keep the existing SimpleCov percentage as the line-weighted =instrumented coverage= number. It only covers modules that SimpleCov saw and has real executable-line denominators for. - Also display a separate module-weighted =project module coverage= score over all direct =modules/*.el= files. Modules present in SimpleCov contribute their per-file coverage percentage; modules absent from SimpleCov count as 0%. - Do not pretend missing modules have known executable-line counts. Counting them as 0% at the module level is honest about risk without inventing a line denominator. Done 2026-05-15: - =make coverage-summary= now prints both the existing line-weighted summary and a separate =Project module coverage= line that includes missing modules as 0%. - The missing-module section now states that missing modules count as 0% in the project-module score. - Updated =tests/test-coverage-summary.el= to assert the policy and the displayed project-module percentage. *** 2026-05-15 Fri Add a lightweight architecture smoke test for startup contracts :tests: CLOSED: [2026-05-15 Fri] After the above refactors start, add one or two smoke tests that protect the architecture instead of individual functions. Candidate checks: - All modules can be loaded directly with only =modules/= on =load-path=, or skipped with a clear external package reason. - No module other than =keybindings.el= binds =C-;= itself. - Startup-only modules do not run timers in batch test mode. Keep this small. The goal is to catch accidental return to hidden load-order coupling, not to build a full static analyzer. Done 2026-05-15: - Added =tests/test-architecture-startup-contracts.el= with two source-level smoke checks: - only =keybindings.el= may globally own the exact =C-;= prefix; - top-level timer scheduling forms must be guarded by =noninteractive= so batch/test loads do not schedule startup timers. - Gated existing startup timers in =org-agenda-config.el=, =org-refile-config.el=, =quick-video-capture.el=, and =wrap-up.el=. - Focused tests passed for the new architecture smoke file and the affected agenda/refile helpers. *** PROJECT [#A] Un tangle the eager =init.el= load graph :architecture:refactor: =init.el= currently functions as the dependency graph by eagerly requiring almost every module in a fixed order. That makes modules harder to test in isolation and hides real dependencies behind "loaded earlier in init.el" assumptions. Spec: [[file:docs/design/init-load-graph.org][docs/design/init-load-graph.org]] **** 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 [#A] Move package bootstrap out of =early-init.el= where possible :startup:refactor: =early-init.el= currently handles package archives, package refresh, installing =use-package=, and =use-package-always-ensure=. That is more than early startup needs and can make startup network-sensitive. **** TODO [#B] Split early startup from package bootstrap :refactor: Keep =early-init.el= focused on things that must happen before package and UI startup: - GC/file-name-handler startup tuning. - =load-prefer-newer=. - frame/UI suppression. - minimal debug behavior. Move package archive setup and =use-package= installation to a normal module or bootstrap command, unless there is a specific reason it must run in =early-init.el=. Acceptance criteria: - Fresh install/bootstrap still works from a documented command or script. - Normal startup does not refresh archives or install packages unexpectedly. - Offline startup remains quiet and predictable. **** TODO [#A] Revisit package signature policy =package-check-signature= is disabled. Decide whether that is still necessary for the localrepo/mirror workflow. Expected outcome: - Prefer signatures on by default. - If signatures must be disabled for local mirrors, scope that exception and document why. - Add a note to the local repository docs so future package failures do not lead to permanent insecure defaults. ** TODO [#B] Rework dev F-keys: compile+run (F4), test (F6), coverage (F7) :feature: :PROPERTIES: :LAST_REVIEWED: 2026-05-22 :END: *** TODO [#B] Format keybindings move off F6 :refactor:cleanup: Move blacken-buffer (python), shfmt-buffer (sh), and clang-format-buffer (c) off F6 onto the =C-; f= prefix, which already hosts format-buffer bindings. Also remove projectile-run-project from F6 (it folds into the new F4). Touch the per-language config modules that currently bind F6 for formatting. Acceptance: F6 has no remaining format-or-run bindings in any module; =C-; f= prefix triggers the right formatter per major mode. Depends on: none (start here -- clears F6 before F4/F6 work lands). *** TODO [#B] Project-type detection helper :feature: Single helper that returns a project-type symbol (=compiled=, =interpreted=, =unknown=) from the current buffer's project. Uses =projectile-project-compilation-cmd= when set, then heuristic fallbacks: =go.mod=, =Makefile=, =Eask=, =package.json=, =pyproject.toml=, =docker-compose.yml=. Lives near the F4 dispatcher. Acceptance: ERT tests cover each heuristic in isolation plus a precedence case where projectile's cached cmd wins over the file heuristics. Depends on: none. *** TODO [#B] F4 compile+run dispatcher :feature: Build the F4 binding per spec: plain F4 opens a completing-read whose candidates depend on project-type (Compile / Run / Compile + Run [default] / Clean + Rebuild for compiled; Run only for interpreted). C-F4 fast-paths to Compile; M-F4 fast-paths to Clean + Rebuild. Both fast paths show a "not a compiled language" message and no-op on interpreted projects. Reads projectile's per-project compile/run commands; no Docker-specific logic. Acceptance: each candidate dispatches to the right projectile command; fast paths no-op cleanly on interpreted projects; F4 bindings live in one module. Depends on: project-type detection helper. *** TODO [#B] Per-language test discovery :feature:tests: Provide a single =cj/--tests-in-buffer= function returning a list of test names for the current buffer's language. Tree-sitter queries for Python, Go, TS/JS (treesit-auto already configured); built-in sexp scan for elisp (=ert-deftest= forms). Parsing unopened test files uses with-temp-buffer + insert-file-contents + -ts-mode + treesit-query-capture. Queries are spelled out in the spec above. Acceptance: ERT tests feed each language a fixture file and assert the expected test-name list comes back; missing grammar surfaces a clear error. Depends on: none (parallel-safe with F4 work). *** TODO [#B] F6 test dispatcher :feature:tests: Build the F6 binding per spec: plain F6 opens completing-read with "All tests", "Current file's tests", "Run a test..."; C-F6 fast-paths to current file's tests; M-F6 fast-paths to "Run a test...". "Current file's tests" runs the buffer directly if it's a test file, otherwise finds matching test files via language conventions (elisp =tests/test-*.el=, python =tests/test_.py=, etc.) and runs them aggregated. "Run a test..." pre-selects =cj/--last-test-run= (buffer-local) and errors with "No tests found for " when discovery returns nothing -- no silent fallthrough. Acceptance: each entry point dispatches to the right runner; buffer-local last-test memory persists per source file; no-match error fires correctly. Depends on: per-language test discovery. *** TODO [#B] F7 hand-off to dev-fkeys story :feature: Once the coverage track ships ([[file:../docs/design/coverage.org][docs/design/coverage.org]]), confirm F7 binds =cj/coverage-report= and lives alongside F4/F6 in the same dev-fkeys module so the three keys read as one unit. No new coverage logic here -- only the binding placement and a short comment block in the module pointing at the coverage design doc. Acceptance: F7 invokes coverage-report; F4/F6/F7 are visibly grouped in one module; coverage track is shipped before this lands. Depends on: the coverage-config track shipping; F4 and F6 sub-tasks above. *** 2026-05-15 Fri @ 19:16:08 -0500 Specification 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-*.el; python: tests/test_.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 ". 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 ...) 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] Fix up test runner :PROPERTIES: :LAST_REVIEWED: 2026-05-22 :END: *** 2026-05-16 Sat @ 11:15:51 -0500 Ideas **** Current State =modules/test-runner.el= is a solid first pass for an Emacs-config-specific ERT workflow: - project-scoped focus lists - run all vs focused mode - run ERT test at point - load all test files - clear ERT tests from other project roots - keybindings under =C-; t= The universal test-running direction is currently split across modules: - =test-runner.el= owns ERT focus/state/UI. - =dev-fkeys.el= owns F6 language detection and command generation for Elisp, Python, Go, and partial TypeScript. That split is the biggest architectural pressure point. The test runner should eventually own runner discovery, scopes, command construction, result handling, and UI. F6 should become a thin entry point into the runner. **** Critical Design Issues ***** Too ERT-specific at the core The current state model is named generically, but most operations assume: - test files live in =test/= or =tests/= - files match =test-*.el= - tests are ERT forms - individual tests can be selected by ERT selector regex - loading tests into the current Emacs process is acceptable This makes the module hard to extend cleanly to pytest, Jest, Vitest, Go, Rust, or shell test runners. The common abstraction should be "test run request" and "test runner adapter", not "ERT file list". ***** In-process ERT causes state contamination =cj/test-load-all= and focused runs load test files into the current Emacs session. This is fast and ergonomic, but it can leak: - global variables - advice - loaded features - overridden functions - ERT test definitions - load-path mutations The runner should support two ERT execution modes: - =interactive= / in-process for fast local TDD - =isolated= / batch Emacs for reliable verification The isolated path should be preferred for "before commit", CI parity, and agent-driven verification. ***** Test discovery is regex-based and fragile =cj/test--extract-test-names= scans files with a regex for =ert-deftest=. That misses or mishandles: - macro-generated tests - commented forms in unusual shapes - multiline or reader-conditional forms - non-ERT Elisp tests such as Buttercup - stale ERT tests already loaded in the session Better approach: - for ERT in isolated mode, let ERT discover tests after loading files - for source navigation, use syntax-aware forms where possible - store discovered tests as structured records with file, line, name, framework, tags, and runner ***** Path containment has at least one suspicious edge =cj/test--do-focus-add-file= checks: #+begin_src elisp (string-prefix-p (file-truename testdir) (file-truename filepath)) #+end_src That should use =cj/test--file-in-directory-p= or ensure the directory has a trailing slash. Otherwise sibling paths with a shared prefix are a recurring class of bug. ***** Runner commands are shell strings too early =cj/--f6-test-runner-cmd-for= returns shell command strings. That makes it harder to: - inspect command parts - safely quote arguments - offer command editing - run via =make-process= / =compilation-start= without shell ambiguity - attach metadata - rerun exact invocations - convert commands into UI labels Prefer a structured command object: #+begin_src elisp (:program "pytest" :args ("tests/test_foo.py" "-q") :default-directory "/project/" :env (("PYTHONPATH" . "...")) :runner pytest :scope file) #+end_src Render to a shell string only at the final compilation boundary. ***** F6 and =C-; t= workflows duplicate the same domain F6 already handles "all tests" and "current file's tests" for multiple languages. =C-; t= handles ERT-only focus and run state. These should converge on one runner service: - F6: quick entry point - =C-; t=: full runner menu - both call the same scope/adapter engine ***** Test directory discovery is too narrow Current discovery prefers =test/= then =tests/=, with a global fallback. Real projects often need: - Python: =tests/=, package-local =test_*.py=, =pytest.ini=, =pyproject.toml= - JS/TS: =package.json= scripts, =vitest.config.*=, =jest.config.*=, =*.test.ts=, =*.spec.ts= - Go: package directories, =go.mod= - Rust: =Cargo.toml=, integration tests under =tests/= - Elisp packages: =Makefile=, =Eask=, =ert-runner=, Buttercup, =tests/= Discovery should be adapter-specific and project-config-aware. ***** No structured result model =cj/test-last-results= exists but is not meaningfully populated. A powerful runner needs a normalized result model: - run id - started/finished timestamps - status: passed/failed/errored/cancelled/skipped/xfail/xpass - command - runner adapter - scope - exit code - duration - failed test records - file/line locations - raw output buffer - coverage artifact paths This enables last-failed, failures-first, summaries, dashboards, and AI-assisted failure explanation. ***** No failure parser / navigation layer Compilation buffers are useful, but the runner should parse common failure formats and provide: - next/previous failure - jump to source line - failure summary buffer - copy failure context - rerun failed test at point - annotate failing tests in source buffers Adapters can provide regexes/parsers for ERT, pytest, Jest/Vitest, Go, Rust, and shell. ***** Missing watch/rerun modes Modern test runners optimize the feedback loop: - pytest supports selecting tests, markers, last-failed, failures-first, stepwise, fixtures, xfail/skip, plugins, and cache state. - Jest/Vitest support watch workflows, changed-file selection, coverage, snapshots, and rich interactive filtering. Vitest also defaults to watch in development and run mode in CI. - Go and Rust runners commonly support package-level runs, regex selection, race/coverage flags, and cached test behavior. The Emacs runner should expose the subset that maps well to editor workflows: - current test - current file - related test file - focused set - last failed - failed first - changed since git base - watch current scope - full project - coverage for current scope **** Proposed Architecture ***** Core Types Use plain plists initially; promote to =cl-defstruct= only if helpful. #+begin_src elisp ;; Test runner adapter (:id pytest :name "pytest" :languages (python) :detect cj/test-pytest-detect :discover cj/test-pytest-discover :build-command cj/test-pytest-build-command :parse-results cj/test-pytest-parse-results :capabilities (:current-test :file :project :last-failed :coverage :watch)) ;; Test run request (:project-root "/repo/" :language python :framework pytest :scope file :file "/repo/tests/test_api.py" :test-name "test_create_user" :extra-args ("-q") :profile default) ;; Test run result (:run-id "..." :status failed :exit-code 1 :duration 2.14 :failures (...) :output-buffer "*test pytest*" :artifacts (...)) #+end_src ***** Adapter Registry Create a registry like: #+begin_src elisp (defvar cj/test-runner-adapters nil) (cj/test-register-adapter 'pytest ...) (cj/test-register-adapter 'ert ...) (cj/test-register-adapter 'vitest ...) #+end_src Runner selection should consider: - buffer file extension - project files - explicit user override - available executables - package manager scripts - existing Makefile targets ***** Scope Model Make scopes explicit and shared across languages: - =test-at-point= - =current-file= - =related-file= - =focused-files= - =last-failed= - =changed= - =package/module= - =project= - =coverage= - =watch= Each adapter can say which scopes it supports. Unsupported scopes should produce clear user-errors with suggestions. ***** Command Builder Pipeline 1. Detect project. 2. Detect language/framework candidates. 3. Resolve user-requested scope. 4. Build structured command object. 5. Optionally let user edit command. 6. Run via =compilation-start= or =make-process=. 7. Parse output/result artifacts. 8. Store normalized result. 9. Update UI/modeline/messages/failure buffer. ***** Keep Makefile Support But Do Not Require It For this Emacs config, =make test-file= and =make test-name= are useful and should remain the default Elisp isolated path. But adapter detection should support: - direct =emacs --batch= ERT invocation - =make test= - =make test-file= - =make test-name= - Eask - Buttercup **** Elisp-Specific Improvements ***** Add isolated ERT runs Support batch commands for: - all project tests - one test file - one test name - focused files - last failed, once result parsing exists Use the same Makefile targets in this repo, but design the adapter so other Elisp projects can run without this Makefile. ***** Support Buttercup/Eask Later Buttercup uses BDD-style =describe= / =it= suites and is common in Elisp package testing. Eask is often used to run package tests. Add adapter slots for these instead of hard-coding ERT forever. ***** Avoid unnecessary global ERT deletion =cj/ert-clear-tests= is a pragmatic fix for project contamination, but the stronger long-term answer is isolated runs plus project-scoped discovery. Keep the cleanup command, but do not make correctness depend on deleting global ERT state. **** Python / pytest Ideas - Detect pytest by =pyproject.toml=, =pytest.ini=, =tox.ini=, =setup.cfg=, or presence of =tests/=. - Build commands for: - project: =pytest= - file: =pytest path/to/test_file.py= - test at point: =pytest path/to/test_file.py::test_name= - class method: =pytest path::TestClass::test_method= - marker: =pytest -m marker= - last failed: =pytest --lf= - failed first: =pytest --ff= - stop after first: =pytest -x= - coverage: =pytest --cov=...= - Parse output for failing node ids and file:line references. - Read pytest cache for last-failed where useful. - Offer marker completion by parsing =pytest --markers= or config files. - Surface xfail/skip separately from hard failures. **** TypeScript / JavaScript Ideas ***** Detection Detect runner by project files and scripts: - =vitest.config.ts/js/mts/mjs= - =jest.config.ts/js/mjs/cjs= - =package.json= scripts: =test=, =test:watch=, =vitest=, =jest= - lockfile/package manager: =pnpm-lock.yaml=, =yarn.lock=, =package-lock.json=, =bun.lockb= Prefer project scripts over raw =npx= when present: - =pnpm test -- path= - =npm test -- path= - =yarn test path= - =bun test path= ***** Scopes - current file: =vitest run path= or =jest path= - test at point: use nearest =it= / =test= / =describe= string and pass =-t= - watch current file - changed tests where runner supports it - coverage current file/project - update snapshots ***** Result Parsing Parse: - failing test names - file paths and line numbers - snapshot failures - coverage summary Treat snapshot updates as an explicit command, not an automatic side effect. **** Go Ideas - Detect =go.mod=. - Current file/source: run package =go test ./pkg=. - Test at point: nearest =func TestXxx= and run =go test ./pkg -run '^TestXxx$'=. - Bench at point: nearest =BenchmarkXxx= and run =go test -bench '^BenchmarkXxx$'=. - Add toggles for =-race=, =-cover=, =-count=1=, =-v=. - Parse =file.go:line:= output and package failure summaries. **** Rust Ideas - Detect =Cargo.toml=. - Use =cargo test= by default, optionally =cargo nextest run= when available. - Current test at point: nearest =#[test]= function. - Current file/module where possible. - Integration test file: =cargo test --test name=. - Support =-- --nocapture= toggle. - Parse compiler/test failures and file:line links. **** Shell / Generic Ideas - Adapter for Makefile targets: - detect =make test=, =make check=, =make coverage= - expose project-level commands even when language-specific detection fails - Adapter for arbitrary project command configured in dir-locals or a project config plist. - Let users register custom command templates per project: #+begin_src elisp ((:name "unit" :command ("npm" "run" "test:unit" "--" "{file}")) (:name "integration" :command ("pytest" "tests/integration" "-q"))) #+end_src **** UI Ideas ***** Transient Menu Replace or complement the raw keymap with a =transient= menu: - scope: current test/file/focused/last failed/project - runner: auto/ert/pytest/vitest/jest/go/cargo/make - toggles: watch, coverage, debug, fail-fast, verbose, update snapshots - actions: run, rerun, edit command, show failures, open report ***** Result Buffer Create a normalized =*Test Results*= buffer: - latest status per project - command and duration - pass/fail/skip counts - failure list with clickable file:line - actions to rerun failed/current/all - links to coverage artifacts ***** Modeline / Headerline Signal Show the last run status for the current project: - green passed - red failed - yellow running - gray no run Keep it quiet and optional. ***** History Store recent run requests per project: - rerun last - rerun last failed - choose previous command - compare duration/status against previous run **** Configuration Ideas - =cj/test-runner-default-scope= - =cj/test-runner-prefer-isolated-elisp= - =cj/test-runner-project-overrides= - =cj/test-runner-known-adapters= - =cj/test-runner-enable-watch= - =cj/test-runner-result-retention= - per-project override through =.dir-locals.el= Example: #+begin_src elisp ((nil . ((cj/test-runner-project-overrides . (:adapter pytest :default-args ("-q") :coverage-args ("--cov=src")))))) #+end_src **** Safety And Robustness - Use structured commands until the final boundary. - Quote only at render time. - Avoid shell when =make-process= / =process-file= is sufficient. - Keep command preview/editing available for surprising cases. - Detect missing executables before running. - Add timeouts/cancel commands for long-running or hung tests. - Do not silently fall back from a missing runner to a different runner unless the fallback is visible in the command preview. - Avoid mutating global =load-path= permanently. - Keep remote/TRAMP behavior explicit; do not accidentally run local commands for remote projects. **** Coverage Integration Tie this into the existing coverage work: - run coverage for current file/scope - open latest coverage report - summarize uncovered lines for current file - support Elisp SimpleCov/Undercover, pytest-cov, Vitest coverage, Go cover, and Rust coverage later - store coverage artifact paths in the normalized run result **** AI-Assisted Debugging Ideas - Summarize failing tests from the parsed failure records and raw output. - Include command, changed files, failure snippets, and relevant source/test locations. - Redact env vars, tokens, Authorization headers, and secrets before sending to =gptel=. - Add commands: - =cj/test-runner-explain-failure= - =cj/test-runner-suggest-related-tests= - =cj/test-runner-summarize-coverage-gap= **** Migration Plan ***** Phase 1: Internal cleanup - Fix the task typo and rename current ERT-specific functions or wrap them under an ERT adapter. - Move F6 language detection/command construction from =dev-fkeys.el= into =test-runner.el= or a new =test-runner-core.el=. - Replace shell-string command builders with structured command plists. - Fix path containment in =cj/test--do-focus-add-file=. - Make =cj/test-last-results= real for ERT runs. ***** Phase 2: ERT adapter - Implement adapter registry. - Add ERT adapter with in-process and isolated modes. - Preserve all current keybindings by routing them through the adapter. - Add failure/result normalization for ERT. - Add "rerun last" and "rerun failed" for ERT. ***** Phase 3: Python and JS/TS adapters - Add pytest adapter. - Add Vitest/Jest adapter with package-manager/script detection. - Support current file and test-at-point for both. - Add parser/navigation for common failures. ***** Phase 4: UI and watch modes - Add transient menu. - Add result buffer. - Add cancellation and rerun history. - Add watch commands where supported. ***** Phase 5: Coverage and AI - Connect coverage commands to adapter capabilities. - Add failure summarization with redaction. - Add coverage-gap summarization. **** Acceptance Criteria For First Fix-Up Pass - Existing ERT workflow still works. - F6 and =C-; t= use the same underlying runner API. - Current-file test command generation is covered for Elisp, Python, Go, TypeScript, and JavaScript. - At least one isolated ERT command path exists. - Path containment checks are robust against sibling-prefix paths and symlinks. - Runner requests and results are represented as data, not only messages. - Missing runner/tool errors are clear and actionable. - Tests cover adapter detection, command building, scope resolution, result storage, and key interactive paths. ** DOING [#B] Module-by-module hardening :harden:no-sync: :PROPERTIES: :LAST_REVIEWED: 2026-05-22 :END: 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. Re-review pass 2026-05-15: - Each of the six existing review tracks (foundation, custom editing, UI / navigation, Org workflow, programming workflow, integrations and applications) was re-walked as if it had not been reviewed before. - 32 new sub-task findings filed across the tracks above (foundation 5, custom editing 6, UI / navigation 9, Org workflow 3, programming 6, integrations 2). Findings already covered by an existing sub-task were dropped during consolidation. - A separate =Review newly added modules= task lists the 24 modules that were either added after the parent task was written (post-2026-04) or fell outside the original scope lists. Each is routed to its target track; module-specific findings are filed under the relevant track. 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. *** DOING [#B] Harden foundation modules :harden: 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. Completion review 2026-05-15: - Re-read the parent =Module-by-module review and hardening= context and the adjacent architecture follow-up so this review stays module-specific. - Re-checked all scoped files against the review protocol. Existing child tasks below still cover the actionable module findings for =user-constants.el=, =host-environment.el=, =system-defaults.el=, and =early-init.el=. - =system-lib.el=, =keybindings.el=, =config-utilities.el=, and =init.el= do not need additional module-specific child tasks from this pass; remaining concerns are already tracked by the utility-consolidation, keymap registration, debug-profiling, and 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. **** 2026-05-23 Sat @ 03:33:30 -0500 Fixed env-desktop-p doc and normalized the X predicates Corrected =env-desktop-p='s docstring (it described a laptop; the function returns t for the desktop/no-battery case). Switched =env-x-p= from =(string= (window-system) "x")= to =(eq (window-system) 'x)= to match =env-x11-p='s style, and documented the difference: =env-x-p= is any X display incl. XWayland, =env-x11-p= is a real X11 session with no WAYLAND_DISPLAY. Behavior unchanged, existing display-predicate tests stay green. Fixed in 14ec32b2. Left =cj/match-localtime-to-zoneinfo= caching alone — it was a "consider if this runs during startup" note, not an acceptance item, and it doesn't run at startup. File a separate task if it ever shows up in a profile. **** TODO [#B] Add minimal =system-defaults.el= setting smoke tests :tests:solo: =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 [#B] 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. **** 2026-05-16 Sat @ 02:34:22 -0500 Consolidated user-home-dir into early-init as canonical Canonical defconst in =early-init.el= kept (the package-archive paths need it during package bootstrap, before normal modules load). =modules/user-constants.el= switched to a `defvar` with the identical =(getenv "HOME")= expression and a comment explaining the pattern: defvar is a no-op at runtime (early-init's defconst wins, defvar doesn't reassign a bound symbol), but it lets the module load / byte-compile standalone when early-init hasn't run. Drift risk is mitigated by both expressions being =(getenv "HOME")= literally; the comment flags the requirement to keep them identical. **** 2026-05-16 Sat @ 02:34:22 -0500 Dropped redundant autoload alongside compile-time require in system-defaults.el Kept the =eval-when-compile= requires for =host-environment= and =user-constants= (they silence free-variable / free-function warnings during byte-compile in isolation) and dropped the =(autoload 'env-bsd-p ...)= line — both modules are loaded earlier in init.el at runtime, and the eval-when-compile already exposes =env-bsd-p= to the byte-compiler. Added a comment documenting the chosen boundary. **** 2026-05-16 Sat @ 02:34:22 -0500 Converted cj/debug-modules and cj/use-online-repos to defcustom Both toggles now live as =defcustom= with explicit =:type= and =:group 'cj=. =cj/debug-modules='s type is the natural choice form: either =t= (all modules) or a list of module symbols. =cj/use-online-repos='s type is boolean. Added a top-level =(defgroup cj ...)= in early-init.el so the group exists for both, plus the package-priority constants below it. **** TODO [#B] Surface custom-file redirection so accidental Customize use isn't silent :safety: =modules/system-defaults.el:91-92= sends Customize UI writes to a temp file (=emacs-customizations-trashbin-...=) that is never read back. This is intentional -- the convention is to manage config in Elisp -- but it silently discards user edits made through =M-x customize=. A user who occasionally clicks "Save for Future Sessions" in a Customize buffer loses those changes on Emacs exit. Either surface a =display-warning= on first =custom-set-variables= attempt, or set =custom-file= to a versioned path under =data/= so the discard is at least durable for the session. **** 2026-05-16 Sat @ 02:34:22 -0500 Named the package archive priorities in early-init.el Nine =defconst= entries replace the magic numbers: =cj/package-priority-localrepo= (200) for the project-pinned repo, four =cj/package-priority-mirror-*= entries for the local ELPA mirrors (125 / 120 / 115 / 100), four =cj/package-priority-online-*= entries for the online archives (25 / 20 / 15 / 5). A header comment above the block explains the local-first ordering and the gnu > nongnu > melpa > melpa-stable trust ranking within each tier. **** 2026-05-16 Sat @ 02:34:22 -0500 Deleted dead world-clock block in chrono-tools.el The 19-line commented-out =use-package time= block is gone. The =time-zones= use-package directly above it is the active replacement; git history preserves the old config if anyone needs to dig it back up. **** 2026-05-16 Sat @ 02:34:22 -0500 Added coverage for cj/tmr-select-sound-file with a pre-test refactor The prefix-arg branch now delegates to =cj/tmr-reset-sound-to-default= directly (single source for the reset path). Extracted a pure helper =cj/tmr--available-sound-files= so the directory scan is testable without driving =completing-read=. =tests/test-chrono-tools-tmr-sound.el= covers Normal / Boundary / Error: available-sounds against a populated dir, empty dir, missing dir; reset path; select with prefix-arg (delegates to reset, no prompt); select normal (picks a file); select boundary paths for empty dir, missing dir, cancel (empty completion). 9 tests, all green. *** DOING [#B] Harden custom editing utility modules :harden: 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. Completion review 2026-05-15: - Re-checked the scoped custom editing utility modules and their test files. - The pure editing modules remain well covered by focused ERT tests. - Remaining actionable issues are already logged below: process-launch hardening and coverage for =external-open.el= / =media-utils.el=, destructive buffer/file keybinding policy, and explicit cross-module autoload/require boundaries. **** 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. **** 2026-05-23 Sat @ 03:41:00 -0500 Added media-utils coverage; external-open already covered =external-open= already had three test files (=test-external-open-commands.el=, =test-external-open-lib-command.el=, =test-external-open-lib-launcher-p.el=). =media-utils.el= had none, so I added =test-media-utils.el= (8 cases): player availability from =cj/media-players=, the play command-builder (direct vs yt-dlp -g stream wrap), and the missing-tool error paths for the player, =yt-dlp=, and =tsp=. All process/exec boundaries mocked. Added in the test-media-utils commit. **** TODO [#B] 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 [#B] Add explicit autoloads/requires for cross-module command keybindings :cleanup:refactor:solo: 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 [#C] Reconcile region-or-buffer scope across editing helpers :bug:solo: =modules/custom-text-enclose.el:135-180= helpers (=cj/append-to-lines-in-region-or-buffer=, =cj/prepend-to-lines-in-region-or-buffer=) fall back to the whole buffer when no region is active. =modules/custom-ordering.el:38-41= and =:86-88= helpers (=cj/--arrayify=, =cj/--unarrayify=) accept explicit =(start end)= parameters but their docstrings imply "region or entire buffer" -- the implementation does not match. Pick one contract per pair and update docstrings, or extract a shared helper that decides the target range. **** 2026-05-16 Sat @ 02:47:15 -0500 Preserved trailing newlines in custom-ordering output Both =cj/--arrayify= and =cj/--unarrayify= now detect a trailing newline on the input region (via =string-suffix-p=) and re-append it to the result. Matches the pattern =custom-text-enclose.el= already uses. Docstrings updated to document the contract. **** 2026-05-16 Sat @ 02:47:15 -0500 Guarded cj/duplicate-line-or-region against modes without comment syntax =cj/duplicate-line-or-region= now signals a clear =user-error= when =COMMENT= is non-nil but the current mode has no =comment-start= (=fundamental-mode= and similar). Docstring updated to document the error. Picks the "error out clearly" branch from the task body -- restricting the binding per-mode would be a larger refactor that isn't justified for the silent-malformed-output blast radius. **** 2026-05-16 Sat @ 02:47:15 -0500 Made external-open advice install explicit via cj/external-open-install-advice Factored the =advice-remove= / =advice-add= pair into =cj/external-open-install-advice= and called it once at the bottom of the module. Same idempotent shape (remove-then-add) but now the intent is named. Re-running the module updates the advice rather than stacking it; if a future caller wants to opt out, they can =advice-remove= the helper or skip calling it altogether. **** 2026-05-16 Sat @ 02:47:15 -0500 Added cj/--validate-decoration-char across all six divider/border helpers New top-level validator =cj/--validate-decoration-char= rejects anything that isn't a printable single-character string and signals =user-error=. Wired into all six internal helpers: =cj/--comment-inline-border=, =cj/--comment-simple-divider=, =cj/--comment-padded-divider=, =cj/--comment-box=, =cj/--comment-heavy-box=, =cj/--comment-block-banner=. The five nil-decoration ERT tests updated from =:type 'wrong-type-argument= (the old crash signal from =string-to-char= on nil) to =:type 'user-error=, since the guard now produces a clear message instead of a deep crash. **** 2026-05-23 Sat @ 03:38:30 -0500 Filled the remaining title-case edge gaps =test-custom-case-title-case-region.el= already had 29 cases (empty region, unicode words, numbers, separators, colon resets, partial region). The named gaps that were missing — leading-quote and leading-paren handling, plus a caseless RTL first word — are now covered by three boundary tests (32 total). Added in 3841c59e. **** 2026-05-16 Sat @ 02:47:15 -0500 Extracted cj/--require-spell-checker Both =cj/flyspell-toggle= and =cj/flyspell-then-abbrev= now call =cj/--require-spell-checker= instead of carrying their own copy of the executable-find check. The checker list lives in the new defconst =cj/--spell-checker-executables=, so adding nuspell (or any other checker) is a one-line edit. Top-level =(require 'cl-lib)= added since the new helper uses =cl-some=. **** 2026-05-23 Sat @ 03:44:50 -0500 Covered flyspell-and-abbrev testable seams Added =test-flyspell-and-abbrev.el= (8 cases): =cj/--require-spell-checker= (PATH gate, mocked), =cj/find-previous-flyspell-overlay= against synthetic overlays (closest-previous match, non-flyspell skipped, nil when none), and =cj/flyspell-on-for-buffer-type= (prog-mode -> flyspell-prog-mode, text-mode -> flyspell-mode). Left =cj/flyspell-then-abbrev= to manual testing — pinning its flyspell-UI orchestration would mean mocking flyspell internals rather than our logic. Added in the test-flyspell-abbrev commit. *** DOING [#B] Harden UI and navigation modules :harden: 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. Completion review 2026-05-15: - Re-checked the scoped UI/navigation modules and current tests. - =mousetrap-mode.el=, =ui-navigation.el=, =ui-theme.el=, =selection-framework.el=, and selected modeline/UI helpers now have focused tests, but the font/modeline/popper runtime policy remains under-tested. - Existing cleanup below covers the disabled =popper-config.el= load-graph issue; added a separate test-gap task for the remaining UI smoke coverage. **** TODO [#B] Add UI/navigation runtime smoke coverage :tests:solo: Several UI modules are mostly top-level runtime configuration and currently have only partial helper coverage. The highest-value missing assertions are: - =font-config.el=: font fallback/daemon frame setup does not error when optional fonts or emoji packages are absent. - =modeline-config.el=: runtime segment assembly handles missing VC/project data and does not signal in non-file buffers. - =popper-config.el=: if the module remains in =init.el= while disabled, a smoke test should prove requiring it is an intentional no-op. Keep these tests batch-safe by stubbing frame/font/package functions rather than depending on a graphical session. **** TODO [#B] 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. **** 2026-05-16 Sat @ 02:55:14 -0500 Moved popper-mode activation from :init to :config =popper-mode +1= and =popper-echo-mode +1= now live in the =:config= block of =modules/popper-config.el='s use-package form. =:disabled t= now actually disables the mode (=:config= is skipped when disabled; =:init= still runs but it only sets the reference- buffer list and the display-buffer-alist entry, both of which are harmless no-ops when popper itself never loads). Comment in the module explains the split. **** 2026-05-16 Sat @ 02:55:14 -0500 Made cj/modeline-vc-fetch fall back when vc-git--symbolic-ref is missing The =require 'vc-git= now uses =nil 'noerror=, and the call to =vc-git--symbolic-ref= is gated on =(fboundp ...)= so an Emacs version that renames or removes the internal accessor just leaves =branch= at =vc-working-revision='s output instead of crashing the modeline render. Added =ignore-errors= around the call too in case the internal accessor signals on unusual inputs. **** TODO [#C] Use theme-aware faces in =cj/display-available-fonts= :refactor: =modules/font-config.el:266= hardcodes ="Light Blue"= and gray foreground for font labels. Switching themes (especially light themes) makes the labels nearly unreadable. Replace the literal color with a face reference (=font-lock-keyword-face= or a face this config owns) so the labels follow theme contrast. **** TODO [#C] Handle TTY-first frame race in font setup :safety: =modules/font-config.el:168-176= checks =(env-gui-p)= once at module load. In daemon mode, when the first =emacsclient -t= creates a TTY frame, the check returns nil and font setup never runs. A later =emacsclient -c= creating a GUI frame inherits no font configuration. Either move the GUI check inside =server-after-make-frame-hook= (per-frame), or invoke font setup unconditionally and let Emacs handle terminal frames gracefully. **** TODO [#C] Cache =mousetrap-mode= keymap rebuilds per profile :performance:solo: =modules/mousetrap-mode.el:231-233= registers =mouse-trap-maybe-enable= on every major-mode hook (text, prog, special, plus custom profile modes). Each mode switch rebuilds the keymap from scratch (~8 prefixes × ~30 events). Rapid mode-switching workflows (project switching, multi-buffer review) pay a measurable cost. Cache by profile + active-events list and skip rebuild when the cache key matches. **** TODO [#C] Invalidate VC modeline cache on file symlink target changes :tests:solo: =modules/modeline-config.el:131-140= keys the cache on =(list file cj/modeline-vc-show-remote)=. If =file= is a symlink whose target moves (rare but possible on shared drives, CI workspaces), the cache stays warm with the old VC backend. Add the resolved =file-truename= to the key, or invalidate the cache when =vc-backend= disagrees with the cached entry. **** TODO [#B] Move =C-s= binding into =consult= =:bind= for =isearch-mode-map= :safety:solo: =modules/selection-framework.el:253= binds =C-s= globally to =cj/consult-line-or-repeat=. The same module's =consult= =:bind= block rebinds =M-s e= in =isearch-mode-map=. Once isearch is active, pressing =C-s= should advance to the next match -- isearch's own keymap convention -- but the global binding shadows that and exits isearch into consult-line. Move the =C-s= binding into the consult =:bind= block under =:map isearch-mode-map= to preserve isearch's in-mode contract. **** 2026-05-16 Sat @ 02:55:14 -0500 Guarded cursor-color hook behind display-graphic-p (with daemon-mode catch) Both the install (=add-hook= on =post-command-hook=) and the function body now gate on =(display-graphic-p)=. Batch and TTY runs short- circuit cleanly: no per-command overhead, no =set-cursor-color= calls on frames that don't have a cursor color. A =server-after-make-frame-hook= catches the daemon case where the first GUI frame is created after =ui-config= loads -- it installs the hook lazily the first time a GUI frame appears. The two cursor-color test files (=test-ui-config--buffer-cursor-state.el=, =test-ui-cursor-color-integration.el=) stub =display-graphic-p= to return t so the work body still runs in batch. **** 2026-05-16 Sat @ 02:55:14 -0500 Deferred nerd-icons by dropping :demand t plus an after-load safety net =(use-package nerd-icons :demand t ...)= flipped to =:defer t=. The =:config= block already wraps the advice + tint in lazy-on-load semantics, so the advice now installs the first time nerd-icons loads (typically when a feature module like =dashboard-icon-type= or =dirvish-attributes= triggers a load). An additional =(with-eval-after-load 'nerd-icons ...)= block at module bottom catches the "already-loaded when this module re-evaluates" case -- it checks =advice-member-p= so it doesn't stack the advice on every re-eval. *** DOING [#B] Harden Org workflow modules :harden: 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. Completion review 2026-05-15: - Re-checked the scoped Org workflow modules and their test coverage. - The broad parser/cache/helper areas now have useful focused tests, especially =calendar-sync.el=, agenda/refile helpers, org-roam helpers, org-noter, Hugo, reveal, and webclipper processing. - Remaining issues are already logged below, including security-sensitive calendar config and Babel evaluation policy, cache lifecycle/timer behavior, org-roam destructive workflow guardrails, executable checks, capture-template smoke tests, and Org workflow ownership documentation. **** 2026-05-23 Sat @ 04:18:44 -0500 Split personal calendar config out of calendar-sync.el =calendar-sync.el= now defaults =calendar-sync-calendars= to nil and loads the real plists from =calendar-sync-private-config-file= (an ignored file), so the engine carries no private feed tokens in source. =calendar-sync-status= / =calendar-sync-start= report missing config without erroring, and agenda startup is unaffected (tests/test-calendar-sync-no-config-startup.el). Rotating the previously-committed feed URLs remains a manual credential action — tracked under the L2557 calendar-sync hardening finding. **** PROJECT [#B] Normalize Org agenda/refile cache lifecycle :perf:refactor: Two of three children are done (shared cache helper extracted, idle timers gated). Still open: the directory-scan-failure visibility child below. ***** 2026-05-23 Sat @ 04:18:44 -0500 Extracted a shared cache helper =cj-cache-lib.el= now provides =cj/cache-valid-p=, =cj/cache-building-p=, and =cj/cache-value-or-rebuild=, consumed by both =org-agenda-config.el= and =org-refile-config.el=; the contract is documented in =docs/design/cache-helper-design.org=. The agenda and refile public commands are unchanged. ***** TODO [#B] Make directory scan failures visible but non-fatal :solo: =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. ***** 2026-05-23 Sat @ 04:18:44 -0500 Gated cache idle timers out of batch Both cache builders' =run-with-idle-timer= calls are wrapped in =(unless noninteractive)= (=org-refile-config.el:105=, =org-agenda-config.el:203=), so requiring the modules in batch schedules nothing. =tests/test-architecture-startup-contracts.el= scans every module and asserts there are no unguarded top-level timer forms. **** 2026-05-23 Sat @ 19:48:00 -0500 Made org-confirm-babel-evaluate default to t with a toggle =org-babel-config.el= set =org-confirm-babel-evaluate= to nil globally, so every source block in every Org file (cloned repos, downloaded notes, web clips) ran without confirmation. Changed the default to =t= (confirm before running). Replaced the old =babel-confirm= command (which reported, and toggled only with a prefix arg) with =cj/org-babel-toggle-confirm=, a plain toggle bound to =C-; k= for flipping confirmation off in trusted files and back on. 3 ERT tests cover the toggle both directions plus the binding. **** TODO [#C] Rebind babel-confirm toggle off =C-; k= :keybinding:solo: =cj/org-babel-toggle-confirm= landed on =C-; k= as a placeholder. Pick a permanent home — likely under an Org-specific prefix rather than the global =C-;= map. Triggered by: 2026-05-23 org-confirm-babel-evaluate hardening. **** TODO [#B] Add guardrails to =cj/move-org-branch-to-roam= :ux:solo: =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 [#B] 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 [#B] Review external executable assumptions in Org export/publishing modules :cleanup:solo: =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. **** 2026-05-16 Sat @ 03:44:45 -0500 Guarded org-roam completed-task hook with cj/--org-roam-should-copy-completed-task-p =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:solo: =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 [#B] Clarify contradictory Org export task defaults :cleanup:tests:solo: =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. **** 2026-05-23 Sat @ 03:48:50 -0500 Fixed java structure-template typo and pinned the aliases =("java" . "src javas")= expanded to a bogus =#+begin_src javas=; corrected it to =src java=. Added =test-org-babel-config-structure-templates.el=, which requires the module then org-tempo (firing the deferred :config) and asserts =bash=, =zsh=, =el=, =py=, =json=, =yaml=, =java= each map to the intended src language. Fixed in the org-babel commit. **** TODO [#B] 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 [#B] Add an Org workflow health check command :feature:ux:solo: 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 [#B] Add capture-template key collision and target smoke tests :tests:solo: 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 [#B] Document Org workflow module ownership and load boundaries :docs:refactor:solo: 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. **** 2026-05-16 Sat @ 03:44:45 -0500 Removed duplicate org-protocol-protocol-alist registration in cj/webclipper-ensure-initialized The =webclip= protocol handler is registered twice: =modules/org-webclipper.el:72-76= inside =cj/webclipper-ensure-initialized= (unconditional =add-to-list=) and =:207-214= inside a =with-eval-after-load 'org-protocol= block (=unless (assoc ...)= guard). =add-to-list= uses =equal= membership so the two are effectively idempotent, but maintaining two registration paths invites drift if the alist entry shape ever changes. Pick one site -- the =with-eval-after-load= block is the more robust location -- and remove the other. **** 2026-05-16 Sat @ 03:44:45 -0500 Validated :url and :title in cj/org-protocol-webclip before stashing =modules/org-webclipper.el:124-125= extracts =:url= and =:title= from the incoming protocol plist with no type or nil check. An unexpected plist shape silently sets the globals to nil, and downstream code fails inside the capture handler with confusing messages. Guard with =(unless (and (stringp url) (not (string-empty-p url))) (user-error ...))= before stashing. **** TODO [#C] Replace global mutation of =cj/webclip-current-url= / =title= with structured state :refactor:solo: =modules/org-webclipper.el:128-129,151-152= relies on two top-level variables (=cj/webclip-current-url=, =cj/webclip-current-title=) =setq='d by the protocol handler and read by the capture template. Concurrent or rapidly-fired protocol invocations interleave and corrupt each other's state. Pass the data through a plist on =org-capture-plist=, a per-invocation closure, or a queue, instead of global mutation. **** 2026-05-16 Sat @ 03:44:45 -0500 Declared cross-module free vars in mu4e-org-contacts-integration.el The module reads =contacts-file= (defined in =user-constants.el=) and calls =cj/get-all-contact-emails= (defined in =org-contacts-config.el=) without any forward declaration at the top of the file. Byte-compile in isolation warns about both as free variables / unknown functions. Add =(eval-when-compile (defvar contacts-file))= and =(declare-function cj/get-all-contact-emails "org-contacts-config")= near the existing requires so the compile is clean and the cross-module dependency is explicit at the top of the file. **** TODO [#C] Add coverage for =mu4e-org-contacts-integration.el= completion logic :tests:solo: No tests exist for =cj/org-contacts-completion-at-point=, =cj/mu4e-org-contacts-tab-complete=, =cj/mu4e-org-contacts-comma- complete=, or =cj/mu4e-org-contacts-insert-email=. The header-field detection (=mail-abbrev-in-expansion-header-p=) and the TAB-cycle branch are the highest-value coverage targets. Stub =cj/get-all-contact-emails= and run against a temp buffer in =mu4e-compose-mode= / =org-msg-edit-mode= where applicable. *** DOING [#B] Harden programming workflow modules :harden: 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. Completion review 2026-05-15: - Re-checked the scoped programming workflow modules and current tests. - =dev-fkeys.el=, coverage modules, formatter wiring, keyboard compatibility, and test-runner helpers have meaningful focused coverage. - Remaining issues are logged below: F4 project capability classification, LSP ownership and smoke coverage, tree-sitter auto-install policy, Git clone process handling, shell-script executable policy, and formatter process boundaries. **** TODO [#C] Add smoke coverage for lightweight programming modules with no direct tests :tests:solo: Several low-risk programming modules currently have little or no direct test surface, especially =prog-general.el=, =prog-lisp.el=, and =prog-training.el=. Most behavior is package/hook configuration, so this should stay narrow: - require the modules with package side effects stubbed, - assert key hooks/settings that this config owns, - ensure batch loading does not trigger external installs or downloads. Prioritize this after the LSP and tree-sitter policy tasks, because those changes will define the stable assertions. **** TODO [#B] 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 :solo: 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. Originally meant to coordinate with the [#A] Python tree-sitter predicate syntax issue. That one resolved upstream on 2026-05-14 (see =docs/python- treesit-predicate-mismatch.txt= RESOLVED footer), so this task no longer depends on it. **** TODO [#B] Harden git clone from clipboard in =vc-config.el= :robustness:refactor:solo: =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 [#B] 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 [#B] Review language formatter process boundaries :cleanup:solo: 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. **** 2026-05-16 Sat @ 03:54:56 -0500 Added cj/executable-find-or-warn checks for prettier and pyright at load time =modules/prog-webdev.el:34-40= declares =prettier-path= and =modules/prog-python.el:33-40= declares =pyright-path= as string literals. No validation at module load means a missing executable surfaces only at first use -- format-on-save fires, then errors mid-edit, then the user has to discover why. Wrap with =cj/executable-find-or-warn= (already in =system-lib.el=) at module load time so the missing dependency is reported up front. **** 2026-05-16 Sat @ 03:54:56 -0500 Documented keyboard-compat hook idempotence (already correct) =modules/keyboard-compat.el:169-174= adds the frame-setup hook unconditionally. If the module is required twice (e.g. via two =eval-after-load= chains in different load orders), the hook runs twice per new frame and installs duplicate =key-translation-map= entries. Wrap the =add-hook= in a guard, or use a named function and rely on =add-hook='s own duplicate-check. **** 2026-05-16 Sat @ 03:54:56 -0500 Wired F6 TypeScript clause to npx vitest|jest =modules/dev-fkeys.el:261-269= maps =tsx= to =typescript= in the language detection table. =modules/dev-fkeys.el:347-349= (=cj/--f6-test-runner-cmd-for=) has no clause for =typescript= -- the catch-all =(_)= returns nil, so F6 errors instead of routing to a real runner. Either add a =typescript= → =jest=/=vitest= clause or remove the =tsx= mapping until the runner side is implemented. **** 2026-05-16 Sat @ 03:54:56 -0500 Fixed prog-lsp eldoc-provider removal to act on the global hook =modules/prog-lsp.el:51-54,76= attaches =cj/lsp--remove-eldoc-provider= globally to =lsp-managed-mode-hook= but the removal it performs uses =(remove-hook ... t)= -- a buffer-local removal. The first LSP buffer activates the hook, which removes the provider for that buffer. Subsequent LSP buffers still inherit the global default because the hook itself never re-fires the buffer-local removal in their context. Either make the hook itself buffer-local-friendly (add it inside =lsp-managed-mode-hook= per-buffer) or remove from the global provider list once instead of per-buffer. **** 2026-05-16 Sat @ 03:54:56 -0500 Externalized LanguageTool script path via user-emacs-directory =modules/flycheck-config.el:69= hardcodes =~/.emacs.d/scripts/languagetool-flycheck= as the LanguageTool wrapper. Users running from a non-standard =user-emacs-directory= (or anyone auditing the module against a future package) get a broken checker. Replace with =(expand-file-name "scripts/languagetool-flycheck" user-emacs-directory)= or a defcustom. **** 2026-05-16 Sat @ 03:54:56 -0500 Replaced hardcoded Zathura viewer with executable-find candidate list =modules/latex-config.el:28= sets the LaTeX viewer to =zathura= unconditionally. macOS / Windows users and anyone who prefers a different PDF viewer lose document review without explanation. Resolve via =executable-find= over a candidate list (=zathura=, =evince=, =okular=, =Preview.app= via =open=, =SumatraPDF.exe=) and fall back to =pdf-tools=. **** 2026-05-15 Fri @ 18:49:24 -0500 Fixed abbrev-mode no-arg toggle in =cj/prose-helpers-on= Replaced the =(if (not (abbrev-mode)) (abbrev-mode))= shape with =(unless (bound-and-true-p abbrev-mode) (abbrev-mode 1))= and the same for =flycheck-mode= in =modules/flycheck-config.el:36-42=. Dropped "flyspell" from the docstring (the commented-out =cj/flyspell-on-for-buffer-type= line had been gone for a while; the docstring was lying) and removed the stale comment marker. Added =tests/test-flycheck-config-prose-helpers-on.el= -- 4 tests covering Normal (both modes off -> each enabled once with a positive arg) and Boundary (both on -> no-op; each mixed state -> only the off one enabled). The "both on -> no-op" assertion is the regression guard for the no-arg toggle shape: it would record a =(nil)= call list under the bug and a =()= call list under the fix. Test infra needed an explicit =(defvar abbrev-mode)= / =(defvar flycheck-mode)= at the top of the test file: with =lexical-binding: t= and flycheck loaded =:defer t=, =let= on the flycheck-mode symbol creates a lexical-only binding the production code's =bound-and-true-p= can't see; declaring both as special makes =let= dynamic and the test stable. Full suite: =make test= exits 0; 468 lines of output with =ALL UNIT TESTS PASSED= banner; no regressions. *** DOING [#B] Harden integrations and application modules :harden: 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. Completion review 2026-05-15: - Re-checked the scoped integration/application modules with risky-pattern and test-coverage searches. - Many integration modules now have focused tests, including AI config, restclient, mail helpers, Slack commands, Dirvish helpers, music, recording/transcription, system commands, browser/help/jumper/reconcile, and undead buffer helpers. - Remaining issues are already logged below, especially system command safety, REST key persistence, mail privacy/lifecycle policy, quick-video timers and temp state, shell-heavy dwim/recording command hardening, AI conversation persistence coverage, calendar operational behavior, Dirvish dependency/path hardening, EWW/Elfeed network helpers, and Slack which-key registration. **** TODO [#B] Make system restart/shutdown commands more defensive :safety:solo: =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. **** 2026-05-23 Sat @ 19:01:53 -0500 Removed SkyFi key-injection feature from restclient-config Resolved by removing the feature rather than hardening it. =cj/restclient-skyfi-buffer= opened =data/skyfi-api.rest= in a file-visiting buffer and rewrote the =:skyfi-key= line with the real key from authinfo, so an accidental save would persist the key to local disk (the file was gitignored and never tracked, so no repo/public-mirror exposure — local plaintext only). Deleted =cj/skyfi-api-key=, =cj/restclient--inject-skyfi-key=, =cj/restclient-skyfi-buffer=, the =C-; R s= binding, the two SkyFi test files, and the local =data/skyfi-api.rest= template. Generic restclient (=C-; R n=, =C-; R o=, restclient/restclient-jq) kept. **** 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. **** 2026-05-23 Sat @ 03:52:00 -0500 Set compose buffers to kill on exit, both composers First clarified the ownership (dd671f8c): the org-msg comment "always kill buffers on exit" was backwards — org-msg set =nil= (keep), which won over mu4e's =t= because org-msg-mode runs in every compose buffer. Craig then chose to kill compose buffers on exit, so I set the org-msg value to =t= as well (82978c79). Both mu4e and org-msg now kill the buffer on send/exit, so HTML drafts don't linger. **** TODO [#B] Remove automatic startup timers from =quick-video-capture.el= :startup:refactor:solo: =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:solo: 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. ***** 2026-05-23 Sat @ 19:11:30 -0500 Fixed async password temp-file lifetime in dwim-shell The four password commands (PDF protect/unprotect, remove-zip-encryption, create-encrypted-zip) deleted the password temp file in =unwind-protect= the instant the async command launched, so =qpdf=/=7z= could start after the file was gone. Extracted =cj/dwim-shell--run-with-password-file= + =cj/dwim-shell--password-cleanup-callback=: the temp file (mode 600) is now deleted from an =:on-completion= callback that fires after the process exits (success or failure), with the synchronous =unwind-protect= kept only as a pre-launch-failure backstop. Rewrote all four commands onto the helper. 5 ERT tests cover the cleanup callback (success/error/missing-file) and the runner (writes 600 file + defers cleanup; cleans up on launch failure). qpdf already passes the password via =--password-file= (out of argv); the 7z argv exposure is split into its own follow-up below. ***** TODO [#B] Keep 7z password out of the command line :security:solo: =cj/dwim-shell-commands-remove-zip-encryption= and =cj/dwim-shell-commands-create-encrypted-zip= pass the password to 7z as =-p"$(cat tempfile)"=, so it lands on 7z's argv and is briefly visible in the process list. qpdf avoids this via =--password-file=, but 7z has no password-file option. Triggered by: 2026-05-23 async password temp-file lifetime fix. Options to evaluate: - Feed the password to 7z another way (stdin is not supported for the password; investigate =7z='s newer options or a wrapper). - Switch the encrypted-archive commands to a tool that reads a password file (gpg-wrapped tar, or =zip= is worse not better). - Accept and document the brief exposure if no clean option exists (single-user workstation, short-lived process). ***** 2026-05-23 Sat @ 19:18:00 -0500 Quoted/validated user-controlled dwim-shell inputs Closed the four injection-quoting cases. git-clone-clipboard-url now validates the clipboard with =cj/dwim-shell--valid-git-url-p= and passes the URL via =shell-quote-argument= instead of the raw =<>= substitution. GPG recipient and the 7z archive name go through =shell-quote-argument= instead of hand-written single quotes. The ffmpeg thumbnail timestamp is validated with =cj/dwim-shell--valid-ffmpeg-timestamp-p= (digits/colons/dot only) before it reaches =-ss=. The sequential-rename prefix is validated filename-safe with =cj/dwim-shell--safe-rename-prefix-p=. 7 ERT tests cover the three validators (Normal/Boundary/Error); the two =shell-quote-argument= swaps trust the builtin. The fifth case — video concatenation's echo/tr/sed filelist — is a redesign rather than a quoting fix and is split out below. ***** 2026-05-23 Sat @ 19:58:00 -0500 Rebuilt video-concat filelist in Elisp =cj/dwim-shell-commands-concatenate-videos= built the ffmpeg concat list with =echo '<<*>>' | tr ' ' '\n' | sed 's/^/file /'=, which split on spaces and broke on quotes. Extracted =cj/dwim-shell--build-concat-filelist=, which renders each path as an escaped =file '...'= line (single quotes escaped as ='\''=), writes it to a temp file in Elisp, and runs =ffmpeg -f concat -i = with a trailing =; rm -f= to clean up after the process exits. =<<*>>= stays only as an inert trailing shell comment so dwim-shell still runs one command over all marked files. 3 ERT tests cover plain paths, spaces, and an embedded quote. ***** 2026-05-23 Sat @ 20:17:00 -0500 Clarified broad/misleading file-operation commands =remove-empty-directories= ran =find . -type d -empty -delete= from the ambient current directory. Now it prompts for an explicit root (via =read-directory-name=), names that root in the confirmation, and runs =find ...= built by =cj/dwim-shell--empty-dirs-command= (2 ERT tests cover the command shape + space quoting). =secure-delete= called =shred= without =-u=, overwriting file contents but leaving the file in place despite the name and the "permanently destroy" prompt; added =-u= (=shred -vfzu=) so it actually unlinks. Both use the inert =<<*>>= comment trick for single-execution. ***** TODO [#B] Quote X11 and audio recording command paths :bug:solo: =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 [#B] Ensure chosen recording directories are created directly :bug:solo: 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 [#B] 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. **** 2026-05-23 Sat @ 04:18:44 -0500 AI conversation persistence coverage already in place Premise was stale. =tests/test-ai-conversations.el= (47 cases) already covers slug generation, timestamp parsing, candidate sorting (newest/oldest), latest-file selection, save/load header stripping against a temp dir, autosave path/timer, and delete confirmation — with GPTel stubbed throughout. Acceptance list satisfied; no new file needed. **** 2026-05-23 Sat @ 03:31:12 -0500 Dirvish helper coverage already in place The task premise was stale: =dirvish-config.el= now has 14 focused test files (=test-dirvish-config-*.el=, ~100 cases) covering duplicate-naming, resolve-display-path (project/home/absolute/org-link), ediff-pair, html/printable predicates, playlist filtering/sanitizing, wallpaper-program mapping, and the public wrappers. The remaining gaps (playlist name-safety, set-wallpaper nil-file) were filled by the L2668 hardening commit 8fc6432d. No new file needed. **** 2026-05-23 Sat @ 03:21:12 -0500 Declared dirvish-config runtime deps with plain require Switched =user-constants= and =system-utils= from =eval-when-compile= to plain =require= in =dirvish-config.el=, matching the three sibling requires below. The module builds =dirvish-quick-access-entries= from =code-dir=/=music-dir=/=pix-dir= at load and binds keys to =cj/xdg-open=/=cj/open-file-with-command=, so the deps are genuine runtime inputs. Added =tests/test-dirvish-config-runtime-requires.el= as a dependency-contract smoke test. Fixed in b63c4f83. **** 2026-05-23 Sat @ 03:31:12 -0500 Hardened dirvish wallpaper + playlist path helpers =cj/set-wallpaper= passed =(dired-file-name-at-point)= straight into =expand-file-name=, so no-file-at-point raised a bare wrong-type-argument; added a nil guard that signals a clear user-error. =cj/dired-create-playlist-from-marked= expanded a raw name under =music-dir= with no check; added =cj/--playlist-name-safe-p= to reject any name carrying a directory separator (=../=, absolute, nested) before the path is built. Regression tests went into =test-dirvish-config-wrappers.el= and =test-dirvish-config-playlist.el=. The duplicate/copy-path helpers already guarded nil, so they were left alone. Fixed in 8fc6432d. **** 2026-05-23 Sat @ 03:38:30 -0500 Coverage already in place for mail + system-commands The task premise was stale. =mail-config.el= has =test-mail-config-helpers.el= (4), =test-mail-config-transport.el= (7), and =test-mail-config.el= (1) covering executable discovery and transport command assignment. =system-commands.el= has =test-system-commands-keymap.el= (2, keymap shape + candidates) and =test-system-commands-resolve-and-run.el= (13, confirmation routing + command-string construction with shell-command stubbed). Both acceptance lists are satisfied; no new tests needed. **** TODO [#B] Harden EWW/Elfeed synchronous network helpers :cleanup:refactor:solo: =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. **** 2026-05-16 Sat @ 04:00:00 -0500 Moved Slack which-key registration behind with-eval-after-load =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. **** 2026-05-16 Sat @ 04:00:00 -0500 Removed httpd-start side effect from markdown-preview =modules/markdown-config.el:37-51= starts =simple-httpd= inside an interactive command, then opens a browser at =http://localhost:8080/imp=. Starting a network listener as a side effect of a "preview" command surprises users; once started, the server keeps running until Emacs exits. Either gate the start behind an explicit confirmation, document the listener clearly, or move the server start into a separate =cj/markdown-preview-server-start= command so =markdown-preview= just opens the URL once the server is known to be running. **** TODO [#C] Externalize hardcoded SSH hostnames in =eshell-config= :cleanup: =modules/eshell-config.el:74-76= sets up =cj/eshell-aliases= with SSH aliases pointing at specific hostnames (=gosb=, =gowolf=). Those identifiers are personal; they should live in a per-machine config (a defcustom, an alist read from disk, or =host-environment.el='s machine-table) so the module itself is portable. **** 2026-05-16 Sat @ 04:00:00 -0500 Fixed https→http in markdown-preview + extracted server start =modules/markdown-config.el:45= opens =https://localhost:8080/imp= via =browse-url-generic=, but the =simple-httpd= listener bound in =httpd-config.el= serves plain HTTP on port 8080. The =https= scheme causes the browser to attempt a TLS handshake against a plaintext listener -- the request fails before any preview content reaches the page, so the entire feature is broken end-to-end. Change the URL to =http://localhost:8080/imp= (and consider switching the launch to =browse-url= so the user's default protocol handler is respected). **** TODO [#C] Document or vendor strapdown.js CDN dependency in =markdown-preview= :cleanup:solo: =cj/markdown-html= (=modules/markdown-config.el:48-51=) embeds a =