Repository navigation
Description
Summary
The PR comment reports Capability delta: +0, 0 modified, -0 on a pull request whose entire content is one added tool. The number is self-consistent — the tool is unbound, so it is not bound capability — but it sits two lines above a gap tally and reads, to a reviewer, as "this PR changed no capability."
Raised in #433 as "worth a separate look — whether an added-but-unbound tool should appear in the delta at all", and deliberately left out of #434: what counts as a delta is a decision change, not a rendering one.
Reproduction
Two commits in an MCP repository. Base declares list_issues and delete_repository and binds both; head adds find_duplicate to tools.json and does not declare it — updating shipgate.yaml is a trust-root edit a coding agent cannot self-approve, so this is the normal shape, not a contrived one. CLI at 16261d99.
capability_review added/modified/removed: 0 0 0
tool_surface_diff.enabled: True
tool_surface_diff.tools: []
tool_catalog names: ['find_duplicate', 'list_issues', 'delete_repository']
tool_surface.total_tools: 2
binding_surface_facts.unbound: ['tool_v2_7cbbbd23…']
The catalog has three tools and the surface has two. The diff ran, found nothing, and is not wrong about what it looked at.
Where the number comes from
capability_review.added is len(capability_change.added) (cli/verify/capability_review.py), and the tool half of capability_change.added is report.tool_surface_diff.tools filtered to kind == "added" (core/findings/verifier_blocks.py::build_capability_change).
tool_surface_diff is compute_tool_surface_diff(public_tool_surface_facts, base_facts, …) (cli/scan/sanitization.py), and public_tool_surface_facts is built from public_tools — the analysed set, which the binding graph has already narrowed to reachable_tool_ids. An unbound tool is exactly the tool that is not in it, on either side. So +0 is structural: the delta cannot see the class of change the exclusion ledger exists to report.
Why the two lines together are the problem
pr-comment.md renders them adjacent:
- Capability delta: +0, 0 modified, -0
- Summary: … 1 of 83 evidence gap(s) are new in this diff. Not fully analysed:
find_duplicate [github_mcp] — added by this diff and not bound to the root agent.
After #434 the second line names the subject, so the reviewer is no longer left with nothing. But the first line still states a fact that is true of a surface the reader has no reason to know is narrower than the diff — and a reviewer skimming for "did capability change?" gets +0 on a PR that adds a tool.
Options, none of them obviously right
- Leave the count and label the surface.
Capability delta (bound capability): +0— cheapest, changes no number, and makes the two lines consistent by saying what the first one measures. Weakest: it relies on the reader connecting two bullets. - Add a third term.
Capability delta: +0, 0 modified, -0; 1 added but unanalysed— sourced from the ledger'snewly_unbound_toolrows, which are already computed and already the base-comparison-backed set (binding_surface_diff.added_unbound_tool_ids). Keepsaddedmeaning what it means today, so nothing downstream re-interprets an existing number. - Count it in
added. Wrong, I think, and worth writing down why: it would makecapability_change.addedmean "in the catalog" rather than "in the proven capability surface", which is precisely the conflation #385 drew a boundary against —samples/large_multi_framework_agenthas 58 unwired OpenAPI operations by design, and a delta of+58on the PR that declares the spec is not a capability change anyone made.
Option 3 also moves a published count that consumers gate on, so it would need the STABILITY.md treatment; 1 and 2 do not.
My reading is that (2) is the honest one and (1) is the cheap one, and that this should not be decided inside a rendering PR.
Acceptance
- The reproduction above does not let a reviewer conclude the PR changed no capability.
-
samples/large_multi_framework_agentgains no delta from its 58 by-design unwired operations — whatever is added must key off the base comparison, not off catalog membership (the #403 discriminator). - A plain
scan, which has no base, reports nothing new here. - If
capability_change.addedmoves at all,STABILITY.mdcarries the before/after and the change is versioned.
Split out of #433, which flagged it and scoped it out. Evidence collected 2026-08-26 while reviewing #434.
Activity
pengfei-threemoonslab commentedon Aug 27, 2026
See #439 — the same field failing the other way. #437 under-counts when the added tool is unbound; #439 over-counts when it is bound, because the count is over changes (
tool_added+action_addedfor one subject) rather than over subjects.Worth resolving together: a change addressing only the unbound case would still report
+2for one bound tool, and vice versa. Both tracked under #440.Close the trust-boundary drift: eight release-contract gaps before v1…
Freeze one capability schema for the exported delta and the committed…
fix(#470 review): close four holes the self-review found
Publish the capability delta as a standalone in-toto attestation (#470)…