Free, open-source CLI and GitHub Action: the risk of merging a pull request and the files that carry it, from static analysis.
Developer toolsTypeScript · PythonGitHub @winninOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Buzzing on social · #22 now · 2 sources
Checked Oct 30, 18:46 · Scores are copied from each platform at the last check. We don’t copy post bodies or comments, and deleted posts disappear here too.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Know the risk of merging a pull request, and where it lives. Free, open-source CLI and GitHub Action: after your AI review, farol shows the riskiest files, what they reach, diff coverage and contract breaks. TypeScript/React, Go, Python.
Taken from the repository README
Loading the full README.
Read the full README ↗Agents write the code and agents review it. The last call is still a person's: approve the merge or not. That approval is the scarce part, and spread evenly over every pull request it turns into a skim, which is how a one-line change to a shared hook takes down forty pages.
farol comes after the AI review, not instead of it. Your review agent comments on the lines in the diff. farol gives the person who approves what the agent cannot reliably give: the risk of accepting the change, and where it lives. The goal is not only a faster review: low-risk pull requests can go fast, and the risky ones get read line by line, starting with the files that carry the risk. It works from static analysis (the compilers' own view of your code, not a model's guess), so the same pull request always gets the same answer.
The Claude Code team describes the same principle in How the Claude Code team does code review: review effort follows the risk of the change. As they put it, "The change sets the tier, not the author."
farol (Portuguese for lighthouse) does the triage before the reviewer opens the diff:
Free and open source (MIT). Run it as a CLI with npx @winninjs/farol, or on every pull request with the GitHub
Action winnin/farol/action. No account, no hosted service: it runs on your machine or in your CI runner, comments on
every PR and never blocks a merge. It never executes your code and never sends it anywhere.
See farol on a real pull request →
The demo holds the reports farol produced in CI for a pull request on the FastAPI full-stack template: a FastAPI backend and a React frontend with TanStack Router. The pull request adds a status to items and an Insights page, with the kind of problems a reviewer should catch: a breaking API change, SQL built with an f-string, a shared component without tests.
The reports are interactive: check the findings, tick files as reviewed and watch the risk drop, mark a page as behind
a feature flag, and copy the results for the pull request. They were built from the analysis farol ran in CI on
the pull request; the copies live in
docs/demo/.
| AI review agents | farol | |
|---|---|---|
| Reads | the diff, and whatever context fits in the prompt | the whole program at the base and the head, with the compilers (TypeScript type checker, go/types, Python ast) |
| Tells you | what may be wrong in these lines | what these lines reach, how risky the merge is, and where to look first |
| Answers are | probabilistic: worded differently on every run | deterministic: the same PR gives the same report, and every claim comes with its call chain |
| When unsure | may sound confident anyway | says unknown and asks for a careful review |
| Runs | a model, usually a hosted service | locally or in your CI: it never executes your code and never sends it anywhere |
Use both, in that order. The agent reviews the lines first; then farol tells the person who approves how much attention the merge needs, which files deserve it, which pages and endpoints to test by hand and which contract changes need their clients checked. The agent's review is not enough exactly when a small change has a large reach, and that is what farol shows.
go/types, Python's ast). A change to formatPrice lists the twelve pages that render it, not "1 file changed".unknown) and asks for a careful review. It never calls something safe because it did not understand it.You need Node.js 22+ and git (plus go for Go projects, python3 3.11+ for Python ones). Nothing to
install: run farol with npx @winninjs/farol.
1. Describe your project in a farol.config.json at its root. One line is enough to start; pick your stack:
| Your project | farol.config.json | Full recipe |
|---|---|---|
| React app with React Router | { "routes": { "file": "src/routes.tsx" } } (the file that builds the route table) | React Router |
| Next.js app (Pages or App Router) | { "routes": { "adapter": "next" } } | Next.js |
| React app with TanStack Router | { "routes": { "adapter": "tanstack-router", "dir": "src/routes" } } | TanStack Router |
| TypeScript API (GraphQL, REST) | { "surface": "backend" } | TypeScript API |
| Go service | { "language": "go", "surface": "backend" } | Go |
| Python service (FastAPI, Flask, Django, Celery…) | { "language": "python", "surface": "backend" } | Python |
2. Check what farol sees: the pages, endpoints, jobs and scripts it found.
npx @winninjs/farol routes
3. Analyze a branch against its target and open the report.
npx @winninjs/farol analyze --base origin/main --head HEAD --out farol.json
npx @winninjs/farol html --in farol.json --out farol.html && open farol.html # xdg-open on Linux
cd path/to/your/react-app
echo '{ "routes": { "file": "src/routes.tsx" } }' > farol.config.json
npx @winninjs/farol routes # lists your pages: "/", "/products", "/settings"…
git checkout my-feature-branch
npx @winninjs/farol analyze --base origin/main --head HEAD --out farol.json
npx @winninjs/farol html --in farol.json --out farol.html && open farol.html
The report already shows the verdict, the score, the files to start with, the pages the change reaches and what to test by hand. Next steps: add your coverage, lint and audit reports and run farol on every PR with the recipe for your stack. Every option is in the reference and, per stack, in examples/README.md.
Running it often?
npm install --global @winninjs/farolgives you a plainfarolcommand.
Each recipe has three parts: the farol.config.json to commit, the reports to produce, and the GitHub Actions
workflow. The workflows in examples/ are complete and share one shape:
tests runs your tests, linter and audit and keeps the reports. It runs the PR's code, so it has no secrets and a read-only token. If you already have a test job, keep it and add its upload step.farol runs after it (pass or fail), reads git and the reports, and posts a new comment on every push with a link that opens the HTML report in the browser, plus a second report with only the commits since the previous push. It never runs the PR's code.continue-on-error.Pin the action to a commit once it works for you: winnin/farol/action@<sha> # main as of <date>.
examples/README.md goes deeper for each stack: every configuration key, CLI parameter and
action input, and how to check a setup.
React Router route tables (object and JSX forms, lazy routes, split route modules), reach per declaration.
{
"routes": { "file": "src/routes.tsx" },
"roles": { "global": ["src/main.tsx", "src/App.tsx", "src/styles/global.css", "package.json", "vite.config.ts"] }
}
npx vitest run --coverage.enabled --coverage.reportOnFailure --coverage.reporter=json --coverage.reportsDirectory=reports/coverage
npx eslint -f json -o reports/eslint.json <changed files>
npm audit --json > reports/audit.json # or pnpm audit --json
npx @winninjs/farol analyze --base origin/main --head HEAD \
--coverage reports/coverage/coverage-final.json --eslint reports/eslint.json --audit reports/audit.json --out farol.json
CI: copy examples/workflow.yml to .github/workflows/farol.yml.
Set coverage.include: ['src/**/*.{ts,tsx}'] in the Vitest config so files no test loads are reported as untested.
Split route trees go in routes.extraFiles; a fuller config is in examples/farol.config.json.
Pages Router and App Router: every page, API route, layout, _app/_document and middleware, read from the file
names. Dynamic segments ([id]), catch-alls, route groups and parallel slots are understood.
{
"routes": { "adapter": "next" },
"roles": { "global": ["next.config.*", "package.json"] }
}
pages/ and app/ are found in the project or in its src/; set "dir" when they live elsewhere. Reports and CI
are the same as for React Router (Jest or Vitest coverage, ESLint, audit). In a
monorepo whose tsconfig extends a workspace package ("extends": "tsconfig/nextjs.json"), install dependencies
before analyzing, so the package is in node_modules.
File-based routing: every module calling createFileRoute('<id>') under the routes directory, with the root route,
pathless layouts (_app) and parent routes as layouts of their children.
{ "routes": { "adapter": "tanstack-router", "dir": "src/routes" } }
Reports and CI are the same as for React Router.
GraphQL root operations (schema files and gql templates, per resolver) and Fastify/Express-style REST endpoints.
{
"surface": "backend",
"backend": { "graphql": true, "rest": true },
"roles": { "global": ["package.json", "src/app.ts"] }
}
Reports and CI are the same as for React Router: examples/workflow.yml. If
resolvers reach data through an injected context (ctx.loaders.Brand.byId()), tell farol where the providers live:
"backend": { "context": { "loaders": ["src/modules/*/loaders/index.ts"] } }.
chi, gin, echo and net/http routes, gRPC service methods and main binaries, reach per declaration with interface
calls resolved to their implementations.
{ "language": "go", "surface": "backend" }
go test -coverpkg=./... -coverprofile=reports/cover.out ./...
golangci-lint run --output.json.path=reports/golangci.json --issues-exit-code=0 ./... # enable gosec
govulncheck -json ./... > reports/govulncheck.json
npx @winninjs/farol analyze --base origin/main --head HEAD \
--coverage reports/cover.out --eslint reports/golangci.json --audit reports/govulncheck.json --out farol.json
CI: copy examples/workflow-go.yml. farol reads modules offline, so the workflow runs
go mod download for the base and the head first.
FastAPI and Flask routes (routers, blueprints, prefixes), Django URLs, MCP tools/resources/prompts, Celery tasks,
Temporal workflows and activities, Prefect flows and __main__ scripts. farol parses your code; it never imports it.
{ "language": "python", "surface": "backend", "python": { "roots": ["src", "."] } }
pytest --cov=src --cov-report=json:reports/coverage.json
ruff check --output-format json --extend-select S --exit-zero . > reports/ruff.json # S = flake8-bandit
pip-audit --format json -o reports/pip-audit.json
npx @winninjs/farol analyze --base origin/main --head HEAD \
--coverage reports/coverage.json --eslint reports/ruff.json --audit reports/pip-audit.json --out farol.json
CI: copy examples/workflow-python.yml (uses uv; swap in your installer).
Optional, and information only: it never changes the score. With it the report shows how much traffic each affected page or operation gets, its distinct production errors (linked to Datadog), the changed files that already throw errors, and the services that call each changed endpoint.
{
"datadog": {
"env": "prod",
"rum": { "applicationId": "<RUM application id>" },
"apm": { "service": "orders-api", "operations": ["fastapi.request"] }
}
}
DD_API_KEY=… DD_APP_KEY=… npx @winninjs/farol usage --out usage.json # last 14 days
npx @winninjs/farol analyze … --usage usage.json
CI: the example workflows have a farol-usage job: set the DD_API_KEY and DD_APP_KEY secrets and the
repository variable FAROL_DATADOG=true. It reads Datadog once a day and never sees the PR's code. Keys, scopes and
matching rules are in the reference.
| TypeScript / React frontends | TypeScript / JavaScript backends | Go | Python | |
|---|---|---|---|---|
| Entry points | React Router route tables (object and JSX forms, lazy routes, split route modules); Next.js Pages and App Router (pages, API routes, layouts, middleware); TanStack Router file-based routes | GraphQL root operations (schema files and gql templates), REST endpoints (Fastify/Express style) and command-line commands (commander, cac, yargs) | chi, gin, echo and net/http routes; gRPC service methods; main binaries | FastAPI and Flask routes (routers, blueprints, prefixes); Django URLs; MCP tools, resources and prompts; Celery tasks; Temporal workflows and activities; Prefect flows; __main__ scripts |
| Reach precision | per declaration (TypeScript type checker) | per module, per resolver for GraphQL | per declaration (go/types, interface calls resolved to implementations) | per declaration (Python ast with local type inference) |
| Contract changes | — | GraphQL schema (breaking / dangerous / safe) | Handler request and response types (net/http, chi, gin, echo); gRPC messages | FastAPI parameters, body, response and Pydantic models |
| Dependencies | package.json + npm/pnpm/yarn/bun lockfiles | same | go.mod + go.sum | pyproject.toml (PEP 621, extras, groups, uv, Poetry), requirements.txt + uv.lock/poetry.lock/Pipfile.lock |
| Diff coverage | Istanbul coverage-final.json (Vitest, Jest) | same | go test -coverprofile | coverage.py JSON (pytest --cov) |
| Lint | eslint -f json | same | golangci-lint JSON | ruff JSON |
| Security | built-in rules (XSS sinks, eval, hard-coded secrets, javascript: URLs…) | same | gosec (through golangci-lint) | flake8-bandit (ruff S rules) |
In every language: environment, infrastructure and CI files (.env*, Dockerfiles, compose, CI pipelines, helm, k8s,
terraform…) in a section of their own; incremental analysis since any commit (--since); suggested reviewers from
git blame and CODEOWNERS; reports as a GitHub comment, Markdown or a single-file HTML page.
farol reviews its own pull requests with .github/workflows/farol.yml: a TypeScript
command-line tool, with "surface": "backend" and its commands (analyze, html, comment…) as the entry points.
farol produces two things: a PR comment and a single-file HTML report (linked from the comment) where the review happens. The comment is the report's summary in Markdown and counts the same way: merge risk and verdict, where the risk comes from, Must check as a task list, the High and Medium files with their share of the score, and how much new code is tested; every other signal sits in a collapsed All signals block. The report is a workbench: the left rail holds the risk that is left and what to read next, the main area shows what you picked.
The short version: open the report, clear Must check, read the High and Medium files and tick each one, skim the rest, try the pages it can affect, then Copy results into the pull request. When the risk is low and nothing is left to check, the report says so.
The number at the top of the rail, 0–100, is the chance that at least one risky part of the change breaks something. It is live: every part of it belongs to something you can clear (a file you read, a finding you checked, a contract change whose clients you confirmed), and it drops as you clear them. Was 69 tells you where it started.
| Verdict | Means | What to do |
|---|---|---|
| ✓ Fast track | Only new code, tests or low-risk changes. | Skim the files and approve. |
| ◐ Quick review | Existing code changed in a contained way, or a weaker signal (low coverage, lint errors, a config file). | Read the High and Medium files, then approve. |
| ⚠ Careful review | Wide or silent breakage is possible: a score of 60+, a breaking contract change, a high-severity finding, an untested central file, or code farol could not read. | Clear Must check, read the risky files line by line and try what they reach. |
The summary says in one sentence where most of the risk comes from, and What makes up the N shows every part of the score with its share. The shares add up to the score, so a part's share is exactly what clearing it removes. When the score falls below 30 with nothing left in Must check, the risk turns green: likely safe to approve.
Breaking API contract changes, security findings, lint errors and vulnerable dependencies, listed above every file. Reading a file does not clear them: each one needs your confirmation (clients updated or versioned, fixed or safe in this context). A lint error on the same line as a security finding counts once.
Every changed file that adds to the score, in tiers by its own risk: High, Medium, Skim (collapsed) and Test files (collapsed, trimmed tests only). Tick the box next to a file once you read it; the number on its right is its share of the score right now. Opening a file shows:
A dependency manifest shows each package bump (^0.37.0 → ^0.39.1), what the bump means (a 0.x minor may break the
API) and how to check it. Bumps weigh by how much they can break: a major or a removed package more than a 0.x minor,
a compatible minor or patch little.
On the summary, collapsed: the share of changed executable lines that ran in your tests, file by file, with the lines that never ran. A changed file missing from the coverage report counts as untested, except:
coverage.include (they mark such files), in coverage.py reports with source, and in Jest/Istanbul reports with
collectCoverageFrom when one of the unloaded files has code that runs on load (a top-level statement; a file of
functions only proves nothing). Without that proof, and always for Go profiles (they leave out packages without
tests), a missing file stays untested;signals.coverageIgnore.The last two are listed as left out of coverage on purpose: neither tested nor untested, they keep half of their risk. Without a coverage report the line says so; it never counts as tested.
The existing pages or operations that run the changed code, plus the new ones, riskiest first; with Datadog, their traffic and errors. Tick the ones you tried by hand. Mark the ones that ship behind a feature flag: once every page that runs a file is flagged, that file leaves the score, so the risk that is left is what users will hit.
Copy results (top bar or the Results page) copies a Markdown comment for the pull request: the risk before and after, Must check as a checklist, the files reviewed per tier and the ones left, the pages tried and flagged, the coverage and your notes. Paste it in the PR so the next person knows what was looked at. The page also shows, just for curiosity, how much of the change needed a careful read and the time that saves.
The full analysis behind the workbench: the reach and risk points of every changed file, security, dependencies, signals (coverage, lint, suppressions, tests), contracts, reviewers, every page or operation with the import graph that reaches it, all files, the reasons and the diagnostics.
After a push to an open PR, the comment carries a second report with only the commits since the previous push
(analyze --since <sha>; after a force-push, from its merge-base). Its verdict, score and files cover just
those changes. Use it for the second round of a review: check that the fixes do what was asked and nothing
else, then go back to the full report only if the new commits reach somewhere new.
Kept in the report's Details and a collapsed block of the comment, so reviewers form their own opinion first:
isolated (only adds), touches-existing (existing code changed), global (a global file, manifest or critical
dependency) and unknown (part of the change could not be read: never safe). Every reason code is in
docs/reasons.md.
go list + go/types (offline), Python ast. From them, the entry points and a graph of which declaration uses which.On 662 past commits of four repositories, fast track had 0 problems in 56 commits and the score's AUC was
0.72–0.83; run farol calibrate on your own history.
The details per language are in docs/how-it-works.md, and the reasons behind every choice in
docs/DECISIONS.md.
analyze never executes code of the analyzed repository and never uses the network. The Go and Python helpers use only their standard libraries and only parse.comment (GitHub API) and usage (Datadog API) talk to the network. Error messages are not copied from Datadog, only error types.getattr, importlib, DI containers, registries filled in loops). When an entry point cannot be read, the PR is unknown; when a call cannot be resolved, farol assumes it reaches more, never less.web.Router) are not read. Python: Django REST Framework routers are not read.We already have an AI reviewer. Why add farol? They answer different questions. The agent comments on the lines in the diff; after it, farol tells the person who approves how risky the merge is and which files carry the risk, from static analysis of the whole program. Run both: farol's comment and report sit next to the agent's, and neither blocks the merge.
Is it free? Yes. farol is open source under the MIT license, with no account, hosted service or paid tier: run it
with npx @winninjs/farol or the GitHub Action winnin/farol/action.
Will it slow down CI? No. farol runs after your tests and reuses their reports. The analysis takes about 3 s for a 300-file Python service, 8–10 s for a large TypeScript monorepo or a Go service with a cold cache, and under a second for the base when its helper output is cached.
Can I use it outside GitHub? Yes. analyze, html and report work anywhere; only comment is
GitHub-specific. Post the output of npx @winninjs/farol report with your platform's API.
The report says "not verified". That report was not given, or the path is wrong. Paths given to analyze are
relative to the current directory; paths in farol.config.json are relative to the project.
A page I changed is not listed. Run npx @winninjs/farol routes to see the entry points farol reads, and
npx @winninjs/farol impact <file>
to see what reaches a file. Unreadable route constructs are listed under diagnostics.
Go: "farol reads Go modules offline: run go mod download". The module cache lacks a dependency of the base or the
head. Run go mod download for both before analyzing.
Monorepo? Point --cwd at the app. Paths stay relative to the repository root, and changes in apps the project
does not use are listed as outside it.
Contributions are welcome: route adapters, frameworks, languages, report formats and better heuristics. See CONTRIBUTING.md for the setup, the architecture and the conventions, and SECURITY.md to report a vulnerability.
MIT © 2026 Winnin
| Vulnerabilities |
npm audit --json, pnpm audit --json |
| same |
govulncheck -json |
pip-audit --format json |
| Suppressions | eslint-disable, @ts-ignore, @ts-expect-error, as any… | same | //nolint, //lint:ignore, #nosec | # noqa, # type: ignore, # nosec, # pragma: no cover… |
| Traffic and errors (optional) | Datadog RUM page views and errors | Datadog APM requests, error spans and callers | same | same |