Prove that your tests actually test something. Powered by Jev and your own TypeSafe API key.
Your agent wrote 40 tests. They're all green. What do they prove? lgtm reads every test block with its implementation and tells you which ones are useless.
It runs on Jev by TypeSafe, with your own TYPESAFE_API_KEY.
With this, you can prove that your agent actually wrote code that actually works, so you can say it lgtm 😐👍.
Evaluated and hill-climbed on real live apps built by Stardeck: 15,000+ real test blocks scored, 262 each read against its implementation and labelled, every check's threshold fitted so that when lgtm points at a test, the test is worth your time.
- the lgtmCLI: run it on a file, a directory or--diff, in your terminal or in CI
- a /lgtmskill for your coding agents, so the agent that wrote the tests runs the audit and fixes what it finds
- an /actually-testskill: write the tests the change needs, prove them red, then iterate with lgtm until they pass
bun add -g @stardeckai/lgtm
pnpm add -g @stardeckai/lgtm
npm i -g @stardeckai/lgtm# or per project
pnpm add -D @stardeckai/lgtm # then: pnpm lgtm ... (npm: npx lgtm ...)If lgtm is not found after a global install, the package manager's global bin directory is not on your PATH.
Run npm prefix -g (or pnpm bin -g, bun pm bin -g) and add its bin to PATH, then open a new shell or run
rehash in zsh.
lgtm init # paste your API key, then install the /lgtm and /actually-test skillsIt asks for your TypeSafe API key (get one at https://typesafe.ai), then asks whether to install the
/lgtm and /actually-test skills with npx skills.
The key lands in ~/.config/lgtm/config.json (mode 0600); TYPESAFE_API_KEY in the environment wins over it.
Skip the prompts with --skill <where>:
If the skills CLI can't run, init falls back to writing the Claude Code skills itself.
Running lgtm before setup exits with 😐✋ No API key. Run: lgtm init.
lgtm . # every *.test.* / *.spec.* file under a directory (a path is required)
lgtm src/user.test.ts # one file, or a directory
lgtm --diff origin/main # only tests changed vs a base, with the diff as evidence
lgtm --diff # just what you're working on: changed and new tests, plus tests of changed code, vs the default branch
lgtm --dry-run src # only the plan: files, estimated cost and runtime; no key neededEvery run starts with that plan and asks Run? [Y/n]. Outside a terminal (CI, an agent) it stops after the plan
unless you pass --yes.
lgtm . --yes --format json # non-interactiveFor a one-off run without installing: npx @stardeckai/lgtm --dry-run src.
test/payment.test.ts:42 "rejects expired cards"
😐👏 mocks-seam-under-test 0.93 — The collaborator that decides this behaviour is a mock, so the test only proves the mock works.
test/refund.test.ts:17 "refunds a captured charge"
😐🤏 swallowed-error-as-success 0.88 — The test stays green whether the error is caught, logged, or never thrown; it never pins the specific failure.
😐🫵 2 tests prove nothing.
4 contract-integration · 19 mocked-seam · 8 pure-logic
98120 input tokens used ≈ $0.0041 ($0.0001 per test)
9.8s (0.3s per test)
Four faces, one per family: 😐🤏 the assertion proves this much, 😐👏 you tested the mock, 😐🤌 what
exactly are we doing here, 😐🫸 do not merge this. Colour is severity (red at 0.9 and above, yellow at or over
the check's threshold, dim for --verbose suspects). The verdict is one line, 😐👍 N tests. fine. allegedly.
or 😐🫵 N tests prove nothing. --format github and --format json stay plain.
Gets your agent to actually test the code it just wrote, and iterates on /lgtm (don't worry, it's cached)
until it proves the code is actually tested.
Coding agents are prolific test writers and terrible test critics. They mock whatever is inconvenient, assert that the mock was called, compute the expected value with the code under test, and hand you a suite where every line is covered and nothing is verified. A test that checks a trace's name. A test for the thing you decided not to build. Nobody reads those files. The PR says "added tests" and gets merged.
The bug that pages you lives in a seam. One side writes, the other reads, and every unit test mocked at least one of them to agree. No linter catches that. It's a judgment call, and judgment used to cost a senior engineer's afternoon per PR.
Reads like a review, runs like a linter. Every finding is one test, one smell, one probability, one sentence
you can act on. 😐👏 mocks-seam-under-test means you tested the mock. 😐👏 reimplements-logic means the test
and the implementation share the same bug. The summary tells you how much of your suite actually crosses a seam.
Opinionated by design. Few wide tests with real collaborators beat a hundred mocked units. Delete with confidence: a good audit shrinks the suite. And when the suite is clean, it says so.
Now it costs a cent. Jev bills $0.042 per million input tokens and answers in under a second. lgtm shows you
the bill and the runtime before it spends, and caches every answer. Every check holds precision 1.00 on the
held-out slice of the corpus (evals/).
A test that crosses a seam with both sides real and asserts that they agree. That test fails when the wiring breaks, which is how most things actually break.
It dislikes tests of one-line helpers (any real test of the feature covers them for free), tests that
mock everything except the function name, and one-off assertions that would survive the feature being
deleted. So the summary prints what your suite is made of: N contract-integration · N mocked-seam · N pure-logic. --classes lists every test with its class, which is the number to watch during an audit.
Retiring three unit tests for one wider test that really fails is a win, not a coverage loss.
--format json adds a checks map with a longer explanation and the fix, once per check.
The corpus is 521 public + 262 private labelled test cases, synthetic and anonymized real-world, with positives, hard negatives and genuinely good tests. The ground truth is kept in expect.json so it never reaches the model.
The 262 private cases come from real Stardeck customer apps and from Stardeck's own codebase, each read against its implementation, labelled, and anonymized. They are scored in these numbers but not published, because anonymization removes names, not shape. The 521 public cases in evals/cases reproduce with pnpm eval alone.
Scores are at each check's own threshold. 165 of the cases are holdout, never used to fit a threshold or a prompt.
Test class accuracy: 665/783 (0.85) on all cases, 145/165 (0.88) on holdout.
A full cold run of the corpus is about 3,988,609 input tokens ≈ $0.1675 (estimated from the states; the last run spent $0.0301 after cache hits).
Every miss and false positive is listed in evals/RESULTS.md. The public cases reproduce with pnpm eval.
- uses: actions/cache@v4
with:
path: node_modules/.cache/lgtm
key: lgtm-${{ hashFiles('**/pnpm-lock.yaml', '**/package-lock.json') }}
restore-keys: lgtm-
- run: npx @stardeckai/lgtm --diff origin/${{ github.base_ref }} --yes --format github
env:
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}The cache step is optional: --diff already limits a PR run to the tests it touched, and answers are keyed by the
full state (test, imports, implementation, guidelines) plus the check wording and lgtm version, so a hit means
nothing relevant changed. Do not commit the cache directory; it churns on every refactor and never shrinks.
lgtm is advisory by default: it prints findings and exits 0. Fetch enough history for the base ref
(fetch-depth: 0) and add --fail once the findings are clean enough that you want them blocking.
TypeSafe bills $0.042 per million input tokens and nothing for output, so lgtm is cheap enough to run on every PR.
One request per test block. Each state is trimmed to at most 100,000 chars (~25,000 tokens, under TypeSafe's 32k
state limit): the test code, the whole test file with the block fenced, the file's imports and sibling test names,
up to 60,000 chars of the imported implementation (one hop deep) and the test sections of any CLAUDE.md/AGENTS.md
above it. --lean cuts that back to an 8,000-char implementation and no test file or guidelines.
Answers are cached by state hash under node_modules/.cache/lgtm, so a re-run after editing one test only pays for
that test. The implementation source is the main cost lever: --no-impl cuts the bulk of each request at the price
of weaker would-pass-if-broken and reimplements-logic answers. Every run prints its input tokens and the
estimated cost.
The key lives in ~/.config/lgtm/config.json. TYPESAFE_API_KEY in the environment always wins over it.
lgtm key <new-key> # swap the saved key; `lgtm key` alone prompts
lgtm usage # cost so far: all time, last day, last week, this worktree
lgtm clear-cache # drop this project's cached answers (node_modules/.cache/lgtm)
lgtm skill # (re)install the /lgtm and /actually-test skills, e.g. to add another agent
lgtm init # both steps againEach check has its own threshold, fitted on the eval corpus to the lowest probability that still gives zero
false positives (the threshold column above). That trades recall for precision on purpose: a finding should
be worth your time. To see what sits just under the line, run --verbose, or --threshold 0.5 --format json
on a suite you know well and pick your own number. A check that is consistently wrong for your codebase belongs
in --skip.
This repo uses pnpm.
Each check is one file under src/checks/<category>/; add a new one there and register it in src/checks/index.ts.
pnpm install
pnpm typecheck
pnpm test
pnpm build
node dist/cli.js --dry-run src # no API key needed
npm link # expose this checkout as the global `lgtm` (symlink to dist/cli.js; rebuild to update)
pnpm gen:skill # regenerate skills/*/SKILL.md after changing a check or a skill (a test guards drift)
pnpm eval # run the labelled corpus against Jev; --offline re-scores, --fit-thresholds refitsMIT