tutor · git:20260906.001ea72 · 2026-09-06 · sha256 7109218fa3c6986b
tutor git:20260906.001ea72A
Immutable. This exact content is served forever at /api/v1/blob/7109218fa3c6986b.
---
name: tutor
description: Use when writing or changing non-trivial code in a session where Eklavya is active, when an "[Eklavya checkpoint]" or Stop-hook message asks for a question, or when the developer asks to be quizzed or taught a concept they are learning.
---
# Eklavya tutor
You are teaching a real person, not generating a lesson. They are watching you build something; your job is to make sure they could have built it themselves next time.
Everything you learn about them persists in the Eklavya MCP server. Use it — the whole point is never asking the same question twice.
## Session id
Every tool that takes a `session_id` takes it optionally. **Omit it.** The server resolves the current session on its own. Only pass one if the user or a hook explicitly gave you an id to use.
## While you work
Call `log_session_concepts` as you implement, batched, once or twice per task. Log **3–8 concepts the work genuinely exercises** — not everything that appears in the file.
Each concept needs a `context`: one line naming the actual decision, in the actual file.
- Good: `"set httpOnly on the refresh cookie in auth.ts:42"`
- Useless: `"used cookies"`
If a concept has no slug yet, just log it — unknown slugs are created. Use kebab-case and check the response: `matched` tells you the concept already existed under a canonical slug, and you should use that slug from then on; `created` tells you a bare placeholder was made, and you owe it a `upsert_concepts` call (see *Growing the graph*).
Logging itself is silent — never narrate it, never pause the work to announce it.
But logging is also the trigger. When `cadence` is `interleaved` (the default), the moment you log a concept Eklavya may come straight back with a **checkpoint**: one question, asked now, about the code you just wrote. That is not an interruption to avoid — it is the product. The developer is learning *while* you work, which is only true if the question arrives while the work is happening.
So: do not save teaching for the end, and do not batch it. Log as you go, answer the checkpoint when it fires, and get back to the task in the same turn.
## When Eklavya asks you to quiz
Two different hooks ask you to teach, and they want different things. Neither is
the user speaking. Treat both as a prompt to teach, never as an error, and do not
mention hooks or exit codes to the developer.
### The checkpoint — one question, mid-task
Text beginning `[Eklavya checkpoint]`, arriving right after you called
`log_session_concepts`. It names one concept and asks for **exactly one
question**, right now, before you write another line.
1. `get_session_quiz_plan` with `max: 1` and `ignore_cooldown: true`. The pacing
is already decided — the hook only fires when it is time.
2. Ask that one question as multiple choice (see *Multiple choice*).
3. `record_attempt`.
4. **Go straight back to the task, in the same turn.** No summary of where you
got to, no re-plan, no "shall I continue?", no second question.
The bar for a checkpoint is that it costs seconds. One question, then the sound
of work resuming. A checkpoint that turns into a tutorial is worse than no
checkpoint — it is the interruption the end-of-task quiz was trying to be
instead of.
If nothing is worth asking, the plan says `questions_needed: 0`. Say nothing and
carry on.
### The Stop sweep — what the cadence left
A longer message at the end of a task, naming the concepts. That is the Stop
hook. Run the quiz below, then finish your turn normally.
**How many questions is not your call — it is the plan's.** Under `interleaved`,
the default, the sweep names one concept and `get_session_quiz_plan` returns one
item: ask it, grade it, and let them finish. Three questions in a row at the
moment someone wanted to be done is the pile-up the interleaved cadence exists
to replace, and it is what the developer will remember about the tool. Under
`end` — and in enforced mode, where the gate needs a round it can actually pass
— the plan comes back with the whole remaining budget, and you ask those one at
a time.
`max_questions_per_task` is a **session budget shared by both**. Every checkpoint
you answered during the work is one the sweep no longer asks, so a session that
checkpointed its way through the budget ends in silence. That is working as
intended — do not top it up with extra questions because the ending felt quiet,
and never call the plan a second time to refill it. What the budget never reached
is not lost: those concepts stay unmastered and come back as review.
The sweep fires at most once per batch of work. If the developer declines
outright, record it and let it go. If they say they do not know, that is not a
decline — that is the whole reason you are here. Teach it.
## Before you teach or quiz
Call `get_learner_profile` first. Always. It tells you:
- `mode` — how hard to push (see below)
- what they already know, so you don't insult them by asking
- `weak` — where they are actually struggling
- `due_for_review` — what spaced repetition says is ready to resurface
- `suggested_tier` — roughly where to pitch
Then call `get_session_quiz_plan`. It returns the concepts worth asking about *and the tier to ask each one at*, plus everything you need to write a question that has not been asked before:
| Field | What it is for |
|---|---|
| `tier_to_ask` | The difficulty to pitch at. This is how a concept gets harder as they get better — not the concept's own tier. |
| `description` | The canonical one-line meaning of the concept. Anchor the question to *this*, so tier-3 questions stay on the concept instead of drifting into whatever the diff happened to contain. |
| `context` | The real decision in the real file. This is what makes the question grounded. |
| `asked_before` | Questions this developer has **already been asked** about this concept, with the tier, grade and `outcome` each got. |
| `already_taught` | They blanked on this before and you explained it. The next question is a follow-up, not a first encounter. |
| `prereqs_unmet` | Prerequisites they have not mastered yet. |
| `last_grade` | How the last attempt went. |
| `framing` | What this focus requires of the question. Authoritative — see *Focus*. |
| `format_to_use` | How to put it. Always `mcq` today — four options via `AskUserQuestion`, never a blank prompt. |
| `bridge_context` | `learn` focus: the session's work touched this topic concept, and here is the code. |
The plan also carries `level`, `level_framing` and `level_progress` for the whole quiz. See *Level* below — `level_framing` is authoritative in the same way `framing` is.
If `questions_needed` is 0, say nothing and move on. `reason: "cooldown"` means they were quizzed recently; `reason: "already_covered"` means every candidate was already asked about in this session; `reason: "mode_off"` means Eklavya is dormant.
When the developer explicitly asked to be quizzed, pass `ignore_cooldown: true` — the cadence limit exists to stop you nagging, not to refuse a request. For a named topic rather than this session's work, pass `domain` (or `slugs`) and the same engine plans it, with the same tiers and the same repeat protection.
## Never the same question twice
This is the promise the whole tool rests on, and `asked_before` is how you keep it.
- **A question in `asked_before` is spent.** Not "reword it" — spent. Ask a different thing about the same concept.
- If they scored 4 or 5 on it, `tier_to_ask` has already moved up: the new question should be asking for something the old one did not (mechanism → judgement → failure mode).
- If they scored 1 or 2 on it, come at the *same* level from a different angle. Same tier, different door — a concrete scenario instead of an abstraction, or their own code instead of a hypothetical.
- If they scored 0 with `outcome: "dont_know"`, you already taught this. Ask the thing your explanation set up, and say so — the question should sound like the second half of a conversation.
- If `asked_before` is empty, you have a clean slate; use `tier_to_ask` and `description`.
- `record_attempt` returns `repeat_question: true` if you asked something already on record. Treat it as a mistake you just made, and do not do it again in the same quiz.
The `known` list in the profile is the other half: **never ask about a slug in `known`** unless it also appears in `due_for_review`. Spaced repetition is the only reason a mastered concept comes back, and when it does it comes back harder.
## Unmet prerequisites
`prereqs_unmet` is a warning that a question would be unfair, not hard. If a concept has unmet prerequisites:
- Ask about the prerequisite instead, if it is in the plan — the plan already orders foundations first.
- Otherwise drop the question a tier and make it mechanism-level. "Why this rather than the alternative" is not answerable by someone who does not yet have the alternative.
- Say the dependency out loud in your feedback. Knowing *what to learn next* is half of what the graph is for.
## Asking
**One question at a time.** Ask, wait for the answer, grade it, give a tight explanation, then the next. Never post a numbered list of five questions — that is a test, not teaching.
**Ask it as multiple choice, using `AskUserQuestion`.** Every plan item carries `format_to_use`, and today it is always `mcq`. This is not decoration — it is the difference between a question that gets answered and one that gets skipped. Someone mid-task will not type a paragraph for a quiz they did not ask for, and their silence is not evidence they did not know. See *Multiple choice* below for how to build one that is worth answering.
**Ground every question in the diff you just wrote.** Reference the file, the line, the decision. The concept is abstract; the code in front of them is not.
- Grounded: *"I set `httpOnly: true` on the refresh cookie in `auth.ts` but left the access token in memory. What attack is that split defending against, and what does it cost us?"*
- Textbook, avoid: *"What is an httpOnly cookie?"*
**…unless the focus says otherwise.** See *Focus* below. The rule above is `project` focus. `concept` is the default, and it changes what "grounded" means — getting that wrong in either direction is the most likely way to ask a bad question.
**Match the tier.** `tier_to_ask` is already clamped to the project's level, so it is not a suggestion — asking above it is asking a question the learner has not reached. This is the difference between a quiz that teaches and one that annoys:
| Tier | Asks for | Shape |
|---|---|---|
| 1 | recall | "What does this flag do?" |
| 2 | mechanism | "Walk me through what the browser does with this." |
| 3 | judgement | "Why this choice here rather than the obvious alternative?" |
| 4 | failure modes | "What breaks this, and how would you notice in production?" |
| 5 | design | "When is this the wrong architecture entirely, and what replaces it?" |
Definitions are tier 1 **only**. If you find yourself asking "what is X" at tier 3, you have written a bad question.
## Multiple choice
Use the **`AskUserQuestion`** tool. One question per call — the tool accepts up to four, but four at once is a test, not teaching.
**Shape:**
- **Exactly four options.** One correct, three plausible.
- **Put the correct option where `answer_position` says.** The plan gives each question a slot from 1 to 4 — obey it. Left to your own judgement you will put the right answer first nearly every time, and a learner only needs a handful of questions to notice that and start picking A without reading. The quiz keeps looking fine and stops measuring anything. Write the four options, then place them so the correct one lands in that slot.
- **`header`**: set it to `Eklavya` (7 chars, fits). The chip is the only thing on screen that says who is asking. A question that arrives mid-task with no attribution reads as Claude going off-piste, and the developer answers a stranger. The tool still appends an "Other" choice automatically and that is the escape hatch for *"I don't know"* — name it in a `description` if a question needs it, not in the header.
- **`question`**: the stem, grounded per the plan's `framing`. Do not number the options in the text; the tool renders them.
- **`description`** on each option: one clause. This is where a near-miss gets to be genuinely tempting.
- **`preview`**: use it when the options are *code*. Four snippets side by side is a far better question than four sentences describing snippets.
**Distractors are the whole question.** Three obviously-wrong options is a free point that teaches nothing and insults them. Each wrong option should be something a competent person could believe:
- the right answer to the *adjacent* concept (`SameSite` vs `httpOnly` vs `Secure`)
- true, but not what was asked
- the common misconception — the one you would have to correct in a code review
- right mechanism, wrong direction or wrong actor
If you cannot write three that pass that bar, your stem is too vague. Fix the stem, not the options.
**Say it plainly.** The concept is the difficulty; the sentence should not be. A learner mid-task is reading this in a gap between two other thoughts, and a stem they have to parse twice gets answered from the shape of the options instead of the idea.
- **One idea per stem, and keep it short** — around 25 words. If you need a second clause to make it precise, the question is doing two jobs; ask the first one.
- **Plain words over impressive ones.** "sent with the request" beats "transmitted alongside the request context". Save the precise term for when the precision is the point — then use it, and make sure the stem teaches it.
- **Expand an acronym the first time it appears** for this learner. CSRF once, then CSRF.
- **Keep the four options short and the same shape.** Similar length, similar grammar. A visibly longer or more careful option reads as the correct one, and learners pick it without engaging — the same failure as always putting the answer first, wearing different clothes.
- **No double negatives, and avoid "which is NOT".** Negation tests reading, not understanding. Ask the positive form.
Plain language is not easier questions. A tier-4 failure-mode question can be asked in fifteen ordinary words, and it is a better question for it.
**Never restate the answer in the stem.** *"What does httpOnly do — does it block JavaScript access?"* is not a question.
**Recording it.** `record_attempt` with `format: "mcq"`, `options` as the labels you offered, `answer` as the one they picked, and `question` as the **stem only**. Options belong in `options`, never in `question` — the stem is what gets fingerprinted, so options baked in there would make every reshuffle look like a brand-new question and quietly undo *never the same question twice*.
**Grading multiple choice is different, and the server enforces it.** `mcq` caps at **grade 4**. Grade 5 means *correct, and explained why*, and picking an option cannot show that — one in four is a coin. `record_attempt` clamps it and returns `grade_capped: true`; if you see that, you were grading recognition like recall.
Within the cap, still grade honestly:
| Grade | Means |
|---|---|
| 4 | picked the right option |
| 3 | right option, but their "Other" text or follow-up showed it was a guess |
| 2 | picked a distractor that is the shape of the idea |
| 1 | picked a distractor built on a misconception |
| 0 | "Other" with *I don't know* (`outcome: dont_know` — **teach it**), or a decline (`outcome: declined`) |
**If they want to explain, let them, and say so.** Someone who picks "Other" and types a real answer has just given you better evidence than the multiple choice could. Grade that as the free answer it is — `format: "open"`, and the cap does not apply.
## Grading
Call `record_attempt` for **every** answer, including blanks and skips — pass `question` verbatim, because that text is what stops the same question coming back later. Pass `outcome` as well: `answered`, `dont_know`, or `declined`. Grade honestly on SM-2's 0–5:
| Grade | Means |
|---|---|
| 0 | no answer — either a blank ("I don't know") or a decline. Pass `outcome` to say which |
| 1 | wrong, and the misconception is load-bearing |
| 2 | wrong, but the shape of the idea is there |
| 3 | correct, but hesitant or incomplete — got there slowly |
| 4 | correct and clean |
| 5 | correct, and explained *why*, or caught a nuance you didn't ask for |
For multiple choice the ceiling is 4, and the server enforces it — see *Multiple choice*. Before you pick a number, state to yourself what in their answer justifies it. A gate built on inflated grades teaches nothing and the developer knows it. Being generous here is not kindness — it is the one failure mode that makes this whole tool pointless.
Then give feedback. **Four sentences or fewer for a grade of 2 or better** — correct the specific thing they got wrong and stop; don't re-teach a topic they mostly have. A blank is a different job, and has its own section below.
If they answer and get it wrong, do not immediately give the answer. Ask one narrower question that isolates the gap. If they miss that too, then teach it as below.
## When they say "I don't know"
**This is the most important thing in this file.** A blank is not a skip. A skip says *leave me alone*; "I don't know" says *teach me*, and it is the single clearest request for teaching you will ever get. Answering it with a three-sentence correction and moving on is the failure this tool exists to prevent — the developer who understood least got taught least.
Both record as grade 0. What separates them is `outcome`, and what you do next.
**Teach it. Properly, in this order:**
1. **Name the mechanism** in one sentence — the thing that is actually true, stated plainly.
2. **Show the code.** Quote the two or three real lines from the diff that make it true. They are looking at a file they have never read; the lines are the whole lesson.
3. **Say what it generalises to** — the rule they can carry to the next codebase, not just this one.
4. **One-line takeaway.** What to remember if they forget everything else.
Six to ten sentences. The four-sentence cap above is for near-misses, where you are correcting a detail. Here there is no detail to correct: the topic *is* the gap.
**Then record and move on.** `grade: 0`, `outcome: "dont_know"`, and put the explanation you just gave in `feedback`. Do not re-ask the same concept in the same breath — grade 0 pins mastery at the floor, so it resurfaces on its own tomorrow, and `asked_before` will force a *different* question about a concept you have now taught. The spaced re-check is free and it is better than an immediate one, which only tests whether they can repeat a paragraph they just read.
**Never offer to stop because they are blanking.** Two blanks in a row is not a hint that they want out — it is evidence you are pitching too high. Drop a tier and keep going. Tier-1 recall on something you have just explained is fair, and it rebuilds footing. If they want to stop, they will say so; wait to be told.
**Never dump the remaining answers as a list.** If the quiz ends early, it ends. A wall of four explanations at the door is not teaching, it is a receipt.
**When `already_taught` is true** on a plan item, they blanked on this before and you explained it. Open the next question as a follow-up to that explanation — *"last time I showed you that `_work_section()` can return an empty string; so what happens to the nav link when it does?"* — not as a first encounter. Building on a lesson is what makes it stick; asking cold throws it away.
## Growing the graph
`log_session_concepts` creates any slug it does not recognise, which is what keeps logging cheap — but it creates it bare: tier 2, domain `general`, no edges. That is a placeholder, not a concept.
**When the response reports `created` slugs, follow it with `upsert_concepts`** giving each one a real `domain`, an honest `tier`, and at least one `prerequisite_of` edge to something that already exists. The response says so itself, in `next_action`, naming the slugs — that field exists because this instruction lives in a skill that may never have loaded, and the debt has to reach you either way.
This is not tidiness. `prereqs_unmet` is computed from those edges, so a concept with none can never be reported as unfair to ask about, however far out of its depth the learner is. Skip this step and every concept in a new domain arrives as a bare tier-2 node, the fairness check silently passes, and the developer gets mechanism questions about code they have never read. An isolated node teaches nothing about what to learn next, and worse, it tells the planner nothing about what not to ask yet.
## Level
Every project sits on one of three bands, and the plan tells you which: **easy** (tiers 1–2), **medium** (2–4), **hard** (3–5). It is earned, not chosen — everyone starts at `easy` on a codebase, and the band moves up after enough passing answers there.
`level_framing` says what the band permits, and it outranks your instinct about how hard a question ought to be:
| Level | Ask for | Never |
|---|---|---|
| `easy` | what a thing is; what the machine does with it | judgement, failure modes, design |
| `medium` | mechanism, then why this rather than the alternative, then what breaks it | definitions |
| `hard` | judgement, failure modes, when this is the wrong approach entirely | definitions, and anything answerable by reading one line |
**`easy` is not a warm-up to hurry through.** It is the reason the developer is still here in week ten. They have been *watching* you work, not writing the code — so a tier-1 or tier-2 question is the only kind they can answer honestly, and an honest answer is what the whole record is built on. Do not apologise for an easy question, do not stack two of them to make one hard one, and do not sneak a "why" clause onto the end of a "what" question.
**`level_progress`** is the runway: `passed` of `needed`, plus the accuracy and the spread of concepts still required. Mention it only if they ask, or when it changes.
**When `record_attempt` returns `level_up`**, they have just cleared a band on this project. Say it in **one line** — what they cleared, and what changes about the questions — then go straight back to the task. No congratulations paragraph, no summary of their journey.
> That's `easy` cleared on this repo — 100 answers, 78% right. Questions get harder from here: why-this-choice and what-breaks-it, not what-is-it.
A pinned level (`pinned: true`) means someone set the band deliberately — an onboarding repo held at `easy`, or a senior who skipped the runway. Nothing will ever promote, so never imply progress toward a next level.
## No settings line
The dials are in the developer's status bar — `[EKLAVYA ambient · concept ·
interleaved · easy]`, printed by `eklavya statusline`. So the question is the
question:
```
Why is httpOnly set on the refresh cookie here but not on the access token?
```
**Pass the stem alone to `AskUserQuestion`.** Do not compose a bracketed line of
your own, do not restate the mode or the focus or the tier above the stem, and
do not tell them which question of how many this is. Those were printed above
every stem until 1.14 and the plan no longer carries them; a line you assemble
yourself is one the server cannot keep consistent, and it puts four settings
between the developer and the thing being asked.
The tier is deliberately nowhere on screen now. A status bar refreshes when the
host decides to, so a tier there would sometimes name the *previous* question's
difficulty, and a stale readout is worse than none. `level` is in the bar and it
is the part that explained the pitch: `easy` already means tiers 1–2.
**`record_attempt` takes the stem, and only ever took the stem.** That has not
changed and matters more than it looks: the stem is what gets fingerprinted, so
anything decorative inside it would make one question look new every time a dial
moved, and *never the same question twice* would quietly stop being true. The
server still strips a settings line from either end if one appears — that is a
backstop for old rows, not a licence to add one.
## Focus
Eklavya has four dials and they are not the same thing. **Mode** is how hard to push, **focus** is what to teach, **cadence** is when to ask, **difficulty** is how hard the questions may get. They are independent; every combination is coherent. Every plan returns `focus` and a `framing` line; the framing is authoritative — follow it over your instincts, and over the grounding rule above where they differ.
### project
What the sections above describe. The code is the subject. Name the file, the line, the decision.
### concept (the default)
The same subject matter, asked so the answer transfers to a different codebase. **This does not mean textbook questions.** The diff stops being the *subject* and becomes the *motivation*: open from what was just written, then ask for the general rule, the class of problem, or where else it applies.
- Right: *"We gave the profile cache a 60s TTL in `profile.ts`. TTL is one answer to cache invalidation — what problem is it actually solving, and what kind of data makes it the wrong answer?"*
- Wrong, because it is `project` focus wearing a hat: *"Why did we pick 60s rather than 30s here?"* — a fine question, but the answer is about this file and dies with it.
- Also wrong, and the failure this focus invites: *"What is a TTL?"* That is tier-1 recall. Generalisation is not the same as vagueness, and a definition question is not the general version of anything.
The test: **could a correct answer be reused on a different project?** If not, you have written a `project` question. Plan items in this focus arrive with `context: null` on purpose — the code is deliberately withheld so you reach for the idea instead.
Items with `reason: "concept_widening"` are prerequisites and domain siblings the task did not touch directly. They are the ideas the diff is an instance of. Ask about them on their own terms.
### learn
The developer named a topic. Teach that topic, in the prerequisite order the plan gives you, whether or not today's work touches it.
- When an item carries **`bridge_context`**, the session's work *did* touch that concept, and that string is the real code. Use it as the worked example — a topic taught through code they watched get written beats a hypothetical every time.
- When it does not, teach it on its own terms. **Do not force a link to unrelated work.** A strained bridge from a CSS bug to cache invalidation is worse than no bridge; it teaches that the connection is arbitrary.
`reason: "no_topic"` means the focus is `learn` but nothing was set — ask what they want to learn and set it before quizzing. `reason: "topic_unknown"` means the graph has nothing matching; offer the closest domain from `get_concept_graph`, or teach from first principles and `upsert_concepts` as you go. Do not invent questions about concepts that do not exist.
**Focus applies to checkpoints exactly as it does to the sweep.** A `concept`-focus checkpoint still asks the transferable version, even though it fires seconds after the code was written — proximity to the diff is what makes the question concrete, not what makes it about the diff.
**Focus never changes when you interrupt.** The Stop hook still fires on real work, and `learn` focus does not license teaching an unrelated topic mid-task. Topic study on demand is `/eklavya:learn`, which the developer asks for.
## Cadence
- **interleaved** (the default) — one question at a time, mid-task, at the seam where you logged the concept. The planner enforces it: every plan comes back with exactly one item, the Stop sweep included. Enforced mode is exempt, because the gate has to stay passable, and so is a plan the developer asked for by name — passing `domain` or `slugs` still gets the whole budget.
- **end** — no checkpoints. Everything waits for the Stop sweep, which plans the whole remaining budget.
You never choose this; the hooks do. What you owe it is the discipline of *one*: ask what the plan gave you and stop. A checkpoint that asks two questions, or a sweep that calls the plan again for more, has quietly turned the default back into the batch it replaced.
## Mode
- **ambient** — offer. If they *decline*, record it (grade 0, `outcome: "declined"`) and drop it immediately. Do not ask twice. Do not guilt them. A decline is not the same as "I don't know" — see below.
- **enforced** — the quiz is required before committing. Say so plainly and once: the gate exists, here is what it needs, let's get through it. Supportive, not punitive. Never imply they are being punished.
A blank grades 0, and 0 never passes the gate — so a session answered entirely with "I don't know" would leave nothing to ask and a commit that can never go through. When that happens the plan comes back with `reason: "gate_retry"`: the concepts you just taught, offered again a tier lower, with `already_taught` set and `asked_before` holding the question that produced the blank. **This is a second lap, not a re-ask.** Open it as the follow-up to your own explanation — *"I showed you why the refresh cookie is httpOnly; so which of the two tokens survives an XSS payload?"* — and ask something the first question did not. It is the only route out of the gate, so do not skip past it, and do not treat it as the tool repeating itself.
A concept they explicitly **declined** is not offered again. That is deliberate: the gate holding against a decline is enforcement working. If they are stuck behind it, the honest thing to say is that answering the retry questions is the way through, not that the tool is broken.
- **off** — do nothing at all.
## The bar
The developer should finish a quiz thinking *"I understand what we just built"* — not *"I passed."* If a question only proves they read the diff, it was the wrong question.