Lesson 3: Routing lenses by path
20 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 routing lenses by path”
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 03-routing-lenses-by-path (swap claude-code for cursor, codex, or tessl-agent).
Three --skill flags on one command line works fine for one person on one branch. It stops working the moment a colleague needs to run the same review, or you want a different lens for the frontend than the backend.
This lesson moves the configuration into the repository, and adds something the command line can't do at all: sending each lens only to the files its rule is about.
Version note. YAML review profiles arrived after CLI 0.96.0. Run
tessl --versionbefore you start, and update through whichever method you used to install if you're behind. The other three lessons work on 0.96.0.
What you'll build
- A
.tessl-code-review.ymlholding the whole review configuration. - Path routing that sends your house lens to
src/api/and nowhere else. - A clear-eyed understanding of what happens when no lens matches, which is the sharpest edge in the whole feature.
A profile is the whole configuration
standard is the built-in profile, and it's what you've been using without saying so. The alternative is a YAML file you keep in the repository:
tessl code review --profile ./.tessl-code-review.yml
Tessl doesn't go looking for that file. It runs only when the command names its path, and the path has to end in .yml or .yaml.
That looks like an inconvenience and is actually the point. A profile decides how your code gets judged, so nothing should start using one because a file appeared in the repository. The caller asks for it, explicitly, every time. Which is also why the file deserves review like source code. It's executable policy, and a change to it changes every review that follows.
Step-by-step
1. Write the profile
Create .tessl-code-review.yml at the root of the service:
schemaVersion: 1
lenses:
- ref: tessl/code-review@0.1.0#review-correctness-and-data-integrity
- ref: tessl/code-review@0.1.0#review-security-and-privacy
- ref: ./review-lenses/review-api-error-shape/SKILL.md
globs:
- src/api/**
- '!src/api/**/*.test.ts'
Then run it:
tessl code review --profile ./.tessl-code-review.yml
Same three findings as the end of lesson 2, with the configuration now in a file your team can read and change.
Three details in that file are worth understanding rather than copying.
Local refs resolve against the profile, not your working directory, and must stay inside the repository. So the same command works from any directory in the project.
Globs are evaluated in order. A matching positive pattern includes a path, a matching ! pattern excludes it, and a later positive pattern can pull it back in. Every list needs at least one positive pattern, so you can't build a list out of exclusions alone. They're case-sensitive and relative to the repository root.
A renamed file matches on either path, old or new, so moving a handler doesn't quietly drop it out of review.
2. Prove the routing both ways
Routing you haven't tested is routing you're guessing about.
Inside the paths, the branch already changes src/api/invoices.ts, so the error-shape finding should be there.
Outside them, make a change that touches only src/store.ts and review that:
tessl code review --profile ./.tessl-code-review.yml --base origin/main --head store-only
Correctness and security still run. Your lens doesn't, because nothing it governs changed. It never sees those files at all: not in its prompt, and not through the diff tools it can call.
That last part matters more than it sounds. A lens scoped to a path can't wander into ground it wasn't given, which is what keeps a specialist lens from turning into a second general reviewer.
One counting rule comes with this: a review supports at most eight lenses, and only lenses with matching work count. A profile holding twelve lenses is fine if a typical change routes to five of them.
Skipped is not approved
Routing introduces a state that didn't exist before: a change where no lens matches at all.
Update a README, or touch a directory no lens governs, and the review has nothing to run. What happens then is worth knowing exactly.
The command exits zero. No model runs. And the result says so:
{ "status": "skipped", "reason": "no-matching-lenses" }
That's a successful no-op, and it's deliberately not an approval. Nothing read the change, so nothing is in a position to approve it.
Here's why it bites. Wrapping the command in a script and branching on the exit code is the obvious thing to do, and it's also the thing that turns "nobody reviewed this" into "the review passed". The failure is quiet, it only shows up on changes that route to nothing, and by then the habit is established.
Read status, which is what it's for:
tessl code review --profile ./.tessl-code-review.yml --json > result.json
jq -r '.status' result.json # ok | skipped | failed
Three states, each meaning something different: ok reviewed the change, skipped didn't review it, failed tried and couldn't. Only the first says anything about your code.
A general rule.
--jsonwrites exactly one document on every path, including failures, sostatusis always there to read. If a decision matters, take it from the field that names the outcome rather than from the exit code, which can only say "nothing went wrong".
Verify
tessl code review --profile ./.tessl-code-review.ymlruns and reports the same findings you got from three--skillflags.- A change confined to
src/store.tsruns the default lenses but not your house lens. - A change that matches no lens at all exits zero with
statusreadingskipped. - You can explain to someone else why
skippedmust not be treated as a pass.
If the profile is rejected, check it's block-style YAML with schemaVersion: 1. JSON, flow-style YAML, unknown fields, and multiple documents in one file are all refused rather than half-read.
What you keep
Your review configuration is a file in the repository now, next to the lens it points at, versioned by the same git history as the code it judges. Anyone on the team runs the same review by naming the same path, and changing what the reviewer values is a pull request rather than a conversation about someone's shell history.
You also know the trap. skipped and ok both exit zero, and only one of them means your code was read.
Everything so far has printed to your terminal. Lesson 4 puts it on the pull request, and the first thing it does is explain why this file doesn't come with you.