npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

phada

v0.16.0

Published

Review GitHub pull requests with the AI you already use: Claude Code, Codex or Ollama.

Readme

Phada

Phada publishes by default. phada review posts the review on the pull request as the owner of the GitHub token. Use --dry-run to only print it.

Phada reviews a GitHub pull request with the AI you already use — Claude Code, Codex or a model served by Ollama — and publishes the review on the pull request.

It runs on your machine. Phada fetches the pull request, its diff, its conversation and its CI checks, reads the repository's own rules and docs, sends them to the AI provider you choose, lets the AI read the rest of the repository with read-only tools, checks that the answer is a valid review and scores it. It prints the review as Markdown or JSON and posts it on the pull request: one comment on the line of each finding and the rest in the review body (see Publishing).

Requirements

  • Node.js 22.13 or newer
  • git, for the repository investigation and phada index (without it, the AI only sees the diff and the context)
  • A GitHub token that can read the pull request and write reviews, or the login of the GitHub CLI (gh) (see GitHub token)
  • One AI provider:
    • Claude Code (claude), logged in with your subscription,
    • Codex CLI (codex, install with npm i -g @openai/codex, then codex login), or
    • Ollama, running, with a model pulled (e.g. ollama pull qwen2.5-coder:7b)

Install

npm install -g phada
phada

In a terminal, phada opens the setup while you have no config of your own: one step at a time, it gets the GitHub token, the AI, the language and the repositories to watch ready, and writes nothing before you confirm it. phada setup opens it again at any time.

Or run it without installing:

npx phada review owner/repo#123

Usage

phada review owner/repo#123
phada review https://github.com/owner/repo/pull/123 --provider codex

Progress goes to stderr and the review to stdout, so phada review owner/repo#123 > review.md saves only the review, and --format json prints it as JSON for scripts. A review of a large pull request can take a few minutes. phada review owner/repo#123 --dry-run prints exactly what would be posted and posts nothing; phada publish <id> then posts exactly that, without running the AI again (see Look first, publish later).

phada --help lists the commands, and phada review --help, phada publish --help, phada setup --help, phada init --help, phada index --help, phada rules --help, phada prompt --help, phada watch --help, phada history --help, phada misses --help and phada lessons --help list their options. To have new pull requests and pushes reviewed without running anything, see Watch mode; to see what happened to the comments Phada published, see History, and for what people pointed out that Phada did not, Misses and Lessons. To see all of it in one place, run phada alone (see Terminal app).

GitHub token

Phada reads the token from GITHUB_TOKEN, or from GH_TOKEN when GITHUB_TOKEN is not set. Without either, it uses the login of the GitHub CLI: after gh auth login, every command works with no variable to export. Phada asks gh auth token (or, on older gh versions without it, gh auth status --show-token) once per run, only when a command needs GitHub, and never saves or prints the token. The token reads the pull request and its reviews and publishes a review, so the token needs:

  • a fine-grained token with Contents: Read-only and Pull requests: Read and write on the repository, plus Checks: Read-only, Commit statuses: Read-only and Actions: Read-only for the CI checks, or
  • a classic token with the repo scope (public_repo is enough for public repositories).

A read-only token is enough with --dry-run. Without write access, Phada still prints the review and then stops with an error that names the missing permission. Contents: Read-only lets Phada read the repository's rules and docs (see Rules and repository context); without it, the review runs on the diff alone and says so. Without one of the CI permissions, the review runs without that part of the checks and warns with the permission the token lacks, e.g. CI check runs were not read: GitHub denied reading the check runs (HTTP 403): the token needs the checks:read permission.

Terminal app

Run phada with no command in a terminal to open the app: what Phada has set up, running and reviewed, in one place, without remembering commands or folders, and the setup and the changes to your config you would otherwise write by hand. It opens on the terminal's alternate screen, like less or vim, and the terminal is back as it was when you quit.

phada

| Screen | What it shows | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Home | One line each for GitHub (where the token comes from and whose it is), the AI (installed and logged in), the watch, the queue and the repository maps; the last reviews; and what to do next, with the command | | Repositories | The watched repositories, the ones with a config of their own and the ones in the history → the settings of each with where every value comes from, its watch filters, its map, its related repositories and its last reviews (and, for a watched one, whether it publishes); w, p, a, e and R change them, i indexes it, v reviews one of its open pull requests | | Rules | The reviewer lens and your rules at once; the repository's .phada/ and the rules and docs a review of the default branch reads; p and a pull request number show what a review of it reads and its prompt | | Reviews | The history, with f to filter → one review with its score, findings, what was published, the CI line and what happened to each comment; n reviews a pull request | | Queue | The watch running in this app, in another terminal (with its PID) or not running, its last and next cycle, the job running (with the step of its review when the watch runs here), the jobs queued, waiting for CI, skipped for failing CI or failed, a pause, the reviews this hour and the log of the watch started here → a job with its requests; w starts or stops the watch, l opens its whole log | | Automation | The watch, the CI wait, the request sources that are on (pull request comments, phada enqueue, phada mcp, Slack), the hooks and the when commands | | Provider | The provider and model you chose and where they come from, the language, each AI checked (installed, logged in, Ollama running and its models) with the command that fixes it, and your MCP servers; c, L and l change them | | Graph | The map of each repository of the Repositories screen: whether it is indexed, its commit, files, symbols and size → its commit and branch, when it was indexed and how long it took, its files per language, its symbols by kind, its imports, exports, calls and uses, and the files left out and why, and its connections: each related repository it declares (context.repos) linked by the packages one publishes and the other uses, without a link, or not indexed and why; i indexes it and I the related ones not indexed; s explores a symbol | | Learnings | Per repository, the relations learned from reviews, what happened to Phada's comments, per repository and per rule, the open misses with how many the latest replay caught, and the pending lessons to accept or discard |

Keys: ←/→ or Tab move between screens, 1–9 jump to one, / finds a screen by name (and /setup the setup, /language the language, /index the Graph, /review a review), ? lists every key, ↑/↓ and Enter open an item, Esc goes back, r reads again what comes from GitHub or the AI CLIs, and q quits. The footer lists the keys of the screen you are on.

What is on your machine (the config, the history, the queue) is read every 2 seconds; what comes from GitHub or the AI CLIs is read when a screen opens and on r. The app writes only what Changing your config from the app lists, and the history only for a review or the watch you run from it, as phada review and phada watch do (just reading it never creates its file). It reads each map without changing it: a map that is missing, made by another version of Phada or that cannot be read is shown as such on the Graph screen, until you index it. The connections of the Graph come only from your machine: the manifests and the .phada/config.yml of the indexed commit, read from the local copy the index was built from, never fetched. It needs a terminal of at least 80×24 and says so when it is smaller. It follows the language of your config (English, Portuguese and Spanish), uses the full colors your terminal supports (down to 16), and none with NO_COLOR or TERM=dumb. q exits with 0, Ctrl-C with 130 and SIGTERM with 143. Outside a terminal (a pipe, a script), phada with no command prints the same error as before, and phada setup stops with a usage error.

Setup

phada setup opens the app on the setup; phada alone opens it while your own config.yml does not exist, and /setup opens it from any screen, from its first step. It has seven steps, shown as Step n of 7: s skips one, and Esc asks whether to leave the setup for the Home (what you confirmed is already written, and phada setup starts again).

  1. GitHub: where the token comes from (GITHUB_TOKEN, GH_TOKEN or the gh login) and whose it is. Without a token, or when GitHub refuses the token of the gh login, Enter hands the terminal to gh auth login and checks again when it ends. Without gh, it says how to install it or to export GITHUB_TOKEN; a token from a variable that GitHub refuses has to be replaced or unset.
  2. AI: Claude Code, Codex and Ollama, each checked (installed, logged in, running), and r checks again. Choosing a CLI that is not logged in hands the terminal to its login (claude auth login, codex login) and checks again when it ends; the credentials stay with the CLI. A CLI reviews with its own default model or one you type. When Ollama is not running, the step says to start it with ollama serve and press r; when it runs, you pick an installed model, type one, or take the suggested qwen2.5-coder:7b (about 4.7 GB). A model Ollama does not have is pulled through its API (at OLLAMA_HOST too), with its progress, and Esc cancels the pull.
  3. Language: English, Portuguese or Spanish, for the app and the reviews; the app speaks it at once.
  4. Repositories: the repositories of the token owner on GitHub (owned, as a collaborator and of your organizations, without the archived ones, most recently pushed first), with the ones already in watch.repos marked and first. f filters the list, Space marks or unmarks one, a adds an owner/repo that is not listed, and Enter saves the marked ones to watch.repos.
  5. Graph: builds the map of each watched repository, one at a time, with N of M files. Esc stops: what was extracted is kept, and phada watch finishes it when it is idle, or phada index owner/repo now.
  6. This repository: when the app runs inside a git repository without a .phada/ folder, Enter runs phada init there and lists the files it created.
  7. Summary: what was written and to which file, what was indexed or left for later, what phada init created, the steps skipped, and what to run now (phada watch when a repository is watched, phada review owner/repo#N).

A step that changes your config shows the change first, one line per value (provider claude → codex, watch.repos + owner/repo), and writes it only on Enter; Esc cancels it.

Reviewing a pull request from the app

/review in the palette, n on the Reviews screen or v on a repository of the Repositories screen open the review in place of the tabs:

  1. The pull request: type owner/repo#N or its URL and Enter (anything else is refused on the spot), or pick a repository (Esc leaves the field, t types again) → its open pull requests from GitHub, with their number, title, author and whether they are drafts (r reads them again). v opens straight on the open pull requests of that repository.
  2. The mode: Only look reviews here and posts nothing, as phada review --dry-run; Review and publish posts the review on the pull request. While a phada watch runs (in the app or in another terminal), it becomes Put in the queue: the watch reviews and publishes it, as phada enqueue owner/repo#N --publish pr, and the result is the new job, the job it joined or why the queue refused it, with Enter to the Queue. Only look still runs here.
  3. For publishing, the pull request and the GitHub login that will publish it (the owner of the token) to confirm: Enter publishes, Esc goes back. Without a token it does not offer to publish.
  4. The steps, live: the pull request fetched (with a note under it when it is closed or a draft, which it still reviews, as phada review does), the context, the conversation, the CI, the indexing (N of M files), the related repositories, the impact, each run of the AI (review, verify, recheck, skill) with its time, publishing, and the warnings. Esc asks before cancelling (Enter cancels, Esc keeps it running): Phada stops the AI CLI (or the request to Ollama), posts nothing and saves nothing in the history. Once it publishes, it no longer cancels.
  5. The result: the same detail as the Reviews screen (score, verdict, findings with their text, what was published or, for only look, that nothing was posted, what it would resolve or why it would skip, the CI), how long it took and whether the history kept it. A failed review shows its error; with phada --debug, also its causes and stack. Esc goes back to the screen you came from.
  6. Publishing what you looked at: on the result of only look, p asks to publish that exact review (the pull request, the GitHub login and how many line comments and repository debt items it posts) and Enter publishes it without running the AI again, as phada publish; it shows the link, or why it did not publish (the pull request moved since the review, or Phada already reviewed that commit), and Esc goes back to the result, which no longer offers p once it is published. It needs the run in the history: with history: false it is not offered. It also publishes while a watch runs, since no new review starts.

It is the same review as phada review owner/repo#N (with --dry-run for only look): the provider, model and MCP servers of your config for that repository, your GitHub token, and the run saved in the history unless history: false. The AI CLI runs in its own process group, so it never writes on the app. Esc goes one step back; the palette stays closed while the review is open, so it is left only with Esc. While a review runs, q and Ctrl-C ask before quitting (Enter cancels it and quits, waiting for a publication under way; Esc stays; a second Ctrl-C cancels and quits at once).

Running the watch from the app

w on the Queue screen, or /watch in the palette from any screen, starts the watch in the app: the same watch as phada watch (your watch: settings, the lock, the history, the hooks and Slack), until you stop it. The Queue says whether the watch runs in this app, in another terminal (with its PID; w then only says so) or not at all, and what kept it from starting, in one line. Under the jobs, its log: what it queued, started, published, skipped or retried, its warnings, the time of each line, and the step of the review under way on the running job; l opens the last 200 lines. With the watch running here, w (or /watch) asks before stopping it after the current review; Enter stops, Esc keeps it running. Quitting with q, Esc or Ctrl-C asks too: Enter lets the current review finish and quits, a second Ctrl-C stops the AI at once and puts the pull request back in the queue, and a SIGTERM does that without asking. A review you start from the app while the watch runs here (only look) has its own AI CLI: cancelling one never stops the other.

Changing your config from the app

  • Provider: c changes the AI and its model (the choice of the setup's AI step, with the login or the model pull there), L changes the language and l logs in to the chosen AI CLI again (or to one that is not logged in). /language changes the language from any screen.
  • Graph and Repositories: i indexes the selected (or open) repository, or indexes it again, with N of M files as it goes; Esc stops it, and the screen reads the map again when it ends. Leaving the screen stops it too. On an open repository of the Graph, I indexes the related repositories it declares that are not indexed yet.
  • Graph: on an open repository, s asks for a symbol name and searches the graph the review uses, linked to the related repositories it declares that are indexed and linked by package. Several definitions are listed to pick one; one opens as a tree with its callers and its dependencies: Enter or → opens a node one level (a caller into its own callers, a dependency into its own dependencies, across the linked repositories), ← closes it, and Tab moves between screens meanwhile. Each level shows 20 items and how many more, and says when the graph cut the list short. A name not found or a repository that is not indexed says so, with i to index it. Everything is read from your machine; the app keeps the graph of one repository open at a time.
  • Repositories: on the selected repository, w watches or unwatches it, p turns its publish on or off, a adds repositories from your GitHub list or typed, and e and R open its own repos/<owner>/<repo>/config.yml and rules.md (in your config folder) in $VISUAL, $EDITOR or vi, created with a commented header when they do not exist. The app is back when the editor exits, with the file read again.

Like the setup, each change shows what it does before → after and is written only on Enter. The app writes:

  • your own config.yml: provider, model, language and watch.repos (a new file gets the values on top and the commented template of phada init --global below them);
  • the repos/<owner>/<repo>/config.yml and rules.md you open in the editor, created when they do not exist and never overwritten;
  • the map of a repository when you index it in the setup or with i on the Graph and Repositories screens, in the cache, as phada index does (on its default branch, replacing a map made by another version of Phada or that cannot be read);
  • a repository's .phada/ only through phada init, in the setup's step 6;
  • the history, with a review or the watch you run from the app, as phada review and phada watch do (the watch also takes its lock and keeps the index of each default branch up to date).

Comments, the order of the keys and the lines a change does not touch stay as you wrote them. When a line cannot be kept as written (an indentation of 4 spaces, for example), the change says that the file is rewritten in a standard layout before you confirm it. The new text is checked like any config before it is written, and it is written atomically (a temporary file renamed over it); a symbolic link stays a link, and the file it points to changes. A config file that is already invalid is never edited: the app shows its error and offers no change. If the file changed after the change was shown, Enter shows it again.

While a login or the editor runs, the terminal is theirs and Ctrl-C goes to them, not to the app. The login of an AI CLI runs without GITHUB_TOKEN and GH_TOKEN in its environment.

What a review looks like

Phada sends the diff with the line number of every added and context line, so the AI copies the line instead of counting it. The AI answers in a fixed JSON shape that Phada validates, and every finding must point to a file of the pull request and a line inside the diff. Phada then prints:

  • Confidence score (0–5), computed by Phada from the findings, never by the AI: 5 means no problems found, 4 only P2 findings, 3 one P1, 2 two or more P1, 1 one P0, 0 two or more P0. Neither the AI nor text inside the pull request can set the score directly; it only follows the findings.

  • Coverage next to the score when the review did not look at every file, e.g. 5/5 (ready to merge) · low coverage: 11 of 63 files (52 files not accounted for). The AI accounts for every changed file (judged from the diff, read whole, compared with existing code, or skipped with a reason), and Phada checks that account against what the tools actually read: a file said to be read but never read counts only as seen in the diff, and a new file of 50 or more added lines counts only when the AI compared it with existing code it read outside the diff, or with the file of the diff its code moved out of. The coverage is full when every file is covered, low below 70% of the files, partial in between; full shows nothing. It never changes the score or the verdict: it says how far the findings can be trusted. Without the tools the account is taken as given and the label says (declared). When the coverage is low, Phada runs one continuation pass: the same review with the list of what was left out and the findings already made, whose findings join the first ones (review.continuation: false or --no-continuation turns it off). The coverage is in the terminal review, the body posted on the pull request, --dry-run (with each file left out), --format json (review.coverage), the history and the review's detail in the app.

  • Summary of what the pull request changes and a table of the changed files.

  • Findings grouped by severity, each with file, line, confidence (0–100), why it matters and a suggested fix (with the exact code change when the fix is a local edit, see Suggested changes). A finding sits at the changed line where the fix goes, such as the function that copies existing code or the request without a catch, not at a line that only calls it or shows its effect. Confidence is how sure the AI is that the problem is real and happens as described; how often it happens and how much it costs go into the severity, so a real but rare bug is a sure finding with a lower severity (a P0 stays P0 however rarely it happens). Only findings at the confidence cut or above are shown here (60 by default, see --min-confidence):

    • P0 · Must fix: security holes, data loss, crashes, wrong money handling;
    • P1 · Should fix: bugs, incorrect behavior, edge cases, race conditions, leaks;
    • P2 · Consider: maintainability or design risks with a concrete consequence.

    A finding at most 10 points above the cut is marked close to the cut: the AI does not rate a finding the same way every time, so another run of the same commit may leave it out. The mark and their count show in the terminal review, in a "Close to the cut" section of --dry-run, in closeToCut of each finding in --format json and the history, and in the review's detail in the app; the comments posted on the pull request do not carry it.

  • Worth checking: up to five findings with confidence from 50 up to the cut, one line each. They do not change the score.

  • Rejected by verification: each candidate the verification turned down, with its place, confidence and the reason; only in the terminal, never on the pull request.

  • Repository debt: problems that are real but that this change neither introduces nor worsens (an exported function no caller uses that the change only touches, say), one line each with its place and why. See Repository debt.

  • A footer with how many findings there are, how many are worth checking, how many were dropped (rejected by verification, outside the diff, duplicates, below confidence 50, not in the expected shape, repository debt with review.debt: off, or reimplementations without the existing code read) and the provider, model, time and tokens.

At the end, phada review prints on stderr the tokens of each call to the AI (each review pass, one per skill, the continuation pass, the verification and the recheck of earlier comments; a native skill counts its run and the conversion of its output together) and their total: the input tokens, how many of them came from the provider's cache, and the output tokens, e.g. Tokens: review 812.0k in (640.0k cached) · 9.0k out; verify 120.0k in · 2.0k out; total 932.0k in (640.0k cached) · 11.0k out. A provider that does not report them shows not reported (Ollama reports no cache). The same counts are in a "Tokens" section of --dry-run, in usage of --format json and the history (passes with pass, skill and usage, and the total), in the review's detail in the app and next to each AI step while the app runs a review.

Large pull requests

With review.groups: true or --groups (off by default), when the diff has 30 KB or more of code, Phada splits it into diff groups of about 20 KB each, by directory (a directory too large is split by its subfolders, a single file larger than that is a group of its own, and there are at most 6 groups), and each review pass runs once per group: the AI sees the diff of its group, the list of every changed file with the ones of the group marked, the same context and the same tools to read the rest. Findings outside the lines of a group are left out. The answers are joined into one review: the summary is made from the part of each group, the claims are merged, the coverage counts every group, and the continuation pass and the verification run once over the whole diff. A pull request below 30 KB of code, or whose code fits in one group, is reviewed as before. Grouping spreads the attention over every part of a large pull request, at about twice the tokens.

Data files (.json, .lock, .csv, .tsv, .svg, .snap, .txt, .md, .map) and generated files (marked linguist-generated in the .gitattributes at the root of the base branch, or starting with @generated or Code generated … DO NOT EDIT) do not count toward those sizes and never make a group of their own: they are reviewed with the group of the nearest directory.

Groups run as many at a time as the provider takes: one with Ollama, two with Claude Code and Codex; review.groups: { concurrency: 1 } to 4 sets it. A group whose answer is not a valid review becomes a warning and its files are left as coverage gaps; a provider failure, such as a usage limit, fails the review. phada review prints Group 2/4: api/src/services (6 files, 21 KB) as each one starts; the groups, with their files, size, findings, time and tokens, are in a "Groups" section of --dry-run and of phada history <id>, in groups of --format json and the history, in phada prompt and in the review's detail in the app. The body posted on the pull request does not change. Native skills are never grouped. --no-groups turns grouping off for one review when the config turns it on.

By default (turn it off with --no-verify or verify: false), the first pass asks the AI for every candidate from confidence 25 instead of 50, and Phada makes a second call to the same AI before scoring. It sends the pull request, the diff and the candidates, without their severity and confidence, and asks a skeptical reviewer to confirm each one, with its own severity and confidence, or reject it. A finding is confirmed only when it can happen in the repository under review: when the effect it claims depends on a condition (a setting, the callers, the log level, a flag, the inputs that reach the code), the verifier looks for that condition with the investigation tools (without them, in the diff) and rejects the finding, naming what is missing, when the repository does not meet it; how a library behaves on its own is not enough. Rejected findings leave the review and are counted in the footer; --format json lists them with the reason, and gives the verifier's reason for every confirmed finding too (null for a finding it did not check). Confirmed findings still below 50 are dropped like any other. A finding without an answer keeps its first rating and the footer says how many were left unchecked (not verified when none got an answer). Verification adds about half the time and doubles the tokens; the time and tokens shown are the total of both calls.

If the AI's answer is not a valid review, Phada stops with an error instead of printing it; --debug shows the start of the answer.

Claims of the description

The title and description of a pull request are the author's account of the change, so the review checks them instead of repeating them. The AI lists each concrete, checkable thing they assert the change does or guarantees ("X prevents Y", "covers Z", "is backward compatible", "only runs on main") and checks it against the diff and, with the tools, the repository: confirmed or contradicted with the file and line that show it, or unverified with what is missing. A contradicted claim that matters is also reported as a finding at the line that breaks it, and the summary never states an unverified or contradicted claim as fact. The description stays data to check: an instruction in it is never followed. The claims show in a "Claims of the description" section of --dry-run, in claims of the review in --format json and the history, and in the review's detail in the app; they are not posted on the pull request. When several skills review the pull request, the same claim is kept once, with the strongest verdict (contradicted, then confirmed): case, spacing and punctuation do not tell claims apart, and a claim of three words or more that another one only extends (… to the topics and … to the topics of A and B) is the same claim. Once a skill has listed the claims, the unverified ones of the skills after it are left out. An AI answer without the list still gives a review, with a warning that the claims were not checked. A skill that runs natively has its claims taken from what it wrote, and only the ones it confirms or contradicts: one that reviewed nothing adds no claims.

Repository debt

The AI marks each finding with its scope: change when this pull request introduces or worsens the problem, repository when the problem is already there and the change only moves or touches the code. A finding without a scope is a finding of the change. review.debt decides what happens to the ones of the repository:

  • summary (default): they are not posted as line comments. The review body lists them under "Repository debt", one line each with severity, file and line, title and a short why, and they count neither in the score nor in the verdict. The ones below the confidence cut stay in "Worth checking" like any other.
  • comments: they are posted and scored like every other finding.
  • off: they are dropped and counted in the footer (debt in dropped of the JSON).

--format json and the history give the scope of every finding and the debt list of the review; the app marks a finding of the repository and shows the debt in the review's detail.

Reimplementations

The built-in reviewer also looks for code the pull request writes again although the repository already has code that does the same (a helper, a function, a module): a P2 finding, or P1 when the copy diverges from the existing code and causes a bug. Such a finding gives the place of the existing code in existing (path:line, or owner/repo:path:line in a related repository), and Phada keeps it only when the AI read that file during the review, with read_file or in the results of a grep, and the line is not one the diff adds; the place then comes first in the finding's sources. Otherwise a P2 finding is dropped and counted in the footer, e.g. 1 dropped (1 reimplementation finding without the existing code read), in the terminal and in the body posted on the pull request (in the review language; unbacked in dropped of the JSON, and the dropped findings themselves in unbacked); a P0 or P1 finding is a problem on its own, so it stays without existing. Without the investigation tools nothing can be read, so these P2 findings are always dropped. The verifier reads the cited code and rejects the finding when that code does not do the same thing. Findings of a skill that runs natively have no existing and stay ordinary findings. --format json and the history give existing on every finding (null when it has none).

Suggested changes

When the fix of a finding is a local edit, the AI also gives the lines it replaces and the code that takes their place. Phada keeps that suggestion only when the lines are added or context lines of one @@ section of the diff, at most 20 of them, include the line of the finding, are quoted exactly as they are in the diff, and the new code differs from them and holds no hidden control or bidirectional characters. Otherwise the finding keeps only its fix in words. The verifier sees the suggestion too and drops it when the new code would not fix the problem or would break something else. A kept suggestion shows as a diff block in the terminal and is published as a GitHub suggestion that the author applies with one click (suggestion in the JSON, and a suggestions count of valid, invalid and dropped ones).

Publishing

Each run posts one GitHub review (a comment review, never an approval or a request for changes) on the commit it reviewed, as the owner of the token:

  • every finding becomes a comment on its line, with severity, title, confidence, why it matters and the suggested fix, and a "Based on" line with the evidence (the files and CI checks it relies on) and the rule it breaks; the skill that found it stays in a hidden marker of the comment and in the history;
  • the review body starts with the butterfly and the score (## 🦋 3/5 (implementation issues)) and has the summary, the changed files, how many findings were posted, the findings worth checking, the repository debt and what was dropped; it names neither Phada nor the provider and model;
  • a pull request without changes gets nothing.

Phada reads its earlier reviews and threads on the pull request first, and only counts the ones written by the owner of the token. It decides before calling the AI:

| Earlier Phada review on the same commit | What happens | | ------------------------------------------------------ | -------------------------------------------------------- | | None | Reviews and publishes | | Exists, and a Phada thread on the pull request is open | Skips: "already reviewed by Phada; N threads still open" | | Exists, it had findings and every thread is resolved | Reviews and publishes again (see below) | | Exists and found nothing | Skips: "already reviewed by Phada; nothing was found" |

--force always reviews; --dry-run never skips and says what a real run would do. A skipped run exits with 0 and does not call the AI.

After a new push, a finding that matches an open Phada thread (same file, and the same title or a line within three of it) is not posted again as a comment: the review body lists it under "Still open from previous reviews", so it never disappears from the pull request. When every finding is still open, Phada posts only a short note saying there is nothing new and how many findings remain open. A finding with the same title as a Phada thread someone resolved in that file is not posted again either (a different finding near it still is): the run says how many findings were dismissed earlier and left out (dismissed in the JSON).

Look first, publish later

Another run of the AI can find something else, so publishing after a --dry-run should post what the dry run found, not review again. phada publish <id> posts the dry run kept in the history as run <id> (the id is at the end of phada review --dry-run and in phada history), without calling the AI:

phada review owner/repo#123 --dry-run   # … Saved as run 42: phada history show 42
phada publish 42                        # posts exactly that review

It reads the pull request first and refuses when it moved to another commit since the dry run (The pull request moved from a1b2c3d to 9f8e7d6 since run 42): review it again, or use --force to publish on the commit the dry run reviewed, where the line comments still point. It plans the comments against the threads on the pull request now, as a review does: a finding whose thread is still open is not posted again, one dismissed earlier is left out, and the comments the dry run found fixed are resolved if they are still open. A commit Phada already reviewed is skipped unless --force, so publishing the same dry run twice posts it once. The review is written in the language of the dry run. Only a dry run that reviewed something can be published: a run already published, skipped or failed is refused, and so is a dry run saved by a version before phada publish, which did not keep everything the review needs. The publication is saved as a new run of the history (phada history show names the dry run it came from), and --format json prints its JSON, with publishedFrom. In the app, p on the result of Only look does the same, after asking.

Comments fixed in a new commit

On a new commit, Phada first rechecks its own open comments from earlier commits, one AI call for all of them. For each comment it sends what changed in that file since the comment was posted (GitHub's comparison of the two commits) and lets the AI read the file at the new head; a comment whose file did not change stays open without asking the AI, and so does one Phada cannot check (no comparison after a rebase and no access to the repository with --no-investigate). The AI answers fixed, still-valid or unsure for each one, and Phada only acts on fixed: right before posting the new review, it resolves the thread and replies "Fixed in abc1234: …" with the reason. A comment that is still valid or unsure stays open, Phada never reopens a thread, never touches threads of other people, leaves alone a thread a person reopened after Phada resolved it, and a failure on one thread does not stop the others (the run says "Resolved N of M comments found fixed" and warns about each failure). The new review does not post again a finding of a comment found fixed in the same run; if the problem comes back in a later commit, it is posted again, while a finding whose thread a person resolved is never posted again. --dry-run says what it would resolve and resolves nothing, and nothing is resolved when publishing is off. Turn it off with resolveThreads: false in a config file or --no-resolve. The JSON has a recheck block with every verdict and resolved counts in the publication block.

Pushes that change nothing to review

Every review keeps a fingerprint of the reviewed diff, without the files left out of the review and without the line numbers of its hunks. A new commit whose diff has the same fingerprint as the last Phada review of the pull request is skipped without calling the AI: a push that only touches ignored files, or a merge of the base branch that only moves the changed lines, does not trigger a new review. --force reviews anyway and --dry-run says it would skip.

Replaying an earlier commit

--head <sha> reviews the pull request as it was at an earlier commit, to see whether this version of Phada (or your config) finds a problem it missed there:

phada review owner/repo#123 --head 1e6a005 --dry-run

The SHA is a commit of the pull request (at least 7 characters), including one a force-push replaced. The review gets the diff from the base up to that commit and nothing created after the moment it became the head: reviews, comments, threads and replies written later are left out, Phada's own included; the threads from before are shown open and current, on the line where they were written, since GitHub keeps no record of when a thread was resolved; a check that finished later is shown as still running; and a native skill reads nothing of the repository on GitHub (it works on the checkout at that commit, and gh pr view, gh pr diff and gh pr checks answer from the replay). The moment is the first force-push that brought the commit; GitHub does not record when a normal push happened, so otherwise the cut is the commit date, which comes before the push, and the run warns The cut is approximate. The title and the description are the ones of today.

A replay always runs with --dry-run (without it Phada refuses: a replay never publishes), never skips, rechecks and resolves no comment, and does not wait for the checks (--wait-ci is refused). It is kept in the history as a replay, which phada publish refuses and the feedback of the comments does not count. phada prompt owner/repo#123 --head <sha> shows the prompt of the replay without calling the AI.

Text written by the AI cannot mention anyone, link other issues or pull requests (#12, owner/repo#12), add HTML or forge Phada's own markers, and a body longer than GitHub allows is shortened with a visible "(truncated)". Two runs on the same pull request at the same time can both publish. GitHub refuses a new review while you have a pending review of your own open in its interface; submit or discard it first.

Watch mode

phada watch follows the repositories you list in your own config and reviews new pull requests and new pushes on its own, one at a time, and publishes them like phada review. The setup and the Repositories screen of the app list them for you; the other keys are written by hand:

# ~/.config/phada/config.yml
watch:
  interval: 5m # how often to look for changes (60s at least)
  settle: 3m # wait after the last push before reviewing
  maxReviewsPerHour: 6
  repos:
    - owner/repo
    - repo: owner/other
      publish: false # keep the review in the history only
      allowExternal: true
      authors: [alice] # only these authors
      labels: [phada] # only pull requests with one of these labels
      baseBranches: [main]
phada watch              # in the foreground; Ctrl-C once lets the running review finish
phada watch --once       # one cycle and exit, e.g. from cron (always looks for changes)
phada watch --status     # the queue (and jobs waiting for CI), reviews in progress, failures, last reviews
  • watch is only read from your top-level config, never from a repository's .phada/ or from repos/<owner>/<repo>/config.yml: a repository cannot make your machine review anything. The provider, model, language and the other options of each repository come from the usual config files, as with phada review.
  • The first time the watch sees a repository, the pull requests already open are recorded as they are and not reviewed; a new pull request, or a new push on any of them, is. --backfill reviews the open ones too; on a repository the watch already recorded it has no effect, and the watch says so.
  • A push is reviewed once the settle time has passed since the watch first saw it, so a burst of pushes leads to one review of the newest commit. There is one job per pull request and one review at a time, at most maxReviewsPerHour an hour. When a pull request moves to a newer commit while it is being reviewed, the review is kept in the history and not published, and the newer commit is reviewed instead. A commit Phada already reviewed is never reviewed again.
  • Before a job starts, the watch reads the checks of its commit. While some are pending, the job waits for CI (waiting in phada watch --status, with until when and the pending checks), read again every minute, for at most ci.wait (15 minutes by default, 0s turns it off) after the watch saw the commit; then it is reviewed with the checks as they are. A failing check does not end the wait while others are still pending, so the review gets the result of the whole pipeline. A commit without any check 2 minutes after the watch saw it, or whose checks all ended, is reviewed at once, and so is one whose checks cannot be read (with a warning). A push replaces a waiting job like a queued one, a request joins it and reads the checks again at once, and waiting does not count in maxReviewsPerHour.
  • With ci.onFailure: skip, a commit whose checks fail is not reviewed: the job is skipped as ci-failing (in the history, the review.skipped event and Slack), its checks are read again every interval for 24 hours, and the same job is reviewed once they no longer fail. A request for that commit reads them again at once. A new push ends it: the new commit is reviewed on its own, and the requests of the skipped one move to it.
  • Drafts and pull requests opened by bots are never picked up by the polling (someone can still ask for them). On a public repository, only pull requests of owners, members and collaborators are, unless the repository sets allowExternal: true: the AI runs on that code with nobody watching.
  • A failure that may pass (network, GitHub 5xx, a provider error) is tried again after 1 minute and after 5 minutes; after the third attempt the job is marked failed until a new commit. A provider usage limit pauses the queue for 15 minutes (doubling up to 2 hours while it lasts), and the GitHub rate limit pauses it until it resets. When the AI CLI is not installed or not logged in, or GitHub rejects the token, the watch stops with the error. Nothing is published from a review that did not finish.
  • With nothing to review, the watch updates the mirror and the map of each default branch when it moved, a minute at a time, so reviews start from a warm index.
  • One watch runs at a time on a machine (a lock in the data folder). The first Ctrl-C lets the running review finish and stops; the second stops the AI too and puts the pull request back in the queue. To keep it running, start it in tmux or as a service of your own.

phada watch --status --format json prints the same as JSON (schemaVersion: 1).

Asking for reviews from anywhere

Besides its own polling, the watch reviews what people and tools ask for. Phada knows nothing about chat apps or mail: it takes review requests through three doors (phada enqueue, phada mcp and a /phada review comment) plus one built-in client, Slack, and it sends review events to your hooks. Every request goes through the same checks, whatever asked for it, and is refused with a reason when it fails one:

  • the repository is listed under watch: in your own config (not-watched);
  • the pull request is open (closed);
  • on a public repository, the author is an owner, member or collaborator unless the repository sets allowExternal: true (external);
  • the when command of the repository lets it through (when-command);
  • the commit was not already reviewed (already-reviewed, with that review);
  • fewer than maxReviewsPerHour new reviews were queued in the last hour (hourly-limit).

A request for a commit that already has a job waiting or running joins that job: one review, and everyone who asked hears back. The one exception is a request that publishes while the running job does not: it gets a review of its own once that one ends. A request is reviewed at once, without the settle time, and the filters of watch: (authors, labels, baseBranches, drafts) only decide what the polling picks up: a draft someone asks for is reviewed. When the pull request gets a new commit before its review starts, the request moves to the new commit. Each request says where the review goes: pr (the default) publishes it on the pull request, none keeps it in the history and the events only. The review is the same either way.

Requests wait in the queue of phada watch, which runs them; with no watch running they wait until one starts.

From a script: phada enqueue

phada enqueue owner/repo#123                 # exit 0 when queued or joined, 1 when refused
phada enqueue owner/repo#123 --publish none  # review it without posting on the pull request
phada enqueue owner/repo#123 --format json

For example, from cron every morning: 0 9 * * 1-5 phada enqueue acme/shop#42 --publish none.

From your AI: phada mcp

phada mcp is an MCP server over stdio. Add it to your AI CLI, for instance claude mcp add phada -- phada mcp, and the AI can ask for a review and wait for the answer:

| Tool | What it does | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | review_pull_request | pr (owner/repo#N or a URL) and publish (pr or none): queues the review, joins the one already queued, or says why it was refused | | job_status | The state of a job, its place in the queue and, once it ended, its event | | wait_for_job | Waits until the job ends (timeoutSeconds, 300 by default, 1800 at most) and returns its event with the full review | | list_queue | The jobs waiting and the one running |

The AI asks; Phada decides. A message that talks the AI into asking for a review of some other repository is refused like any other request. phada mcp needs a GitHub token (GITHUB_TOKEN, GH_TOKEN or the gh login) to read the pull requests asked for.

From the pull request: /phada review

An owner, member or collaborator of a watched repository can comment /phada review (as the first line) on a pull request. The watch reads the new comments of each repository once per interval, reacts with 👀 to the ones it takes and reviews the pull request; the review is published on it. Comments from anyone else, from bots, or older than when the watch first saw the repository are ignored.

Slack

With slack: in your own config, phada watch also listens to the Slack channels you list (Socket Mode, so no public URL is needed) and reviews the pull requests linked in their messages:

# ~/.config/phada/config.yml
slack:
  channels:
    - C0123ABCD # the channel ID, at the bottom of the channel details
    - id: C0456EFGH
      publish: none # pr (default) or none: answer in Slack only
      reply: full # link (default), summary or full
  emojis: # optional; these are the defaults
    queued: hourglass_flowing_sand
    clean: white_check_mark
    comments: speech_balloon
    blocking: x
    failed: warning
    skipped: eyes
export PHADA_SLACK_BOT_TOKEN=xoxb-…   # the bot token of your Slack app
export PHADA_SLACK_APP_TOKEN=xapp-…   # an app-level token with connections:write
phada watch

The Slack app needs Socket Mode on, the bot scopes channels:history (and groups:history for private channels), chat:write and reactions:write, the message.channels (and message.groups) events, and to be invited to the channels. The tokens are only read from the environment. A message with up to five pull request links of watched repositories gets ⏳ and, when each review ends, the emoji of its verdict and an answer in its thread: link (the verdict and the links), summary (plus the summary, the score and the counts) or full (plus every finding with a link to its line in the reviewed commit, split into several messages when long). A commit Phada already reviewed is answered with the link of that review. A commit skipped for failing CI gets the skipped emoji and CI failing: test, lint. It is reviewed once these checks pass.; once it is reviewed, that emoji gives way to the verdict's. A refused request is answered with the reason; messages in other channels, from bots, or with links to repositories outside watch: are left alone.

Review events and hooks

# ~/.config/phada/config.yml
hooks:
  review.started: ./notify.sh started
  review.finished: ./notify.sh
  review.failed: ./notify.sh
  review.skipped: ./notify.sh
  timeout: 30s # how long a hook may run (default 30s, 10m at most)

Each hook is a shell command run from your config folder, with your environment (tokens included, as in your own shell: do not let a hook print it), for the jobs of the queue. It gets the event as JSON on stdin (schemaVersion: 1):

{
  "schemaVersion": 1,
  "event": "review.finished",
  "job": {
    "id": 7,
    "repo": "acme/shop",
    "number": 12,
    "headSha": "a1b2…",
    "url": "https://github.com/acme/shop/pull/12"
  },
  "requests": [
    {
      "source": "slack",
      "publish": "none",
      "data": { "channel": "C0456EFGH", "ts": "1729688400.1", "thread": "1729688400.1" }
    }
  ],
  "verdict": "blocking",
  "ci": {
    "state": "failing",
    "checks": [
      { "name": "test", "state": "failing" },
      { "name": "lint", "state": "passing" }
    ]
  },
  "publication": { "status": "not-published" },
  "review": { "...": "the same JSON as phada review --format json" }
}

source is watch, enqueue, mcp, pr-command or slack, and data is what that source brought. review comes with review.finished, reason with review.skipped (superseded, closed, already-reviewed, ci-failing, …) and error with review.failed, which is only sent once the job gave up. ci is the state of the checks (none, pending, passing or failing) and each check that counts, once the review or the watch read them (in review.started, as the watch read them before starting the review); null when the checks were not read. A hook that fails or runs out of time is reported in the watch and never stops it.

The verdict is computed by Phada from the findings, never by the AI: blocking when a finding is P0 or P1, comments when there are only P2 findings or findings worth checking, clean when there is nothing, failed and skipped. It is also in the JSON of every review and in the history.

A condition of your own

# ~/.config/phada/config.yml
watch:
  when: { command: ./only-payments-team.sh } # for every repository
  repos:
    - acme/shop
    - repo: acme/web
      when: { command: ./web-rules.sh } # instead, for this one

The command runs like a hook and gets the pull request and the request as JSON on stdin; it has 10 seconds to exit: 0 lets the request through; anything else, an error or no answer refuses it, with the first line the command wrote to stderr as the reason. It runs before a new review is queued, for a request or for a push the polling finds; a request that joins a job or finds the commit already reviewed does not ask it, and a refused push is not asked again until a new commit.

Recipes

  • Slack through your AI: with a Slack MCP server and phada mcp in your AI CLI, ask it to watch a channel, call review_pull_request with publish: none for each pull request link, wait_for_job, and post the findings it gets back in the thread.
  • Teams, mail or anything with a webhook: a review.finished hook that posts the event, e.g. jq '{text: "\(.job.url): \(.verdict)"}' | curl -s -H 'Content-Type: application/json' -d @- "$TEAMS_WEBHOOK_URL".
  • A schedule: phada enqueue from cron, as above.

History

Every review is saved on your machine in ~/.local/share/phada/history.sqlite ($XDG_DATA_HOME/phada when set; PHADA_DATA_HOME to choose another folder): the reviews of the watch, its queue and failures, and every phada review, including --dry-run and skipped runs. Each run keeps the same JSON as --format json, without the diff; findings can still quote code, so the file stays readable only by you. Unlike the cache, the history is never rebuilt or deleted by Phada; a newer version updates it in place, and an older one refuses a file written by a newer one instead of touching it. history: false in your own config turns it off (phada watch needs it).

phada history                         # the last 50 reviews
phada history owner/repo --since 30d  # one repository, the last 30 days
phada history owner/repo#123          # one pull request, with what happened to each comment
phada history show 42                 # one run, with everything it sent and found
phada history --stats                 # what happened to the comments, per repository
phada history --refresh --stats       # read the comments on GitHub first
phada history prune --older-than 180d

Each review of the watch shows who asked for it: watch (the polling), enqueue, mcp, pr-command or slack.

Each run of the list starts with its id, and phada review ends with it when it saves the run (Saved as run 42: phada history show 42). phada history show <id> prints that run again with the sections of --dry-run, read from the history: what was posted on the pull request (or would have been), the findings close to the cut, the claims of the description, the context, conversation and CI sent to the AI, the impact, the related repositories, the skills, what the investigation read and the tokens. It starts with a line naming the run, the pull request, its commit, when, who asked, the provider and model, and how it ended (published with the link, dry run, skipped and why, failed and the error). A run saved by an older version shows the sections it kept. --format json prints the run's history fields under run and its saved JSON under result, with schemaVersion: 1.

What happened to each comment

The history also keeps every comment Phada published and what happened to it, read from the comment's thread on GitHub, without the AI. Each comment has one outcome:

  • fixed: Phada resolved it after a recheck (see Comments fixed in a new commit), or a person resolved it after the lines it points at changed;
  • dismissed: a person resolved it without the lines changing;
  • ignored: it was still open when the pull request was merged or closed;
  • open: none of the above yet.

And two marks: negative reaction (a 👎 or 😕 on the comment) and replied (someone answered in the thread; Phada's own replies and bots do not count). Outcomes and marks follow the thread: a resolved comment that is reopened is open again, and a removed reaction stops counting. The history keeps when each one started and, when it stopped, when.

phada review saves the state of the threads it reads on every run. phada watch also reads the pull requests Phada reviewed (with comments or not, dry runs included) every 30 minutes when the queue is idle, and once more when they are merged or closed. Without the watch, phada history --refresh reads them on demand (it needs a GitHub token); phada history owner/repo#123 --refresh reads that pull request even when the history has nothing about it yet.

--stats counts the comments published in the period per repository, with the share of each outcome. They are signals, not verdicts: a dismissed comment may still have been right.

--format json prints the runs, the comments with their outcome and the history of each signal, or the stats, with schemaVersion: 1. phada history prune --older-than <age> removes reviews, comments and finished jobs older than that age (1d at least); it never touches what the watch needs to know what it already saw.

Misses

A miss is a problem someone pointed out that Phada did not. It comes from one of three places (its origin):

  • person: a thread opened on a pull request by someone who is not a bot, not Phada and not the author of the pull request, on lines that a later commit changed (GitHub marks the thread outdated), with no Phada finding near it;
  • bot: the same, from a bot you list in misses.bots (see below); other bots never count;
  • phada: a finding of Phada (from any review of the pull request, a replay included) that the latest review at a later commit no longer reports, although the lines it points to did not change between the two com