Every item surfaces at least once#
A natural design for a review-comment bot is to only show what's still "live" — filter out anything resolved, outdated, or minimized, and present a clean list of open items. On a PR that's had real review, that filter throws away information the agent needs:
Every review thread and PR comment must be surfaced to the agent at least once, even if it is outdated, resolved, or minimized. Filtering those out before the agent sees them silently discards reviewer intent.
An outdated thread might still describe a real problem the diff moved past without fixing. A resolved thread records what a human already decided. Dropping either before the agent ever sees it isn't tidying — it's deleting review history the agent has no other way to recover.
One scope note: a COMMENTED or APPROVED review summary that is already minimized
before Shepherd ever observes it is filtered out upstream, ahead of the seen-marker
gate — so the at-least-once guarantee below covers active, outdated, resolved, and
later-minimized items, but not a summary that arrived pre-minimized. That gap is
documented as future work, not silently accepted.
How "at least once" is enforced#
Every review thread, comment, and review summary carries a status: outdated, resolved,
or minimized. The first time Shepherd surfaces an item, it writes a small per-item
marker to local state — one file per id, its existence the marker itself. The filename is
the SHA-256 hash of the item's id, not the id itself, so it stays filesystem-safe and
case-collision-free regardless of what GitHub's ID looks like:
$PR_SHEPHERD_STATE_DIR/<owner>-<repo>/<pr>/seen/<sha256-of-id>.json
The marker's JSON body is intentionally minimal and intentionally open-ended, and carries the original id so lookups can key off it rather than the hashed filename:
{ "id": "IC_kwDOSGizTs8AAAABSsxwqg", "seenAt": 1732000000, "bodyHash": "3f9a1c2b7e4d5a6f" }
bodyHash is a truncated SHA-256 of the item's body. On every later fetch, if the
stored hash no longer matches the item's current body, Shepherd re-surfaces it as
edited — under a dedicated section, with the original seenAt preserved — instead of
treating "already seen" as "nothing left to say."
Why the format stays this simple#
The schema is deliberately non-clever:
One file per id — file existence is the marker... Do not adopt formats that lock the schema (empty touch files, a single shared list).
A single shared list would need locking to be safe under concurrent Shepherd runs on the same PR; one file per id doesn't. Under a genuine race, the last writer's hash wins — which is fine, because both writes describe valid current state, and the marker's job is only to suppress repetition, never to gate whether an item was safe to show in the first place.
The gate this creates, everywhere else#
Any code path that would filter threads or comments by isResolved, isOutdated, or
isMinimized has to route through this seen-marker gate before it can suppress anything —
suppression is never allowed to be the first thing that happens to an item. Debounce ticks
follow the same discipline in the other direction: they defer writing seen markers until
the settle window closes, so a comment that arrives mid-debounce isn't marked seen before
the agent-facing result that should have included it.