Lesson 1: Your first code review
25 minTwo ways through this lesson: read it on this page, or run it hands-on in your coding agent. To do it in your agent:
npx tessl install tessl-academy/code-review-loops
Run this once, in a fresh project directory (for example a new code-review folder, because Tessl won't initialize in your home directory). It installs the skills your agent uses to guide you through the lessons interactively.
“guide me through your first code review”
Open your coding agent (Claude Code, Cursor, Codex, or Tessl Agent) in that directory and ask it the prompt above. The installed skill picks it up and walks you through this lesson step by step. Prefer a command? Launch it directly with tessl launch skill --agent claude-code -i 01-your-first-code-review (swap claude-code for cursor, codex, or tessl-agent).
Reviewing code is the part of the job that scales worst. It needs someone who knows the codebase, has time, and is willing to read carefully. When agents are writing most of the changes, that person runs out long before the changes do.
This lesson is the first look at what a Tessl Code Review does about that. You'll set up a small service with a flawed pull request, run a review over it, and learn to read what comes back. Nothing here touches GitHub yet; that's lesson 4. For now the point is to see the reviewer work and understand what it's looking at.
What you'll build
- A small HTTP service with a branch that breaks three things on purpose.
- A review you ran three different ways: over your local change, over an explicit git range, and over a pull request.
- A working sense of what the outcome means, what a severity means, and why they aren't the same thing.
What a review is grounded in
A review is always grounded in a change. Not a file, and not the repository as it stands, but the difference between two states of it. That one fact explains most of the command's behavior.
tessl code review
With no flags, that reviews the current pull request when it runs in a supported CI environment. Anywhere else it reviews your complete local change, diffed against origin/main, including work you haven't committed yet.
Two names sit close together in Tessl and do opposite things, so it's worth separating them now:
tessl review runscores a skill's quality. The subject is the skill.tessl code reviewreviews your code. The subject is the change.
Lenses, then a supervisor
A lens is a focused set of review instructions: one concern, written down. Four ship by default, covering correctness and data integrity, maintainability, scale and resilience, and security and privacy.
Each selected lens reads the change and produces findings on its own. Then a supervisor takes all of them, works out which hold up, which overlap, and which have already been dealt with, and produces one review of the whole change.
That last step is what separates this from running four reviewers and stapling their output together. You get a judgment rather than a pile.
Given a pull request, the review also takes in the surrounding codebase and the conversation on the pull request itself. So a second review knows what you fixed after the first one, and what you pushed back on. That loop is where the value is, and it's what the rest of the course builds toward.
Before you start
You need three things on your machine:
- A coding agent. Examples assume Claude Code or Cursor, but skills are agent-agnostic, so Codex and Gemini work too.
- The Tessl CLI. Follow the installation guide, then run
tessl login. - A git repository to work in.
If you haven't initialized Tessl in this repo yet, do that now:
tessl init --agent claude-code
# or: tessl init --agent cursor
Set up something worth reviewing
Reviews need a change, so build a small service and put a flawed endpoint on a branch.
review-lab/
├── .git/
├── package.json
└── src/
├── store.ts # an in-memory record store
└── api/
├── errors.ts # the shared error helper
├── orders.ts # an existing handler that follows the rule
└── invoices.ts # the new endpoint, added on the branch
The rule this codebase keeps
src/api/errors.ts holds one small helper, and every handler is expected to use it:
export function apiError(res: Response, status: number, code: string) {
return res.status(status).json({ error: { code } });
}
The point is that every client, everywhere, gets the same error shape. orders.ts does it properly, which is what makes it the reference:
const order = findOrder(req.params.id);
if (!order) return apiError(res, 404, 'order_not_found');
res.json(order);
Nothing enforces this. It's a convention, held up by whoever remembers it during review.
The change under review
On a branch called add-invoice-endpoint, src/api/invoices.ts adds a handler:
invoices.get('/invoices/:id', (req, res) => {
const invoice = findInvoice(req.params.id);
console.log(`invoice lookup for ${invoice.customerEmail}`);
if (!invoice) {
return res.status(404).send('invoice not found');
}
res.json({ id: invoice.id, total: invoice.total.toFixed(2) });
});
Three things are wrong with it, and they're wrong in different ways.
The null check sits below the line that dereferences the record, so an unknown ID throws before it can ever return a 404. The log line prints a customer's email address. And the 404 sends a bare string instead of going through apiError(), so this one endpoint answers in a shape no other endpoint uses.
The first two are general faults. A reviewer who had never seen this repository would still flag them. The third is only a fault here, and only if the reviewer knows the rule.
Why plant them. A clean diff proves nothing. Three defects of different kinds let you see where the default lenses reach, and exactly where they stop.
Step-by-step
1. Review your branch
Check out the branch, then review it.
git checkout add-invoice-endpoint
tessl code review
With no flags and no pull request, that reviews your complete local change against origin/main, uncommitted work included. It takes a couple of minutes: each lens reads the change, then the supervisor reconciles what they found.
Expect two of the three planted problems to come back. The dereference before the null check is a correctness fault, and the email in the log line is a privacy one. Both are concerns any codebase shares, which is exactly why the default lenses look for them. The wording will differ from run to run; a review is a model reading your code, not a linter.
The bare error string almost certainly won't appear. Hold that thought. Lesson 2 starts there.
2. Point it at something specific
Two flags narrow the subject. An explicit range needs no pull request at all:
tessl code review --base origin/main --head add-invoice-endpoint
Or review a pull request, which also pulls in its conversation:
tessl code review --pr 42
--pr takes a number, a URL, or a provider ref, and it can't be combined with --base or --head, so you're either reviewing a pull request or a range. It needs GitHub credentials: GITHUB_TOKEN or GH_TOKEN in the environment, or an authenticated gh session. Without them you get anonymous access, which reaches public repositories only.
3. Get it as data
tessl code review --json
Exactly one JSON document goes to stdout, on every path: success, skip, or failure. No scanning for the payload, no empty stdout to interpret. Anything human-readable goes to stderr instead.
That contract is what makes the command safe to wrap in automation, and you'll lean on it in lesson 3.
This publishes nothing. The command prints a review; it never posts to GitHub. Putting reviews on pull requests is the Action's job, in lesson 4.
Reading what came back
A completed review reaches one of two outcomes:
| Outcome | What it means |
|---|---|
| Changes approved | Nothing in the review requires you to change anything. |
| Changes requested (N) | N findings require a change. Optional suggestions aren't counted. |
Alongside that, each finding carries a severity. It's tempting to read the two as the same scale. They aren't, and the difference matters.
Severity describes how much a finding would cost you if you shipped it. The outcome says whether Tessl thinks anything must change before this version of the code is approved. So an approved review can still hand you a Minor suggestion that's worth reading and not worth blocking on. That's the reviewer being useful without being obstructive.
The same split runs through the findings themselves. Some are changes you're expected to make; some are suggestions you can take or leave. Treat that as a real distinction rather than a formatting choice. A reviewer that marks everything required teaches a team to ignore it, which is how most automated review ends up muted.
In the JSON, the equivalent signal is status, which reads ok, skipped, or failed. A failure carries no findings at all, deliberately, because an empty findings list would be indistinguishable from a review that ran fine and found nothing, so a failed run says so plainly and exits non-zero, with a stage and a kind naming what stopped it.
Keep an eye on that field. It's what stops automation drawing a conclusion from a run that never really happened, and it becomes a trap worth its own section once you start routing lenses in lesson 3.
Verify
tessl code reviewcompleted on the branch and printed an outcome with a finding count.- At least two findings landed, anchored to
src/api/invoices.ts. - You can say what the outcome is, and separately what a severity is, without conflating them.
tessl code review --jsonprinted one parseable document, andstatusreadok.
If the review finds nothing at all, check you're actually on the branch and that origin/main resolves. In a checkout without that ref, pass --base with one that exists.
What you keep
There's a service in your working directory now, a branch on it that a reviewer has opinions about, and a set of findings you can go back and reread.
Two of the three planted defects came back. The third didn't, and no amount of rerunning will change that, because nothing in the default lenses knows your codebase requires apiError(). That's the boundary of what a reviewer shipped to everyone can reach, and the next lesson is about crossing it.