Skip to content
Conduit

skill

finetooth

Whole-repository code review by AI agents: blocks, a coverage map, hypotheses with verdicts, four roles. An Agent Skill.

mikey-semy/finetoothcommunitylistedunreviewed

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

Skills do not use an MCP config. Follow the steps in the README below.

README

synced Oct 21, 2026

Русская версия

finetooth — whole-repository code review by AI agents

tests license: MIT Agent Skills 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 coverage and 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 check refuses 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 check red 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:

  1. 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 in assets/invariants.example.md is 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".

  2. Lay out the blocks in docs/review/blocks.json — cross-cutting first, domain next, live-system last (assets/blocks.example.json). Fill in project and gates.

  3. 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".

  4. inventory — the repository tree with sizes and ownership, to cut by; init, then coverage — 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. sizes shows 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"} in blocks.json. The paths are git pathspecs, like a block's paths; 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), and coverage, status, summary and sarif (the run's properties) say the review is partial, name the scope and the reason — so it is never taken for a whole one. check refuses 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.

  5. Add the make targets (or package.json scripts) 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.

  6. 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).

  1. Set up and cut. setup, the invariants, blocks.json (inventory, sizes, an optional scope), then coupling and order — the seams between blocks and the order worth walking them — then init and coverage until no file is unowned.
  2. Manifest. Per block, before its hunt: docs/review/blocks/<ID>-<slug>.md with 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.
  3. Hunter. review set-status <ID> running first — this is where the fix gate stands: it refuses while earlier blocks hold open findings at fix_gate severity, and the way on is to fix, defer or reject those, not to skip the status. Then review prompt <ID> --role hunter → a fresh agent writes its report and the draft findings; review set-status <ID> hunted.
  4. Import. review import <ID> takes the draft into the register (findings.jsonl), so the verifier sees the hunter's findings as recorded ones.
  5. 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 one review set-finding command per change (--severity, --confidence, rejected --reason, duplicate --dup-of); its new findings go in with review import <ID> --append. Then review set-status <ID> verified, review findings, review check, and a line in the journal with review log.
  6. 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>.
  7. Fix review — required once any finding of the block is fixed: check refuses 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 with review 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>".
  8. Close and leave. review set-status <ID> closed. When every block is closed: review summary (and --html) writes the file that outlives docs/review/, and the directory is deleted in one change. In CI, review check is the required check and review sarif puts 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.

commandwhat it does
setupthe skeleton in the project: blocks.json, invariants.md, the entry point; --project, --lang, --cli
versionthe version of the kit this project runs
initcreate or extend state.json from blocks.json
statuswhere the review stands, block by block, with the fix debt as its own line
nextthe id of the next unclosed block
inventorythe repository tree with sizes and ownership, to cut blocks by (--under, --unassigned)
sizesevery block against the readability ceiling
coveragethe file → block map; fails on an unowned file (--no-write for CI), lists files owned by more than one block
couplingpairs 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)
orderthe 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)
backfillstamp fingerprints on records written before fingerprints existed
refsfinding ids of the register named in the code outside docs/review/ and the summary files
findingsregenerate findings.md from findings.jsonl
log <ID> "<text>"append a line to the journal
checkthe whole state for consistency — the gate for CI
summarythe one file that outlives docs/review/ (--html, --out, --aged <file>)
sarifopen 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:

blockoutcomespend
access and visibility, 58 files / 4017 lines12 findings2 agents, 576k tokens, ~35 min
domain core, 13 files14 findings2 agents, 446k tokens, ~34 min
writes and versions, 42 files17 findings2 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_gate severity; deferring needs a reason;
  • seams — coupling between blocks, seams inside one, both reading merges as one change;
  • order by risk, then by churn — order;
  • the summary that outlives the review directory — summary, summary --aged, and summary --html for a reader who never saw the review;
  • findings on the platform — sarif for GitHub code scanning, with ready CI jobs for GitHub Actions (check as 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 — scope in blocks.json, said everywhere the result is;
  • the loop signal — fix rounds stop for a human's decide when 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 — check refuses a confirmation without one;
  • spend measured — run-role.sh and axes.py, turn caps, the one-read rule in the templates, ROLE_DENY as 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 seams learns 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 — seams came 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 3 glues nearby matches into one block. The engine that matches a finding to its allowance by line number lives in skills/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.

WhatWhat it turns into
example/makefile-snippet.mk contains not review targets but pieces of SAST targets from another projectnot one make review-* command from the documentation exists; installation is done blind
the documentation calls prompt BLOCK ROLEthe tool requires prompt BLOCK --role; the command from the instructions answers unrecognized arguments
check requires a manifest for all blocks at oncea 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 reportset-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 caughtthe map on disk diverges from the recount, and the consumer reads yesterday's ownership without knowing it
no block size ceilinga block that physically cannot be read in one session reports coverage that never happened
no command to move a findingthe 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 nothingthe 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 pathsa 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 pattern src/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.
  • exclusions is a list of objects {pattern, reason}, not strings. A list of strings crashes coverage with TypeError.

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:

  • check requires a verifier's report for a block in status verified/closed;
  • check compares coverage.tsv with 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 hunted refuses 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/closed a block remembers a fingerprint of the composition and contents of its files, and check catches a block closed on a different version of the code. To confirm that the changes were reviewed — restamp <BLOCK>, exactly like doorstop review. A block or finding without a fingerprint (recorded by a kit version that had none) is also a failure, not a skip: backfill stamps 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), and check looks 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 until restamp. 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 and restamp <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 to REGION_K in review.py has the numbers. Records stamped by an older version (code_sha, the whole file) are checked as before, and restamp moves 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-rule takes 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) or repository:path for 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. The roots command 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; check warns about the latter. The guard is demanded in the fix phase — once an instance is fixed or its block is fixing/closed; before that check warns (root/guard-due), since a verified block has nobody yet to write a guard and the release gate needs check green 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", check requires 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}} from blocks.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 refuses fixed without a commit, rejected without a reason, duplicate without a reference;
  • check requires a rejection reason for every rejected finding;
  • the hints in messages are gathered into one constant, CLI (in the original half of them called make, half npm run — the rig was carried between projects and never proofread);
  • the make targets snippet finally contains review targets, and next to it lies the same list for package.json (both in skills/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) or measured (test quality, performance, scanners: the proof is the manifest's artifacts, not reading); the prompt's first rule is derived from it, a block without paths is 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) and sizes (blocks against the ceiling), coverage --no-write for 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 running time 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.

↑↓ navigate openesc close