#+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
** PROJECT [#B] Implement ai-kb :feature:ai-kb:
Build v1 of the AI knowledge base per [[file:docs/design/ai-kb.org][docs/design/ai-kb.org]] (Ready; six reviews incorporated, all decisions resolved 2026-05-24). Step 1 splits into 1a (the safe write path — minimum usable) and 1b (retrieval, maintenance, push), since =remember= depends on =index=+=lint= and the adapter depends on =remember=. Step 2 is the Emacs layer: a full org-roam profile on switch, the human-edit safety model (same write path as the agent), and the browsing surface. Step 3 and the LLM-Wiki layer are vNext. Children are ordered by build sequence; the server bootstrap is the prerequisite.
*** TODO [#B] ai-kb bare repo on cjennings.net :ai-kb:
Prerequisite, one-time server bootstrap (not doable by the local script): =sudo git init --bare /var/git/ai-kb.git= + chown on cjennings.net. Leave the github-mirror hook OFF — this repo is private. Required before every per-machine clone.
*** TODO [#B] ai-kb store + contract + seed :ai-kb:
Step 1a. Clone =git@cjennings.net:ai-kb.git= to =~/.local/share/ai-kb=. Author =AGENT_CONTRACT.org= (canonical repo-resident contract: node format, write protocol, operations, routing) and seed =index.org= + a README/index node with a generated =:ID:=. Node format per spec — a *required* one-line =:SUMMARY:= (the index/query read it straight, no inference/LLM), provenance (=:CREATED_BY:/:CONFIDENCE:/:VISIBILITY:/:SOURCE:/:STATUS:=), =:PROJECTS:= slugs, type filetags, relation labels. Define the durable external-pointer format as *ID-first*: =ai-kb:
()=, resolved by ID with title fallback (filenames can change in curation).
*** TODO [#B] ai-kb CLI 1a: index, lint, remember, doctor :ai-kb:
Step 1a. Shell wrapper calling Emacs for org work — =emacsclient= when a daemon is up, =emacs --batch= fallback, lint+index in *one* invocation per =remember=. =index= regenerates =index.org= from node properties incl. =:SUMMARY:= (never hand-maintained); the index references nodes as plain =Title (UUID)= text, never =[[id:]]= links, and is excluded from the scan so it can't manufacture backlinks or hide orphans. =lint= = org-lint fatal checks + duplicate IDs + broken id-links (excl =raw/= + index) + missing required props (incl =:SUMMARY:=) + bad project slugs + stale/incomplete index + credential scan of nodes *and* =raw/= text files (binaries skipped). =remember= = the write protocol: fetch + =pull --ff-only= (abort on diverge/dirty), write, regenerate index, then run the *full =ai-kb lint=* over the change as the commit gate (not just node org-lint — this is the safety boundary), commit locally, =flock=; no push. =doctor= / =status= = health + push-state + raw-dir-size report (repo, private remote, CLI on PATH, =graphviz= if the map needs it, adapter linked, db buildable, no secrets, "ahead N"/"push failed"/"diverged"); =status= is the fast non-diagnostic mode for the dashboard/nudge.
*** TODO [#B] claude-rules/ai-kb.md adapter :ai-kb:
Step 1a. Global L1 rule in rulesets pointing at the repo-resident =AGENT_CONTRACT.org=: path, routing (T1/T2/T3 tiers; per-project =MEMORY.md= shrinks to ID-first pointers into ai-kb), proactive + contradiction rules, concrete "read the index first" triggers, link-grep recipes, "use =ai-kb remember=, never bypass =ai-kb lint=", one-line nudge on unpushed commits / recorded push rejection. =make install= symlinks it into =~/.claude/rules/=.
*** TODO [#B] ai-kb provisioning: setup-ai-kb.sh + make ai-kb-init :ai-kb:
Step 1a (core; the timer-install line is added with 1b). Idempotent =scripts/setup-ai-kb.sh=: clone (or init+add-remote on first machine), seed, install the CLI on PATH, =ai-kb index=, =ai-kb doctor=. =make ai-kb-init= wraps it. The one-time server bootstrap stays a separate documented step.
*** TODO [#B] ai-kb Step-1a tests :ai-kb:tests:
Write-path: a write with the remote unreachable still commits locally and does not error; =flock= serializes concurrent =remember=; each org-lint *fatal* check (malformed drawer, missing/dup =:ID:=, invalid required property, missing =#+title:=, unparseable org) rejects the commit, a style warning does not; a node missing =:SUMMARY:= fails lint; =remember= aborts the commit when the *full* lint fails (stale index, broken link, secret in a node or =raw/= text file); the credential scan skips binaries. Index: regen from a fixture produces expected entries; an out-of-band node appears only after regen; a node referenced only by =index.org= still reports as an orphan (the index is not a backlink source). Link recipes: backlink (excl =raw/= + index) + forward correct. Provisioning (bats): idempotent, valid =:ID:= + =:SUMMARY:=, =doctor= passes.
*** TODO [#B] ai-kb CLI 1b: query, curate, sync :ai-kb:
Step 1b. =query = with a *testable contract*: plain-text default + =--json=; fields title/ID/summary/projects/status/updated/path + *match reason*; searches index rows + title/tags/properties/body; ranks by lexical score — sum of each matched field's weight, counted once per field: title 100, tag/project/status 50 each, summary 20, body 5; no term-frequency weighting in v1 — with most-recently-updated (=:UPDATED:=) only as the *tie-break* on equal scores (recency alone buries stable old preferences); default max-results; =raw/= paths only as source references; exit codes for no-match / invalid KB / lint-index failure. =show = (resolve ID-first, print the node) and =backlinks = (excl =raw/= + index) as the inspection primitives the Emacs commands wrap. =curate --dry-run= (four buckets; also flags orphan =raw/= captures and any =raw/= file over 256 KB; destructive ops human-only). =sync= (=org-roam-db-sync= against ai-kb) only when the db is missing/stale or forced.
*** TODO [#B] ai-kb push timer + failure observability :ai-kb:
Step 1b. =ai-kb-push.timer= + =ai-kb-push.service= =systemd --user= units: push only if ahead, ~15 min; installed + =enable --now= by the setup script (add this line to =setup-ai-kb.sh=). A failed push is logged to a state file (=$XDG_STATE_HOME/ai-kb=), never fatal; surfaced by =ai-kb doctor= and the adapter's startup nudge.
*** TODO [#B] ai-kb-curate workflow in rulesets :ai-kb:
Step 1b. =~/code/rulesets/.ai/workflows/ai-kb-curate.org= — human-gated curation: the four buckets, node-count trigger (nudge at 150 nodes, re-fire every +50), =:LAST_CURATED:= rotation, pointer-integrity (merge/supersede changes the canonical ID, so grep inbound =[[id:]]= + =MEMORY.md= =ai-kb: ... (UUID)= refs and repoint before deleting). Surfaced by =ai-kb doctor= + session startup when due.
*** TODO [#B] ai-kb Step-1b tests :ai-kb:tests:
=query --json= returns the specified fields (incl. match reason)/exit-codes on a fixture KB and =raw/= appears only as a source ref; a title match outranks a body-only match with recency only breaking ties (an old preference is not buried under a newer body-only hit); a simulated push failure is recorded to the state file and surfaced by =ai-kb doctor= / =status=. Performance (=:perf= tag): 100- and 1,000-node fixtures keep =index=/=query=/=lint=/=remember= under a stated time budget (catches an accidental per-check Emacs startup or an O(n²) scan).
*** TODO [#B] Emacs: org-roam ai-kb profile + switch :ai-kb:
Step 2.
=org-roam-config.el=: =cj/org-roam-switch-to-ai-kb= / =cj/org-roam-switch-to-personal= install a full org-roam *profile*, not a two-variable swap — dir + =org-roam-ai.db= + =org-roam-file-exclude-regexp= (=raw/= + =index*.org=), and dailies, capture templates, topic/project/recipe find wrappers, and the agenda/refile + completed-task→daily hooks all rescoped or neutralized so ai-kb nodes never leak into personal journals/agenda. Restore everything exactly on exit; re-assert personal state at startup (abnormal-exit safety). =cj/ai-kb-db-sync= syncs only when the db is missing/stale or forced, with a status indicator.
*** TODO [#B] Emacs: ai-kb edit safety (same write path) :ai-kb:
Step 2. An =ai-kb= minor mode whose =after-save-hook= runs the agent's post-write sequence under =flock= — =ai-kb index=, full =ai-kb lint=, commit, push-state update — so a human Emacs edit can't bypass index/lint/commit. One write path for both agent and human. Failure UX: the save always writes to disk and the buffer stays editable (never read-only/blocked); on lint failure it does *not* commit, pops findings to a =*ai-kb-lint*= buffer (no focus steal), and shows the uncommitted-failing state in the modeline + dashboard — Craig fixes and re-saves, a clean save commits. Recursion guard, two layers: the mode's activation predicate excludes =index*.org= + =raw/=, and the pipeline binds a re-entrancy flag (=cj/ai-kb--in-pipeline=) the hook early-returns on; index regen prefers =write-region= over =save-buffer=.
*** TODO [#B] Emacs: ai-kb browsing surface :ai-kb:
Step 2. =cj/ai-kb-dashboard= (status banner: active KB, node count, unpushed commits, push-failure state, curation due, last index/sync), =cj/ai-kb-find-node= (=org-roam-node-find= in the ai-kb profile), =cj/ai-kb-search= (=ai-kb query= or scoped =consult-ripgrep=), =cj/ai-kb-show-node= (resolve ID-first, open), =cj/ai-kb-backlinks= (excl =raw/= + index), =cj/ai-kb-map= (built-in =org-roam-graph= *first* — the profile's exclude regexp already keeps =raw/= + index out of the db, so the graph inherits the right scope; custom DOT export only if project/tag/status filtering proves necessary; =graphviz= dep). Simple wrappers over the CLI primitives where possible.
*** TODO [#B] Emacs: ai-kb keybindings + which-key :ai-kb:
Bind the switch + sync + browsing commands under the =C-c n= roam prefix (e.g. =C-c n a= → ai-kb, =C-c n A= → personal, a small transient for the browsing commands), avoiding the dense existing set; which-key labels.
*** TODO [#B] Emacs: ai-kb Step-2 ERT tests :ai-kb:tests:
Profile: switch installs the ai-kb dir + db + exclude regexp and switch-back restores personal *exactly* — completed-task hook, agenda/refile finalize hook, dailies, and capture templates all untouched by ai-kb while switched; startup re-asserts personal state after a simulated abnormal exit. Edit path: a save in an ai-kb buffer runs index+lint+commit (a bad save surfaces the lint failure rather than committing). Sync runs only when stale.
** 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.
**** 2026-05-24 Sun @ 04:01:02 -0500 Verified C-s already advances isearch — non-bug, no change
The premise didn't hold on Emacs 30.2. Investigated in the live daemon: while isearch is active, =overriding-terminal-local-map= is =isearch-mode-map= and the effective =C-s= resolves to =isearch-repeat-forward=, not =cj/consult-line-or-repeat=. isearch installs its own map as an overriding map, so the global =C-s= binding can't shadow it during a search. =isearch-mode-map= already binds =C-s= to =isearch-repeat-forward= by default. Adding an explicit binding to the consult =:bind= block would only duplicate that default, so I left =selection-framework.el= unchanged.
**** 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.
**** 2026-05-24 Sun @ 04:15:36 -0500 Made Emacs restart and destructive confirms defensive
Restart-Emacs scheduled an unconditional =kill-emacs= one second after firing the systemctl restart, so a missing or failed service killed the session with nothing to replace it. Restart now guards on =(daemonp)= and a present =emacs.service= (new =cj/system-cmd--emacs-service-available-p= via =systemctl --user cat=) before doing anything, and drops the separate =kill-emacs= — =systemctl restart= cycles the daemon itself, so a failed restart leaves the current Emacs alive. Shutdown and reboot moved to a strong =yes-or-no-p= confirm (a stray RET/space on the old quick prompt could power off the machine); logout and suspend keep the quick confirm since they are recoverable. Tests cover service detection, both restart guards, and the strong-confirm paths with system primitives stubbed. Commit =f1dbec16=.
Not done: the detached restart+reconnect (=nohup sh -c '... && emacsclient -c'=) may still race systemd's cgroup teardown of =emacs.service= before =emacsclient -c= runs. Couldn't verify from here without cycling the live daemon — eyeball the reconnect on the next real restart.
**** 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.
***** 2026-05-24 Sun @ 04:10:20 -0500 Shell-quoted X11 and audio recording command paths
The Wayland =wf-recorder= path already quoted its args, but the X11 =ffmpeg= path and the audio-only =ffmpeg= path interpolated mic device, system device, and output filename raw — breaking on directories with spaces or unusual device names. Wrapped all three in =shell-quote-argument= on both paths. Extracted =cj/recording--build-audio-command= (mirroring =cj/recording--build-video-command=) so the audio command is unit-testable, then quoted there. Tests cover device names and filenames with spaces on both builders. Commit =39795e85=.
***** 2026-05-24 Sun @ 04:10:20 -0500 Scoped wf-recorder stop signal to our own process
Stop ran =pkill -INT wf-recorder=, signalling every wf-recorder on the system including an unrelated screen capture. Added =cj/recording--interrupt-child-wf-recorder=, which scopes the producer-first interrupt to the wf-recorder child of our own recording shell via =pkill -P =. Producer-first ordering preserved (ffmpeg still sees a clean pipe EOF). The orphan-cleanup at recording start stays a broad by-name kill on purpose — those leftovers come from crashed sessions whose shells are already dead, so there is no live PID to scope to. Tests cover the scoped call, the nil-PID no-op, and that the bare system-wide form is never used. Commit =556f48a2=.
***** 2026-05-24 Sun @ 04:10:20 -0500 Created the selected recording directory, not its parent
The toggles ran =(file-name-directory location)= before =make-directory=, which returns the *parent* for a path without a trailing slash — so the selected directory went uncreated and ffmpeg failed to write into it. Both toggles now route the destination through =cj/recording--normalize-recording-dir= (expand + =file-name-as-directory=) and =make-directory= that, creating the selected directory itself (including names with spaces). Tests cover trailing-slash normalization, idempotence, spaces, and relative-to-absolute expansion. Commit =dc033c75=.
**** 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
=