# Glean Review

> Reading and responding to comments in an open glean review (the neovim git diff reviewer). Use this when the user pastes a glean review snippet, asks you to answer review comments, or mentions a `Glean:` buffer.

- Skill: `dlants/glean-review` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add dlants/glean-review`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dlants/glean-review/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: dlants (https://skillmd.com/u/dlants)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dlants/glean-review

---


# glean review comments

glean renders a git diff into a neovim buffer. The human leaves comments on
lines; you answer them. Drive it with the `nvim_lua` tool through
`require("glean.api")` — everything crosses the boundary as plain strings and
numbers, so each call is self-contained and there is no handle to keep.

## The model

Comments live in the repository, not in the review, so every call below also
works with **no review open** — address the repo instead of a session.

A comment has exactly two slots:

- `text` — the human's words. **You never edit or delete this.** You may leave
  your *own* comment with `add_comment`, but a human's comment is theirs.
- `reply` — your answer. At most one per comment, and replying again replaces
  the previous one, so re-running over the same review is idempotent. `reply`
  is `nil` when the comment is unanswered — that is your work queue.

Each comment also reports `id`, `path`, `lnum` (where it currently resolves),
`side` (`"new"` for adds/context, `"old"` when every captured line is a
deletion), `content` (the block of lines captured when the comment was written,
as a list), `code` (the same block joined), `state`, `outdated` and `origin`.

`state` is where the comment resolves right now:

- `"diff"` — its lines are in the review's diff; `lnum` is the diff-side line.
- `"file"` — its lines are not in the diff but are in the working-tree file;
  `lnum` is the file line. The comment is live and correctly located, just on
  lines this review does not touch. (Repo-mode calls, which have no diff,
  report `"file"` for everything that still matches.)
- `"outdated"` — the block matches neither, i.e. the code moved out from under
  the comment. `lnum` is only the last place it was seen; say so in your reply
  rather than guessing. `outdated` is the boolean form of this state.

`origin` is `{ sha, dirty }`: which version of the file the human was looking at
when they wrote the comment. `dirty = true` means "that commit plus uncommitted
edits". When a comment is outdated, `origin.sha` is what to `git show` to see
the code they meant.

## Recovering ids from a paste

Two ids address any comment, and both are visible in the review buffer, so a
pasted snippet is a sufficient address.

- **Session id** — in the buffer name: `Glean:g2 glean main [8df5247a..09997456]`
  → the session is `"g2"`. Pass `nil` when only one review is open.
- **Comment id** — the bracketed token that opens a comment row: `💬 [7] why is
  this needed?` inline in the diff, or `[7] L51  <code preview>` in the
  comments summary at the bottom → the comment is `7` (a number, not a string).
  Your reply renders beneath the comment as an indented `↳` block.

## Entry points

List the open reviews:

```lua
return require("glean.api").sessions()
-- { { id = "g1", repo = "/path/to/repo", base = "...", target = "...",
--     scope = "combined", title = ".../Glean:g1 repo main" } }
-- `title` is the raw buffer name, so it may carry a directory prefix; the
-- session id is the `g<N>` right after `Glean:`.
```

List comments (`session` may be `nil` with one review open):

```lua
return require("glean.api").comments("g1")
```

Just the ones needing an answer, optionally scoped to a file:

```lua
return require("glean.api").comments("g1", { unanswered = true, path = "lua/glean/init.lua" })
```

Answer one, and clear an answer:

```lua
return require("glean.api").reply("g1", 7, "Yes — the guard is needed because ...")
```

```lua
return require("glean.api").unreply("g1", 7)
```

With no review open — or to address a repo rather than a review — pass
`{ repo = "/path/to/repo" }` in the session slot (`repo` defaults to the cwd):

```lua
local api = require("glean.api")
api.comments({ repo = "/path/to/repo", unanswered = true })
api.reply({ repo = "/path/to/repo" }, 7, "...")
api.unreply({ repo = "/path/to/repo" }, 7)
```

Leave your own comment on a file, with or without a review open. `path` is
repo-relative and `lnum`/`end_lnum` are working-tree line numbers, so comment on
what you just read off disk. The captured lines become the comment's identity,
so it follows the code as it moves. Returns the new comment id:

```lua
return require("glean.api").add_comment({
  repo = "/path/to/repo", path = "lua/glean/init.lua", lnum = 51, end_lnum = 53,
  text = "This retries forever if the remote is down.",
})
```

Your comment shows up as a `💬` sign in the human's file buffer and in any open
review. Use it sparingly, for findings they asked for — it is their review queue.

Replies are undoable (`u` in the review buffer), persist to glean's store, and
re-render immediately, so the human watches your answers land live.

## Hunks

The same api exposes the review body, so you can read the diff and mark it seen
the way the human does with `m`.

```lua
return require("glean.api").hunks("g1", { mode = "combined", path = "lua/**/*.lua", limit = 20 })
```

- `mode` — `"combined"` (default; the whole base..target diff, "what changed
  overall") or `"commits"` (the same review commit by commit). Independent of
  the scope the human is viewing, so asking for one does not disturb their
  buffer.
- `path` — a glob over the file path (`"lua/**/*.lua"`, `"*.txt"`).
- `seen` — `true` / `false` to filter; omit for both.
- `limit` (default 20) and `cursor` — paging. The result is
  `{ hunks, cursor, total }`; pass the returned `cursor` back to get the next
  page and stop when it comes back `nil`. `total` counts the matches after the
  cursor, ignoring `limit`.

Each hunk is `{ id, mode, sha (commits mode), path, kind, header, old_start,
old_count, new_start, new_count, seen, adds, dels, unseen_lines, lines }`, and
each entry of `lines` is `{ i, kind, lnum, side, text, seen }`. `i` is the line's
address within the hunk; `seen` is `false` for context lines, which are not
markable. `seen` on the hunk is true only when *every* changed line is seen.

`id` is positional (`"b:000002:000001"`), so it is only valid for the review as
it stands now: page and then act, don't stash ids. `mark` re-resolves and errors
rather than marking the wrong hunk.

Mark whole hunks, or specific lines within one:

```lua
local api = require("glean.api")
api.mark("g1", id)                            -- the whole hunk, like `m`
api.mark("g1", { id = id, lines = { 3, 4 } }) -- those lines, like a visual mark
api.mark("g1", { id1, id2 })                  -- a batch: one undo step
api.mark("g1", id, false)                     -- unmark
```

Returns `{ hunks, lines }` — selectors applied, and identities whose seen state
actually flipped (`lines = 0` means it was already in that state). Marks are
undoable with `u`, persist, and re-render live, exactly like the human's.

Only mark when the human asks you to triage; seen-ness is their reading
progress, not yours.

## Errors

Every call errors loudly rather than no-opping:

- no review open and no `repo` → the call falls back to the repo containing the
  cwd; if that is not a git work tree it errors. Do not open a review yourself —
  ask the human, so you answer exactly what they are looking at.
- several reviews open and no `session` → the error lists the candidate ids with
  their repo and range; pick one.
- unknown comment id → re-list with `comments()`; ids are per repo.
- `add_comment` with an unknown path, an out-of-range line or empty text errors.
- `reply` with an empty or non-string text errors; use `unreply` to clear.

## Working a review

1. `comments(session, { unanswered = true })` to get the queue.
2. For each, read the code around `path`:`lnum` before answering — `code` is
   only the anchored block, not the surrounding context.
3. `reply(session, id, ...)` one at a time; keep answers short, since they
   render inline in the diff.

