crysk
Health Uyari
- License — License: Apache-2.0
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Gecti
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
Multi-agent PR review engine for Claude Code and OpenCode: parallel specialty hunters, an adversarial refuter that challenges every severe finding, and deterministic blocking/advisory tiers. High precision, narrow coverage — lifecycle and state-machine bugs, not style.
Crysk
A multi-agent PR-review engine. Specialty hunter agents fan out in parallel over a repo checkout and
its diff, a mechanical dedupe merges their drafts, an adversarial refuter challenges every severe
finding, and deterministic code — never a model — assigns blocking/advisory tiers.
Its measured profile, stated up front: high precision, narrow coverage. What it reports is
almost always real (13% false positives measured). Its hunters check value contracts across the diff
(units, domain, conversions, what consumers assume), resilience (lookup and dedupe keys, retries and
locks, CI guards, test isolation), local logic errors, and access-control parity with sibling code;
stale state, races and component lifecycle have no dedicated hunter since 2026-11-08. It says nothing
about style or convention. Locally it is an assistant, not a merge gate: every report closes with that
disclaimer, and nothing it prints blocks a merge. In CI its commit status is a gate by default: require thecrysk check in branch protection, and blocking findings block the merge. mode: advisory keeps that check
green.
Renamed from pr-hero. Crysk used to be
pr-hero. The package is nowcrysk(npm install -g crysk,npx -y crysk), and thepr-herocommand stays as an alias for one version. What an install made under the
old name wrote is still read for one version:/pr-hero …comments,PRHERO_*variables,<!-- pr-hero-…
markers,pr-herocommit statuses (read, no longer written),~/.prhero,.prhero/and.prheroignore. A workflow file scaffolded
before the rename only answers/pr-hero …: re-runcrysk ci init --forcefor it to accept/crysk …too.
The repository moved tocryskai/cryskand new workflows useuses: cryskai/crysk@v0.juanmaagd/pr-herostays as a mirror with the same tags and releases, so a workflow that still saysuses: juanmaagd/pr-hero@v0keeps working.
Two ways to use it
- Local CLI: install Crysk on your machine and review branches or PRs from your terminal.
Start at Requirements. - CI only, no local install: install the skills into your coding agent, add one workflow file
to the repository, and every pull request is reviewed in GitHub Actions. Nothing below
Requirements applies to this path. It is described next.
CI only: skills plus one workflow
You need Node (for npx), an authenticated gh, and a GitHub repository with Actions enabled. You do
not need Bun, the Claude Code CLI, or a global Crysk install: the review runs on the GitHub
runner, which installs its own runtime. npx -y crysk setup --ci checks gh for you: when it is
missing or logged out, it says why it is needed and prints the commands, and in a terminal it offers
to install it (macOS with Homebrew) and to run gh auth login. It never blocks the setup.
Install the skills into your coding agent from skills.sh:
npx skills add cryskai/cryskcrysk-ci-setupsets up the workflow,crysk-propens a PR and waits for its review, andcrysk-loopanswers the findings.cryskcovers the local CLI, which this path does not use.Ask your agent to set up Crysk CI in the repository.
crysk-ci-setupfirst asks you how
re-reviews should work: a level (minimal, balanced, thorough or manual), gate or advisory, how CI
pays (a subscription gets no spend ceiling, an API key gets one you choose), the changed-lines
threshold above which a PR is skipped (default 4000, on any billing), and whether to tune the
advanced settings (score weights, observe-only, trusted actors, files limit, exclusions). Then it runsnpx -y crysk setup --ciwith your answers (writes.github/workflows/crysk.yml, or updates an existingpr-hero.ymlin place, and the
level's keys in.crysk/config.json, and prints a summary) andnpx -y crysk init
(writes aREVIEW.mdscaffold when the repo has no review instructions), writes the repository's review instructions, and stores
one secret withgh secret set:CRYSK_CLAUDE_OAUTH_TOKEN(subscription;claude setup-token
prints it on any machine that has the Claude Code CLI, so this one does not need it) orCRYSK_CLAUDE_API_KEY(pay as you go). To run some steps on OpenCode models, it also sets theCRYSK_MODELSvariable and oneCRYSK_OPENCODE_*secret per provider. Without an agent, run the same
commands yourself and follow the Quick path.Merge the workflow,
.crysk/andREVIEW.mdinto the default branch. A comment-triggered run
(/crysk review) reads them from the base branch, so it starts nothing until the workflow is
there, and reviews without your instructions untilREVIEW.md(or.crysk/gotchas.md) is.
Then require thecryskstatus check in branch protection if you want it to block merges. The legacypr-herostatus is no longer posted, so a rule that requires it must move tocrysk.Open pull requests as usual. Every new PR gets a review posted as PR comments; later pushes
follow the level you chose (balanced re-reviews a push only when it is risky or the last pass
found enough, at most 2 automatic passes), and a PR above the size threshold is skipped. Comment/crysk reviewto review a skipped push anyway. Ask your agent
to open the PR (crysk-pr), and it waits for the review withnpx -y crysk status --pr <n> --wait --json, then hands the findings tocrysk-loop, which replies to each one in its
thread and pushes the fixes.
Everything about the workflow (credentials, models and OpenCode, spend limits, the gate, error codes)
is in docs/github-actions.md.
Requirements
These apply to the local CLI. The CI-only path needs none of them
on your machine.
Crysk does not call any model API of its own. It spawns the Claude Code CLI as a subprocess and
inherits that CLI's authentication. claude is the engine; everything else here is plumbing.
| Dependency | Needed for | Without it |
|---|---|---|
| Claude Code CLI, installed and authenticated | everything | nothing runs — it is the engine |
| git | everything | nothing runs |
Bun ≥ 1.3 on PATH |
install options B and C only | option A carries its own runtime |
GitHub CLI (gh), authenticated |
review --pr, --post, watch, reverts, corpus |
local review still works in full |
| codegraph | optional code intelligence | hunters navigate with git and grep, with less structural context |
Run crysk doctor first: it checks every one of these and prints what is missing. The one thing it
cannot check is Bun — on an npm or source install the launcher itself is a Bun script, so a missing
Bun fails before any Crysk code runs, with env: bun: No such file or directory. Install Bun
(brew install oven-sh/bun/bun, or curl -fsSL https://bun.sh/install | bash) or use option A,
whose binary needs no Bun on your machine.
What a review bills against
A run uses exactly one Claude credential, chosen when it starts, in the order yourcredentials.claude sets (Credentials):
- Your Claude subscription (
claudesigned in on this machine, orCRYSK_CLAUDE_OAUTH_TOKENfromclaude setup-token) → runs consume your subscription quota. No per-token invoice arrives. - An Anthropic API key (
CRYSK_CLAUDE_API_KEY, orANTHROPIC_API_KEY) → the same runs are billed as
API usage.
The default order is the subscription first, so with both available the subscription runs and the key is
not used: on macOS a Claude login always counts. To bill the key, put it first in your person config,{"credentials": {"claude": ["api_key", "subscription"]}}. The Claude CLI child sees only the chosen
credential.
The dollar figures Crysk prints — the cost band before a run, crysk usage after one — are
always a token-derived estimate, because that is the only unit comparable across both models. On
a subscription, read them as "how much of my window will this consume", not as a bill. A step on an
OpenCode model bills to that provider's credential instead (see docs/configuration.md).
Installation
# Option A: Standalone binary (macOS / Linux, bundles its own Bun — still needs claude and git)
curl -fsSL https://raw.githubusercontent.com/cryskai/crysk/main/install.sh | bash
# Option B: Global npm install (brings Bun 1.4.4 as an optional dependency, or uses
# the Bun >= 1.3 on PATH when npm skips optional dependencies)
npm install -g crysk
# Option D: No install, for an agent or a one-off run (needs only Node). It cannot
# register the MCP server or the launchd agents: npx's cache is temporary, so those
# refuse and ask for option B. The launcher uses the bundled Bun only when it runs
# (`bun --version` is checked first), otherwise the Bun on PATH, and exits 127 naming both if neither works.
npx crysk doctor
# Option C: From source / development (the bin runs on Node; on a machine with Bun
# and no Node, run `bun bin/pr-hero.js` or use option A)
git clone https://github.com/cryskai/crysk.git && cd crysk
bun install && bun link
See Requirements above — claude and git are needed by every option.
From install to first review
Only want the CI review? You do not need to install anything: see GitHub Actions — Quick path (npx -y crysk setup --ci, npx -y crysk init, one secret).
Seven steps, in this order. Steps 2, 5 and 6 are the ones that get skipped, and
each one breaks the step after it.
# 1. Install (option A shown; B and C put crysk on PATH themselves)
curl -fsSL https://raw.githubusercontent.com/cryskai/crysk/main/install.sh | bash
# 2. Put it on THIS shell's PATH. The installer edited your rc file, and an rc
# file only reaches shells started after it — the one you are in is not one.
# (The installer prints the exact line. It is ~/.crysk/bin, or ~/.prhero/bin
# on a machine that already had a ~/.prhero from before the rename.)
export PATH="$HOME/.crysk/bin:$PATH" # fish: set -gx PATH $HOME/.crysk/bin $PATH
# 3. What is installed, what is missing, what blocks a review.
crysk doctor
# 4. Skills + MCP registration, and — inside a git repo — scaffold .crysk/.
cd /path/to/your-repo && crysk setup
# 5. Track that scaffold, or ignore .crysk/. Local review refuses to run on a
# dirty tree, and an untracked .crysk/ or REVIEW.md is a dirty tree.
git add .crysk && git add REVIEW.md # REVIEW.md only if setup wrote one
git commit -m "chore: crysk config"
# 6. Replace the placeholder review instructions with real ones. Not paperwork — see below.
$EDITOR REVIEW.md # or the .crysk/gotchas.md the repo already had
# 7. Review the checked-out branch against its merge base.
crysk review
On step 4. setup is fully non-interactive — it asks nothing and is safe to run
unattended. It is also optional for reviewing: review never checks whether it ran,
only crysk doctor reports it. What it buys is the skills, the MCP registration and
the repo scaffold; skip it and you scaffold the repo yourself with crysk init. It does
not commit or .gitignore what it scaffolds, which is why step 5 exists.
Skill drift. setup (and upgrade) installs the bundled skills into each agent's skills
directory. A skill copy that differs from the bundled one and has no recorded digest is kept as it is and reported as drift, never overwritten, so your local edits survive; crysk setup prints each kept skill, and crysk upgrade replaces it.
Skills without Crysk. The public skills are also on
skills.sh, installable into any agent with the
skills CLI:
npx skills add cryskai/crysk # pick interactively
npx skills add cryskai/crysk --skill crysk # or crysk-pr, crysk-loop, crysk-ci-setup
On step 6. REVIEW.md is injected verbatim into every hunter's system prompt, and
what setup/init write there is a placeholder: - <subsystem>: <the thing that looks like a bug but is deliberate, and why>. A review that reads that is a review billed in
full by hunters who know nothing about your repo, and it comes back looking clean. So
Crysk refuses to run while the scaffold's human-attention-required marker line is
still present. Write a handful of true lines and delete the marker — or delete the file
to review with the default guidance only.
Other entry points, once the above works:
crysk # the interactive TUI menu, in a TTY
crysk review --pr # the current branch's PR, in an isolated worktree
crysk review --pr 123 # any PR by number
Breaking change in v2.0: Invoking
cryskwith zero arguments in a TTY now opens the interactive TUI menu instead of implicitly starting a localreview. In non-interactive environments, it prints the help text and exits with code 2. SetCRYSK_NO_TUI=1(PRHERO_NO_TUI=1still works) to bypass the TUI in automation.
Every paid run prints a plan and a cost band, then asks for confirmation (--yes skips the prompt).
For AI agents
You are most likely here because someone told you to install Crysk and review a repo.
Four things go wrong when an agent runs the sequence above:
- Step 2 is not optional, and not deferrable. If each of your commands runs in its own
shell, anexport PATH=...in one does not reach the next. Either prepend the export to
every Crysk invocation, or call the binary by absolute path:~/.crysk/bin/crysk(~/.prhero/bin/pr-heroon an install from before the rename). - Step 5 is yours.
crysk setupscaffolds.crysk/andREVIEW.mdand neither commits
nor ignores them, so the nextcrysk reviewfails the clean-tree gate — on filessetup
itself created. Commit them (or add.crysk/to.gitignore) before reviewing. - Write the review instructions yourself (step 6). You have the repository in context, which
makes you the best author available. Read the code and write the real ones: the
deliberate oddity that looks like a bug, the invariant enforced in a file the diff never
shows, the ownership rule a newcomer would violate. Leaving the placeholder is not a
softer option — the review is refused until you replace it and delete thehuman-attention-requiredmarker line. - You cannot authenticate
claude. Stop and ask the human. Crysk spawns the Claude
Code CLI and inherits its authentication; there is no Crysk-side key to set. Ifcrysk doctorreportsclaudemissing or unauthenticated, say so and wait — every
step after it fails until a person fixes it.
Configuration — two layers
There are two config files, and the difference between them is who they belong to:
| File | Whose | Committed? |
|---|---|---|
~/.crysk/config.json (or ~/.prhero/config.json on an install from before the rename) |
yours, across every repo on this machine | no — it is your machine |
<repo>/.crysk/config.json |
the team's | yes, normally — it is shared through git |
crysk init creates the repo file and, when the repo has no review instructions, a rootREVIEW.md; it never overwrites existing files, and a repo that already has.crysk/gotchas.md keeps it and gets no REVIEW.md (one would shadow it). crysk setup
scaffolds the same files when it runs inside a git repo, so a repo you set up needs no separateinit. (Running init afterwards is harmless: it keeps what exists and reprints the git
reminder.) Commit .crysk/ (or add it to .gitignore) and REVIEW.md — an untracked one trips
local mode's clean-tree gate.
Old directory names. The directories used to be ~/.prhero/ and <repo>/.prhero/, and the ignore file.prheroignore. Those are still read, for one version, when the new name does not exist. Nothing is moved for
you: crysk doctor prints the full procedure for your machine. For the home that is the mv ~/.prhero ~/.crysk
plus whatever still points at the old path (the .prhero/bin PATH line, the launchd agents, the MCP registration),
and for a repo git mv .prhero .crysk (plain mv plus a .gitignore edit if the directory is not tracked) andgit mv .prheroignore .cryskignore. A new-named directory only wins while it holds Crysk's own files
(~/.crysk needs a config.json, prhero.db, repos/ and the like; <repo>/.crysk needs config.json orgotchas.md), so an empty ~/.crysk never hides a ~/.prhero in use. Do not create ~/.crysk by hand while~/.prhero is in use. When both hold files, the .crysk one wins and the old one is ignored.
Run crysk config to see which layer decided what. It lists every key with its value, the
layer it came from, and both file paths whether or not they exist. It never edits anything.
Which model each step runs on (models, per role, with fallbacks and effort), and how Crysk reads
credentials for each backend, are their own topic — see docs/configuration.md.
The one rule
The more specific layer wins — except where the less specific one is protecting your money.
So the team file usually beats your global one. The exception exists because a committed file must
never be able to enlarge your bill: on the two keys that spend, the team can only ask for less.
config.json keys
| Key | Where it can live | What it does |
|---|---|---|
agents_dir |
both | Directory holding the agent prompt files (deep-review-*.md, review-*.md). Relative paths resolve against the file that named it. |
default_base |
repo only | Base ref when --base is not given (e.g. "dev"). Without it: the remote head (origin/HEAD), then main. Rejected in the global file on purpose — see below. |
parity_trigger_paths |
repo only | Glob list. The parity hunter runs only when a changed path matches one — no globs, no parity hunter. |
suspicion_priors |
repo only | [{path, weight, reason}] hints injected into hunter prompts — point them at your known hotspots. |
summary.enabled |
both — team may only turn it off | Whether the engine-owned summarizer runs. Defaults to true, because it spends money and a silent opt-out would make the bill differ from the plan. |
models |
global only | The models per role (default, hunter.<key>, refuter, consolidate, scout, summary, judge), each a list of one to three tried in order, e.g. {"summary": ["claude:haiku-latest"]}. Unset, each step runs the model its prompt declares. See Models. summary.model is gone: "haiku" still reads, with a warning; anything else is refused. |
credentials |
global only | Which Claude credential a run uses, in order: {"claude": ["subscription", "api_key"]} (the default). See Credentials. |
max_verification_steps |
both — team may only lower it | Cap on re-review verification spawns (item 7). Defaults to 8; 0 is legal and pauses verification. |
max_concurrent_steps |
both — team may only lower it | Cap on steps (hunters, refuters, verify, scout, summarizer) in flight at once. Unset means unlimited. Set it below a provider's concurrency limit (e.g. 8 for one that allows 10 in-flight requests) so a refuter fan-out does not draw HTTP 429s. |
max_changed_lines |
both — team may only lower it | Line budget threshold before the size gate skips a review. Defaults to 4000; 0 disables line budget check. |
max_changed_files |
both — team may only lower it | File count budget threshold before the size gate skips a review. Defaults to 150; 0 disables file budget check. |
scout |
both — team may only turn it off | Whether the reconnaissance scout hunter runs before review. Defaults to false. |
materiality_gate |
both — team may only turn it off | Defaults to false. When true, an extra isolated judge checks each blocking finding against Crysk's posting policy; a finding judged not material under normal use is posted as advisory instead of blocking. Findings about security, credentials or data loss are never demoted. Adds one judge call per blocking finding. Claude route only: on other routes findings stay blocking. Known limitation: on a re-review, a finding that was blocking on an earlier head still counts toward the gate. |
post |
both — team may only turn it off | Whether review reports publish as GitHub PR comments by default. Defaults to false. |
post_min_severity |
both — the repo file wins either way | The lowest finding a PR review posts: suggestion (everything), warning (hides advisory suggestions) or blocking (hides every advisory finding). A blocking finding always posts. Hidden findings get no comment; the summary counts them in one line. The gate and local reports still see every finding; re-review admission scores only what was posted, so a hidden finding never triggers a re-review. Unset posts everything; setup --ci writes the recommended warning when the file has no value. |
Why default_base is repo-only, since it is the one restriction that looks arbitrary: the
resolver checks the config value before the remote head. A global default_base would therefore
preempt autodetection on every repo that stays quiet, so the first repo whose default branch differs
would silently review the wrong commit range — with a plausible branch name on the plan card.
Flags always win, and may exceed a cap. A flag is you typing an intent in front of the cost band,
which is a different thing from a committed file asking on your behalf. --agents overridesagents_dir (then the repo file, then the global, then CRYSK_AGENTS_DIR, or PRHERO_AGENTS_DIR on an install from before the rename); --base overridesdefault_base; --summary / --no-summary and --model override the summary keys.
REVIEW.md and gotchas.md
Repo-specific facts the hunters must know (fragile invariants, deliberate oddities, past incidents),
in a root REVIEW.md — the file name Claude Code Review reads. Without one, Crysk reads.crysk/gotchas.md (or the legacy .prhero/gotchas.md); when both exist, REVIEW.md wins and
the run and crysk doctor say gotchas.md ignored: REVIEW.md takes precedence. The hunters and
the refuter read the file as facts about the codebase (the refuter still needs code to refute a
claim). With materiality_gate on, the materiality judge also applies its "Not blocking here" rules
to blocking findings, but only for style, naming, test hygiene, generated code and dev scripts: it
never demotes a logic, correctness, data, contract or security finding, whatever the file says.
Nothing filters advisory findings by it.
Optional, but a file that exists must be real — not merely non-empty. With neither file the
review runs on the default guidance and doctor reports it as degraded. The file init/setup
scaffold carries a human-attention-required marker line, and review and doctor both refuse it
by name, as they refuse an empty file: a placeholder passes any emptiness check while telling every
hunter nothing, which buys a full-price review that reports clean. Replace the <subsystem> lines
with facts about your repo, then delete the marker line. Reviewing a tree you cannot write to?
Supply a file from outside with --gotchas <file> (and --config <file>); it overrides both.
Where it is read from. Local crysk review reads the working tree. crysk review --pr and the
watcher read the operator checkout (--repo), never the PR worktree. In GitHub Actions it is the
workflow's checkout: a pull_request run reads the PR merged onto its base, so a PR that edits the
file is reviewed with its own edit; a /crysk review comment run on the generated workflow reads the
default branch.
Commands
| Command | What it does |
|---|---|
crysk / crysk menu |
Open the interactive terminal user interface (TUI) with context-aware navigation, review launcher, and submenus. |
crysk review |
Review the checked-out branch against the merge base with --base. Requires a clean tree sitting on --head (default HEAD). |
crysk review --pr [n] |
Review a GitHub PR: by number, or — bare, no number — the PR that belongs to the current branch (errors loudly if it has none). Resolves the range through gh, runs in a detached worktree with its own codegraph index, then records the result in the run's findings.json. |
crysk review --pr <n> --post |
Same, then publish the report as one marked PR comment — re-runs update it in place, never stack. |
crysk doctor |
Run diagnostic environment and dependency checks (git, claude auth, gh, codegraph, config). Works inside or outside a repository. |
crysk setup |
Machine-level onboarding: skill synchronization and MCP registration — and, when run inside a git repo, it also scaffolds that repo's .crysk/config.json and, when the repo has no review instructions, a root REVIEW.md, so no separate init is needed there. Fully non-interactive. It does not commit or .gitignore what it scaffolds; do that yourself before reviewing. |
crysk activity [--kill <pid>] |
Inspect running reviews (with elapsed time, PID, repo/PR), watcher spend against daily cap, and completed run history. --kill <pid> safely terminates a running review. |
crysk post --pr <n> --from <run-dir> |
Replay-publish a previous run's findings onto the PR (same plan as --pr --post). --dry-run previews at $0. |
crysk triage reply --pr <n> --from <run-dir> --finding F00N --tag <tag> --body-file <path> |
Post one triage reply. The driver picks the parent from the posted <!-- crysk-finding marker (never path/line), renders the marker + badge, posts, and resolves the inline review thread. Reads anchor.patch (else diff.patch) from the run dir to remap post lines the same way --post does. --body-file is reasoning only. |
crysk status --pr <n> [--wait] [--json] |
Read-only, through gh only: the PR's Crysk gate, its posted findings, and a next_action (wait, review, triage, fix, recheck, override-or-fix, done). --wait polls until an in-flight or newly-triggered review settles before emitting (a review that has not posted its first status within 3 minutes is not waited for: counted from the /crysk (or /pr-hero) comment, or, after a push, from when --wait starts; a run still queued after that returns the current status); --json prints one pr-hero.status/v1 object on stdout. See docs/github-actions.md. |
crysk config |
Read-only, $0: every config key with its value and which layer decided it (repo / global / capped / default), plus both file paths whether or not they exist. |
crysk config set <key> <val> [--person|--team|--watch] |
Set a scalar configuration value in the selected layer (defaults to --person). Capped team keys accept and annotate over-ceiling values. |
crysk config unset <key> [--person|--team|--watch] |
Remove a configuration value from the selected layer. |
crysk config --edit |
Open the interactive configuration editor across Person, Team, and Watcher layers with buffered draft editing, Save/Discard/Clear actions, and capped ceiling annotations. |
crysk upgrade [--check] |
Upgrade crysk to the latest release and reconcile installed skills and MCP registrations. --check checks for updates without installing. |
crysk uninstall [--purge] |
Managed uninstallation: unloads launchd background daemons, cleans up skills, unregisters MCPs, and optionally purges ~/.crysk with --purge. |
crysk init |
Scaffold .crysk/ in the current repo — the command for every repo after the one you ran setup in. Omits keys your global file already supplies, so a new repo does not re-state what you have already said once. Idempotent: existing files are kept untouched, and it reprints the commit-or-ignore reminder. |
crysk watch add |
Opt the current repo (or --repo <path>) into the watcher; --post makes its reviews publish to the PR. Idempotent — re-adding updates the post flag. See Watching PRs automatically. |
crysk watch remove |
Take the current repo (or --repo <path>) back out. Idempotent — removing what is not listed just says so. |
crysk watch status |
Read-only, $0: config summary, today's launches vs the cap, launchd state, lock, last activity. |
crysk watch --once |
Run ONE watcher tick: scan the watched repos, pick the next unreviewed open PR, review it. --dry-run shows the whole decision for $0. |
crysk watch install |
Install the macOS launchd agent that runs a tick every --interval minutes (default 15). This is the opt-in to automatic spend. |
crysk watch uninstall |
Unload and remove that agent. |
crysk gc |
Collect worktrees under ~/.crysk that are merged/closed or idle >72h. --dry-run prints the table and removes nothing. --repo <clone> scopes to one origin; default is the whole home. Also removes leftover verified execution snapshots under $TMPDIR/prhero-exec-snapshots (dead owner process or older than 6h). |
crysk gc install |
Install the macOS launchd agent that runs crysk gc every --interval minutes (default 360 = 6h). No reviews, no watch.json, no daily cap — the $0 sweeper so trees do not wait for a review or the watcher. |
crysk gc uninstall |
Unload and remove that GC agent. |
crysk gc status |
Read-only, $0: whether the GC agent is installed, its interval, plist, and tick log. |
crysk usage [--all] |
Read-only, $0: print the observability store's per-run rows (cost, tokens, findings) — every completed review, local and PR, auto-ingests into one global ~/.crysk/metrics.db. Scoped to the current checkout's origin by default; --all shows every origin instead. |
Flags worth knowing: --dry-run (plan + cost band, creates nothing), --yes, --model <m>,--scout / --no-scout (toggle pre-hunt reconnaissance scout),--no-shared-prefix (turn off the default shared hunter prefix: hunters share one cached system prompt
holding the patch, their roles move to the user message and a one-turn warm-up writes the cache first; Anthropic routes only,
since other providers already cache each hunter's own loop),--post / --no-post (toggle PR comment publishing),--force (local only: review this diff even when the size gate would skip it;
in GitHub Actions the same forced re-review is a /crysk review comment on the pull request,
which runs on the Actions runner; /crysk override instead unlocks a red gate with no review),--full (run un-capped refuter verification steps),--no-summary (skip the summarizer; see summary in config above),--out <dir> (run dir for review),--all (usage only: every origin instead of just this checkout's).crysk --help lists everything.
PR mode, in one paragraph
--pr resolves the range from the PR itself: a merged PR diffs from its merge commit's first parent
(the base as it was when the PR landed — merge, squash and rebase all converge at the fork point),
an open one from the recorded base tip. The review runs in a worktree at~/.crysk/repos/<origin>/worktrees/pr-<n> — your checkout, your index and your uncommitted work are
never touched, and reviews run while you keep working. Two checkouts of the same GitHub repo share
one worktree. Worktrees are kept and reused across re-runs until GC (crysk gc: merged/closed or
72h idle). That collector is not a background timer — it runs when you invoke it, at the end ofreview --pr, on a watcher tick, or on a schedule via crysk gc install (macOS launchd, default
every 6h, no reviews, no watch.json). Each run prints the cleanup command (git worktree remove --force … — never rm -rf, a live
codegraph daemon holds a socket in there).
When to run it
Reviews launch two ways: by hand, or through the opt-in local watcher (crysk watch, below)
— nothing else ever fires on its own. The two moments where a manual run makes sense:
Before opening the PR —
crysk reviewon your branch. The cheapest moment to catch
something: you are still in context and nothing is published yet.On an existing PR — the moment it opens, standing on its branch, the whole command is:
crysk review --pr --postBare
--prresolves the branch's own PR (loud error if there is none), the review runs in its
worktree while you keep working, and--postpublishes the one marked comment. A PR number works
anywhere (--pr <n>), no matter what you have checked out.
Automation and CI — status
| Stage | Status |
|---|---|
Manual CLI (review, --pr, --post) |
Available now — this README. |
| Auto-trigger on new PRs / new pushes (local watcher) | Available now — crysk watch, below. Deliberately opt-in: every review spends real money, so nothing fires until you watch add a repo AND watch install the agent. |
| CI mode (GitHub Actions) | Available now — official action.yml at the repo root, canonical .github/workflows/crysk.yml (a repo that already has pr-hero.yml keeps it), full setup in docs/github-actions.md. The commit-status gate (mode: gate, the default; mode: advisory opts out), exit 0 on findings, the size/budget gates, and crysk setup --ci scaffolding all apply. |
| Required status check that blocks merges | Available in CI — the crysk commit status is the check (mode: gate, the default). The legacy pr-hero status is no longer posted. Require it in branch protection. Fail-closed for a head with no run stays deferred. |
Watching PRs automatically — crysk watch
The watcher is a tick, not a daemon: crysk watch --once makes one pass — list open PRs in
each configured repo, skip what is already covered, launch at most one review — and exits.
launchd (installed below) fires a tick every N minutes and is the supervisor. Drafts are skipped,
and each PR is reviewed once by default — see the on_push knob below.
Opt in
cd /path/to/your-repo
crysk init # once per repo, if .crysk/ does not exist yet
crysk watch add --post # opt THIS repo in; --post publishes each review to its PR
crysk watch install # start ticking — this is the moment automatic spending starts
crysk watch status # anytime: config, cap usage, launchd, lock, last activity
A repo is watched only if added (--repo <path> works from anywhere; watch remove takes it
back out; both are idempotent). Preview any tick for $0 first: crysk watch --once --dry-run
prints every candidate, every skip and its reason (draft / reviewed-local / reviewed-remote /
in-flight / attempts / cap / window), and the one (pr, head) a real tick would launch. install accepts--interval <min> (default 15) and captures your current PATH (launchd's own PATH knows nothing
of bun, gh, claude or codegraph) — re-run install after moving tools around;watch uninstall stops the schedule.
Worktree GC is a separate agent: crysk gc install (default every 6 hours, --interval in
minutes). It runs crysk gc — no reviews, no watch.json, no daily cap. Preview withcrysk gc --dry-run; gc status / gc uninstall match the watcher verbs.
The underlying file — ~/.crysk/watch.json
watch add/watch remove own the repo list — there is no need to edit the file by hand for
membership, and their rewrites preserve any keys they do not know. The two global knobs are still
plain JSON you may tune directly:
{
"repos": [{ "path": "~/Desktop/your-repo", "post": true }],
"daily_cap": 5,
"window": { "start": "09:00", "end": "19:00" }
}
| Key | Default | What it does |
|---|---|---|
repos[].path |
— | Operator checkout of a repo to watch (its .crysk/ config and review instructions are used, exactly like review --pr). Managed by watch add/remove. |
repos[].post |
false |
Pass --post to the spawned review — publish each result as the PR's one marked comment. Managed by watch add [--post]. |
repos[].on_push |
false |
Re-review the repo's PRs on every new push. Default: one review per PR — see below. Managed by watch add [--on-push]. |
daily_cap |
5 |
Global max reviews launched per local calendar day, across all repos. 0 pauses launching entirely. |
window |
null (always) |
Local-time window outside which ticks do nothing, e.g. {"start":"09:00","end":"19:00"}. Overnight windows (start > end) work. |
One review per PR — the on_push knob
By default a push to an already-reviewed PR does not re-trigger a review: an active PR takes
many pushes, and re-billing $2–5 for each would drain the cap on one PR's churn. The posted
comment names the exact sha it reviewed, so it never lies about coverage — a reader sees at a
glance that later pushes are unreviewed. When the new head deserves a fresh pass, run it by hand
(crysk review --pr <n> --post); crysk watch add --on-push restores per-push re-review for
a repo that wants it. Planned follow-up: a cheap independent judge that reacts to pushes by
checking whether the new head addressed the posted findings and replies in-thread, instead of
re-reviewing from scratch.
What keeps the bill bounded
- The cap and the window exist because the watcher and your interactive Claude sessions share
the same subscription quota — a runaway watcher would starve the work you are actually doing.
The cap is counted from~/.crysk/watch.log(thelaunchedlines ARE the counter), and a
launch is logged before it starts, so even a crashed review counts. - Max 2 attempts per (pr, head): a PR that keeps killing the review is dropped until it gets
a new push, instead of eating the cap every day. - Cross-machine guard: a posted Crysk comment declares which head it reviewed; any watcher
that sees the current head already declared skips the PR (and under the one-review-per-PR
default, any Crysk comment at all is proof enough). A fresh pendingcryskcommit status
on the head (the yellow dot while a review is running) skips asin-flight.
Operating model and honest limits
Run one watcher per repo per team. The comment-based cross-machine guard only protects repos
with post: true, and only after the first comment lands. A pending crysk commit status
closes the rest of the race once either review has started — two watchers can still duplicate
if both launch before either posts pending. With post: true the PR still converges to a
single comment. Not macOS? watch --once from cron works the same —
the PID lockfile keeps overlapping ticks from doubling up. Logs: structured events in~/.crysk/watch.log, raw tick/review output in ~/.crysk/launchd.log.
What a run produces
Run artifacts land outside the repo, in ~/.crysk/repos/<origin>/runs/<run>/:
| File | What it is |
|---|---|
report.md |
The human-readable review. |
findings.json |
The machine artifact (schema v1.0.0): findings, tiers, refuter verdicts, usage, provenance. |
diff.patch, pipeline.json, steps/ |
The exact diff reviewed — after exclusions, so it is byte-for-byte what the hunters read — plus pipeline provenance (including the excluded paths), per-agent drafts and logs. |
diff.raw.patch |
The unfiltered diff of the same range. Written only when exclusions actually dropped a file, since otherwise it would duplicate diff.patch. |
anchor.patch |
The full PR diff (merge-base..head, after exclusions) that inline comments anchor against. On a first review it equals diff.patch; on an incremental re-review diff.patch is only the push delta, and this file still covers the whole PR, so a finding on an untouched line of the PR gets a review thread. |
Cost and expectations
- $2–5 per review of a typical PR-sized diff — the floor is real, because hunters read the tree,
not just the diff — and larger trees cost more (a 45-file tree measured $11–15). Wall time is
minutes, not seconds: ~10 for a small diff, up to ~25 measured on a large tree. The printed band
is an order-of-magnitude guide, not a quote — the same tree has billed 34% apart across runs.
The size gate — a cost gate, not a quality gate
Past a size threshold Crysk skips the review instead of running it. Small trees bill $1.9–$4.8;
the 45-file / +2775 −1237 bench tree billed $6.58–$17.92 across 18 iterations — the cost roughly
triples and its spread widens to ~2.7x. The gate exists because that bill is unpredictable, and
because an unattended watcher must not be the thing that discovers it.
It is not a claim about quality. We have no evidence that a bigger diff reviews worse: attention
dilution was tested and falsified (fixtures/scale-probe.ts), and the one measured Greptile-only
miss came from a 7-file PR. If a large diff is worth its price, review it anyway.
Locally that is --force. In GitHub Actions it is a pull request comment, /crysk review,
from a user with write access (a workflow scaffolded before the comment trigger usesgh workflow run pr-hero-force.yml -f pr=<n> instead). The comment does not review on your
machine; the Actions runner does. It also clears the CI admission rules and the budget ceiling for
that one run. gh run rerun on a skipped run does not: it measures the gates again and skips again.
The gate counts effective changed lines (insertions + deletions) and files — excluded content is
subtracted first, so a regenerated lockfile beside a ten-line change does not trip it. Excluded by
default: bun.lock, package-lock.json, yarn.lock, pnpm-lock.yaml, Cargo.lock, go.sum,*.min.js, *.min.css, *.snap. A repo can add its own exclusions with .cryskignore — see
below.
Those exclusions come out of the reviewed diff itself: diff.patch is what the hunters are
handed, so the number the gate measures is the number that gets paid for. If every changed file is
excluded, there is nothing to review — Crysk exits without spawning anything.
.cryskignore — repo-defined exclusions
A .cryskignore file at the repo root adds your own exclusions on top of the 9 defaults above.
The dialect is a subset of gitignore(5), not a full reimplementation — most everyday patterns
work exactly like .gitignore, but two things are worth knowing before you rely on it:
- Only one file, at the repo root, is read. Nested per-directory
.cryskignorefiles (the way
git supports a.gitignorein every directory) are not read. A file atopenspec/.cryskignoreis silently ignored — put every rule in the one at the repo root instead. - Matching is case-sensitive, matching git's own behavior. This is a deliberate departure from
theignorenpm package (the common JS gitignore implementation), which is case-insensitive by
default — if that is where your intuition comes from,README.mdwill not matchreadme.mdhere.
Supported: comments (#) and blank lines, negation (!pattern re-includes), anchoring
(a leading or interior /), trailing-slash directory-only rules, */?/[a-z]-style classes,** at any position (including matching zero directories), and literal {/} (this dialect has no
brace-alternation syntax — braces are always literal characters, unlike some glob libraries).
The last matching rule wins, builtins evaluated first, then your file's rules in order — so a
rule can re-include one of the 9 defaults (!bun.lock) or exclude something the defaults do not.
One documented deviation from real git: unlike git, which cannot re-include a file whose parent
directory is itself excluded (gitignore(5) states this outright — it is a tree-walk optimization),
this engine matches a finite diff file list, not a tree walk, so docs/ then !docs/keep.md DOES
re-include docs/keep.md here.
Under CI (GitHub Actions, or any CI/GITHUB_ACTIONS environment), .cryskignore is read from
the PR's base ref, never the PR's own branch — a PR cannot widen its own exclusions to hide
changes from its own review. Local review, and local --pr <n> review without CI, read it from your
working tree instead. A malformed file aborts the whole review before anything is spent, naming the
file, line number, and offending text.
crysk watch honors .cryskignore too, in its per-tick scheduling filter (both the aggregate
check and the per-file rescue), and in a one-call pre-launch check that catches a PR whose files are
all excluded but whose aggregate alone would not have flagged it. It reads .cryskignore from the
watched repo's own working tree on the machine running the watcher — never a base ref — because the
watcher runs from an operator's own checkout of a repo that operator explicitly opted in, with no PR
author supplying that checkout the way CI's PR-triggered runs have one.
The count is also whitespace-blind wherever git is reachable (local mode and PR mode count fromgit diff -w --ignore-blank-lines --numstat), so a formatter or linter sweep does not consume the
budget. Two paths cannot be: --pr <n> --dry-run and the watcher's per-tick checks read GitHub's own
counters, which carry no whitespace information — both label their verdict an estimate, and both can
only ever over-count, never under-count.
| Profile | Lines | Files | For |
|---|---|---|---|
| Conservative | --max-changed-lines 800 |
--max-changed-files 150 |
Tight budget; only small PRs auto-review. |
| Default (shipped) | 4000 | 150 | Everyday PRs pass; only very large trees are skipped. |
| Permissive | --max-changed-lines 0 |
--max-changed-files 150 |
You would rather pay than skip — disables the line limit. |
crysk review --pr 42 --dry-run # prints the gate verdict, spends nothing
crysk review --pr 42 --force # local: review it anyway (does NOT skip the cost prompt)
gh pr comment 42 --body "/crysk review" # CI: same forced re-review, on the Actions runner
gh pr comment 42 --body "/crysk override" # CI: unlock a red gate on this head, no review, $0
gh pr comment 42 --body "/crysk stop" # CI: skip automatic reviews on later pushes, until /crysk review
crysk review --pr 42 --max-changed-lines 0 # 0 disables that limit entirely
crysk watch add --max-changed-lines 800 # per-repo threshold for the watcher
A skipped review exits 1 with a one-line reason and no stack. In watch mode it logsskipped … reason=too-large (or reason=nothing-to-review, when every changed file is excluded)
and costs nothing further: it does not consume a poison-PR attempt,
writes no review marker, and does not arm the one-review-per-PR state — a force-push that shrinks the
PR makes it eligible again on the next tick, because the gate is recomputed every tick.
- Findings vary run to run on the same tree. That is the nature of the instrument: treat single
runs as evidence, not verdicts, and triage before acting. - Verify what it reports. Findings are claims with cited code, tiered by deterministic rules — the
report says so itself. An advisory report reads assistant report, not a merge gate; every line is a claim to
verify. A CI report in gate mode backs thecryskcommit status, the required check: a blocking finding
sets that status to failure. A finding the PR did not introduce (pre-existingorbase-only) is
reported as advisory and does not block it, unless it touches auth, secrets, isolation or data loss,
which keep blocking.
Isolation model
Spawned agents run with a read-only tool surface (Read/Grep/Glob plus the codegraph MCP server and
nothing else): no Write, no Bash, no network tools, --strict-mcp-config, no user settings. The
driver owns every file write and all gh/git I/O. In PR mode the reviewed tree is a detached
worktree containing only tracked bytes at the PR's head — agents never see your working copy.
Development
bun test # offline test suite — no network, no spawns, fake runners
bun run typecheck # tsc --noEmit, strict
bun run check # biome over src + test
The architecture in one line each lives in CLAUDE.md; the stage plan lives in ROADMAP.md,
and the earlier phases in docs/history/. The short version: the flow is data (ReviewSpec), agents are
prompt files, orchestration is testable TypeScript — models judge, code governs.
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi