1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
#+TITLE: Design: gptel git tools on a magit backend
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-16
* Status
Draft. Supersedes the three current git-tool implementations
(=gptel-tools/git_status.el=, =gptel-tools/git_log.el=,
=gptel-tools/git_diff.el=) shipped in commit =ceeae9b5=. Trigger:
Craig flagged that magit already does much of this and could carry
the backend for more git tools cheaply.
* Problem
The three current git_* tools shell out to git directly via
=process-file= and parse stdout. Each carries:
- Its own =--is-inside-work-tree= path-validation step.
- Its own =-c color.ui=false= color suppression workaround (`git
status' doesn't accept =--no-color= the way `git log' / `git diff'
do).
- Boilerplate to set up a temp buffer, run =process-file=, capture
output, return the string.
There's also an opportunity cost: adding more git context tools
(=git_blame=, =git_show=, =git_branches=, etc.) would mean
duplicating the same boilerplate per tool.
* Wins from a magit backend
Three concrete things magit provides:
1. *Path validation via =magit-toplevel=.* One call replaces the
two-step =process-file= + =rev-parse --is-inside-work-tree=
check. Returns the working-tree root or nil.
2. *Process plumbing via =magit-git-insert= / =magit-git-string= /
=magit-git-lines=.* These wrap git invocation with magit's
environment, encoding handling, and the right color posture.
Drops the per-subcommand color-flag bikeshedding.
3. *Typed helpers for higher-level concepts* -- =magit-get-current-branch=,
=magit-list-branches=, =magit-rev-ancestor-p=, etc. Most
relevant for the *new* tools (branches, show, blame), not the
three we already wrote.
What magit doesn't give us: high-level "give me status as a string"
helpers. =magit-status= / =magit-log-current= etc. populate
interactive magit buffers, not strings. For tool output we'd still
call =magit-git-insert "status" "--short" "--branch"= and grab the
buffer string. Same shape, less boilerplate.
* Costs
- *Magit loads on first invocation* of any git_* tool. Magit pulls
in transient, with-editor, magit-section, magit-core -- heavyweight.
Mitigation: lazy =(require 'magit)= inside each tool's function
body so cold-start Emacs sessions don't pay the cost unless the
user actually calls a git tool.
- *Tools no longer portable* to a no-magit Emacs. Acceptable here
because magit is a non-negotiable in this config; a future
drop-in distribution would need to publish a magit-free fallback.
* Proposed shape
** Single-file module: =gptel-tools/git_tools.el=
The current "one file per tool" convention exists because the
existing tools share little. These six tools share a lot
(validate-path, run-git, truncate-output), so a single file with
shared helpers is more honest.
** Shared helpers
- =cj/gptel-git--toplevel-or-error PATH=
- Wraps =magit-toplevel=. Signals =user-error= when PATH escapes
HOME, doesn't exist, or isn't inside a working tree.
- Returns the resolved working-tree root on success.
- =cj/gptel-git--insert ARGS...=
- Wraps =magit-git-insert= in a =with-temp-buffer=, returns
=buffer-string=. Single chokepoint for color / encoding / error
handling.
- =cj/gptel-git--truncate TEXT MAX-BYTES=
- Caps output, appends a one-line truncation marker when
triggered.
Open question: consolidate the matching helper from =web_fetch.el=
(=cj/gptel-web-fetch--truncate=) and the
=cj/update-text-file--*= analogue into a shared
=cj/gptel-tools--truncate-bytes= in =system-lib.el=, or keep
per-tool.
** Six tools
| Name | Magit-flavored shape |
|------------------+--------------------------------------------------------------------|
| =git_status= | =magit-git-insert "status" "--short" "--branch"= |
| =git_log= | =magit-git-insert "log" "--oneline" (format "-n%d" N) ?--since= |
| =git_diff= | =magit-git-insert "diff" REF1 REF2 "--" FILE= (each optional) |
| =git_blame= | =magit-git-insert "blame" "--line-porcelain" FILE [-L S,E]= |
| =git_show= | =magit-git-insert "show" REF= (message + full diff) |
| =git_branches= | =magit-list-branches= (optionally filtered by =--list PATTERN=) |
Each tool:
- Validates =path= via =cj/gptel-git--toplevel-or-error=.
- Calls =cj/gptel-git--insert= with the appropriate args.
- Truncates via =cj/gptel-git--truncate=.
- Registered as a separate tool with =gptel-make-tool= for
description / argv clarity at the model side.
** Caps
| Tool | Default cap | Hard cap |
|---------------+----------------+--------------|
| =git_status= | uncapped | uncapped |
| =git_log= | 100 commits | 100 commits |
| =git_diff= | 500 KB | 500 KB |
| =git_blame= | 500 KB | 500 KB |
| =git_show= | 500 KB | 500 KB |
| =git_branches=| uncapped | uncapped |
=git_log='s cap is on commit count; the rest cap output bytes.
** :confirm posture
All six tools are read-only. Same posture as the current
implementation: =:confirm nil= (the model can call them
autonomously, since they can't mutate state). The current
git_status / git_log / git_diff already ship with =:confirm nil= --
keeping it.
** Tests
Single file =tests/test-gptel-tools-git-tools.el=, replacing the
three current per-tool test files. Real temp git repos via
=process-file= (same pattern as current tests). Coverage per tool:
Normal / Boundary / Error.
Rough count: ~12 shared-helper tests (validator, insert wrapper,
truncate) + ~7 per tool × 6 tools = ~54 tests total.
* Migration
1. Delete =gptel-tools/git_status.el=, =git_log.el=, =git_diff.el=.
2. Delete =tests/test-gptel-tools-git-status.el=,
=test-gptel-tools-git-log.el=, =test-gptel-tools-git-diff.el=.
3. Create =gptel-tools/git_tools.el= containing all six tools +
shared helpers.
4. Create =tests/test-gptel-tools-git-tools.el=.
5. Update =cj/gptel-local-tool-features= in =modules/ai-config.el=:
replace the three =git_*= symbols with one =git_tools= symbol
(or six if each tool wants its own feature file -- decide during
implementation).
6. Make sure =modules/ai-config.el= can re-load without breaking the
live gptel session if the old tool symbols are still registered
from a prior Emacs.
* Risks
| Risk | Mitigation |
|-------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------|
| Magit load slows first git_* tool call | One-time hit per session, scoped to the tool's :function body. Acceptable for opt-in tools. |
| Tool registration name collision with the old git_* symbols | Use distinct names (git_status / git_log / git_diff stay; new tools join them). Or remove + restart. |
| =magit-toplevel= behavior on TRAMP / remote paths | Validator rejects paths outside HOME first, so TRAMP paths can't reach magit-toplevel. |
| =git_blame= exposes code surfaces the model shouldn't read | =:confirm nil= is the wrong posture if blame is sensitive. Open question for review. |
| =git_show= reveals past-self commit message wording | Same as blame -- low risk on personal repo, but worth flagging. |
* Open questions
1. Build all six tools in one push, or phase status/log/diff first
and add blame/show/branches in a follow-up? My read: one push.
The helpers are shared, marginal cost of three more tools is
small, and the model gets meaningfully more useful git context.
2. Consolidate the output-truncation helper into =system-lib.el=,
touching =web_fetch.el= and =update_text_file.el= for a cleaner
API? Or defer that to a separate refactor commit?
3. =git_blame= and =git_show= -- =:confirm nil= or =:confirm t=?
Personal repo lowers the stakes but the model could ask for
blame on /any/ file under HOME.
4. Tool feature symbols: one =git_tools= entry in
=cj/gptel-local-tool-features=, or six (one per tool)?
Currently each tool lives in its own provide-symbol file. With
the single-file design we'd register one feature symbol that
loads all six.
* Effort estimate
M (1-3 hours). Helpers + six tool wrappers + ~50 tests + migration.
Most of the time is test authoring; the production code is small
because magit absorbs the boilerplate.
|