Skip to content

Buddy

Buddy reviews recent edits and adds short advisory notes beside source code. Enable it per buffer or globally, request an immediate review, or use the guidance command for suggestions about a region. Notes can be ignored or dismissed; they do not become instructions or edit your code.

M-x mevedel-buddy-mode        watch this buffer
M-x mevedel-buddy-global-mode watch every buffer in `mevedel-buddy-tracked-modes'
M-x mevedel-buddy-review      review now, without waiting for the idle timer
M-x mevedel-buddy-guide       ask what to build here, rather than what went wrong
M-x mevedel-buddy-dismiss-note   dismiss the note at point
M-x mevedel-buddy-dismiss-notes  dismiss every note in this buffer
M-x mevedel-buddy-clear-changes  forget the edits recorded so far
M-x mevedel-buddy-abort          abandon a review that has wedged

Two channels

Buddy runs one reviewer and one advisor over shared machinery. They differ in when they fire, what they are sent, and what the prompt permits.

automatic review mevedel-buddy-guide
Trigger idle timer, unasked you invoke it
Input consolidated diff of recent edits region, or whole buffer
Uses change tracking yes no
Requires mevedel-buddy-mode yes no
May speculate no yes
Default when unsure stay silent say something

They are separate because their noise economics are opposite. Unasked review must default to silence; comment on everything and the user disables it within a day. Guidance that defaults to silence is worthless, because speculation is the product. One always-on prompt cannot be tuned against both targets, so the automatic channel stays strict and guidance is a command.

Tools, note records, overlays, ids, severity, dismissal, and the request path are identical between them. Every request carries a bounded set of active and dismissed notes from its scope, so a guidance note raised on a sketch is visible to the review that runs later, once the code exists, and can be retracted once you have acted on it. Guidance opens a thread; review closes it. Do not partition notes by originating channel — the composition is the point.

Guidance cancels its source buffer's queued automatic review and preempts one already in flight, abandoning it without recording its changes as reviewed. The request you made outranks the one a timer made.

When the buddy workload resolves to Claude Code, Buddy starts an isolated ACP conversation and exposes only its captured note tools over a private MCP connection. The same note handlers enforce buffer scope and review ownership. The native post-tool-batch hook checks the iteration limit before further model work. Abandonment preserves unreviewed changes and closes the tool scope.

Scope

Buddy is workspace-scoped, not buffer-scoped. Changes are keyed by the project root when the buffer has one, and by buffer name otherwise, so edits spread across several files of one project are reviewed together. That is what lets the model see a caller and its callee change in the same round.

Buddy uses no session. It derives its workspace from the source buffer, the way directive dispatch already does, so it works on a file you have not started a conversation about. A live session is used only to attribute telemetry when one happens to exist.

Workspace detection reads default-directory, not buffer-file-name, so a brand-new unsaved buffer in a project still gets AGENTS.md and persistent memory in its prompt. That matters most for guidance, where "what should this module need" depends on project conventions.

The cached scope is bound to the buffer name, visited file, and default-directory that produced it. Changing any of them discards that buffer's old edit records and resets its pending review timer before deriving the new scope. A buffer cannot carry one project's edits or prompt context into another project merely by changing what it visits.

Everything is ephemeral

Notes, dismissals, recorded edits, note ids, and the last-reviewed time live in memory and die with the Emacs process. Buddy does not persist that state, write curated memory, or insert notes into a session transcript. It can record diagnostic lifecycle events through an available session's telemetry writer. Later reviews derive notes from the source they are shown.

To make durable guidance, edit the applicable AGENTS.md or explicitly request a memory update through the ordinary workflow. Buddy's passive request has no memory-saving policy.

Dismissed notes remain eligible for the bounded in-process note context, labelled as rejected advice. That evidence helps the model avoid repeats; it does not guarantee suppression, and disappears with the Emacs process.

Notes are not instructions

Instruction enumeration selects on the mevedel-instruction overlay property. Buddy notes never set it, so they are structurally invisible to instruction navigation, tinting, persistence, deletion, and subdirective resolution. No third instruction type exists and no instruction code path knows about notes. See ADR 0108.

What a review looks at

A review covers the region around your change, not only the lines you touched. The payload carries six lines of context either side of each change, and every numbered line in it can carry a note — so a bug sitting next to an edit gets named even though you did not introduce it this round.

It does not stop there. read_buffer returns a bounded range of a buffer already in the review, for a question the diff raised but cannot settle — a signature to check, a declaration further up. Both bounds are required and one call returns at most mevedel-buddy-note-read-limit lines. An unbounded read would ship a whole file to the provider on an idle timer, for a one-line edit, through a tool that takes no permission step.

Lines returned by read_buffer are annotatable, so notes can address evidence beyond the original diff while remaining within the captured buffer scope.

Findings need concrete evidence and enough value to justify an inline note; no notes is a valid result. Severity describes the actual impact, including findings a linter or compiler could also report. Whether you ever see it is mevedel-buddy-severity-floor's job, and that is yours to set — at trivial you get cleanups and checkdoc-style remarks along with everything else, which is a perfectly reasonable way to run it.

Reviewed edits are retired

Review preparation runs in scheduled steps, assembling one source buffer's diff per callback before sending the request. The one-review slot covers preparation as well as the provider request. Editing, renaming, killing, or untracking a source during preparation abandons that snapshot without retiring pending edits. Guidance and abort can cancel preparation through the same review ownership. Each individual buffer diff still runs synchronously; an unusually large file can exceed the normal callback budget.

A settled review discards the changes it covered, so the next one sends only what you have written since. Edits made while a request was in flight survive it, and an abandoned or timed-out review retires nothing, so its edits are offered again rather than silently skipped.

A review that never settles is abandoned after mevedel-buddy-timeout, which releases its markers and frees the one-at-a-time slot. mevedel-buddy-abort does the same by hand. Abandoning also retires the review's generation, so a callback racing with cancellation settles into nothing rather than retiring changes it never got notes for. Each review uses its own hidden live request buffer as the exact gptel-abort identity, so cancellation cannot select an ordinary request from the source buffer and still works after that source dies.

Turning mevedel-buddy-mode off in a buffer, or disabling mevedel-buddy-global-mode, discards that buffer's recorded edits along with the tracking. Pending unreviewed feedback is lost, deliberately: without the tracking hooks nothing would drop those records later, and a buffer that reused the name — reopened, or uniquified once a second file of that name is visited — would have their offsets replayed against unrelated content.

Scope is an allowlist, and empty means nothing

While a review runs, mevedel-buddy-note--scope-buffers maps each allowed name to the exact live buffer the review may touch, and every tool — read_buffer, add_note, update_note, remove_note — checks it. The note set described to the model is filtered the same way, so a review of one project never sees another's buffer names, line numbers, or note text.

Names are only model-facing addresses. Killing an allowed buffer and creating another under the same name does not transfer the old review's authority to the replacement.

An empty scope denies everything. That matters because a tool call can still arrive after a review is abandoned or times out, and "no review is running" must not read as "every buffer in Emacs".

How a note is laid out

A note's text is worth more at the end than the beginning — the observation comes first, the reason it matters comes last — so truncating one loses the half that justified interrupting you. But laying every note out in full turns a busy buffer into a wall of blocks.

Buddy picks the style per line. By default, notes on the current line show in full and notes on other lines use a compact end-of-line preview.

default shows
mevedel-buddy-note-current-line-style below the whole note, wrapped, indented under the code
mevedel-buddy-note-other-lines-style eol one line after the code, shortened to fit

Either may also be nil to hide notes on those lines. Setting mevedel-buddy-note-other-lines-style to nil annotates only the line at point, the way Neovim and Helix show diagnostics.

Layout uses mevedel-buddy-note-width — a fixed column budget, not the window width. An overlay's after-string is shared by every window showing its buffer, so a layout fitted to one window would be wrong in another and wrong again after a split. Truncation is acceptable in eol precisely because the full text is one cursor move away.

Notes are laid out again from post-command-hook, which does nothing unless point actually changed line, and the hook is removed from a buffer once it holds no notes.

sideline (flush right, lsp-ui-sideline style) is deliberately not implemented: it is the one style that needs window geometry, and below plus eol already solve the problem.

Line numbers resolve through markers

The model answers with the line numbers it was shown, but you keep typing while the request is in flight. Buddy captures markers before sending and resolves the model's line numbers against them, so a note lands on the text it was written about even when lines were inserted above it meanwhile. Markers move with buffer edits; raw line numbers do not.

Only the lines the model was actually shown are marked. Emacs walks a buffer's marker list on every insertion, so marking every line of a large file would make typing lag for as long as the request runs.

A new note requires the live captured boundaries for that shown line. Unshown, deleted, nonpositive, out-of-range, and already released line numbers are rejected rather than falling back to current raw line counting. A note on the wrong line is worse than no note, so this is not an optional refinement.

read_buffer registers markers for the lines it returns, which is what makes them annotatable. A line already marked keeps its original markers: recapturing one would move an existing note's anchor to wherever that number points now, which is the failure markers exist to prevent.

After the initial payload markers are captured, reads do not grow the set past mevedel-buddy-note--marker-ceiling. A review may issue several reads per round and several rounds, and without a ceiling the marker set would grow with what the model asked for rather than with what the user edited. A read stops before the first line whose marker would exceed the ceiling, so every line it returns remains annotatable. Initial diff and guidance markers are not truncated; they are the authority the request started with rather than model-requested growth.

If editing moves an older marker so that its numeric key now names different text, a later read stops before reusing that ambiguous number. The next review supplies fresh line numbers. Preserving the older marker keeps a note about the original diff line correct; returning new text under the same number would not.

Model selection

Buddy resolves the buddy entry in mevedel-model-workloads, defaulting to the fast tier. Retier it, pin an exact provider, or override it per preset through the usual :model-workloads key:

(setf (alist-get 'buddy mevedel-model-workloads)
      '(:provider "Ollama:qwen2.5-coder"))

The selected provider receives the captured source evidence when Buddy runs. Automatic requests follow the idle and minimum-interval settings below. A pause that comes too soon after the scope's last automatic review is not dropped: the review waits until the interval has passed, unless a further edit restarts the idle delay first.

Request boundary

Buddy creates request-local tools for bounded reads and note maintenance. They are not registered as general tools and do not use the ordinary mutation pipeline. A response without another tool call ends the review; there is no end tool or forced tool choice. Buddy has no auto-fix tool: code edits use the normal patch and permission workflow. The port's rationale is recorded in ADR 0108.

Configuration

Option Default Meaning
mevedel-buddy-tracked-modes (prog-mode text-mode conf-mode) Modes the global mode watches
mevedel-buddy-idle-delay 10 Seconds idle before an automatic review
mevedel-buddy-min-interval 60 Least seconds between automatic reviews of one scope; edits settling sooner are reviewed once it passes
mevedel-buddy-coalesce-window 180.0 Seconds within which nearby edits merge into one record
mevedel-buddy-severity-floor "significant" Lowest severity the model is asked to report
mevedel-buddy-max-iterations 8 Tool rounds before a review is abandoned
mevedel-buddy-timeout 120 Seconds before a review that never settles is abandoned
mevedel-buddy-note-serialize-limit 40 Notes described to the model per request
mevedel-buddy-note-read-limit 200 Lines one read_buffer call returns
mevedel-buddy-note-width 72 Column budget for laying a note out
mevedel-buddy-note-current-line-style below Style for the line at point
mevedel-buddy-note-other-lines-style eol Style for every other line

mevedel's own surfaces — view, chat, cockpit, inspector — are excluded from tracking regardless of their major mode.