Files
kz-tob 4b1b74b181 Give differential-review a trigger, and name every component in its README (#278)
* Give differential-review a trigger, and name every component in its README

differential-review's description listed what it does and never named a
situation, so it competed on capability wording alone. It now closes with
the triggers its own README already documents — reviewing a PR, commit,
or diff; checking whether a change re-introduces a fixed bug; asking what
else a change could break; finding modified code with no test.

The same plugin's README never mentioned adversarial-modeler, which is
what Phase 5 dispatches for HIGH RISK changes. Checking whether that was
isolated turned up more of it, and the sweep found three kinds of gap:

zeroize-audit's agent table was missing three of its eleven agents —
0-preflight, which gates the entire run, plus 5b-poc-validator and
5c-poc-verifier. All three appear in the phase diagram directly above the
table, which is why they read as present.

constant-time-analysis documents the ct-analyzer CLI end to end and never
says the plugin also ships a skill and a command. entry-point-analyzer
lists phrases that trigger its skill but never names the skill or its
command.

Three more READMEs describe their skill without naming it. That matters
most where the skill name is not the plugin name and a user cannot guess
it: chrome-mcp-troubleshooting and interpreting-culture-index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix review findings and make the README sweep a gate

The two PoC rows I added to zeroize-audit said Phase 4. The diagram three
lines above them, SKILL.md, and workflows/phase-5-poc-validation.md all
say Phase 5, steps 5a and 5b. "Wave 5a" is a label that exists nowhere.
A debugger consulting the table — the artifact this branch designates as
what runs when — would have opened phase-4-poc-generation.md and found no
validation in it. Also corrected the sentence introducing that table,
which still said 10 agents across 8 phases against 11 across 9, and the
Phase 0 diagram line, which still credited the orchestrator for a gate
the new row credits to 0-preflight.

differential-review's README claimed the agent is "dispatched", and named
it bare in a column whose other rows are namespaced. Nothing dispatches
it: the only instruction is prose in SKILL.md, and a bare subagent_type
fails at runtime. Namespaced both, and corrected the five stale line
counts in the same file — reporting.md is 369 lines, not the ~120 the
token-efficiency section budgets for.

Drop the dead `name: trailofbits:<cmd>` key from five command files.
The three newest command files carry no name: at all, #275 namespaced 22
bare invocations, and this branch documents the `/<plugin>:<cmd>` form —
so the key contradicts the docs it sits next to.

Then make the sweep repeatable. Doing this by hand three times found
eight gaps and missed two more, both of the same shape: a workflow ships
under meta.name, not its filename, so a README citing the filename never
writes the name a reader types. The validator now checks that a README
names every skill, agent, command, and workflow its plugin ships, reading
meta.name for workflows. It refuses a run that inspected zero components,
and six self-test assertions hold it to known-bad fixtures.

It found git-cleanup on its first run: ships as /git-cleanup:git-cleanup-analysis,
README cites workflows/analyze-branches.js four times and that name never.
static-analysis had the same gap for codeql-build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix both P2s: the gate was a substring test, and the dispatch was still bare

The README gate ran `name not in text`. That reads as thorough and could
not fail for a large share of what it counted: `draw` was satisfied by
"(draw cards instead)", `semgrep-rule` by the plugin's own name in the
install line, `burp-search` by a `scripts/burp-search.sh` path that is a
different thing, and `audit` by the prose "shared-state struct audit".

Match by kind instead. Commands and workflows are reachable only as
`/<plugin>:<name>`, so require that literal — it is the only string a
user can type. Agents are dispatched by identifier and never typed as
prose, so require an identifier-shaped mention. Skills are genuinely
referred to by bare name, so require only a delimited occurrence, which
is what stops "draws" counting as `draw`.

That surfaced seven real gaps, the four above plus insecure-defaults'
audit-pipeline workflow, mutation-testing's skill, and trailmark's
code-slice-worker. All seven fixed.

adversarial-modeler was still bare at SKILL.md:96. Line 77 was the
decision-tree mention; line 96 is the "Delegate to this agent"
instruction a model actually acts on, so the runtime failure the last
commit claimed to fix survived it. Namespaced, and it now says why.

Also from the review: a per-kind floor, since a single total stays
healthy while skill_files() — 63% of coverage — silently stops matching;
workflow_names anchored to the meta block, because a bare search takes
any earlier `name:` in a comment, and .mjs was invisible; and AGENTS.md
documents the new hard failure. Self-test 88 -> 96, each new rule with a
negative control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 08:54:12 -04:00
..

c-review

C/C++ security code review. Bug-class knowledge based on the Trail of Bits Testing Handbook.

Invoke with /c-review:c-review. Artifacts land in $(pwd)/.c-review-results/<iso-timestamp>/.

uv must be on PATH — two phases run Python through it.

How it works

Most review tools split the work by bug type: one agent hunts buffer overflows, another hunts use-after-free, and so on. c-review splits it by place in the source instead.

A parser first cuts the codebase into units — one function, or a slice of a long function, capped at 150 lines. Every line of the tree lands in exactly one unit, and each unit belongs to exactly one agent. That is the coverage claim: not "the agents said they read it", but "a parser assigned it and we can check".

Then each agent works through its units by asking the same fixed set of ten questions of every one:

bounds at every write · integer width and signedness · allocation/free pairing · sizeof arithmetic · NUL termination · unchecked return values · what the unit assumes of its caller · banned APIs · macro contracts · initialisation

A question is only asked where the parser found something for it to be about — no writes in the function, no bounds question. And crucially, the parser counts the relevant spots (the twelve writes, the four malloc calls) and tells the agent the number but not the line numbers. The agent has to read the code to find them.

For each question the agent writes down what it found — the lines it examined and what was at them, whether or not it filed a bug. Those written answers are the coverage record (called the ledger in the code and artifacts). At the end, check_ledger.py re-derives the spots from the source and checks the agent's answers against them. An agent that skipped a function, or claimed "no bugs here" over twelve writes while naming none, fails that check and the run is reported as assembled but unverified.

Two things this buys, both of which a plain "review this code" prompt gives up:

  • A bug found does not end the question. A finding is a reason to look harder in that function, not to move on — the record still owes an account of the other eleven writes.
  • Coverage is a number you can check, reported as checks satisfied / checks required rather than "functions touched". A function can be touched by every agent while most of its questions go unanswered.

The bug-type catalogue — 56 classes in 18 groups — still exists, in two roles. Review agents get the class names so they can label what they find. Then a sweep phase takes the classes that got no finding anywhere — minus the ones the platform gate, the threat model or Detect's candidate-site check already removed — and sends one agent to look for them across the whole tree, which catches the scattered single-site slips a reader working through a region in order tends to walk past.

Pipeline

/c-review:c-review
└── SKILL.md                    collect parameters, resolve paths, one Workflow call
    └── workflows/c-review.js
        ├── 1. Detect               1 agent
        ├── 2. Review            4-14 agents
        ├── 3. Sweep              0-2 agents
        ├── 4. Dedup              0-1 agent
        └── 5. Assemble             1 agent
Phase What it does
Detect Runs the parser to build the unit list. Also works out which platform the code targets (from real API usage, not from a single #include), which structs hold shared state, and which bug classes have any candidate site at all
Review The main pass. One agent per contiguous slice of the unit list; each returns findings with severity, plus one coverage-record row per (unit × question)
Sweep The bug-type axis: one agent covering every class that got no finding anywhere and that Detect did not rule out. Plus the shared-state struct audit, only on request
Dedup Only the near-duplicate findings the assembler cannot merge by rule. Usually skipped
Assemble Runs the coverage check, merges duplicates, and writes every artifact

About 8-10 agents on a mid-size target. Everything that can be deterministic is code rather than an agent: the coverage check and the duplicate merges both live in the assembler, so no agent ever retypes another agent's findings.

There is no false-positive judge. Severity is assigned by the reviewer that found the bug and nothing rejects anything, so treat the output as unadjudicated — findings.json records severity_source: "reviewer" and run.judge_ran: false. Expect some findings to be wrong or out of scope.

Why the review agents have no shell

The coverage check only means something if the agent had to read the code to pass it. But the code that computes the answer — which lines each question is about — ships inside this plugin, and units.json sits in the run directory. One shell command over enumerate_units.py would reproduce the whole answer key without opening a single source file, and a review agent could then write a perfect coverage record having reviewed nothing. That is not hypothetical: it was demonstrated end to end, scoring 100% coverage with zero violations.

Hiding the file does not fix it, because the agent can read the plugin too. So the review, sweep and dedup agents are dispatched through agents/c-review-worker.md, whose tools: list is Read, Grep, Glob, Write — no shell, so the derivation cannot be run. That tool list is the control; a "please don't" in a prompt is not.

Two consequences worth knowing:

  • It is a hard dependency. If that file is missing or renamed, every producing agent fails to start and the run produces nothing.
  • The detect and assemble agents keep their shell, because their whole job is to run a command. They are trusted rather than controlled, which is why a green coverage check means "no gap this could see", not "nobody tampered with it". See AGENTS.md for the full threat model and its two known holes.

What each file does

File Role
skills/c-review/SKILL.md The entry point. Collects the four parameters, resolves paths, makes one Workflow call, returns the report
workflows/c-review.js The orchestrator, shipping as /c-review:audit — workflows are namespaced by meta.name, not the filename. The bug-class catalogue, every agent prompt, the phase sequence, and the deterministic duplicate merges
agents/c-review-worker.md The tool scope for review, sweep and dedup agents — see above
scripts/enumerate_units.py Parses the tree and cuts it into units, deciding which questions each unit owes and counting the spots each question is about. Everything downstream is partitioned from this
scripts/check_ledger.py The coverage check: re-derives those spots from the source and diffs them against what the agents wrote down
scripts/assemble_findings.py Reads the per-agent part files, runs the coverage check, merges duplicates, and writes all four artifacts
scripts/findings_model.py Shared loader; decides which findings count as "reported", so the report and the SARIF cannot disagree
scripts/render_report.py Writes REPORT.md
scripts/generate_sarif.py Writes REPORT.sarif (SARIF 2.1.0)
tests/ The suite, kept out of scripts/ so what ships and what only proves it works are separable — the tests are larger than the code they cover

Everything under scripts/ is plain Python with no agent in the loop, which is the point: merging, filtering and coverage-checking are deterministic, so they are code.

Parameters

Parameter Values Effect
threat_model REMOTE / LOCAL_UNPRIVILEGED / BOTH Which bug classes are in scope, and the severity table the reviewer scores against
worker_model haiku / sonnet / opus / inherit Model for every agent in the workflow
severity_filter all / medium / high What reaches REPORT.md and REPORT.sarif
scope_subpath repo-relative dir, optional Where findings may live, and the tree the unit list is built from. Reading for context defaults to the whole repo and is a separate knob (contextRoots)

Five workflow arguments are optional and rarely set by hand: maxUnitLines (150), reviewAgents (derived), linesPerAgent (1500), invariantAudit (off) and benchmarkMode (eval-only). See SKILL.md for what each does and the traps in linesPerAgent.

Output

.c-review-results/<stamp>/
├── REPORT.md         ← start here: severity-grouped, filtered, human-readable
├── REPORT.sarif      the same reported set as SARIF 2.1.0, for CI
├── findings.json     every finding, including ones merged as duplicates
├── ledger-gate.json  the coverage check: how many checks were required, how many were
│                     satisfied, which rows were missing, and every violation
├── units.json        the unit list, with per-question COUNTS — never the line numbers
├── detect.json       platform flags, entry points, shared-state structs
├── assignments/      one file per review agent: the units it owns
└── parts/            one file per agent: exactly what that agent produced

REPORT.md and REPORT.sarif share one definition of "reported", so the two cannot describe different sets. parts/, assignments/ and the four artifacts are cleared at the start of every run, so a run that fails partway cannot leave the previous run's files sitting in place looking current.

Not for

  • Kernel drivers or modules (Linux, Windows, macOS)
  • Managed languages (Java, C#, Python, Go, Rust)
  • Embedded or bare-metal code with no libc

Contributing

Design rationale, the coverage-record rules, the coverage check's threat model and its known limitations are in AGENTS.md. Read it before changing a prompt, a gate rule or how agents are dispatched — the rules there come from measured failures.