skill
finetooth
Whole-repository code review by AI agents: blocks, a coverage map, hypotheses with verdicts, four roles. An Agent Skill.
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/mikey-semy/finetooth itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 21, 2026finetooth — whole-repository code review by AI agents
Works in Claude Code, Codex, Gemini CLI, Cursor and any agent that reads the skills standard.
finetooth is an Agent Skill that turns "review the whole project"
into work with a provable result. The repository is cut into blocks, every tracked file is
owned by exactly one block, every block carries numbered hypotheses that must each get a
verdict, and four roles — hunter, verifier, fixer, fix reviewer — work through it with all
state kept on disk in docs/review/, not in the chat. The tool is a single Python file on the
standard library, no dependencies. The name comes from the idiom go through with a
fine-tooth comb: comb through without skipping a single file. Before 0.6.0 the kit was called
review-kit.
npx skills add mikey-semy/finetooth
Four roles, separate prompts. The hunter reads the block's files and raises findings. The verifier makes its own independent pass and gives each finding a verdict. The fixer fixes, proves the fix with a test and runs the project's gates. The fix reviewer reads the whole diff — someone who did not write it — before the pull request opens, not after the merge.
What makes it different from PR-review bots:
- a coverage map (file → block) that is rebuilt by
coverageand fails if a single tracked file is unowned — proof is "no unowned files", not "we looked at the important bits"; - hypotheses with mandatory verdicts: each block's manifest lists numbered questions about
this project, and
checkrefuses to close the block until every one is checked, not checked or marked not applicable; - fingerprints: a block closed on one version of the code, or a finding whose code has
moved, turns
checkred until someone re-stamps it; - a findings register in git — every finding, status and decision lives in files under
docs/review/, so the next session (or the next month) starts where the last one stopped; - guards from the third recurrence: findings carry a root cause, and the third instance of the same root cannot be closed without a guard — a test, lint rule or CI check that keeps the class from coming back;
- tests proven by mutations: for every check the tool makes there is a change that breaks it and a test that goes red on that change.
Language. Docs, prompts and reports are available in English and Russian; see README.ru.md.
Why it is needed
The request "review the project" does not work for three reasons, and each kills the result in its own way.
The context is shorter than the work. Reviewing a large project takes dozens of hours and many sessions. The window runs out, the history is compressed, and two days later the agent in a new window starts from scratch. → Here not a single finding, status or decision lives in the dialogue: only in files.
There is no way to prove completeness. "I looked at the code" is a claim, not a fact. You
cannot tell whether a file was read, and especially whether there are files nobody read.
→ Here the coverage command rebuilds the file → block map and fails if even one tracked
file belongs to nobody. That is the proof: not "we looked at what matters" but "there are no
unowned files".
Whoever hunts must not fix. As soon as the model starts editing, it stops searching. And the one who found the defect is the worst reviewer of their own fix: they check the very thought they got wrong. → Here there are four roles with separate prompts — hunter, verifier, fixer, fix reviewer — and a rule: the fix diff is read by agents who did not write it, before the pull request opens, not after the merge. For the kit's author, the heaviest defect of the access block came on the third round of fix review and had been in the project from the very beginning.
How to install
The kit is a skill under the open Agent Skills standard: it is read by Claude Code, Codex, Gemini CLI, Cursor and other agents. It is installed with the standard installer:
npx skills add mikey-semy/finetooth # into the project: .claude/skills/finetooth/ and the like
npx skills add mikey-semy/finetooth -g # into the home directory, for all projects
The agent finds the skill on its own — by the description, when asked for a whole-repository
review or when the repository already has docs/review/. The tool runs from the skill folder
and reviews the repository it was started in.
Install into the project, not only into the home directory, if CI runs the check: a copy
of the skill in the repository pins the tool's version to the commit, and check in CI runs
with exactly the version the review was done with. A skill installed into the project is
committed along with it.
Then, in the project root:
python3 .claude/skills/finetooth/scripts/review.py setup --project "Name"
setup creates a skeleton of the block definitions, the invariants and the entry point
docs/review/README.md. The tool itself and the role templates are not copied into the
project — they are in the skill. A project that calls the tool its own way passes
--cli "npm run review --": the string is written to the cli field in blocks.json and
goes into all hints. Without it the hints name the real path to the tool. --lang ru sets
lang in blocks.json, and the role templates and scaffolds come in Russian. When the
project's own .claude/settings*.json deny a gate command or one the roles run (pytest,
uv run …), setup names the rule. A repeated run adds what is missing and does not touch
what was edited by hand.
Next comes the work nobody will do for you:
-
docs/review/invariants.md— the rules of your project. The most important file: it is pasted to every agent and decides what the agent will count as a defect. The sample inassets/invariants.example.mdis someone else's — look at it for the structure, not the content: it has a section "context that changes the assessment of findings" and a section "what is NOT a finding". -
Lay out the blocks in
docs/review/blocks.json— cross-cutting first, domain next, live-system last (assets/blocks.example.json). Fill inprojectandgates. -
Write the first block's manifest: 10–15 hypotheses about your project and an acceptance criterion that cannot be met without reading the code. The longest part, and it cannot be cut short: a manifest without project-specific hypotheses gives a review "on general considerations".
-
inventory— the repository tree with sizes and ownership, to cut by;init, thencoverage— and deal with the unowned files until there are zero. This is where everything forgotten surfaces: for the author — a whole microservice, for us — 89 route files.sizesshows the blocks above the ceiling — split them by subject, not alphabetically.Reviewing only a part on purpose — a trial run of the kit, a release gate, one risky area — declare it instead of excluding the rest:
"scope": {"paths": ["src/billing/**"], "reason": "release gate: billing only"}inblocks.json. The paths are git pathspecs, like a block'spaths; the reason is required. Coverage is then counted inside the scope only (a file without a block there is refused as before, a file outside is not), andcoverage,status,summaryandsarif(the run'sproperties) say the review is partial, name the scope and the reason — so it is never taken for a whole one.checkrefuses a scope without a reason and a scope pattern that matches no tracked file. The first live run needed 183 exclusions for one block; the scope is one line. -
Add the
maketargets (orpackage.jsonscripts) and the banner to the project's root instructions file — without the banner a new session will not know the review is in progress and will start its own in parallel. -
Run the first block all the way through and do not be afraid to adjust the rig as you go. Put your own version of a role template in
docs/review/prompts/<role>.md— it is used instead of the skill's.
How a review goes
The agent follows SKILL.md, and the project's entry point
docs/review/README.md repeats it; this is the same path in brief. Below, review stands for
the tool (python3 <skill>/scripts/review.py, or the project's cli).
- Set up and cut.
setup, the invariants,blocks.json(inventory,sizes, an optionalscope), thencouplingandorder— the seams between blocks and the order worth walking them — theninitandcoverageuntil no file is unowned. - Manifest. Per block, before its hunt:
docs/review/blocks/<ID>-<slug>.mdwith the hypotheses and the acceptance criterion.review seams <ID>lists the pairs of the block's own files that must be read together — a hypothesis for each of the top ones. - Hunter.
review set-status <ID> runningfirst — this is where the fix gate stands: it refuses while earlier blocks hold open findings atfix_gateseverity, and the way on is to fix, defer or reject those, not to skip the status. Thenreview prompt <ID> --role hunter→ a fresh agent writes its report and the draft findings;review set-status <ID> hunted. - Import.
review import <ID>takes the draft into the register (findings.jsonl), so the verifier sees the hunter's findings as recorded ones. - Verifier.
review prompt <ID> --role verify→ a different agent checks each finding by execution and makes its own pass. Its verdicts on recorded findings come as a table with onereview set-findingcommand per change (--severity,--confidence,rejected --reason,duplicate --dup-of); its new findings go in withreview import <ID> --append. Thenreview set-status <ID> verified,review findings,review check, and a line in the journal withreview log. - Fix — only when the block has confirmed findings to fix; a block with nothing to fix
goes from the verifier straight to closing, two agents in all.
review prompt <ID> --role fix [--round N]→ another agent, at most three findings a run, a commit per finding, under the project's own commit rules (DCO and the like, looked up in its files).review set-finding <ID…> fixed --commit <sha>; the third instance of a class needs a guard,--rule <path>. - Fix review — required once any finding of the block is
fixed:checkrefuses a closed block with fixes and no fix reviewer's report.review prompt <ID> --role fixreview --diff <base>...HEAD [--round N]→ a fresh agent reads the whole diff; its findings go in withreview import <ID> --append --round N --diff <base>..<tip>. When a round's top finding lands in the previous round's own code, the tool stops for a human:review decide <ID> "<decision>". - Close and leave.
review set-status <ID> closed. When every block is closed:review summary(and--html) writes the file that outlivesdocs/review/, and the directory is deleted in one change. In CI,review checkis the required check andreview sarifputs the findings into code scanning.
Block statuses: todo → running → hunted → verified → triaged → fixing → closed (plus
blocked). Finding statuses: open, fixed, rejected, duplicate, deferred.
Commands
Every command of review.py; review <command> --help has the flags.
| command | what it does |
|---|---|
setup | the skeleton in the project: blocks.json, invariants.md, the entry point; --project, --lang, --cli |
version | the version of the kit this project runs |
init | create or extend state.json from blocks.json |
status | where the review stands, block by block, with the fix debt as its own line |
next | the id of the next unclosed block |
inventory | the repository tree with sizes and ownership, to cut blocks by (--under, --unassigned) |
sizes | every block against the readability ceiling |
coverage | the file → block map; fails on an unowned file (--no-write for CI), lists files owned by more than one block |
coupling | pairs of files that change together but sit in different blocks (--write) |
seams <ID> | pairs of a block's own files linked by an import or by joint changes (--top) |
order | the blocks by cost of failure, then by change frequency |
prompt <ID> --role … | the ready prompt for hunter, verify, fix, fixreview (--round, --diff, --scope) |
set-status <ID> <status> | move a block; refuses what the phase does not allow (--report, --note) |
import <ID> | take a block's draft findings into the register (--append, --round, --diff, --force); --dry-run only checks the draft row by row and writes nothing |
set-finding <ID…> <status> | move findings: fixed --commit, rejected --reason, duplicate --dup-of, deferred --reason; --rule / --clear-rule for the class guard, --severity / --confidence for the verifier's verdict, --fixed-in when the fix is in another file |
decide <ID> "<text>" | record a human's decision on a block — the answer to the loop signal |
hypotheses <ID> | the block's hypotheses and where each got its verdict |
roots [ID] | defect classes: instances, and which guard each instance carries |
restamp <ID> | confirm that changes under a block or a finding were reviewed (--line for a finding that moved, --file for one whose code went to another file) |
backfill | stamp fingerprints on records written before fingerprints existed |
refs | finding ids of the register named in the code outside docs/review/ and the summary files |
findings | regenerate findings.md from findings.jsonl |
log <ID> "<text>" | append a line to the journal |
check | the whole state for consistency — the gate for CI |
summary | the one file that outlives docs/review/ (--html, --out, --aged <file>) |
sarif | open and deferred findings as SARIF 2.1.0 for code scanning (--out) |
Two scripts sit beside the tool: assets/run-role.sh <ID> <role> runs a role headless and
measures it, scripts/axes.py breaks a run's spend down by axis — both under
"What a run costs, measured" below.
How the mechanisms work
The fix gate
The method finds faster than a project fixes (the first project: 77 findings
on 5 blocks, 9 fixed), and a finding that never reaches a fix is debt — a month later the
register describes code that no longer exists. So set-status <ID> running refuses while
findings at fix_gate severity or above (high by default; "fix_gate": "none" in
blocks.json switches it off) are open in the blocks already passed. The way out is to fix,
defer with a reason or reject — never to ignore. status prints the debt as its own line, and
check warns about open findings older than a week. Hunting every block first and fixing
afterwards is a strategy the gate leaves to the project, not a defect of it: "fix_gate": "none",
and it pays with what the gate exists to prevent — findings wait while the code under them
moves (their fingerprints go red, each is re-checked or restamped), and a class found in the
first block is found again in every later one instead of being closed by a guard before them.
Seams between blocks
A block is the unit inside which an agent sees everything; the seam
between two blocks is seen by nobody. On the first project 76% of the file pairs that change
together sit in different blocks. review coupling reads git log, drops mass commits and
shared nodes, and prints the cross-block pairs with a ready ref_paths entry and a hypothesis
for the manifest; clusters of pairs between the same two blocks are where a seam block is due.
--write keeps the pairs in docs/review/coupling.tsv. Both cutoffs are derived from the
repository rather than fixed, and the run prints the ones it used: a commit is a mass commit
above the 95th percentile of files per commit here — or, on a history of fewer than twenty
commits, where the percentile cannot separate anything, above Tukey's fence; a file is a
shared node when it is coupled with a tenth of the review's blocks, never fewer than three
(a seam runs between two, so a third means it no longer describes one seam).
Seams inside a block
Reading every file of a block whole is not reading the pair: on
blocks of real size, defects visible only when several files are joined were the weakest
class of the recall measurement (of 7 such cases, 3 found in full). review seams <ID> lists
the pairs of the block's own files linked by an import or by joint changes. Imports are read
for TS/JS (import … from, export … from, require, import(); relative paths, and the
paths/baseUrl aliases of the nearest tsconfig.json/jsconfig.json with its relative
extends — an alias no config declares is not guessed) and for Python (ast: absolute and
relative imports); a specifier that resolves to no tracked file is counted and skipped. Joint
changes use the history reading and the thresholds of coupling. Pairs with both kinds of
link come first, then by joint changes, then by the number of imported names; --top (10 by
default). What one side assumes about the other is not generated — that is the manifest's
hypothesis; the hunter prompt carries the same top pairs so the hunter knows where to join.
Order of walking
review order ranks the blocks by the cost of failure first (risk on
the block) and by change frequency second — measured on the first project as a prediction:
the top 10% of files by change frequency collected 34% of the later fixes, by size 29%, at
random 6% (Nagappan & Ball 2005; Moser et al. 2008). Frequency catches defects; the cost of
failure catches irreversibility, so it stays the first key. The command reports; the order in
blocks.json is the human's.
Which history coupling, seams and order read
They count changes that landed on the
first-parent line, and a merge on it is one change — the merged branch's diff against the
first parent, the record a squash merge would leave. So a project gets the same numbers
whether it squashes, rebases or merges its pull requests, and a history made of merge
commits alone is no longer read as empty (a block once showed 0 of its 24 commits).
All three print which it was: no merges on the line, or how many of the changes are merges.
Reading every commit of every branch instead was measured and rejected: on eight
repositories with merged branches, 6–60% of the pairs it lifts over together ≥ 3 rest on
fewer than three landed changes — one branch's work-in-progress commits clearing the floor
alone.
A criterion that sweeps beyond the block
An acceptance criterion that enumerates places across the program ("every place that
changes data") is not proven by reading the block's files. Such a block declares sweep in
blocks.json — the paths the criterion reaches — and the hunter enumerates the places with
a script committed at docs/review/sweeps/<ID>.<ext>, then reads what it found: the script
is the proof that the list is complete. check refuses an enumerating criterion without a
declared sweep.
The loop signal
The kit's own review showed where fix rounds stop converging: from round 2 on, the fix
reviewer's top finding kept landing in the code the previous round had written — usually in
the guard that round added — and only a human's decision ended it. So the stop is mechanical.
import --append --round N --diff <range> records which review found a finding (found_in,
the range pinned to commit ids); when the top finding of review N−1, medium or higher, lies on
a line fix round N−1 wrote (by the lines of git diff -U0, not by file), prompt --role fix --round N refuses and check warns until review decide <ID> "<decision>" is recorded —
and the decision goes into the next fix and fix-review prompts.
What a run costs, measured
assets/run-role.sh <ID> <role> runs a role headless through
claude -p, keeps the event stream and writes one line to the journal: turns, tool calls,
input tokens and the share from cache, output, re-reads. scripts/axes.py breaks any such
stream down by axis. The first measured block: the cost is turns × context — reading the
block whole was 1% of the spend; the verifier's 118 shell calls were most of the rest. The role
templates now say so (one file — one read; the stand is one script, run once), and the runner
caps turns at twice what the measured run needed (ROLE_MAX_TURNS overrides the cap). A run
cut off by that cap leaves its work uncommitted, so the fixer is handed at most three findings
a run, commits each as it lands, and the runner will not start a fixer or a fix reviewer in a
dirty tree (ALLOW_DIRTY=1 to insist); the agent's PID sits in a .pid file next to the
stream while it runs, so a stop reaches it, and stopping the script passes the stop on.
An interrupted role is started again, not continued. The context is sent again on every
turn and the prompt cache holds it only for minutes, so an agent continued after a pause (a
usage limit, a crash, the night) pays for its whole context again. A field run (29.09, reported
by the operator, on 0.7.0 with the roles as agents inside one session): 5–8 agents of 500–800k
tokens each, continued after the plan's limit, took 15–20% of a five-hour window within a
minute, and the limit itself came within the hour. The state of a review is on disk, so a role
cut off is started anew on its block — run-role.sh never continues a session — and two or
three roles at once is what a plan's window holds, with one writing role (fix, fixreview)
per checkout: two writers in one tree commit each other's edits, so a second one gets a
worktree of its own.
The per-role tool lists the runner passes are pre-approvals, not limits: they add to what the
operator's own permission settings allow. What a run must not reach goes into ROLE_DENY,
passed to claude -p as --disallowedTools — details and a measured pitfall in
SECURITY.md.
What outlives the review directory
The method ends by deleting its own directory — a
register nobody updates describes fixed things as open. review summary writes one immutable
file outside it: the date and the base commit, the blocks with their acceptance criteria and
fingerprints, the rejected findings with reasons (so the next review does not find them again),
the accepted risks, what closed each class (guards, commits), and what the review cost (the
measured role runs of the journal, which dies with the directory). review summary --aged <file>
answers, from git log alone, how far each block has drifted since that commit — an auditor's
re-test starts from there, not from zero. review summary --html writes the same summary, the
same numbers, as one self-contained HTML file for a reader who never saw the review
(docs/review-summary.html by default, or --out): goal and coverage, the blocks table with
findings by severity and status and the spend of each block, a chart of findings per block,
the open findings at the line their code sits on now with the block report's path, the guards
per defect class, the accepted risks with reasons, the economy by role, and what is left. SVG
drawn by the tool, a light and a dark theme, wide tables scroll inside their own box on a
phone; nothing is loaded over the network and there is no script — a file to attach to a PR.
Findings where the platform shows them
review sarif prints the open and deferred
findings of the register as SARIF 2.1.0 for GitHub code scanning: the rule is the finding's
defect class (its block when it has none), the level comes from the severity (critical and
high → error, medium → warning, low → note), the location is the file and line from the
repository root, the fingerprint is the finding's id — an edited line does not open a second
alert. A deferred finding carries an accepted SARIF suppression and says "accepted risk" in
its message, because GitHub does not read suppressions. security-severity is not written:
the register does not say which finding is a vulnerability. The ready jobs are
assets/github-actions-snippet.yml (check as a required check plus the upload through
github/codeql-action/upload-sarif, pinned to a commit) and assets/gitlab-ci-snippet.yml
(check as a job). GitLab shows SARIF on the Ultimate tier only, so on a merge request the
GitLab snippet hands the same SARIF to reviewdog,
which posts the open findings on the lines the merge request changes as discussions — on any
tier (reviewdog -f=sarif -reporter=gitlab-mr-discussion -filter-mode=added, a pinned release
checked by its SHA-256). The GitHub snippet has the same as an optional job,
-reporter=github-pr-review: review comments in the pull request next to, or instead of,
alerts in the Security tab. A deferred finding is skipped by both — its suppression is
accepted.
What it costs
Full measurements with the method and caveats are in measurements (in the knowledge base).
In short, from the review journal:
| block | outcome | spend |
|---|---|---|
| access and visibility, 58 files / 4017 lines | 12 findings | 2 agents, 576k tokens, ~35 min |
| domain core, 13 files | 14 findings | 2 agents, 446k tokens, ~34 min |
| writes and versions, 42 files | 17 findings | 2 agents, 679k tokens, ~48 min |
A block with nothing to fix closes with two agents. What is expensive is not the search but the fix rounds: for the author, fixing one block went through three rounds of "fixed → the diff was read" — 26 findings, then 13, then 8, then four trifles and the verdict "safe to merge". By our estimate such a block costs about twelve agents against two for a survey one. Convergence is the only honest sign that it is time to stop — the stopping rule is set in advance, and not by the number of findings but by their kind; where convergence fails, the loop signal above stops the rounds for a human.
What not to do
- do not start with domain blocks: until the cross-cutting ones have set the language, they duplicate each other;
- do not let one agent both hunt and fix;
- do not close a block whose acceptance criterion is not met;
- do not leave findings only in the conversation;
- do not keep more than two or three agents at once: the plan's usage window runs out before the machine does, and a build on the same machine makes it fewer still.
The method's main blind spot is how much it misses. We know that what it finds is real (6 planted findings out of 6 rejected), and we do not know what share of what exists that is.
Where it is going
The detailed roadmap — with measurements, sources and the order of work — lives in the maintainer's knowledge base; this is the short version.
Released in 0.8.0 (2026-09-29):
- the fix gate — the next block does not start while earlier ones hold open findings at
fix_gateseverity; deferring needs a reason; - seams —
couplingbetween blocks,seamsinside one, both reading merges as one change; - order by risk, then by churn —
order; - the summary that outlives the review directory —
summary,summary --aged, andsummary --htmlfor a reader who never saw the review; - findings on the platform —
sariffor GitHub code scanning, with ready CI jobs for GitHub Actions (checkas a required check plus the SARIF upload) and GitLab, and the findings in GitLab merge request discussions (any tier) and in pull request review comments through reviewdog; - a declared partial review —
scopeinblocks.json, said everywhere the result is; - the loop signal — fix rounds stop for a human's
decidewhen they start fixing their own last fix; - a finding's fingerprint is the lines around it, not the whole file, and a finding is shown at the line its code sits on now;
- a confirmed hypothesis names the finding it produced —
checkrefuses a confirmation without one; - spend measured —
run-role.shandaxes.py, turn caps, the one-read rule in the templates,ROLE_DENYas the runner's one limit; - sweeps for acceptance criteria that reach beyond the block's files;
- the tool split into a package, one module per concern, behind the same command.
In work:
- the verdict parser derived from a corpus of real reports, not from the last defect (#17);
- what the field runs asked for next: a verifier's correction of a finding's claim and scenario reaching the register (#74), an external hypothesis list that no manifest may lose (#77); and the guard gaps the kit's own review deferred (#29, #30, #31).
Next, roughly in order:
- A large block and a closed block that moved. Every block declares its sweep explicitly (or says it has none), with a method — read in full, or places enumerated by a command written before the hunt — and parts shared out between several hunters, each held to the readability ceiling. A closed block whose files changed is re-read by its diff from the commit it was closed at, with a verifier of its own, instead of a restamp or a full rerun. Asked for by a field review moving onto the kit (#78, #79).
- Seams in Go — the import graph behind
seamslearns Go (#80). - Blind zones. What the review's own roles were required to read and did not — a map of what was actually opened, kept with every run; what the agent that wrote the project changed without reading what the change depends on — from its session logs, or from traces in git once the logs are gone; and the reviewer's own blind spots, including the one it shares with the model that wrote the code. The promise is visibility, not a guarantee of quality.
- Algorithms instead of agent turns. What is mechanical is done by code on the standard library — scheduling roles within a usage window, near-duplicate findings, import edges between blocks, a reading order for each hypothesis, honest intervals in measurements; the model judges, and an algorithm only proposes.
- Review kinds over one core. Beside the whole-repository review: a pull request review,
a review of the plan and the design before code, a lens run, gates before a release — each
a separate edition writing to the same register, invariants and
check. - Measuring what is missed. A corpus of real past defects with a known answer; cheap
sampling of closed blocks by a different model as an upper bound. A first recall
measurement has been run —
seamscame out of it. - A lens bank. Property question sets (access, money, privacy, data integrity, reliability, tests, documentation truth) as sources of hypotheses for manifests — in the form "what to check → how to prove it", never as a checklist.
- Threat model as a block kind. A textual data-flow diagram per trust boundary with STRIDE hypotheses; the diagram outlives the review directory.
- Token economy beyond the measurement: cache TTL, output filtering.
- A run manifest, two reviewers at once, a block spanning two repositories, portability to another language.
What we will not do: turn the whole-repository review itself into a diff reviewer (lighter review kinds are separate editions over the same register), add dependencies, build a web UI or a database, automate finding without a human accepting each one, reward being first. Each has a reason in the knowledge base.
Where it came from
The kit arrived ready-made: the archive finetooth.zip dated 16.09.2026. The author,
Georgiy Khudobandaev, is the founder of the
method: the kit was born on his large project, and the whole design came from there. The first
commit of this repository is that archive byte for byte, without a single change, so that
everything changed afterwards — and why — stays visible.
⚠️ The archive carried no license. Until 24.09.2026 the MIT in LICENSE
covered only the changes made here, and the repository stayed private: the author's
description and examples named internal details of his project, including the access-rights
defects found in it. Since then the author has granted full rights to the kit, both authors
are named in LICENSE, and everything that identified his project has been anonymised — see
NOTICE.md.
How it was created is the main thing in it. The kit was not designed up front. It was pulled out of one block that was really worked through: the author ran an access-rights review on his project, rewrote the tool six times along the way and assembled the result into a kit. That is why almost every rule in it is the trace of a specific mistake, not a general consideration:
- An empty list of paths meant "the whole repository". Blocks that run against the live system own no files — and coverage came out at 100% on the first run, fictitiously. Now an empty list means an empty set.
- Ownership by directory handed over someone else's files. A comment editor sitting in the lists route folder would have gone to the lists block, and the communications block would never have opened it. Ownership is assigned per file, and "the path pattern matched nothing" is a separate check: otherwise a block silently shrinks and the agent is not given half of its code.
- The verifier wrote the entire verification protocol into the finding's title, a thousand characters of it, and the summary table stopped being a table. A 220-character limit and a check for it appeared.
- A security fix passed every gate and broke what worked. Lint clean, tests green, migrations valid — and the foreman stopped seeing his technicians' rounds. The cause is universal: all the new tests proved that an outsider no longer sees too much, and not one proved that an insider still sees their own. Hence the rule "a test is written in both directions" and an independent review of the diff before the merge.
- The hole was in the protection itself. Six grep gates in the build forbade dangerous
calls, and an allowing comment next to a call lifted the ban — and all six were written so
that the comment freed the neighbouring call:
grep -B 3glues nearby matches into one block. The engine that matches a finding to its allowance by line number lives inskills/finetooth/assets/guard-grep.sh. - A dead permission is worse than a missing one. A checkbox "Close requests" turned up that had existed since the first migration and was checked nowhere: the administrator unticked it to forbid, and nothing happened.
The author's full description is how-it-works (in the knowledge base), Georgiy's
text, given as is. The numbers in it are the author's account; we have not verified them.
Everything marked as verified below has been verified by running it here.
What was broken in the kit
The kit was sent as working, and it is — but installing it by its own instructions stumbles. Everything listed was reproduced on a clean repository, not spotted by eye.
| What | What it turns into |
|---|---|
example/makefile-snippet.mk contains not review targets but pieces of SAST targets from another project | not one make review-* command from the documentation exists; installation is done blind |
the documentation calls prompt BLOCK ROLE | the tool requires prompt BLOCK --role; the command from the instructions answers unrecognized arguments |
check requires a manifest for all blocks at once | a manifest is written before its own block, so the check is red from day one — and people start ignoring it |
| a block closes with only a hunter's report | set-status H1 closed passes without a verifier's report: unverified findings disappear from the remaining work, the check is green |
a stale coverage.tsv is not caught | the map on disk diverges from the recount, and the consumer reads yesterday's ownership without knowing it |
| no block size ceiling | a block that physically cannot be read in one session reports coverage that never happened |
| no command to move a finding | the README demands "edited only by the tool" and "set fixed and the commit", and there is no command for it — the register is edited by hand |
| the rejection reason is demanded by nothing | the review's completion condition requires a reason for every rejected finding, nothing checks it, the field stays empty |
| the repository root is "two directories above the file" | a tool placed anywhere other than scripts/review/ does not fail but silently looks for state in the wrong place and starts a second set |
| a coverage failure prints only a list of paths | a human appends the file to the first block at hand, and it counts as read without having been read |
Separately — installation pitfalls, not the author's but ours, from another language and another keyboard layout:
[handle]in a git pathspec is a character class. The patternsrc/app/[handle]/[slug]/edit/**matches nothing: the glob reads the brackets as "one of the characters h,a,n,d,l,e". A directory without**works. The "pattern matched nothing" check catches this — without it 46 files would have stayed unowned unnoticed.exclusionsis a list of objects{pattern, reason}, not strings. A list of strings crashescoveragewithTypeError.
What changed since the archive
Some changes were road-tested on a live review (67 blocks, 1691 files, three blocks
completed), some were added here and proven by mutation — break the state and make sure the
check goes red. What each release added is in CHANGELOG.md; this is the
reasoning behind the core.
Road-tested on a live review:
checkrequires a verifier's report for a block in statusverified/closed;checkcomparescoverage.tsvwith a recount — a stale map no longer stays silent;- a manifest is asked of only the block that has reached work, not all at once;
- a block readability ceiling: 6000 lines, exclusions are not counted (otherwise the
ceiling measures
package-lock.json, not the code the agent will read) — a refusal before the block is read (set-status huntedrefuses too and records the size read), a warning when a block read within it grows later (the next review splits it); - a coverage failure says what to do and why the choice is made by a human, not by a pattern;
- the hunter's report must contain a file-by-file list of what was read: without it the report cannot be told from a retelling. In a neighbouring project this is exactly what exposed padding — 15 blocks out of 26 counted as reviewed, and 56 files were never named once.
Taken from neighbours in the niche (analysis in comparison-with-practice (in the knowledge base)):
- a fingerprint of what was reviewed — from doorstop,
where a requirement stores a hash of its text and an edit itself moves it to "unreviewed
changes". Here: on moving to
verified/closeda block remembers a fingerprint of the composition and contents of its files, andcheckcatches a block closed on a different version of the code. To confirm that the changes were reviewed —restamp <BLOCK>, exactly likedoorstop review. A block or finding without a fingerprint (recorded by a kit version that had none) is also a failure, not a skip:backfillstamps fingerprints from the current code and writes to the journal from which commit changes are tracked. The same fingerprint is taken of the manifest's hypothesis text: verdicts are given by number, and reordering or replacing a question after verification would otherwise credit the old answers to the new one; - a fingerprint of the code under a finding — from the same place (suspect links) and from
claude-review-all, where a finding's key
includes a hash of the code, and from GitHub code scanning, which matches a result by the
context of its line (
primaryLocationLineHash). Here: a finding with a line keeps a fingerprint of that line and the three above and below it (trailing whitespace and line endings ignored), andchecklooks for those lines by content anywhere in the file. An edit elsewhere in the file leaves the finding alone; if the lines above it came or went,findings.md, the SARIF export, the summary and the prompts show the line the window sits on now, and the register keeps the recorded one untilrestamp. An edit inside the window fails the check — either the defect has already been fixed, or the description is stale, or it is still there andrestamp <finding-ID>confirms it (--line <N>if it now sits elsewhere,--file <path>if its code moved to another file). A finding without a line keeps the fingerprint of the whole file. The window size is measured on the kit's own review, not chosen: the note next toREGION_Kinreview.pyhas the numbers. Records stamped by an older version (code_sha, the whole file) are checked as before, andrestampmoves each one to the window; - a cheap fabrication filter — from mergejury: a reference to a line that does not exist in the file is caught without any model;
- a budget inside the assignment itself — from repomix (exceeding it fails the build) and ai-digest (what did not fit stays in the output as a stub — path visible, contents absent). Here the prompt prints the block's size and, if it is larger than what can be read in a session, shows where the boundary runs: files in descending order of size with a running total and a mark of what is over the line. Not a ban on reading — an obligation to name what was not read;
- from a class to a rule — the variant-analysis mechanic at Trail of Bits, where a rule is
written from a finding and run across the whole codebase. Here: a finding carries a
root(the class name), and from the third instance the check requires a guard —set-finding <ID>... <status> --rule <path>, recorded only on the findings named in the command (a root string can carry defects that need different guards;--clear-ruletakes a guard off a finding it does not go red on). A guard is a path to a file in the repository (a test, a linter config, a CI gate) orrepository:pathfor a neighbouring one; a non-existent path is a failure. The threshold comes from practice: a second recurrence can still be a coincidence, a third means the defect is produced by the structure of the code. Therootscommand shows the classes, the number of instances and which guard each instance carries, and flags a class whose instances disagree or where some carry none;checkwarns about the latter. The guard is demanded in the fix phase — once an instance isfixedor its block isfixing/closed; before thatcheckwarns (root/guard-due), since a verified block has nobody yet to write a guard and the release gate needscheckgreen right after verification.
Added after checking against world practice (see comparison-with-practice (in the knowledge base)):
- a second coverage denominator — hypotheses. The file map answers "the file was opened";
a professional audit counts coverage in questions asked of the system, and OWASP ASVS
requires every requirement to have a "pass or fail" outcome and a written justification of
non-applicability. The manifest's hypotheses are numbered in order, each is closed with a
verdict "checked / not checked / not applicable",
checkrequires a verdict for all of them,hypotheses <BLOCK>shows what is closed and what is hanging; - a mandatory "Coverage limits" section in the hunter's report: completeness is proven by listing what was not reviewed — in audit firms' reports this is a separate chapter;
- the coverage map is compared against a recount, not against a signature: without this the figure "1691 of 1691 covered" stays true-looking when the repository has already moved on. At first the map named the commit in its header — but a commit name proves nothing (there is nothing to compare it against, and a matching line of history does not mean a matching composition), so the code version is pinned by the fingerprints of blocks and findings, and the map is compared with a fresh recount line by line;
- the verifier's prompt requires verification by execution, not by re-reading, and defines a duplicate through the root: "if fixing the root makes the second finding cease to exist";
- the fixer's prompt requires closing the class of defect with a guard, not a list of places.
Added during the transfer:
- the repository root is asked of git — the tool can be placed wherever convenient;
- the project name and gates became the substitutions
{{PROJECT}}and{{GATES}}fromblocks.json: a template copied without proofreading greeted the agent in the name of someone else's project; - the command
set-finding <ID> <status>— moves a finding and refusesfixedwithout a commit,rejectedwithout a reason,duplicatewithout a reference; checkrequires a rejection reason for every rejected finding;- the hints in messages are gathered into one constant,
CLI(in the original half of them calledmake, halfnpm run— the rig was carried between projects and never proofread); - the
maketargets snippet finally contains review targets, and next to it lies the same list forpackage.json(both inskills/finetooth/assets/).
Taken from the second version of the kit by its author (handed over 23.09.2026, developed
in parallel; the mechanisms were carried over, the texts rewritten, his project's data was not
carried over — details in CHANGELOG.md, 0.5.0):
- the fix reviewer role — the diff is pasted into the prompt whole, rounds and halves of the diff get their own report names, and at the end an explicit verdict "is another round needed"; a block with fixed findings cannot be closed without such a report;
- a block's proof kind —
read(every file read) ormeasured(test quality, performance, scanners: the proof is the manifest's artifacts, not reading); the prompt's first rule is derived from it, a block withoutpathsis a live system; - the gate "every file of a readable block is named by full path in at least one report" — "read 25 of 25" turned out to be the agent's own word: it named five, and a top-up found 11 more defects;
- the commands
inventory(the repository tree for cutting) andsizes(blocks against the ceiling),coverage --no-writefor gates in CI; - five defects of our copy that his tool already refused: a fixed finding on a deleted
file, a deferred one without a reason, phases out of order, "review finished" with a
blocked block, the
runningtime from the first start; references/lessons.md— 42 lessons from two reviews with the stories of where each came from.
Tests
python3 -m unittest discover -s tests
561 scenarios, about a quarter of an hour, no dependencies other than git. Each one creates a fresh temporary
repository and calls the tool from the skill folder, with the working directory in that
repository — the way the agent calls it. Behaviour is checked through the command line, not by
importing internals. A separate class checks the skill itself against the specification: the
name equals the directory, the description is within limits, the links from SKILL.md lead to
existing files, the version in the header equals the tool's version. In CI this is joined by
skills-ref validate — the validator from the standard's repository.
The tests are proven by mutation: for every check the tool makes there is a change that breaks it and a test that goes red on that change.
What is inside
skills/finetooth/ THE SKILL — this is what gets installed into the agent
SKILL.md when to apply and the order of work (read by the agent)
LICENSE terms — travel with the skill
scripts/review.py the command: every command in the table under "Commands";
the code is the `finetooth/` package next to it
scripts/finetooth/ the tool, one module per concern; a module imports only
modules listed before it here, never one after
base.py, git.py the skill's location, version, errors; git.py is the ONE
place a process is started
workspace.py, model.py where the review lives on disk; statuses, roles, limits
i18n.py, text.py messages en/ru; reading markdown: quoted vs said, sections
fingerprint.py, blocks.py fingerprints of code; blocks, their state and stamps
register.py, verdicts.py the findings register; hypotheses and their verdicts
coverage.py, history.py who owns which file, freshness; co-change, churn, order
seams.py, journal.py seams between blocks; the journal and the loop signal
gates.py, importing.py refusals and the gates of a finding row; importing a draft
roles.py, settings.py pieces of a role prompt; the project's own deny rules
report/sarif.py, summary.py, SARIF export, the markdown and the HTML summary
report/html.py, refs.py finding ids left in code and documents
checks/*.py what `check` looks at, a function per check: state.py,
findings.py, coverage.py, reports.py
commands/*.py the commands, a file per group: setup.py, status.py,
coverage.py, history.py, findings.py, prompt.py,
report.py, check.py
cli.py, __init__.py arguments and dispatch; __init__.py marks each package
scripts/axes.py the spend of a headless run, broken down by axis
references/hunter.md hunter: reads the block's files and raises findings
references/verify.md verifier: its own independent pass, three verdicts
references/fix.md fixer: fixes, proves with a test, runs the gates
references/fixreview.md fix reviewer: reads the whole diff, rounds, verdict on the next one
references/lessons.md lessons from two reviews, from which the rules grew
references/*.ru.md the same five in Russian — hunter.ru.md, verify.ru.md,
fix.ru.md, fixreview.ru.md, lessons.ru.md; `lang` in
blocks.json picks the language of the pair
assets/entry-point.md template of docs/review/README.md for the project
assets/blocks.example.json three blocks of different kinds + exclusions with justification
assets/manifest.example.md block manifest: hypotheses and acceptance criterion
assets/invariants.example.md someone else's invariants — as a sample of the structure
assets/journal.example.md header of the decisions journal
assets/agent-banner.md banner for the project's root instructions file
assets/*.ru.md the Russian copies of those five — entry-point.ru.md,
manifest.example.ru.md, invariants.example.ru.md,
journal.example.ru.md, agent-banner.ru.md
assets/makefile-snippet.mk make targets
assets/package-json-snippet.json the same for an npm project
assets/github-actions-snippet.yml CI for GitHub: `check` as a required check + SARIF upload + reviewdog
assets/gitlab-ci-snippet.yml CI for GitLab: `check` as a job + findings in MR discussions
assets/guard-grep.sh grep-gate engine: allowance by line number
assets/run-role.sh runs a role headless through `claude -p` and writes the
spend to the journal — the one part that leaves the machine
tests/ tests of the tool and the skill format: 561 scenarios
test_verdict_corpus.py the verdict parser on real reports (tests/corpus/verdicts)
examples/toy a real docs/review/ after one block, on a toy app
.github/ CI (tests on 3.12 and 3.14, skills-ref validate, the DCO
check), issue and PR templates, the logo
README.md, README.ru.md this file and its Russian copy
AGENTS.md rules for whoever edits the kit itself
CLAUDE.md points the agent at AGENTS.md — one source, two names
CONTRIBUTING.md how to contribute: DCO, test + mutation, no dependencies
RELEASING.md how versions are numbered and what a release must prove
NOTICE.md origin and rights: two authors, consent to MIT
SECURITY.md what the kit writes and sends, and how to report a hole
LICENSE MIT, two copyright holders
CODE_OF_CONDUCT.md Contributor Covenant 2.1 (+ CODE_OF_CONDUCT.ru.md)
CHANGELOG.md version history (+ CHANGELOG.ru.md)
.gitignore the tool's bytecode must not reach a commit
The project is maintained by one person, and PRs are looked at roughly once a week; how to
contribute — CONTRIBUTING.md, origin and rights — NOTICE.md.