Distilled diffs
Before submitting its PR description the coding agent gets a distilled summary of the change, not the raw diff: per-file +adds/-dels, totals, and a "risky files" section that flags migrations/schema, auth/permission-sensitive files, whole-file deletions, lockfiles, and very large files. The digest shapes the submitted description — which files the Tour must walk, what belongs in risks (pr-description.md) — so the reviewer meets the shape and the danger zones through the rendered body. The review agent uses the same digest to orient before reading.
The review agent reads the code; it does not run the project's tests or build. CI runs them as the repo's verify gate and reports on the PR, so a review that runs them too duplicates that signal and slows the review. (This supersedes the earlier "validated review" behavior, which had the review agent run the tests and build in its warm worktree and report pass/fail.)
The parsing/rendering core is distillDiffStats, a pure function over git's per-file statistics (git diff --numstat and git diff --name-status for one range; no I/O, no process, no platform SDK), so it is provider- and channel-agnostic and unit-testable in isolation. The diff_digest tool is the only thing that touches the world, and only through the Executor seam — never a direct shell-out — honoring the "tools never touch the host directly" invariant.
Why statistics and not the unified diff: every Executor caps a command's output (truncate, 120k characters), and the unified diff of a mid-sized PR is larger than that. The digest used to be parsed from the capped text, so it counted the first files in git diff's alphabetical order and nothing after them — a 41-file, +2459/−579 PR was digested as 13 files, +144/−53, and the review approved on it. The stat listings cost one line per file, so the digest covers every file however large the change, and a listing that is itself cut is reported as incomplete rather than rendered as a smaller change. The digest states its own totals, and the review post-step holds them against the PR's size from GitHub (agent-review.md item 15).
- Code:
src/core/diffDigest.ts(the puredistillDiffStats, theDigestReportshape and its ledger parser);src/tools/workspace.ts(thediff_digestRunnableTool+full/readonlytoolset wiring; reusessrc/execution/shellQuote.tsto quote the base ref);src/agents/registry.ts(CODING_SYSTEM_RESIDENThas the digest inform the submitted description;REVIEW_SYSTEM/REVIEW_SYSTEM_RESIDENTorient with it, read the whole change, and forbid running tests/build). - Tests:
src/core/diffDigest.test.ts,src/tools/workspace.test.ts,src/agents/registry.test.ts.
Behavior
distillDiffStatsis pure and provider-agnostic:git diff --numstatandgit diff --name-statusoutput for one range in, a compact digest out — totals line, per-file+adds/-dels(largest churn first), a risky-files section — plus the totals as data ({ files, additions, deletions }). No Executor, no filesystem, no network. Empty/whitespace input returns a clear "no changes" message with zero totals.Per-file/total counts come from the listings:
--numstatgives each file's added and removed lines (-\t-for a binary file, counted as zero);--name-statusgives its status (added / deleted / renamed / modified) and, for a rename, both paths plainly (R<score>\told\tnew) — git emits both listings over the same diff queue in the same order, so they are zipped by position. A renamed file is shown at its new path, never in git's compact{old => new}form. A listing line that does not align keeps its numstat path and counts as modified rather than being dropped.Risky-file heuristics (biased to over-flag — a false positive costs one glance, a missed migration costs more): path matches for migrations/schema (
migrations/,*.sql,*.prisma,schema), auth/permission (auth,permission,credential,secret,password,.env,login,session,oauth,rbac,acl), infra/deploy/CI config (.github/workflows/,*.tf,terraform/,Dockerfile,deploy/,wrangler.*); whole-file deletions; lockfiles (exact basename:package-lock.json,yarn.lock,pnpm-lock.yaml,go.sum,Cargo.lock, …); and single-file churn ≥ 300 lines. Ordinary source files are not flagged. Risk is scored against BOTH the old and new paths, so a risky file renamed to an innocuous name (src/auth/x.ts→src/misc/y.ts,.env→config.json) is still flagged. Non-ASCII paths (gitcore.quotepathoctal quoting) are decoded to the real filename.diff_digesttool bridges the Executor: runsgit diff --numstat <base>...HEADandgit diff --name-status <base>...HEAD— the merge-base range, neverHEAD~n— in onectx.executor.execand renders the listings withdistillDiffStats.baseis optional and defaults to the repo's default branch (origin/HEAD, falling back toorigin/main). A caller-supplied base isshellQuoted into one inert token (no shell injection) behind--end-of-options(no git option injection; a base starting with-is refused before any command runs). A git failure (fatal:/exit/…) is surfaced as an error, never rendered as a misleading empty diff. A shallow clone is deepened first: when the listing fails andgit rev-parse --is-shallow-repositoryanswerstrue(the cold sandbox's owngit clone --depth), the tool runsgit fetch --unshallow originfor every branch once and retries; a full clone is never fetched (the resident's read-only tree has an origin it cannot fetch from by design, and its failure stays legible). A cut listing is never a smaller change: output carrying the Executor's...[truncated N chars]marker makes the tool answer that it cannot state totals, and report the digest as incomplete.Enablement:
diff_digestis in thefull(coding) andreadonly(review) toolsets;webandnoneare unchanged. ≥2-implementations and channel-agnostic invariants hold —distillDiffStatsis our own implementation and does not depend onmeat.dev(absent from the node-only resident image).The digest informs the submitted description: the resident coding prompt directs the agent to call
diff_digestafter pushing and beforesubmit_pr_description, using the distilled digest — not the raw diff — to shape the description's content: which files the Tour must walk, what belongs in risks (pr-description.md item 5). The digest is never pasted into a PR body — the agent never authors body markdown and never opens the PR.Review reads, CI tests: the resident review prompt directs the agent, in its gather phase, to use
diff_digestto orient before reading. Both review prompts (sandbox and resident) forbid running the project's tests or build — CI's verify gate owns that and reports on the PR — and neither asks the agent to report commands run or pass/fail. The agent stays read-only (no modifying tracked code, commits, or pushes) and keeps the existing "gather once, analyze once" discipline.The digest states its totals, and the run holds them: every
diff_digestcall reports aDigestReportthroughToolContext.onDigest—{ complete: true, base, totals }or{ complete: false, base, reason }— the last call wins, and the dispatcher keeps it on the run's ledger row beside the verdict (a resumed run keeps what its earlier generation digested, re-validated byparseDigestReport). The review post-step compares those totals with the PR's size from GitHub and refuses to post a verdict whose digest covered less (agent-review.md item 15). Both review prompts carry one shared text (REVIEW_WHOLE_CHANGE): the REVIEW TARGET block states the PR's size, the digest states what it covered, the two must agree; a tool output ending in...[truncated N chars]was cut short, and a digest or diff that shows less than the PR is read the rest of the way file by file (git diff <base>...HEAD -- <path>) — never a verdict from a partial diff.
Validation criteria
| Criterion | Evidence |
|---|---|
| distillDiffStats computes correct per-file and total add/delete counts, orders by churn, and returns the totals as data | [unit] src/core/diffDigest.test.ts::distillDiffStats::computes correct per-file and total add/delete counts, ::orders files by churn, largest first, ::uses singular wording for a single-file diff |
File status from the name-status listing; a rename shown at its new path, never {old => new}; binaries counted as zero | [unit] ::labels file status (added / deleted / renamed) from the name-status listing, ::detects binary files (numstat \-\t-`) without counting content lines` |
| Risky flags: migration, auth, whole-file deletion, lockfile, large file; ordinary files not flagged | [unit] ::flags risky files: migration, auth, whole-file deletion, lockfile, ::flags a very large file by total churn, ::does not flag ordinary source files as risky |
| Risk scored on BOTH paths: a risky file renamed to a bland name is still flagged | [unit] ::flags a risky file renamed to a bland name (old-side risk), ::flags a secrets file relocated to a non-secret name (.env → config.json) |
| Infra/deploy/CI config flagged; git-quoted non-ASCII paths decoded | [unit] ::flags infra/deploy/CI config files, ::decodes git-quoted non-ASCII filenames |
| Empty listings → the no-changes message with zero totals; misaligned listings never throw or drop files | [unit] ::returns an empty-diff message with zero totals for empty or whitespace input, ::tolerates listings that do not align (a stray line) without throwing or dropping files |
A DigestReport read back from the ledger is re-validated: both shapes accepted, anything malformed refused | [unit] src/core/diffDigest.test.ts::parseDigestReport (a ledger row read back on resume)::accepts both shapes and refuses anything malformed |
diff_digest runs the merge-base range as --numstat + --name-status (never the unified diff, never HEAD~n), distills the listing, reports totals through onDigest; defaults to origin/HEAD | [unit] src/tools/workspace.test.ts::diff_digest tool::distills the file listing the executor returns and reports its totals through onDigest, ::runs the merge-base range as stats — \--numstat` and `--name-status` over `, ::defaults to the repo's default branch (origin/HEAD) when no base is given` |
| diff_digest surfaces git failures (no digest reported); base ref is neither shell- nor git-option-injectable | [unit] ::diff_digest tool::surfaces a git failure instead of reporting an empty diff, and reports no digest, ::does not shell-inject through the base ref, ::rejects a base ref starting with '-' (git option injection) without running git, ::passes --end-of-options so a ref is never parsed as a git option |
| A shallow clone is deepened once, then retried; a full clone is never fetched | [unit] ::diff_digest tool::a shallow clone with no merge base is deepened once (every branch), then the listing is retried, ::a full clone is never fetched: the failure is reported as is |
| A listing cut by the output cap is reported incomplete, never as a smaller change — wherever the cut lands, with no shallow probe; a path carrying the marker text never corrupts the split | [unit] ::diff_digest tool::a listing cut by the executor's output cap is reported as incomplete — never rendered as a smaller change, ::a listing cut BEFORE the marker is still incomplete — never 'could not compute', and no shallow probe or fetch, ::a path that contains the marker text does not corrupt the split — the marker is a whole line, never a substring |
| On a real multi-commit branch through the real LocalExecutor, with a unified diff larger than the output cap, the digest lists every commit's files and states exact totals | [unit] src/tools/workspace.test.ts::diff_digest tool on a real multi-commit branch (LocalExecutor)::covers every commit's files and states exact totals even when the unified diff exceeds the output cap |
| Enablement: diff_digest in full + readonly, not web/none | [unit] src/tools/workspace.test.ts::diff_digest toolset wiring::is in the coding (full) and review (readonly) toolsets, not web/none |
| Coding resident prompt has the digest inform the submitted description (shapes the object's content; never pasted into a body) | [unit] src/agents/registry.test.ts::distilled-diffs prompt behavior (resident variants)::coding resident: calls diff_digest to inform the submitted description … |
Both review prompts forbid running tests/build and name CI as the owner; no run-tests instruction, no npm test, no pass/fail report ask; read-only kept | [unit] ::review prompts read the code and never run the project's tests or build — CI does (item 7) |
| Review resident prompt orients with the digest and keeps gather-once | [unit] ::review resident: orients with the digest, ::review resident keeps the gather-once discipline |
| Both review prompts read the whole change (item 8): the digest's totals against the PR's size, the truncation marker as the tell, the file-by-file read as the remedy, never a verdict from a partial diff | [unit] src/agents/registry.test.ts::distilled-diffs prompt behavior (resident variants)::both review prompts: the digest's totals must match the PR's size, a cut output is read file by file, never a verdict from a partial diff |
Live: after deploy, the first review of a multi-commit PR states a digest whose file count equals the PR's changed_files on GitHub, and the review posts | [agent] human-gated: send agent:review <PR URL> for an open PR of several commits and more than ~120k characters of diff; on the run page the diff_digest result's first line reads N files changed with N equal to the PR's file count on GitHub (and +/− equal to its additions/deletions), and the verdict posts to the PR — no ℹ️ Review not posted … digest covered note in the thread. |
| Live: the digest actually shapes the coding PR's description; a review run makes no test/build call | [agent] Send agent:coding … for a change touching several files and confirm the run page shows a diff_digest call before submit_pr_description and the bot-opened PR body's Tour steps cover the digest's top-churn files (flagged risky files surfacing under Risks); send agent:review <PR> and confirm the run page's bash calls contain no npm test / npm run build (no tests/build tag on any call) and the reply carries findings without a "commands run" section. Requires the resident deployment. |