Code Review Loops · Lesson 1

Lesson 1: Your first code review

25 min
What you keep A review you ran yourself over a real branch, and the vocabulary to read what it returned.

Two ways through this lesson: read it on this page, or run it hands-on in your coding agent. To do it in your agent:

1 · Install once per course 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.
2 · Start the lesson: ask your agent “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 run scores a skill's quality. The subject is the skill.
  • tessl code review reviews 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 review completed 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 --json printed one parseable document, and status read ok.

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.

Lesson 1 complete ✓