<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dotemacs/modules/ai-config.el, branch load-graph-classify-end</title>
<subtitle>My Emacs configuration
</subtitle>
<id>https://git.cjennings.net/dotemacs/atom?h=load-graph-classify-end</id>
<link rel='self' href='https://git.cjennings.net/dotemacs/atom?h=load-graph-classify-end'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/'/>
<updated>2026-05-24T21:57:56+00:00</updated>
<entry>
<title>docs(load-graph): classify domain, integration, and optional modules</title>
<updated>2026-05-24T21:57:56+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-24T21:57:56+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=cad351ec00c3f78cfb6e203d87c7309a620e485c'/>
<id>urn:sha1:cad351ec00c3f78cfb6e203d87c7309a620e485c</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>refactor(auth): consolidate the auth-source secret lookup into one helper</title>
<updated>2026-05-23T00:32:32+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-23T00:32:32+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=f6e5885b47e3ab244b293f4e478af7e520180710'/>
<id>urn:sha1:f6e5885b47e3ab244b293f4e478af7e520180710</id>
<content type='text'>
The auth-source-search + funcall-the-secret block was copied four times: calendar-sync--calendar-url, cj/auth-source-secret (ai-config), cj/--auth-source-password (transcription), and cj/slack--get-credential. Each searched authinfo, pulled :secret, and called it when the netrc backend returned a function.

I pulled that into cj/auth-source-secret-value in system-lib (a leaf, so calendar-sync doesn't have to depend on ai-config and drag in the gptel stack). It takes an optional user and returns the secret or nil. The four callers now delegate to it: ai-config layers its required-secret error on top, and the others keep their nil-on-miss behavior. With the direct auth-source-search calls gone, I dropped the now-unused (require 'auth-source) from transcription, slack, and calendar-sync. The helper's autoload covers it.

The transcription tests that exercise the delegated path stay green, and the primitive and the error wrapper get their own tests.
</content>
</entry>
<entry>
<title>fix(ai-config): require gptel backend libs so the fork's constructors load</title>
<updated>2026-05-22T20:34:07+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-22T20:34:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=c835491b716e2c1243c124a65e01b22f641657c4'/>
<id>urn:sha1:c835491b716e2c1243c124a65e01b22f641657c4</id>
<content type='text'>
cj/toggle-gptel and gptel chat errored with "Symbol's function definition is void: gptel-make-anthropic". The local gptel fork on :load-path with :ensure nil ships no generated autoloads, so (require 'gptel) loads gptel.el but never gptel-anthropic.el or gptel-openai.el, where the gptel-make-* constructors live. cj/ensure-gptel-backends then reached gptel-make-anthropic before it was defined.

cj/ensure-gptel-backends now requires gptel-anthropic and gptel-openai first, through a small cj/--gptel-load-backend-libs helper. Verified end-to-end: with the fork on load-path, the constructors are fbound and both backends build.
</content>
</entry>
<entry>
<title>refactor(ai-config): switch gptel to local fork, drop tab-width advice</title>
<updated>2026-05-18T06:24:30+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-18T06:24:30+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=b3d41f9a0c63b13ad497a48677de933a3fb5a5cf'/>
<id>urn:sha1:b3d41f9a0c63b13ad497a48677de933a3fb5a5cf</id>
<content type='text'>
I switched the gptel use-package form to `:load-path "~/code/gptel"`
with `:ensure nil` so Emacs loads from the fork instead of the MELPA
release. The fork now carries the narrow `tab-width' copy in
`gptel-org--create-prompt' that karthink redirected the upstream PR
to, which replaces the local `:around' advice on
`gptel--with-buffer-copy-internal' I'd been carrying.

I also dropped the stale test file
`tests/test-ai-config-gptel-prompt-tab-width.el' and the matching
stub in `tests/testutil-ai-config.el'. Both existed only to test the
advice I removed.
</content>
</entry>
<entry>
<title>fix(ai-config): gptel-model must be a symbol, not a string</title>
<updated>2026-05-16T11:07:46+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T11:07:46+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=a3723120073f9486ac771ccf150ec32008d8c2ef'/>
<id>urn:sha1:a3723120073f9486ac771ccf150ec32008d8c2ef</id>
<content type='text'>
The default-backend swap to gpt-5.5 (commit 0f029ab5) set
`gptel-model' as the string "gpt-5.5".  gptel's modeline-display
code calls `symbolp' on the model value and signals
`wrong-type-argument symbolp "gpt-5.5"' on every render, which
manifested as Emacs freezing in the AI-Assistant buffer ("Querying
ChatGPT..." → error in process sentinel → repeated redisplay errors).

Both default-setting sites now use `'gpt-5.5' (interned symbol).
The Anthropic backend tolerated string model names so the original
"claude-opus-4-7" string worked, which is why this hadn't surfaced
before.
</content>
</entry>
<entry>
<title>chore(ai-config): switch default gptel backend to ChatGPT / gpt-5.5</title>
<updated>2026-05-16T10:58:24+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T10:58:24+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=0f029ab5f844c6399584aec4f5e1cc352e8ab726'/>
<id>urn:sha1:0f029ab5f844c6399584aec4f5e1cc352e8ab726</id>
<content type='text'>
Two places set the default backend + model on gptel initialization
-- `cj/ensure-gptel-backends' (the lazy-init fallback) and the
`use-package gptel :config' block (the eager-set after initialization).
Both now pick the ChatGPT backend with `gpt-5.5' instead of Claude
with `claude-opus-4-7'.
</content>
</entry>
<entry>
<title>feat(gptel-tools): wire web_fetch as a local tool</title>
<updated>2026-05-16T10:17:21+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T10:17:21+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=99d93203a867294addf4927ceec5644b9d3bf322'/>
<id>urn:sha1:99d93203a867294addf4927ceec5644b9d3bf322</id>
<content type='text'>
Fourth ADOPT entry from `docs/design/gptel-tools-shortlist.org'.
Lets gptel pull a URL into the conversation so the model can read
docs / current API shapes / etc. without me copy-pasting.

Shape:

- URL must be `http://' or `https://' (file://, ftp://, javascript:,
  scheme-less, etc. are rejected at the validator).
- HTML responses go through `pandoc -f html -t plain' so the model
  gets a reading shape that isn't full of markup; falls back to
  `w3m -dump -T text/html' if pandoc isn't on PATH; signals
  `user-error' if neither is.  Pass `raw=t' to skip stripping.
- Output capped at 200KB by default, hard cap 1MB; `max_bytes'
  argument lets the caller pick a lower cap.  Truncation reported
  inline.
- 4xx / 5xx response codes signal `error' with the code -- the
  alternative is returning an error page body, which the model
  would treat as content.

`:confirm t' on the tool because every call is a real outbound
network request.  The tool's description warns that URLs go
wherever the user-agent points, including internal networks if
that's what the URL names.

`tests/test-gptel-tools-web-fetch.el' -- 20 tests across Normal /
Boundary / Error.  URL validator covers http / https / non-string
/ empty / non-http schemes.  `--effective-max-bytes' covers default
/ low-clamp / hard-cap / passthrough.  Truncate helper covers
under-cap / at-cap / over-cap with the marker.  HTML stripper runs
against real pandoc / w3m (both installed in dev env, neither
should mangle simple markup).  Orchestrator stubs
`cj/gptel-web-fetch--retrieve' via `cl-letf' to cover normal /
raw / 4xx / 5xx / oversize / bad-scheme paths.

Wired into `cj/gptel-local-tool-features' so gptel exposes the
tool on next restart.

Note: `call-process-region' invocation flattened to a single
`with-temp-buffer' with DELETE=t -- the initial draft nested a
second temp buffer and routed output to the inner one, which got
killed before `buffer-string' on the outer ran.  Test caught it.
</content>
</entry>
<entry>
<title>feat(gptel-tools): wire git_status / git_log / git_diff as local tools</title>
<updated>2026-05-16T09:26:20+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T09:26:20+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=ceeae9b5e2625e23e6e3792d06a6c8122a36d18b'/>
<id>urn:sha1:ceeae9b5e2625e23e6e3792d06a6c8122a36d18b</id>
<content type='text'>
Three read-only git context tools so gptel can see what's changed
without me pasting `git status` / `git log` / `git diff` output into
every chat turn.  Builds the first batch from the ADOPT bucket in
`docs/design/gptel-tools-shortlist.org`.

Shape per tool:

- `gptel-tools/git_status.el` — `git status --short --branch` for a
  directory inside a git working tree under HOME.  Returns the
  porcelain output, or a "Clean working tree" marker when only the
  branch line is present.
- `gptel-tools/git_log.el` — `git log --oneline -nN` with an optional
  `--since` filter.  N defaults to 20, capped at 100; nil / non-
  integer / out-of-range N falls back to the default.
- `gptel-tools/git_diff.el` — `git diff [REF1 [REF2]] [-- FILE]`.
  Output capped at ~500KB so a runaway diff can't blow up context;
  truncation is reported inline.

Validation is uniform: path must resolve under HOME, must be a
directory, must be inside a git working tree (verified via
`git rev-parse --is-inside-work-tree`).  Color is disabled via
`-c color.ui=false` at the git level (`git status` doesn't accept
`--no-color` directly).

Tests run against real temp git repos created via `process-file`,
not mocked — there's nothing in gptel-tools/git_*.el that's
process-mockable in a meaningful way, and a real `git init` + a
couple of commits is cheaper than building a fake.  31 tests total:
7 for git_status, 11 for git_log, 13 for git_diff.

Wired into `cj/gptel-local-tool-features` so gptel exposes the
three tools on next restart.
</content>
</entry>
<entry>
<title>feat(ai-conversations-browser): dired-style browser for saved GPTel conversations</title>
<updated>2026-05-16T07:00:19+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T07:00:19+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=3f50f682053dd31d5fac96ecdf2b98aad1ce56d7'/>
<id>urn:sha1:3f50f682053dd31d5fac96ecdf2b98aad1ce56d7</id>
<content type='text'>
`cj/gptel-load-conversation` prompts via `completing-read`.  A
dedicated browser shows what each conversation is about at a
glance and supports single-key load / delete / rename without
having to scroll a minibuffer list.

New module `modules/ai-conversations-browser.el` +
`cj/gptel-browse-conversations` entry point bound to `C-; a b`
("browse conversations").  Opens `*GPTel-Conversations*` in
`cj/gptel-browser-mode` (a `special-mode` derivative).

Each row shows date, time, topic slug, and a preview of the most
recent message (length configurable via
`cj/gptel-browser-preview-length`, default 60 chars).  Rows sort
newest first.

In the browser:
- `RET` / `l`: load the conversation (delegates to
  `cj/gptel-load-conversation` with the file pre-selected via a
  `cl-letf` stub on `completing-read` so the user isn't prompted
  twice), then bury the window.
- `d`: delete the file under point after `y-or-n-p` confirmation,
  re-render.
- `r`: rename the file under point.  Preserves the timestamp,
  slugifies the new topic, refuses unchanged input and existing
  targets.
- `g`: refresh.
- `n` / `p`: next / previous row.
- `q`: quit-window.

21 tests cover the helpers (topic parsing, header stripping,
preview shaping for truncate / short / empty cases, row-for-file
with conversation + non-conversation filenames, rows enumeration,
render output for empty + populated cases, newest-first sort,
rename-target preservation of timestamp + slug, rename-target
error on missing timestamp) and the file-touching actions (delete
with y, cancel with n, rename, rename-on-empty-line error).
</content>
</entry>
<entry>
<title>feat(ai-rewrite): add directive-picker wrappers around gptel-rewrite</title>
<updated>2026-05-16T06:55:16+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T06:55:16+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=6ee37e0a68d31909861cf59684d3601bf40f5abe'/>
<id>urn:sha1:6ee37e0a68d31909861cf59684d3601bf40f5abe</id>
<content type='text'>
`gptel-rewrite` is the killer feature for the keep-gptel decision,
and it now lives behind two commands instead of the bare call:

- `cj/gptel-rewrite-with-directive` (`C-; a r`, replacing the
  former bare `gptel-rewrite` binding): completing-read on a
  directive name from `cj/gptel-rewrite-directives`, then rewrite
  the active region.
- `cj/gptel-rewrite-redo-with-different-directive` (`C-; a R`):
  replay the prior region with a different directive.  The region
  is preserved via markers stored buffer-local on the first call so
  it survives accept/reject of the prior rewrite.

I picked the hook injection approach over an `:after`-advice +
state-capture pattern.  `gptel-rewrite-directives-hook` is an
abnormal hook gptel-rewrite already consults for a per-call
system message.  Wrapping the call in a one-shot `let`-binding on
that hook gives the directive exactly the lifetime of the rewrite
and leaves nothing to clean up.  Mutating `gptel-directives`
globally would mean either restoring it afterward or living with
the change -- both worse than the hook.

Directives ship inline as a `defcustom` alist with the six names
called out in the task -- `terse`, `fix-grammar`,
`refactor-readability`, `add-docstring`, `explain-as-comment`,
`shorten`.  Customization is a `customize-variable` or `setq`
away.

9 tests cover the defcustom shape (default names present, bodies
non-empty strings), the wrapper (normal path, no-region error,
unknown-directive error, last-state recording), and the redo
(replays the prior region, errors when no previous, excludes the
current directive from the re-pick prompt).  `gptel-rewrite`
stubbed in tests so no rewrite UI fires.
</content>
</entry>
</feed>
