Arrange Act Assert

Jag Reehal's thoughts on things, mostly product development

Shepherd fixes the PR, Stamp approves it

09 Oct 2026

Shepherd and Stamp take a pull request from opened to approved.

Shepherd runs in your coding agent, reviews the change, fixes what it can and repairs the build.

Stamp runs in CI and decides whether the result earns an approval.

A Lego shepherd with a wrench fights red bugs on a PR platform beside a board of passing unit tests, lint and security checks, then sends a green PR brick across a bridge to a CI platform, where a second figure holds a red APPROVED stamp

I give approval to a separate reviewer. You keep the decisions that need a person.

PostHog’s list of AI coding mistakes argues that lint, types and tests matter more once an agent writes the code. Shepherd and Stamp run those checks inside the pull request loop.

I built Shepherd to prepare the change and Stamp to decide approval. Shepherd can push a fix, but it cannot grant the approval that lets you merge.

---
alt: "Shepherd runs review rounds inside a coding agent: swarm reviews, triage fixes or defers threads, and simplify checks the result. After a round with no changes or new findings, Shepherd repairs CI and reads Stamp’s verdict. Both tools record their work on GitHub."
---
flowchart LR
  subgraph inner["Shepherd: in your coding agent"]
    S["Swarm<br/>reviews the diff"] --> T["Triage<br/>fixes or defers threads"]
    T --> SI["Simplify"]
    SI --> S
  end
  inner --> C["CI repair"]
  C --> ST["Stamp in CI<br/>gates + model review<br/>approves or refuses"]
  ST --> GH[("GitHub<br/>threads, commits,<br/>statuses, verdicts")]
  inner --> GH
Figure 1: Shepherd gets the PR ready, and Stamp decides whether it gets approved.

How Shepherd works

Shepherd is a set of skills that runs inside a coding agent such as Claude Code or OpenCode. You install it with bunx @jagreehal/shepherd install, or with npx skills add jagreehal/shepherd for any agent the skills CLI supports, and run /shepherd 42 on a PR. One iteration works like this:

  1. Swarm runs the repository’s own lint, typecheck and nearby tests first, plus any scanners you have installed, such as gitleaks, semgrep and osv-scanner. A cheap model then reads the whole diff and hands the risky hunks to specialist lenses (correctness, security, simplicity, maintainability, slop, and any your team adds), which run in parallel. For a PR that closes an issue, a spec lens checks the diff against what the issue asked for. A second model checks each high-severity finding against the code before it posts.
  2. Triage works through the open review threads. It fixes the concrete ones, resolves the ones it can show are mistaken, and leaves the decisions that belong to the author. Threads another person has joined stay with that person.
  3. Simplify checks the result for code the change no longer needs. A fix gets another review round.
  4. After a round with no changes or new findings, CI repair brings the branch up to date and fixes what the PR broke. Shepherd then reads Stamp’s verdict. The loop allows up to four review rounds before handing unresolved work back to you.

Wrap it in /loop 5m /shepherd 42 and you can leave it running. Shepherd never replies to people and never approves.

Set the checks and the fix mode

Your team can set the checks, reviewers and fix permissions in .shepherd/config.yml. A fresh shepherd init starts in review-only mode. Choose init --preset local-fix to leave fixes in your working tree, or init --preset push to let Shepherd commit and push them.

You can extend an organisation’s policy pack and override settings for one repository. A pack from another repository needs a full commit SHA. Shepherd reads team policy from the default branch, so a PR author cannot use their change to remove a reviewer or relax the fix rules.

Before merging a policy edit, run:

bunx @jagreehal/shepherd config validate
bunx @jagreehal/shepherd preview --working-tree --recipe pr

The preview lists the checks and models, the steps that can write code or touch GitHub, and the decisions left to a person. You can keep using the older .shepherd/lenses.yml configuration.

How Stamp decides

Stamp runs in CI. bunx @jagreehal/stamp init writes its policy and workflows into your repository. Stamp checks deterministic gates first: sensitive paths, size limits, credentials on added lines, test deletions and skip markers, and who wrote the PR. A model review with read-only access to the checkout judges what the gates allow, and reads the issues the PR closes to check the diff does what they ask. The model can refuse a change the gates passed, and only the gates can make a change eligible. Stamp posts an approval as a GitHub review pinned to the reviewed commit, so it counts toward branch protection. Before posting, it checks that the head and base match the change it reviewed.

The model review runs through the AI SDK, so you choose the provider with one variable: STAMP_MODEL=bedrock:zai.glm-4.7-flash, opencode-go:kimi-k3, openrouter:moonshotai/kimi-k3, or a Claude model on Anthropic.

The model reads the checkout through three tools, read_file, grep and glob, confined to the repository, and ends the review by calling submit_verdict, whose input schema is the verdict. Every provider I tested supports tool calling, so one verdict format works across them. On Bedrock, Stamp signs with an API key or your AWS credentials, including a named profile. In my tests the models read the code before deciding and returned a valid verdict through the tool. A model that answers in prose gets one follow-up call that must be submit_verdict.

Stamp records each decision at the point it makes it, as I argued in You Can’t Support What You Didn’t Record, so every review leaves a run record. A Kimi K3 review of a one-line change made five tool calls, read 22,656 of its 33,580 input tokens from cache, and took 57 seconds. Limits from autotel’s guard bound each review: $2 of spend, three million tokens, a spin-loop rule, a tool-call cap, and a 15-minute timeout that cuts off a model call in flight. Set an OTLP endpoint and each review becomes a trace, with one span per model call and tool call.

Teams that prefer Claude Code or Codex can run those as the reviewer instead. Stamp runs them in a pinned container with a read-only copy of the checkout and only their own credential, on a private network whose one route out leads to their own model provider. The egress proxy opens a tunnel only when the TLS handshake inside it names that provider’s host, so a reviewer can’t reach another site that shares the provider’s CDN address.

Read what the tests checked

Repositories that use executable-stories give Shepherd more than a test exit code. Shepherd reads each failed scenario’s steps, expected and actual values, and covers paths. It reruns the affected scenarios after a fix. For a skipped scenario, triage restores the test and fixes the code if that test fails.

Shepherd compares runs of the same scope. A focused run leaves out tests it did not target, so you can’t read a test missing from it as one the PR removed. For an incident or a linked issue, Shepherd runs executable-stories goal with the issue’s ticket against full current and base runs. The command checks that the required scenarios pass and flags scenarios the change dropped, skipped or shortened.

Stamp can read the review report CI uploads:

# .stamp/policy.yml
stories:
  workflow: ci.yml
  artifact: executable-stories
  file: index.review.json

The report needs executable-stories-formatters 1.27 or later to record the commit where the tests ran. Stamp checks that commit against the PR head, or a merge whose parents match the reviewed base and head. It labels filtered runs, and reports stale or missing evidence. A comparison label such as headRef cannot establish where tests ran.

The PR author wrote those tests. Stamp keeps the report inside its untrusted input and uses it as review context. A passing scenario, a declared covers path or a trace grants no exemption from a gate and supplies no independent reviewer approval.

See where each PR stands

Shepherd sets a shepherd commit status on the head at the end of each iteration:

The status lives on GitHub, so it outlasts the session that set it. You can come back to a PR hours later and read what it needs from you without opening a transcript. A push Shepherd hasn’t seen arrives with no status at all, so a branch rule that requires the status keeps the PR from merging until Shepherd has looked at it.

Stamp handles description edits in a separate workflow, so the stamp / review check on each commit shows the verdict that counts.

On a re-review, Stamp withdraws its standing approvals before fetching the PR or taking a skip path. One exception keeps an approval when a base merge leaves the PR’s diff byte-identical and today’s policy allows it. After posting, Stamp checks for a changed head or base and a verdict from a later-started run. An older run cannot leave its approval standing over a newer refusal.

You can trace a Shepherd iteration through autotel. With an OTLP endpoint configured, Shepherd exports a span for the iteration and one for each recorded stage, including the wait for Stamp. Repository, PR and commit fields let you correlate that work with Stamp’s review. Separate processes can link their spans through traceparent values. The CLI records stage timing and outcomes; your coding-agent harness must supply model usage.

The gardener can read those traces from OTLP JSON exports, Grafana Tempo or Datadog. It flags failed stages, repeated retries, long approval waits and high Stamp review cost for investigation. It matches each sample to a known PR commit and reports gaps in collection or pricing. You can follow a proposal back to its trace ID and GitHub evidence; the trace alone cannot count a fix as accepted.

Run the loop before you open a PR

You can run the same review on your machine before GitHub sees the change. /shepherd --local captures your unpushed commits, staged and unstaged edits and untracked files, then runs the same lenses, verification and fix rules as the PR loop. Fixes land in your working tree. Shepherd never commits, pushes, comments or sets a status in this mode, so you review the fixes and commit them yourself.

A clean report needs shepherd local finish to say complete. That check confirms every lens finished, every changed line range reached a reviewer, and the files didn’t change mid-review. It proves each line went in front of a reviewer, which falls short of proving the reviewer understood it. A complete review writes a receipt keyed to the exact content, and shepherd local status tells you whether the files on disk match it. Put that in a pre-push hook if you want one.

Local mode ends at “local checks passed”. Merge readiness stays with the PR loop and Stamp. To see it work, bunx @jagreehal/shepherd local demo builds a repository with seven planted bugs, one for each way a change reaches the review, including a deleted check.

One rule pack for both tools

A service team cares about observability and API compatibility. A frontend team cares about accessibility and its design system. Shepherd lets your team add its own reviewers on top of the built-in lenses:

bunx @jagreehal/shepherd lens new observability --from observability --applies 'src/**'

That command copies Shepherd’s bundled observability pack into .shepherd/lenses/observability/SKILL.md and registers it in your team configuration, .shepherd/config.yml when present, or the older .shepherd/lenses.yml. Drop --from and you get a blank skill to fill in. The ## Review section lists rules with IDs, so a finding names the rule it applies: [observability/error-context] means a caught error lost its cause or the IDs you need to find the request. The pack’s seven rules check for the telemetry Instrument Before You Know the Question says you need before production breaks. They cover outbound calls with no span or log, unstructured logs, secrets in telemetry, silent data drops and unbounded metric labels. The ## Fix section tells triage to use the logger and tracer the repository has, and to hand you any change that adds a telemetry dependency.

Shepherd reads the lens file from the default branch, so your team decides who reviews a PR and the PR itself has no say. You can try a lens on a real PR with /swarm --preview, which posts nothing.

Stamp reads the same file as a rule pack. Point .stamp/policy.yml at it and the reviewer applies those rules to matching files, then refuses, escalates or notes each broken one:

rules:
  observability:
    skill: .shepherd/lenses/observability
    applies_to: ['src/**']
    on_break: escalate

Your team edits one checklist for both the fixer and the approval reviewer. A pack can only add checks. Stamp reads it from the default branch, and a PR that edits a configured pack goes to a person, because later reviews trust its text.

Catch an agent silencing a check

An agent chasing a green build can silence a rule and leave the code as it was. Swarm lists each suppression comment a PR adds, such as oxlint-disable, @ts-ignore or nosec, as a finding and reads the code it covers. Stamp flags the same comments for its reviewer. The reviewer refuses a suppression that hides a finding the change introduced and approves a narrow one that gives its reason.

A PR that loosens lint, test or scanner config gets the same treatment. A linter can’t see its own config change, so Stamp’s reviewer reads that diff and refuses a disabled rule, a widened ignore or a lowered coverage threshold. It approves tightening.

Stamp’s tests gate holds a PR that deletes a test file, renames it out of the runner’s collection pattern or adds a recognised skip or focus marker. Conditional skips and rewritten assertions reach the reviewer. With that split, a model can’t explain away a disabled test the gate can identify.

After a failure reaches production

Run /incident <run-url|issue|error.txt> with a red default-branch build or a production failure. Shepherd tries to reproduce the symptom in a test, checks which PR introduced it, and records whether Stamp approved that PR. It opens a fix PR after verifying the fix. If it cannot reproduce the failure, it stops without claiming a fix.

The gardener reads those incidents alongside review history. For a regression Stamp approved, it can propose a check that flags the culprit change and allows the fix. You review the proposed rule before it affects later PRs.

Work alongside Claude Code Review

Claude Code Review posts findings on a PR and leaves the approval to you, which is the decision Stamp makes. Stamp waits up to five minutes while the Claude Code Review check run on the head is queued or running, so those findings reach Stamp’s reviewer.

Triage fixes 🔴 Important and 🟡 Nit findings like any bot finding and leaves 🟣 Pre-existing ones to you as follow-ups. Shepherd posts no @claude review comment, because each one starts a billed review; its summary tells you when a review failed or ran out of budget. Code Review reads REVIEW.md from the default branch, so Stamp treats a PR that edits it like any change to review rules and refuses one that skips paths, lowers severities or caps findings.

Spend money where judgement matters

Your team configuration sets the models. A ladder lists models from cheapest to strongest, and models pins a lens or a runner to one model, from any provider your coding agent can reach:

ladder: [opencode-go/deepseek-v4-flash, opencode-go/glm-5.3, opencode-go/kimi-k3]
models:
  security: opencode-go/qwen3.8-max

In one swarm review through OpenCode, the router on DeepSeek V4 Flash cost $0.013, two specialist lenses on GLM-5.3 cost about $0.16 each, and the orchestrating session on Kimi K3 cost $0.71. The orchestrator ran up most of that bill, so I would tune its model first.

aiagentsengineering