aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* fix(latex): make PDF-viewer selection idempotentCraig Jennings2026-05-252-5/+92
| | | | | | cj/--latex-select-pdf-viewer runs on every LaTeX-mode buffer and was blindly pushing an (output-pdf VIEWER) entry onto TeX-view-program-selection, so each LaTeX buffer opened in a session stacked another duplicate. The head still won, so the viewer worked, but the list grew and the docstring's idempotency claim was false. I drop any existing output-pdf entry before consing the chosen viewer, which also makes "wins over any default" actually true. Added a test file (the module had none) covering selection, preference order, the PDF-Tools fallback, idempotency, and default override, with executable-find mocked so the run doesn't depend on which viewers are installed.
* docs(todo): close the byte-compile-paths task and drop the prog-shell taskCraig Jennings2026-05-251-8/+3
| | | | The byte-compile load-paths task shipped as the make compile-file target, so I closed it DONE with a note on what landed and where the parallel hook fix went. I dropped the prog-shell config-home task — it isn't worth tracking as its own item.
* build: add compile-file for single-file byte-compile with the project load pathCraig Jennings2026-05-251-2/+17
| | | | | | Bare emacs -Q --batch byte-compile-file fails on local compile-time requires (undead-buffers for a module, dupre-palette for a theme file) because nothing is on the load path, even though init and the test harness load fine. I added a compile-file target that compiles one file with the project load path (modules, themes, tests, plus package-initialize), and put themes on make compile's path too. Bare emacs -Q stays unsupported by design; compile-file is the documented command, and it guards against a missing FILE. Verified modules/dashboard-config.el and themes/dupre-faces.el both compile through it.
* chore: sync bundled claude rules and git hooksCraig Jennings2026-05-2510-8/+568
| | | | Routine sync of the .claude/rules and git hooks distributed with the language bundle. Adds the cross-project, emacs, interaction, todo-format, triggers, and working-files rules; refreshes the elisp and elisp-testing rules, the elisp validation hook, and the pre-commit hook.
* docs(todo): close shipped tasks and refile completed sub-tasksCraig Jennings2026-05-251-19/+44
| | | | | | I marked the tasks that shipped as DONE: the ai-vterm third-split fix, projectile open-todo in the other window, the elfeed-config byte-compile-safe tests, the org TODO-keyword color theming, and the manual cj/org-finalize-task verification. The load-graph spec and the elfeed-config header annotation are level-4 sub-tasks, so they become dated event-log entries rather than DONE keywords. I also reformatted a few headings, bumped the dashboard over-scroll task to [#D], and filed a task to make the standalone byte-compile load paths match module dependencies.
* feat(dashboard): render the butterfly banner with a transparency maskCraig Jennings2026-05-253-4/+10
| | | | | | The dashboard banner showed the butterfly PNG without its transparency, because the image descriptor was built before the mask props were set. I moved the banner setup (dashboard-startup-banner, dashboard-banner-logo-title) ahead of dashboard-setup-startup-hook and added dashboard-image-extra-props with :mask heuristic, so the mask is in place when the startup hook builds the buffer. If a *dashboard* buffer already exists I refresh it, so the change shows without a restart. I also re-encoded the PNG smaller and kept the previous encoding as M-x_butterfly.png.bak.
* feat(auto-dim): dim vterm windows by blending terminal colorsCraig Jennings2026-05-252-0/+283
| | | | | | | | Window dimming via face-remap never reached vterm. The terminal resolves its own colors per cell while redrawing, so it bypasses the remapped faces, and agent and shell windows stayed bright when they lost focus. I advise vterm--get-color to blend each looked-up color toward the auto-dim faces whenever every window showing the buffer is dimmed. The foreground and background blend amounts are separate defcustoms (foreground stays more legible, background fades harder). After a dim-state change I force a full vterm repaint by briefly nudging the terminal size, because vterm only repaints the rows libvterm marked dirty. A post-command hook and a select-window advice cover the windmove and Shift-arrow focus paths that window-selection-change-functions misses. Tests cover the dimmed-buffer predicate, the color blend, the selection-change scheduling, and the auto-dim-before-repaint ordering.
* fix(theme): register dupre faces so org status colors are themedCraig Jennings2026-05-254-15/+114
| | | | | | | | The dupre theme defined its own faces (dupre-accent, the headings, and the org status faces) only through custom-theme-set-faces, never defface. That leaves them unregistered, so they render through :inherit but silently fail when applied directly as a text property. org-todo-keyword-faces and org-priority-faces apply faces that way, so the org keyword and priority colors never showed as dupre tones. I added a defface registration block to dupre-faces.el for all of dupre's own faces, so they're real faces. The theme still sets their colors. Then I pointed org-todo-keyword-faces and org-priority-faces (in org-config.el) at named dupre-org-* faces, each the closest palette color to its former hard-coded name, and gave each a dimmed variant that auto-dim-config.el swaps in for unfocused windows. A keyword in a dimmed window now shows a darker shade of its own color rather than flat gray or full brightness. A regression test asserts dupre's faces stay registered, since that was the latent bug behind all of this.
* feat(auto-dim): dim non-selected windows via auto-dim-other-buffersCraig Jennings2026-05-256-4/+111
| | | | | | | | I added auto-dim-config, a module that loads my local auto-dim-other-buffers fork and dims windows that don't have focus so the selected window stands out. A non-selected window drops to a pure-black background with faded gray text. The dimmed faces live in the dupre theme (themes/dupre-faces.el) so they track theme switches, and the module remaps default, the font-lock faces, and org-block onto them so syntax-highlighted code fades too rather than staying lit. Fringe is left out because dimming it forces a full-frame refresh that flickers on this non-pgtk build. dim-on-focus-out is nil, so tabbing to a browser or terminal on Hyprland doesn't dim the whole frame. vterm and agent windows don't dim either, because the terminal paints its own per-cell colors past the face remap. I'm keeping that, since the agent's output stays readable while I work in code on the other side. The module loads after the theme, carries a load-graph header, joins the header-contract allowlist, and the inventory moves to 103 of 103 classified.
* docs(load-graph): classify elfeed-config, the last init moduleCraig Jennings2026-05-253-21/+19
| | | | | | elfeed-config was the only init module without a load-graph header. It was deferred because annotating the header triggers a byte-compile, which broke its tests. With that test rewritten to use real structs, I added the header (Layer 4, optional, currently eager but a command-loaded deferral candidate, runtime requires user-constants, system-lib, media-utils), added elfeed-config to the header-contract allowlist, and moved it from the inventory's deferred and pending sections into the Batch 8 table. That brings the inventory to 102 of 102 modules classified, completing the Phase 1 classification pass.
* test(elfeed-config): use real structs so tests survive byte-compilationCraig Jennings2026-05-251-26/+49
| | | | | | | | The cj/elfeed-process-entries tests faked entries as bare symbols and stubbed the elfeed-entry-link accessor, which only works while elfeed-config loads as interpreted source. Byte-compiled, the cl-defstruct accessor inlines to an elfeed-entry-p check plus an aref, so the stub is bypassed and the inlined check rejects the fake entry. Three tests failed the moment a .elc existed. I rewrote the five process-entries tests to build genuine elfeed-entry structs with elfeed-entry--create, calling package-initialize so the installed elfeed lands on the load-path, and guarded them with skip-unless for an environment that lacks the package. The elfeed-search UI boundary is still stubbed. The four extract-stream-url tests are unchanged. This unblocks annotating elfeed-config with its load-graph header, which triggers the byte-compile that surfaced the problem.
* fix(prog-general): open the project todo in the other window when splitCraig Jennings2026-05-252-1/+73
| | | | | | C-c p t (cj/open-project-root-todo) called find-file, which always opened todo.org in the selected window, replacing whatever I was looking at. Now it opens in the other window when the frame is split and in the current window when it isn't, through a small cj/--find-file-respecting-split helper. The helper is a top-level defun rather than buried in the projectile :config block so it can be unit-tested without loading projectile. I left cj/project-switch-actions alone. Opening the todo on a project switch is a different trigger and not what this fixes.
* fix(ai-vterm): reuse the frame's half instead of splitting a thirdCraig Jennings2026-05-255-268/+480
| | | | | | F9 split a third window into a frame that was already divided in two, wedging the agent into the middle or a skinny extra column instead of taking the half it should occupy. The display rule only knew how to reuse a window already showing an agent or to split a fresh one. With a plain two-pane layout it fell through to the split and added a window. I added a display action, cj/--ai-vterm-reuse-edge-window, that reuses the window already forming the target half (the right column on a desktop, the bottom row on a laptop), found by a new cj/window-at-edge helper. It records the displaced buffer with display-buffer-record-window, so toggling off restores that buffer through the native quit-restore-window. The slot's buffer swaps between the agent and whatever it displaced, and no window is created or deleted. The split path still handles a single-window frame or a layout split on the other axis, and the lone fullscreen agent keeps its bury-and-restore-in-place behavior.
* refactor(load-graph): route C-; registration through the keymap APICraig Jennings2026-05-2425-42/+34
| | | | | | Migrated all 31 cj/custom-keymap registration sites across 24 modules from direct (keymap-set cj/custom-keymap ...) calls to cj/register-prefix-map and cj/register-command. Consumers no longer reference cj/custom-keymap directly, so keybindings.el is the sole owner of the C-; prefix and modules reach it only through the API (each already requires keybindings from Phase 2). Behavior-preserving: I dumped every C-; binding before and after the migration and they're identical: 279 bindings, each resolving to the same command. The which-key label blocks are untouched, since they use string key descriptions and never assumed the keymap existed. I byte-compiled all 24 files (no new free-variable warnings, because the cj/custom-keymap references are gone), and make test, validate-modules, and an init load all pass.
* feat(keybindings): add cj/custom-keymap registration APICraig Jennings2026-05-243-1/+72
| | | | | | Phase 3 of the load-graph project. cj/register-prefix-map and cj/register-command bind a prefix map or command under the C-; prefix and register the which-key label once which-key loads. Feature modules will route their registration through these instead of mutating cj/custom-keymap directly, so keybindings.el stays the sole owner of the prefix and modules stop assuming the keymap already exists at load. Adds test-init-keymap-registration.el covering prefix-map and command resolution, the optional label, and invalid-key rejection. No modules are migrated yet; that follows in batches.
* refactor(load-graph): make hidden module dependencies explicitCraig Jennings2026-05-2410-89/+59
| | | | | | | | | | | | | Phase 2 of the load-graph project. I fixed the seven hidden dependencies the classification surfaced, so each module declares what it uses instead of relying on init order. - system-defaults now requires host-environment and user-constants at runtime. They were eval-when-compile only, but env-bsd-p and user-home-dir are read at load, so the compiled module couldn't load standalone. - custom-buffer-file, dev-fkeys, calendar-sync, and video-audio-recording require keybindings and drop their (when (boundp 'cj/custom-keymap) ...) shims. The shim silently dropped the C-; binding when the module loaded before keybindings. The explicit require makes the dependency real. - flycheck-config and mail-config require keybindings for their cj/custom-keymap bindings (a use-package :map and a direct keymap-set). - Removed a dead eval-when-compile (defvar cj/custom-keymap) in transcription-config; nothing there used the variable. No init.el load-order change. keybindings and the foundation modules already load before these, so the requires are no-ops at startup and only fix standalone and test loading. I verified each fix with a fresh emacs --batch (require 'X), then swept all modules standalone: every one loads or fails only with a clear missing-package message. Full make test, make validate-modules, and an init smoke all pass. Module headers and the inventory's hidden-dependency section are updated to mark the seven resolved.
* docs(todo): close module-classification task, split out elfeed-configCraig Jennings2026-05-241-16/+4
| | | | Rewrote the classify-modules child as a dated log entry now that 101 of 102 modules carry load-graph headers. Pulled the lone deferral — annotating elfeed-config — into its own task, blocked on the elfeed test-byte-compile fix.
* docs(load-graph): classify remaining domain and optional modulesload-graph-classify-endCraig Jennings2026-05-2421-30/+249
| | | | | | Final classification batch: the last 19 modules — linear-config, local-repository, lorem-optimum, mail-config, markdown-config, music-config, pdf-config, quick-video-capture, reconcile-open-repos, restclient-config, slack-config, system-commands, telega-config, tramp-config, transcription-config, video-audio-recording, vterm-config, weather-config, wrap-up. I annotated each header, added a Batch 9 table to the inventory, and extended the validation allowlist. 101 of 102 modules are now classified; only elfeed-config remains, deferred on its test fix. Two more hidden dependencies turned up. video-audio-recording uses the boundp shim for its C-; r binding, and mail-config registers C-; e directly without requiring keybindings, so it errors standalone rather than degrading. Both recorded for Phase 2.
* docs(load-graph): classify domain, integration, and optional modulesCraig Jennings2026-05-2420-33/+240
| | | | | | | | | | Eighth classification batch: 17 domain/integration/optional modules — ai-config, ai-vterm, browser-config, calendar-sync, calibredb-epub-config, chrono-tools, dirvish-config, dwim-shell-config, erc-config, eshell-config, eww-config, flyspell-and-abbrev, games-config, gloss-config, httpd-config, jumper, latex-config. I annotated each header, added a Batch 8 table to the inventory, and extended the validation allowlist. 82 of 102 modules are now classified. Almost all are eager only by init order and become command/hook/mode-loaded. calendar-sync stays eager when its .local.el is present. One new hidden dependency: calendar-sync guards its C-; g registration with a boundp shim and doesn't require keybindings, so the binding drops standalone. I deferred elfeed-config rather than annotate it. Its header edit triggers byte-compilation, and the existing tests only pass when the module loads as interpreted source — the compiled cj/elfeed-process-entries inlines an elfeed struct accessor the stubs can't intercept, and the batch test environment has no elfeed package to build real structs. It needs its tests rewritten first, recorded in the inventory and a new todo task. Also made the header allowlist scoping test durable: it used games-config (now classified) as its unclassified example; switched to a sentinel name plus a duplicate-entry guard.
* docs(load-graph): classify Org modulesCraig Jennings2026-05-2415-17/+172
| | | | | | Seventh classification batch: the thirteen Org modules — config, agenda, babel, capture, contacts, drill, export, noter, refile, reveal, roam, webclipper, hugo. I annotated each header, added a Batch 7 table to the inventory, and extended the validation allowlist. 65 of 102 modules are now classified. The daily workflows (config, agenda, capture, refile, roam) keep their eager reason per the spec's Phase 6 target. Babel and contacts move to after-load; export, reveal, drill, noter, webclipper, and hugo become command-loaded. The agenda and refile idle-timer caches are recorded as the side effects the spec already tracks for cache-lifecycle work. No new hidden dependencies.
* docs(load-graph): classify programming modulesCraig Jennings2026-05-2412-13/+137
| | | | | | Sixth classification batch: prog-general plus the language modules — prog-c, prog-go, prog-lisp, prog-python, prog-webdev, prog-json, prog-yaml, prog-shell, prog-training. I annotated each header, added a Batch 6 table to the inventory, and extended the validation allowlist. 52 of 102 modules are now classified. prog-general owns the shared defaults and tree-sitter/LSP policy and stays eager. The language modules are eager only by init order and should load by major mode, so they're tagged Phase 6 deferral candidates. prog-shell's after-save executable hook is the one side effect worth scoping. No new hidden dependencies.
* docs(load-graph): classify dev, diff, help, lint, and VC modulesCraig Jennings2026-05-2411-15/+126
| | | | | | Fifth classification batch: the development-workflow entry points and package config — coverage-core, coverage-elisp, dev-fkeys, diff-config, help-config, help-utils, flycheck-config, test-runner, vc-config. I annotated each header, added a Batch 5 table to the inventory, and extended the validation allowlist. 42 of 102 modules are now classified. Two more hidden dependencies turned up, both about cj/custom-keymap. dev-fkeys repeats the custom-buffer-file boundp shim for its C-; P binding. flycheck-config binds (:map cj/custom-keymap ...) through use-package without requiring keybindings, so it fails to load standalone. Both recorded for the Phase 2 dependency pass.
* docs(load-graph): classify UI and core-UX modulesCraig Jennings2026-05-2412-19/+133
| | | | | | Fourth classification batch: the modules that shape the first interactive frame — ui-config, ui-theme, ui-navigation, font-config, selection-framework, modeline-config, mousetrap-mode, popper-config, dashboard-config, nerd-icons-config. I annotated each header, added a Batch 4 table to the inventory, and extended the validation allowlist. 33 of 102 modules are now classified. These mostly stay eager: each has a real first-frame reason (theme, font, modeline, completion stack, landing page). No new hidden dependencies. popper-config carries the spec's open question about its enabled/disabled state, noted for the deferral phase.
* docs(load-graph): classify core libraries and command modulesCraig Jennings2026-05-249-10/+92
| | | | | | Third classification batch: the remaining core and library command modules from init.el's early block — external-open, media-utils, auth-config, keyboard-macros, system-utils, text-config, undead-buffers. I annotated each with the load-graph header contract, added a Batch 3 table to the inventory, and extended the validation allowlist. 23 of 102 modules are now classified. No new hidden dependencies in this batch. auth-config stays eager because other modules need credentials early; the command libraries (external-open, media-utils, keyboard-macros) are eager only by init order and flagged as Phase 4 deferral candidates.
* test: extend header allowlist to text/editing modulesCraig Jennings2026-05-241-2/+13
| | | | I added the nine custom-* modules to the classified allowlist so the header-validation test enforces the contract on them too. 16 of 102 modules are now covered.
* docs(load-graph): classify text/editing command modulesCraig Jennings2026-05-2410-14/+130
| | | | | | Second classification batch: the nine custom-* text/editing command helpers (case, comments, datetime, buffer-file, line-paragraph, misc, ordering, text-enclose, whitespace). I annotated each with the load-graph header contract and added a Batch 2 table to the inventory. They're all Layer 2, eager only to register a C-; submap at load, with no necessary eager reason, so all are Phase 3/4 deferral candidates. The inventory records a second hidden dependency for Phase 2: custom-buffer-file guards its C-; b registration with (when (boundp 'cj/custom-keymap) ...) and declares the keymap only via eval-when-compile, so the binding silently drops when the module loads without keybindings.
* docs(init): retire stale module comments and track follow-upsCraig Jennings2026-05-242-4/+10
| | | | Three init.el requires carried vague comments: latex-config "WIP need to fix", prog-shell "combine elsewhere", and a "Modules In Test" banner. I replaced them with descriptive comments and moved the real follow-up work into todo.org tasks: investigate the latex-config state, and decide whether prog-shell config folds into prog-general. I also marked the module-classification task DOING.
* test: enforce load-graph headers on classified modulesCraig Jennings2026-05-241-0/+111
| | | | I added test-init-module-headers.el, which checks that every classified module declares the seven required header lines and names any that are missing. The classified set is an explicit allowlist that grows one batch at a time. Parity with the init.el require set is the Phase 1 exit criterion. The cases cover the happy path, a single missing line, the eager-reason conditional, and allowlist scoping.
* docs(load-graph): seed module inventory and annotate foundation headersCraig Jennings2026-05-248-2/+258
| | | | | | I started the init.el load-graph classification with the foundation batch. I added docs/design/module-inventory.org as the living per-module inventory and annotated the seven foundation modules (system-lib, user-constants, host-environment, system-defaults, keyboard-compat, keybindings, config-utilities) with the load-graph header contract: layer, category, load shape, eager reason, top-level side effects, runtime requires, and direct-test-load safety. I changed no load order, so init.el keeps its current eager order. The inventory records one hidden dependency for Phase 2: system-defaults uses host-environment and user-constants symbols at load while declaring them eval-when-compile, so the compiled module cannot load standalone.
* test: require host-environment in system-defaults testsCraig Jennings2026-05-241-3/+7
| | | | system-defaults reads `env-bsd-p` (host-environment) and `user-home-dir` (user-constants) at load, but the module declares both only via eval-when-compile. Loading the compiled module in isolation leaves `env-bsd-p` void, so the test failed whenever it ran outside a full init. I added the host-environment require alongside the existing user-constants require so the unit loads standalone. The production fix (promoting those eval-when-compile requires to a runtime require) is Phase 2 work, recorded in the module inventory.
* docs(todo): close coverage backlog after assessing the sub-60% clusterload-graph-classify-startCraig Jennings2026-05-241-19/+13
| | | | Read each remaining sub-60% module to separate real untested logic from interactive/config glue. Filled the three genuine gaps (markdown-html, media-utils select-media-player, elfeed helpers — committed separately). The rest — flyspell, dashboard, ai-quick-ask — already have their pure logic tested with Normal/Boundary/Error; their low percentages come entirely from interactive commands. prog-general, restclient, vc-config, quick-video-capture are config/interactive-only with no pure logic to cover. Backlog cleared: every module's testable logic is covered; residual low % is code the testing rules say not to chase.
* test: cover markdown-html filter and media-player selectorCraig Jennings2026-05-242-0/+53
| | | | Two real-logic gaps from the refreshed coverage backlog. cj/markdown-html (markdown-config) is the impatient-mode filter that wraps a source buffer's text in the strapdown HTML shell — tested for normal content and an empty buffer. cj/select-media-player (media-utils) was the one untested function there — tested that choosing an available player updates cj/default-media-player and that a non-matching selection leaves it unchanged. Both mock at the boundary (completing-read, the source buffer).
* docs(todo): refresh stale coverage backlog against a clean runCraig Jennings2026-05-241-318/+18
| | | | The per-module percentages in the coverage audit were stale — this session's test-writing covered most of the listed modules (prog-python 0%->100%, hugo-config 17.7%->91.7%, undead-buffers 5.7%->85.7%, and selection-framework / keyboard-compat / system-utils / system-defaults / ui-navigation / prog-go now ~100%). Re-measured against a clean make-coverage run and replaced the ~75 stale entries with current numbers for only the modules genuinely below ~60%. Modules in the 60-80% band are adequately covered and dropped. vc-config and quick-video-capture are flagged config/interactive-only (their pure logic is tested; the uncovered lines are use-package/interactive glue), so they're recorded as no-action rather than chased.
* test(elfeed): cover extract-stream-url and process-entries helpersCraig Jennings2026-05-241-0/+109
| | | | elfeed-config had only the youtube-feed-format helper under test; cj/extract-stream-url and cj/elfeed-process-entries were untested despite having clear error/boundary paths. Added characterization + Normal/Boundary/Error coverage: extract-stream-url returns the trimmed URL on success, nil on non-URL output or nonzero exit, and signals when yt-dlp is absent; process-entries applies the action per selected entry and marks read, errors when nothing is selected, skips entries with no link, catches per-entry action errors by default, and propagates them under skip-error-handling. yt-dlp (call-process) and the elfeed-search API are stubbed at the boundary.
* docs(todo): log the third solo-hardening batch (move-branch, keymaps, ↵Craig Jennings2026-05-241-64/+10
| | | | export, elfeed/eww, capture tests)
* test(org-capture): smoke-test template key uniqueness and file targetsCraig Jennings2026-05-241-0/+67
| | | | Org capture templates are assembled across org-capture-config, quick-video-capture, org-contacts-config and other modules, so a duplicate dispatch key or a file target pointing at an unset path variable would be easy to miss. Added a smoke test that loads the cleanly-loadable capture modules, applies their lazy additions, and asserts no two templates share a key and that every symbol-valued file target resolves to a non-empty string path. Literal-string targets (the video template's no-save (file "")) and lambda targets (the drill file pickers) are intentionally excluded; webclipper templates need org-web-tools and are covered by their own test.
* fix(elfeed): bound and clean up the synchronous YouTube fetchCraig Jennings2026-05-243-35/+171
| | | | | | | | cj/youtube-to-elfeed-feed-format called url-retrieve-synchronously with no timeout, so a hung YouTube request would block Emacs indefinitely, and it only killed the temporary URL buffer when an ID was successfully extracted — a page without the expected markers leaked the buffer. Passed cj/elfeed-url-fetch-timeout (10s) to the synchronous fetch, and moved the fetch+parse into an unwind-protect that always kills the temp buffer (live-p guarded), including the parse-failure path. Tests mock the network boundary and cover a normal channel parse, that a timeout is passed, and that the buffer is not leaked when parsing fails. Also added tests for the EWW user-agent advice (no code change): it already injects the desktop UA only from eww-mode buffers, so package.el and other non-EWW url callers pass through untouched — the tests pin that scoping and the replace-not-duplicate header behavior.
* fix(org-export): remove contradictory org-export-with-tasks defaultCraig Jennings2026-05-242-1/+22
| | | | | | org-export-config.el set org-export-with-tasks twice in a row — first to ("TODO"), then to nil. The final value won (export no tasks), but the stale first assignment and its "export with tasks by default" comment contradicted it, so the intended policy was ambiguous on a read. Removed the leftover ("TODO") line. nil is the deliberate default: it is the value that was already winning, its comment matches, and it sits with the adjacent "export without tags / section numbers by default" settings. Added a smoke test that fires the deferred ox :config and pins org-export-with-tasks to nil so a future flip is caught.
* refactor: declare cross-module commands bound in custom keymapsCraig Jennings2026-05-242-0/+10
| | | | | | custom-ordering.el binds cj/org-sort-by-todo-and-priority (owned by org-config) and custom-text-enclose.el binds change-inner/change-outer (the change-inner package). Both work at runtime — org-config loads eagerly and text-config autoloads change-inner via use-package :commands — but byte-compiling either module standalone warned "not known to be defined", and the dependency was implicit. Added declare-function for each so the compile is clean and the cross-module relationship is explicit at the top of the file. No autoload needed: the runtime autoload/eager-load already exists, so only the compiler needed telling. custom-buffer-file.el byte-compiles clean already, so it needed no change.
* fix(org-roam): guard move-branch-to-roam against data lossCraig Jennings2026-05-242-22/+63
| | | | | | cj/move-org-branch-to-roam cut the subtree from the source buffer before writing the new roam file, so a failure in the demote/format/write/db-sync steps left the subtree gone from the source and not persisted anywhere — a destructive operation with no rollback. Reordered so the node file is written and verified on disk before org-cut-subtree runs; a failed write now aborts with the source untouched. Added a no-clobber guard (refuse an existing target file) and a confirmation prompt for large subtrees (>= cj/move-org-branch-confirm-lines, 30) or buffers with unsaved changes. The source buffer is deliberately left modified and undoable rather than auto-saved, so the move stays reversible. New test drives the write-failure-preserves-source invariant via an unwritable roam dir; the existing creates-roam-file test gained the confirm mock.
* refactor(linear): point config at the renamed pearl packageCraig Jennings2026-05-244-94/+94
| | | | | | The linear-emacs package was renamed to pearl (~/code/pearl, feature pearl, all symbols pearl-*). Swapped every linear-emacs-* reference to pearl-* across linear-config.el (the use-package form, :load-path, the 26 :commands, the api-key/default-team-id/org-file-path vars, and the lazy-key advice targets pearl--graphql-request-async and pearl-check-setup), the dashboard launcher, and the two test files. Kept the Linear-domain naming intact, since pearl is just a client for the Linear service: the C-; L prefix, the cj/linear-* wrapper helpers, the "Linear" dashboard label, the api.linear.app authinfo host, and the data/linear.org synced file are unchanged. Verified the wiring in a live daemon — pearl loads, the team id and org-file path apply, and the key advice installs on both entry points.
* docs(todo): log the second solo-hardening batch (webclip, qvc timers, dir ↵Craig Jennings2026-05-241-50/+11
| | | | scans, vc cache, region scope)
* refactor(text-enclose): extract shared region-or-buffer bounds helperCraig Jennings2026-05-242-24/+64
| | | | | | The append/prepend/indent/dedent *-in-region-or-buffer commands each inlined the same (if (use-region-p) (region-beginning) (point-min)) / (region-end)/(point-max) block — four copies of the "operate on the region, else the whole buffer" contract. Extracted cj/--region-or-buffer-bounds as the single source of that decision and routed all four through it. Behavior is unchanged; the public-wrapper tests still pass. This was the "extract a shared helper that decides the target range" option from the reconcile task. The sibling custom-ordering.el helpers (cj/--arrayify, cj/--unarrayify) already document an explicit (start end) contract accurately and are region-required by design, so they needed no docstring change — each pair now has one clear, consistent contract. Tests cover the helper for the region case, the no-region whole-buffer case, and an empty buffer.
* fix(modeline): key VC cache on resolved truename for symlink movesCraig Jennings2026-05-242-2/+62
| | | | | | The VC modeline cache keyed on (list file cj/modeline-vc-show-remote). If file was a symlink whose target moved to a different VC tree (shared drives, CI workspaces), the key was unchanged and the cache kept serving the old branch/state. Added the resolved file-truename to the key, so a symlink re-pointed at a new target produces a different key and the cache refreshes. The extra file-truename is one stat per modeline refresh, cheap next to the VC calls the cache exists to avoid. Tests cover truename inclusion, key stability for an unchanged file, and a symlink whose target moves.
* fix(org): surface directory-scan failures instead of crashing or hiding themCraig Jennings2026-05-244-18/+124
| | | | | | | | The refile target scan caught permission-denied and silently dropped the directory, and would crash outright on a missing root (only permission-denied was caught, so a missing code-dir/projects-dir raised file-missing and aborted the whole build). The agenda build had the same crash: cj/add-files-to-org-agenda-files-list called directory-files on projects-dir with no existence check. Extracted cj/--org-refile-scan-dir, which warns (display-warning) and returns nil for a missing, unreadable, or permission-denied root so the rest of the scan continues. Guarded the agenda scan the same way. Both now log a concise warning naming the skipped directory rather than failing silently or fatally. Also fixed a latent bug surfaced here: org-refile-targets was never declared special, so under make compile cj/org-refile-in-file let-bound it lexically and the scoped targets never reached org-refile. Added (defvar org-refile-targets) so the binding stays dynamic when byte-compiled. Tests cover the helper (missing/permission-denied/normal) and the agenda missing-dir guard.
* refactor(video-capture): drop startup timers for lazy protocol initCraig Jennings2026-05-242-29/+80
| | | | | | quick-video-capture scheduled an after-init-hook idle timer plus a 2-second fallback run-with-timer to call cj/setup-video-download, which require-d org-protocol and org-capture and registered both the protocol handler and the capture template. That loaded Org protocol/capture plumbing at every startup even when the video workflow was never used. Split the two concerns the way org-webclipper already does. The org-protocol handler is registered in a with-eval-after-load (quote org-protocol) block — a lightweight add-to-list that needs no org-capture — so it is in place whenever org-protocol loads (org-config requires it at startup). cj/setup-video-download now registers only the capture template, lazily, on the first capture (org-capture-mode-hook) or the first protocol call (the handler ensures it). Both startup timers are gone. Tests pin that setup registers the template idempotently and no longer touches the protocol alist; verified in a live daemon that the protocol registers on load.
* refactor(webclipper): scope clip URL/title to dynamic bindingsCraig Jennings2026-05-242-45/+60
| | | | | | org-webclipper passed the org-protocol URL and title through globals cj/webclip-current-url / cj/webclip-current-title: the protocol handler setq them, and the "W" capture template plus its handler read them, with the handler clearing them afterward. An aborted or erroring capture left the stale values for the next clip. Renamed them to cj/--webclip-url / cj/--webclip-title and let-bind them around the org-capture call in the protocol entry point instead of mutating globals. The template %(identity ...) forms and the handler run within that dynamic extent, so they see the values while the capture runs, and an abort/error unwinds the binding automatically — no stale state, no manual clear. This mirrors the quick-video-capture fix. Tests updated to the new contract: URL/title visible during the capture, nothing left bound after, and an aborted capture leaves no stale state.
* docs(todo): log the org-drill, git-clone, and video-capture hardeningCraig Jennings2026-05-241-33/+7
|
* refactor(video-capture): scope capture URL to a dynamic bindingCraig Jennings2026-05-242-13/+90
| | | | | | quick-video-capture passed the org-protocol URL through a global cj/video-download-current-url: the protocol handler setq the global, the capture handler read and cleared it. If a capture was aborted or errored between those steps, the stale URL survived into the next manual capture. Renamed it to cj/--video-download-url and let-bind it around the org-capture call in the protocol handler instead of setq-ing a global. The binding lives only for the dynamic extent of the capture, so the handler still sees the URL while the capture runs, and an abort or error unwinds the binding automatically — no stale state, no manual clear. The handler still prompts when invoked manually with no URL bound. Tests cover the bound-URL download, the manual prompt, the empty-URL error, that the URL is visible during the capture, and that an aborted capture leaves nothing behind.
* fix(vc): harden clipboard git-clone process and path handlingCraig Jennings2026-05-242-13/+122
| | | | | | cj/git-clone-clipboard-url shelled out via shell-command and derived the clone directory with file-name-nondirectory, which mishandles scp-style SSH URLs with no slash (git@host:repo.git became git@host:repo). It also ran git in default-directory and only checked whether the clone dir appeared afterward, so a failed clone was silent. The clone now runs as a direct git process (call-process, no shell) with clone -- url dir so a URL beginning with - cannot be read as a flag. The destination path comes from cj/--git-clone-dir-name, which takes the last component splitting on / and :, handling HTTPS, scp-style and ssh:// SSH, and local paths. It validates the clipboard is non-empty and the target is a writable directory that does not already contain the destination, and surfaces a non-zero git exit as a user-error with the *git-clone* output. Tests cover the deriver across URL schemes plus the empty-clipboard and clone-failure paths.