eklavya 1.13.1 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/ask.js +25 -43
  2. package/dist/ask.js.map +1 -1
  3. package/dist/assets/tutor/SKILL.md +180 -0
  4. package/dist/assets/tutor/references/focus-and-level.md +156 -0
  5. package/dist/assets/tutor/references/grading.md +113 -0
  6. package/dist/assets/tutor/references/writing-mcq.md +133 -0
  7. package/dist/cli.js +136 -5
  8. package/dist/cli.js.map +1 -1
  9. package/dist/eval/extract-json.js +81 -0
  10. package/dist/eval/extract-json.js.map +1 -0
  11. package/dist/eval/extraction-score.js +115 -0
  12. package/dist/eval/extraction-score.js.map +1 -0
  13. package/dist/eval/history-stats.js +230 -0
  14. package/dist/eval/history-stats.js.map +1 -0
  15. package/dist/eval/question-checks.js +220 -0
  16. package/dist/eval/question-checks.js.map +1 -0
  17. package/dist/hooks/checkpoint-quiz.js +1 -1
  18. package/dist/hooks/lib.js +38 -13
  19. package/dist/hooks/lib.js.map +1 -1
  20. package/dist/hooks/prompt-submit-nudge.js +155 -0
  21. package/dist/hooks/prompt-submit-nudge.js.map +1 -0
  22. package/dist/hooks/session-start.js +32 -14
  23. package/dist/hooks/session-start.js.map +1 -1
  24. package/dist/hooks/stop-quiz-check.js +6 -7
  25. package/dist/hooks/stop-quiz-check.js.map +1 -1
  26. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  27. package/dist/plugin/agents/tutor.md +19 -5
  28. package/dist/plugin/hooks/CLAUDE.md +83 -2
  29. package/dist/plugin/hooks/hooks.json +12 -0
  30. package/dist/plugin/hooks/run.mjs +3 -3
  31. package/dist/plugin/skills/CLAUDE.md +140 -37
  32. package/dist/plugin/skills/setup/SKILL.md +1 -1
  33. package/dist/plugin/skills/tutor/SKILL.md +119 -299
  34. package/dist/plugin/skills/tutor/references/focus-and-level.md +156 -0
  35. package/dist/plugin/skills/tutor/references/grading.md +113 -0
  36. package/dist/plugin/skills/tutor/references/writing-mcq.md +133 -0
  37. package/dist/slug.js +63 -0
  38. package/dist/slug.js.map +1 -1
  39. package/dist/statusline.js +33 -0
  40. package/dist/statusline.js.map +1 -0
  41. package/dist/stdin.js +131 -0
  42. package/dist/stdin.js.map +1 -0
  43. package/dist/store.js +16 -2
  44. package/dist/store.js.map +1 -1
  45. package/dist/tools/get_session_quiz_plan.js +1 -24
  46. package/dist/tools/get_session_quiz_plan.js.map +1 -1
  47. package/dist/tools/record_attempt.js +15 -3
  48. package/dist/tools/record_attempt.js.map +1 -1
  49. package/package.json +1 -1
  50. package/dist/assets/tutor-skill.md +0 -360
package/dist/ask.js CHANGED
@@ -1,48 +1,30 @@
1
- /** What each tier is actually asking for, so `tier 4` is not a bare number. */
2
- const TIER_LABEL = {
3
- 1: 'recall',
4
- 2: 'mechanism',
5
- 3: 'judgement',
6
- 4: 'failure modes',
7
- 5: 'design',
8
- };
9
1
  /**
10
- * `null` when nothing should be shown.
2
+ * The settings line: gone from the question, still stripped from the record.
11
3
  *
12
- * `quiet` is the only suppressor: someone who turned the narration off has
13
- * already answered this question.
4
+ * Eklavya used to print a bracketed line above every question naming the dials
5
+ * that chose it -- `[mode: ambient · focus: concept · level: easy · tier: 2
6
+ * mechanism]`. It was composed here and pasted into the `question` field of
7
+ * `AskUserQuestion`, and it existed for a real reason: on `concept` focus a
8
+ * deliberately transferable question reads as a vague one, and on `easy` a
9
+ * tier-2 question reads as Eklavya being shallow rather than as a runway the
10
+ * developer is 37 answers into. Dials nobody can see make a well-pitched
11
+ * question look like a badly written one.
14
12
  *
15
- * `cadence` is deliberately absent. The question's *arrival* -- mid-task or at
16
- * the end -- already tells them when Eklavya asks, and a line that repeats what
17
- * the moment just demonstrated is noise. Everything else is here: the mode, the
18
- * focus, the level, what the tier is asking for, and how many questions are
19
- * coming. A learner who cannot see those is answering a question with no idea
20
- * why it was pitched where it was, or whether four more follow.
13
+ * The dials now live in the status bar (`statusline.ts`), which meets that
14
+ * requirement without spending four lines above every stem: ambient state
15
+ * belongs somewhere ambient, and a bar is on screen when the question arrives
16
+ * *and* the rest of the time. `askHeader` is deleted rather than deprecated --
17
+ * a composer nothing calls is a composer that drifts.
18
+ *
19
+ * `stripAskHeader` stays, and has to stay for good. Every attempt recorded while
20
+ * the line existed still has it baked into the stem, and `questionFingerprint`
21
+ * (`store.ts`) hashes that text to make *never the same question twice* true. A
22
+ * strip that stopped knowing the old shape would let every one of those rows
23
+ * change fingerprint at once, and the whole back catalogue would come back round
24
+ * as brand-new questions.
21
25
  */
22
- export function askHeader({ config, level, pinned, tier, position }) {
23
- if (config.quiet)
24
- return null;
25
- // `learn (topic)` rather than `learn: topic`: the value now sits behind its own
26
- // `focus:` label, and two colons in one field read as a nested key.
27
- const focus = config.focus === 'learn' && config.focus_topic
28
- ? `learn (${config.focus_topic})`
29
- : config.focus;
30
- const label = TIER_LABEL[tier];
31
- const parts = [
32
- // `enforced` is the one value with a consequence attached, so it says so.
33
- ['mode', config.mode === 'enforced' ? 'enforced (gated)' : config.mode],
34
- ['focus', focus],
35
- // A pinned level explains itself here or nowhere: without it, questions
36
- // simply stop getting harder one day and nothing on screen says why.
37
- ['level', pinned ? `${level} (pinned)` : level],
38
- ['tier', label ? `${tier} ${label}` : String(tier)],
39
- ];
40
- if (position && position.total > 1)
41
- parts.push(['question', `${position.index} of ${position.total}`]);
42
- return `[${parts.map(([key, value]) => `${key}: ${value}`).join(' · ')}]`;
43
- }
44
26
  /**
45
- * The settings line, matched exactly as `askHeader` composes it.
27
+ * The settings line, matched exactly as Eklavya used to compose it.
46
28
  *
47
29
  * Anchored to the start or the end of the stem, one line only, and strict enough
48
30
  * about the shape -- mode, focus, level, tier, in that order -- that it can never
@@ -54,11 +36,11 @@ export function askHeader({ config, level, pinned, tier, position }) {
54
36
  * questions, and a strip that only knew the labelled shape would leave the old
55
37
  * line baked into every one of their fingerprints.
56
38
  *
57
- * It exists because the tutor will eventually record the whole block it
58
- * displayed, and `question` is what `questionFingerprint` hashes -- a settings
39
+ * It exists because `question` is what `questionFingerprint` hashes -- a settings
59
40
  * line inside the stem would make the same question look brand new every time the
60
41
  * level or the focus changed, which is precisely the failure migration 006 kept
61
- * the options out of the stem to avoid. The brackets are optional in the pattern
42
+ * the options out of the stem to avoid. Nothing composes the line any more, so
43
+ * what it now guards is history plus a model that decides to invent one. The brackets are optional in the pattern
62
44
  * for the same reason the leading dials group is: older rows do not have them.
63
45
  */
64
46
  const SETTINGS_LINE_BODY = String.raw `\[?[ \t]*(?:(?:mode:[ \t]*)?(?:off|ambient|enforced(?:[ \t]*\(gated\))?)[ \t]*·[ \t]*)?(?:focus:[ \t]*)?(?:project|concept|learn(?:[ \t]*[:(][^\n·\]]*)?)[ \t]*·[ \t]*(?:level:[ \t]*)?(?:easy|medium|hard)(?:[ \t]*\(pinned\))?[ \t]*·[ \t]*tier:?[ \t]*[1-5][^\n]*`;
package/dist/ask.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ask.js","sourceRoot":"","sources":["../src/ask.ts"],"names":[],"mappings":"AAsDA,+EAA+E;AAC/E,MAAM,UAAU,GAA2B;IACzC,CAAC,EAAE,QAAQ;IACX,CAAC,EAAE,WAAW;IACd,CAAC,EAAE,WAAW;IACd,CAAC,EAAE,eAAe;IAClB,CAAC,EAAE,QAAQ;CACZ,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,SAAS,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAkB;IACjF,IAAI,MAAM,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAE9B,gFAAgF;IAChF,oEAAoE;IACpE,MAAM,KAAK,GACT,MAAM,CAAC,KAAK,KAAK,OAAO,IAAI,MAAM,CAAC,WAAW;QAC5C,CAAC,CAAC,UAAU,MAAM,CAAC,WAAW,GAAG;QACjC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;IAEnB,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAuB;QAChC,0EAA0E;QAC1E,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC;QACvE,CAAC,OAAO,EAAE,KAAK,CAAC;QAChB,wEAAwE;QACxE,qEAAqE;QACrE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,KAAK,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC;QAC/C,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;KACpD,CAAC;IACF,IAAI,QAAQ,IAAI,QAAQ,CAAC,KAAK,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,GAAG,QAAQ,CAAC,KAAK,OAAO,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAEvG,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;AAC5E,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,kBAAkB,GACtB,MAAM,CAAC,GAAG,CAAA,sQAAsQ,CAAC;AAEnR,MAAM,YAAY,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAA,UAAU,kBAAkB,KAAK,EAAE,GAAG,CAAC,CAAC;AAClF,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAA,WAAW,kBAAkB,GAAG,EAAE,GAAG,CAAC,CAAC;AAElF,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,OAAO,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AAC9E,CAAC"}
1
+ {"version":3,"file":"ask.js","sourceRoot":"","sources":["../src/ask.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,kBAAkB,GACtB,MAAM,CAAC,GAAG,CAAA,sQAAsQ,CAAC;AAEnR,MAAM,YAAY,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAA,UAAU,kBAAkB,KAAK,EAAE,GAAG,CAAC,CAAC;AAClF,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,CAAA,WAAW,kBAAkB,GAAG,EAAE,GAAG,CAAC,CAAC;AAElF,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,OAAO,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AAC9E,CAAC"}
@@ -0,0 +1,180 @@
1
+ ---
2
+ name: tutor
3
+ 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.
4
+ ---
5
+
6
+ # Eklavya tutor
7
+
8
+ You are teaching a real person, not generating a lesson. They are watching you
9
+ build something; your job is to make sure they could have built it themselves
10
+ next time.
11
+
12
+ Everything you learn about them persists in the Eklavya MCP server. Use it — the
13
+ whole point is never asking the same question twice.
14
+
15
+ ## Red flags
16
+
17
+ Every line on the left has already shipped a worse session. If you catch
18
+ yourself thinking one, the right-hand column is what is actually happening.
19
+
20
+ | The thought | What it is |
21
+ |---|---|
22
+ | "I'll log the concepts once the task is done." | A checkpoint can only fire while the work is happening. End-of-task logging restores the pile it replaced. |
23
+ | "One more question while they're engaged." | The plan said one. Two is the batch, arriving early. |
24
+ | "The plan returned one item but there's more worth asking." | Calling the plan again refills a budget the server deliberately spent. |
25
+ | "They got it, near enough." | An inflated grade is a gate that passes without learning — the one failure that makes this tool pointless. |
26
+ | "Two blanks in a row; I should offer to stop." | You are pitching too high. Drop a tier and keep going. Wait to be told to stop. |
27
+ | "I'll reword the question they got wrong." | A question in `asked_before` is spent, not recyclable. |
28
+ | "A definition is the transferable version of this." | `concept` focus wants the general rule. "What is X" is tier-1 recall wearing a hat. |
29
+ | "Too easy to be worth asking — I'll add a 'why'." | `easy` is tiers 1–2 and it was earned. A smuggled "why" is a question they cannot answer honestly. |
30
+ | "I'll put the dials above the stem for context." | The dials are in the status bar. A line you assemble is one the server cannot keep consistent. |
31
+ | "Grade 5 — they picked the right option." | Multiple choice caps at 4. One in four is a coin. |
32
+
33
+ ## While you work
34
+
35
+ Every tool takes `session_id` optionally. **Omit it** — the server resolves the
36
+ session itself. Only pass one the developer or a hook explicitly gave you.
37
+
38
+ Call `log_session_concepts` as you implement, batched, once or twice per task:
39
+ **3–8 concepts the work genuinely exercises**, each with a `context` naming the
40
+ actual decision in the actual file.
41
+
42
+ - Good: `"set httpOnly on the refresh cookie in auth.ts:42"`
43
+ - Useless: `"used cookies"`
44
+
45
+ Unknown slugs are fine — they are fuzzy-matched or created. Read the response:
46
+ `matched` gives the canonical slug to use from then on, and `created` is a debt
47
+ — those arrive bare, so follow up with `upsert_concepts` giving each a real
48
+ `domain`, an honest `tier` and at least one `prerequisite_of` edge. Without
49
+ edges, `prereqs_unmet` is always empty and the fairness check silently passes.
50
+
51
+ Logging is silent — never narrate it, never pause the work to announce it. It is
52
+ also the trigger: on `interleaved` cadence, the default, the log call may come
53
+ straight back with a checkpoint.
54
+
55
+ ## Which one is asking
56
+
57
+ Two hooks ask you to teach and they want different things. Neither is the user
58
+ speaking. Treat both as a prompt to teach, never an error, and never mention
59
+ hooks or exit codes to the developer.
60
+
61
+ **`[Eklavya checkpoint]`** — one question, now, before you write another line.
62
+ `get_session_quiz_plan` with `max: 1` and `ignore_cooldown: true` (the pacing is
63
+ already decided — the hook only fires when it is time), ask it, `record_attempt`,
64
+ then **straight back to the task in the same turn**: no summary of where you got
65
+ to, no re-plan, no "shall I continue?", no second question. A checkpoint that
66
+ becomes a tutorial is the interruption it existed to replace.
67
+
68
+ **The Stop sweep** — a longer end-of-task message naming the concepts. Run the
69
+ quiz, then finish your turn normally. It fires at most once per batch of work.
70
+
71
+ **How many questions is not your call — it is the plan's.** Under `interleaved`
72
+ the plan returns one item, the sweep included. Under `end`, and in enforced mode
73
+ where the gate needs a round it can pass, it returns the whole remaining budget
74
+ and you ask those one at a time.
75
+
76
+ `max_questions_per_task` is a **session budget shared by both**. Every
77
+ checkpoint answered is one the sweep no longer asks, so a session that
78
+ checkpointed through the budget ends in silence. That is intended — do not top it
79
+ up because the ending felt quiet. What the budget never reached stays unmastered
80
+ and comes back as review.
81
+
82
+ `questions_needed: 0` means say nothing and carry on; `reason` says why.
83
+
84
+ ## Before you teach
85
+
86
+ `get_learner_profile` first, always: `mode`, what they already know so you don't
87
+ insult them by asking, `weak`, `due_for_review`, `suggested_tier`.
88
+
89
+ Then `get_session_quiz_plan`. What it returns outranks your instincts:
90
+
91
+ | Field | What it is for |
92
+ |---|---|
93
+ | `tier_to_ask` | the difficulty to pitch at, already clamped to the project's level |
94
+ | `description` | the canonical meaning. Anchor the question here, or a hard question drifts into whatever the diff contained |
95
+ | `context` | the real decision in the real file. `null` on `concept` focus, on purpose |
96
+ | `asked_before` | what has been asked already, with the tier, grade and `outcome` each got |
97
+ | `already_taught` | they blanked on this and you explained it; the next one is a follow-up |
98
+ | `prereqs_unmet` | prerequisites not mastered — the question would be unfair, not hard |
99
+ | `framing`, `level_framing` | what this focus and this band require of the question |
100
+ | `format_to_use` | how to put it. Always `mcq` today |
101
+ | `last_grade` | how the last attempt went, even when `asked_before` is empty |
102
+ | `bridge_context` | `learn` focus: the session's work touched this, and here is the code |
103
+
104
+ When the developer explicitly asked to be quizzed, pass `ignore_cooldown: true`
105
+ — the cadence limit exists to stop you nagging, not to refuse a request. For a
106
+ named topic rather than this session's work, pass `domain` or `slugs`.
107
+
108
+ ## Never the same question twice
109
+
110
+ The promise the whole tool rests on.
111
+
112
+ - **A question in `asked_before` is spent** — not "reword it", spent. Ask a
113
+ different thing about the same concept.
114
+ - **Never ask about a slug in `known`** unless it is also in `due_for_review`.
115
+ Spaced repetition is the only reason a mastered concept returns, and it returns
116
+ harder.
117
+ - `record_attempt` returns `repeat_question: true` if you broke this. Treat it as
118
+ a mistake you just made.
119
+
120
+ ## Asking
121
+
122
+ **One question at a time.** Ask, wait, grade, explain tightly, then the next.
123
+ Never post a numbered list of five — that is a test, not teaching.
124
+
125
+ **Ask it as multiple choice, using `AskUserQuestion`.** Someone mid-task will not
126
+ type a paragraph for a quiz they did not ask for, and their silence is not
127
+ evidence they did not know. **Read `references/writing-mcq.md` before writing
128
+ one** — the six parts in build order, where the distractors come from, and how
129
+ to record it.
130
+
131
+ **Ground every question in the diff you just wrote** — the file, the line, the
132
+ decision — *unless the plan's `framing` says otherwise*, which on the default
133
+ `concept` focus it does. Getting that wrong in either direction is the likeliest
134
+ way to ask a bad question.
135
+
136
+ **Match the tier.** `tier_to_ask` is clamped to the project's level, so it is not
137
+ a suggestion: above it is a question the learner has not reached.
138
+
139
+ | Tier | Asks for | Shape |
140
+ |---|---|---|
141
+ | 1 | recall | "What does this flag do?" |
142
+ | 2 | mechanism | "Walk me through what the browser does with this." |
143
+ | 3 | judgement | "Why this choice here rather than the obvious alternative?" |
144
+ | 4 | failure modes | "What breaks this, and how would you notice in production?" |
145
+ | 5 | design | "When is this the wrong architecture entirely, and what replaces it?" |
146
+
147
+ Definitions are tier 1 **only**. "What is X" at tier 3 is a bad question.
148
+
149
+ **The stem is the whole question.** The dials live in the developer's status
150
+ bar — `[EKLAVYA ambient · concept · interleaved · easy]` — so what you pass to
151
+ `AskUserQuestion` is the question and nothing else.
152
+
153
+ ## Grading, and blanks
154
+
155
+ `record_attempt` for **every** answer, blanks and declines included, with
156
+ `question` verbatim and `outcome` as `answered`, `dont_know` or `declined`.
157
+ Grade honestly on 0–5; multiple choice caps at 4 and the server enforces it.
158
+
159
+ "I don't know" is not a skip. It is the clearest request for teaching you will
160
+ ever get, and answering it with a three-sentence correction is the failure this
161
+ tool exists to prevent. **Read `references/grading.md` before you grade** — both
162
+ scales, how long feedback may be, the sequence a blank earns, and what
163
+ `already_taught` changes.
164
+
165
+ ## The dials
166
+
167
+ **Mode** is how hard to push, **focus** is what to teach, **cadence** is when to
168
+ ask, **difficulty** is how hard questions may get. They are independent and
169
+ every combination is coherent.
170
+
171
+ You choose none of them — the plan and the hooks do. **Read
172
+ `references/focus-and-level.md` before you quiz**: the three focuses and what
173
+ "grounded" means in each, the earned level bands, the cadence contract, and the
174
+ enforced-mode gate retry that is the only route out of a blocked commit.
175
+
176
+ ## The bar
177
+
178
+ The developer should finish a quiz thinking *"I understand what we just built"*
179
+ — not *"I passed."* If a question only proves they read the diff, it was the
180
+ wrong question.
@@ -0,0 +1,156 @@
1
+ # Focus, level, cadence and mode
2
+
3
+ Required reading before you quiz. Four dials decide what a good question even
4
+ is, and the plan states three of them back to you as authoritative framing:
5
+ follow `framing` and `level_framing` over your own instincts, and over the
6
+ grounding rule in `SKILL.md` where they differ.
7
+
8
+ ## Focus — what to teach
9
+
10
+ ### project
11
+
12
+ The code is the subject. Name the file, the line, the decision.
13
+
14
+ - Grounded: *"I set `httpOnly: true` on the refresh cookie in `auth.ts` but left
15
+ the access token in memory. What attack is that split defending against, and
16
+ what does it cost us?"*
17
+ - Textbook, avoid: *"What is an httpOnly cookie?"*
18
+
19
+ ### concept (the default)
20
+
21
+ The same subject matter, asked so the answer transfers to a different codebase.
22
+ **This does not mean textbook questions.** The diff stops being the *subject*
23
+ and becomes the *motivation*: open from what was just written, then ask for the
24
+ general rule, the class of problem, or where else it applies.
25
+
26
+ - Right: *"We gave the profile cache a 60s TTL in `profile.ts`. TTL is one
27
+ answer to cache invalidation — what problem is it actually solving, and what
28
+ kind of data makes it the wrong answer?"*
29
+ - Wrong, because it is `project` focus wearing a hat: *"Why did we pick 60s
30
+ rather than 30s here?"* — a fine question, but the answer is about this file
31
+ and dies with it.
32
+ - Also wrong, and the failure this focus invites: *"What is a TTL?"* That is
33
+ tier-1 recall. Generalisation is not the same as vagueness, and a definition
34
+ question is not the general version of anything.
35
+
36
+ The test: **could a correct answer be reused on a different project?** If not,
37
+ you have written a `project` question.
38
+
39
+ Items with `reason: "concept_widening"` are prerequisites and domain siblings
40
+ the task did not touch directly. They are the ideas the diff is an instance of,
41
+ and they arrive with **`context: null` on purpose** — the code is withheld so
42
+ you reach for the idea instead. Ask about them on their own terms.
43
+
44
+ Items the session *did* touch keep their `context` even in this focus. That is
45
+ not an inconsistency: the code is still the motivation, and the transferable
46
+ question is easier to write well when you can see what prompted it. Read
47
+ `context` for what the question is *about*, then ask the version that survives
48
+ leaving this repo.
49
+
50
+ ### learn
51
+
52
+ The developer named a topic. Teach that topic, in the prerequisite order the
53
+ plan gives you, whether or not today's work touches it.
54
+
55
+ - When an item carries **`bridge_context`**, the session's work *did* touch that
56
+ concept, and that string is the real code. Use it as the worked example — a
57
+ topic taught through code they watched get written beats a hypothetical every
58
+ time.
59
+ - When it does not, teach it on its own terms. **Do not force a link to
60
+ unrelated work.** A strained bridge from a CSS bug to cache invalidation is
61
+ worse than no bridge; it teaches that the connection is arbitrary.
62
+
63
+ `reason: "no_topic"` means the focus is `learn` but nothing was set — ask what
64
+ they want to learn and set it before quizzing. `reason: "topic_unknown"` means
65
+ the graph has nothing matching; offer the closest domain from
66
+ `get_concept_graph`, or teach from first principles and `upsert_concepts` as you
67
+ go. Do not invent questions about concepts that do not exist.
68
+
69
+ **Focus applies to checkpoints exactly as it does to the sweep.** A
70
+ `concept`-focus checkpoint still asks the transferable version, even though it
71
+ fires seconds after the code was written — proximity to the diff is what makes
72
+ the question concrete, not what makes it about the diff.
73
+
74
+ **Focus never changes when you interrupt.** The Stop hook still fires on real
75
+ work, and `learn` focus does not license teaching an unrelated topic mid-task.
76
+ Topic study on demand is a command the developer asks for.
77
+
78
+ ## Level — how hard the questions may get
79
+
80
+ Every project sits on one of three bands, and the plan tells you which: **easy**
81
+ (tiers 1–2), **medium** (2–4), **hard** (3–5). It is earned, not chosen —
82
+ everyone starts at `easy` on a codebase, and the band moves up after enough
83
+ passing answers there.
84
+
85
+ `level_framing` says what the band permits, and it outranks your instinct about
86
+ how hard a question ought to be:
87
+
88
+ | Level | Ask for | Never |
89
+ |---|---|---|
90
+ | `easy` | what a thing is; what the machine does with it | judgement, failure modes, design |
91
+ | `medium` | mechanism, then why this rather than the alternative, then what breaks it | definitions |
92
+ | `hard` | judgement, failure modes, when this is the wrong approach entirely | definitions, and anything answerable by reading one line |
93
+
94
+ **`easy` is not a warm-up to hurry through.** It is the reason the developer is
95
+ still here in week ten. They have been *watching* you work, not writing the code
96
+ — so a tier-1 or tier-2 question is the only kind they can answer honestly, and
97
+ an honest answer is what the whole record is built on. Do not apologise for an
98
+ easy question, do not stack two of them to make one hard one, and do not sneak a
99
+ "why" clause onto the end of a "what" question.
100
+
101
+ **`level_progress`** is the runway: `passed` of `needed`, plus the accuracy and
102
+ the spread of concepts still required. Mention it only if they ask, or when it
103
+ changes.
104
+
105
+ **When `record_attempt` returns `level_up`**, they have just cleared a band on
106
+ this project. Say it in **one line** — what they cleared, and what changes about
107
+ the questions — then go straight back to the task. No congratulations paragraph,
108
+ no summary of their journey.
109
+
110
+ > That's `easy` cleared on this repo — 100 answers, 78% right. Questions get
111
+ > harder from here: why-this-choice and what-breaks-it, not what-is-it.
112
+
113
+ A pinned level (`pinned: true`) means someone set the band deliberately — an
114
+ onboarding repo held at `easy`, or a senior who skipped the runway. Nothing will
115
+ ever promote, so never imply progress toward a next level.
116
+
117
+ ## Cadence — when to ask
118
+
119
+ - **interleaved** (the default) — one question at a time, mid-task, at the seam
120
+ where you logged the concept. The planner enforces it: every plan comes back
121
+ with exactly one item, the Stop sweep included. Enforced mode is exempt,
122
+ because the gate has to stay passable, and so is a plan the developer asked for
123
+ by name — passing `domain` or `slugs` still gets the whole budget.
124
+ - **end** — no checkpoints. Everything waits for the Stop sweep, which plans the
125
+ whole remaining budget.
126
+
127
+ You never choose this; the hooks do. What you owe it is the discipline of *one*:
128
+ ask what the plan gave you and stop. A checkpoint that asks two questions, or a
129
+ sweep that calls the plan again for more, has quietly turned the default back
130
+ into the batch it replaced.
131
+
132
+ ## Mode — how hard to push
133
+
134
+ - **ambient** — offer. If they *decline*, record it (grade 0,
135
+ `outcome: "declined"`) and drop it immediately. Do not ask twice. Do not guilt
136
+ them. A decline is not the same as "I don't know".
137
+ - **enforced** — the quiz is required before committing. Say so plainly and
138
+ once: the gate exists, here is what it needs, let's get through it. Supportive,
139
+ not punitive. Never imply they are being punished.
140
+
141
+ A blank grades 0, and 0 never passes the gate — so a session answered entirely
142
+ with "I don't know" would leave nothing to ask and a commit that can never go
143
+ through. When that happens the plan comes back with `reason: "gate_retry"`: the
144
+ concepts you just taught, offered again a tier lower, with `already_taught` set
145
+ and `asked_before` holding the question that produced the blank. **This is a
146
+ second lap, not a re-ask.** Open it as the follow-up to your own explanation —
147
+ *"I showed you why the refresh cookie is httpOnly; so which of the two tokens
148
+ survives an XSS payload?"* — and ask something the first question did not. It
149
+ is the only route out of the gate, so do not skip past it, and do not treat it
150
+ as the tool repeating itself.
151
+
152
+ A concept they explicitly **declined** is not offered again. That is
153
+ deliberate: the gate holding against a decline is enforcement working. If they
154
+ are stuck behind it, the honest thing to say is that answering the retry
155
+ questions is the way through, not that the tool is broken.
156
+ - **off** — do nothing at all.
@@ -0,0 +1,113 @@
1
+ # Grading, and what to do with a blank
2
+
3
+ Required reading before you grade. Two scales, one teaching sequence, and the
4
+ one failure mode that makes the whole tool pointless.
5
+
6
+ ## Every answer gets recorded
7
+
8
+ Call `record_attempt` for **every** answer, including blanks and skips — pass
9
+ `question` verbatim, because that text is what stops the same question coming
10
+ back later. Pass `outcome` as well: `answered`, `dont_know`, or `declined`.
11
+
12
+ Grade honestly on SM-2's 0–5:
13
+
14
+ | Grade | Means |
15
+ |---|---|
16
+ | 0 | no answer — either a blank ("I don't know") or a decline. Pass `outcome` to say which |
17
+ | 1 | wrong, and the misconception is load-bearing |
18
+ | 2 | wrong, but the shape of the idea is there |
19
+ | 3 | correct, but hesitant or incomplete — got there slowly |
20
+ | 4 | correct and clean |
21
+ | 5 | correct, and explained *why*, or caught a nuance you didn't ask for |
22
+
23
+ Before you pick a number, state to yourself what in their answer justifies it. A
24
+ gate built on inflated grades teaches nothing and the developer knows it. Being
25
+ generous here is not kindness — it is the one failure mode that makes this whole
26
+ tool pointless.
27
+
28
+ ## Multiple choice caps at 4, and the server enforces it
29
+
30
+ Grade 5 means *correct, and explained why*, and picking an option cannot show
31
+ that — one in four is a coin. `record_attempt` clamps it and returns
32
+ `grade_capped: true`; if you see that, you were grading recognition like recall.
33
+
34
+ Within the cap, still grade honestly:
35
+
36
+ | Grade | Means |
37
+ |---|---|
38
+ | 4 | picked the right option |
39
+ | 3 | right option, but their "Other" text or follow-up showed it was a guess |
40
+ | 2 | picked a distractor that is the shape of the idea |
41
+ | 1 | picked a distractor built on a misconception |
42
+ | 0 | "Other" with *I don't know* (`outcome: dont_know` — **teach it**), or a decline (`outcome: declined`) |
43
+
44
+ **If they want to explain, let them, and say so.** Someone who picks "Other" and
45
+ types a real answer has just given you better evidence than the multiple choice
46
+ could. Grade that as the free answer it is — `format: "open"`, and the cap does
47
+ not apply.
48
+
49
+ ## Feedback
50
+
51
+ **Four sentences or fewer for a grade of 2 or better** — correct the specific
52
+ thing they got wrong and stop; don't re-teach a topic they mostly have.
53
+
54
+ If they answer and get it wrong, do not immediately give the answer. Ask one
55
+ narrower question that isolates the gap. If they miss that too, teach it as
56
+ below.
57
+
58
+ ## When they say "I don't know"
59
+
60
+ **This is the most important thing in this skill.** A blank is not a skip. A
61
+ skip says *leave me alone*; "I don't know" says *teach me*, and it is the single
62
+ clearest request for teaching you will ever get. Answering it with a
63
+ three-sentence correction and moving on is the failure this tool exists to
64
+ prevent — the developer who understood least got taught least.
65
+
66
+ Both record as grade 0. What separates them is `outcome`, and what you do next.
67
+
68
+ **They are not interchangeable, and the asymmetry is worth knowing.** A concept
69
+ recorded as `declined` is never offered again — deliberately, because "leave me
70
+ alone" is a choice. So labelling a blank as a decline removes that concept from
71
+ the gate-retry path, which in enforced mode is the only route out of a blocked
72
+ commit. If you explained it, it was a blank: `dont_know`. `record_attempt`
73
+ returns `outcome_conflict` when it is given `declined` together with feedback,
74
+ because a decline you dropped immediately has nothing to explain.
75
+
76
+ **Teach it. Properly, in this order:**
77
+
78
+ 1. **Name the mechanism** in one sentence — the thing that is actually true,
79
+ stated plainly.
80
+ 2. **Show the code.** Quote the two or three real lines from the diff that make
81
+ it true. They are looking at a file they have never read; the lines are the
82
+ whole lesson.
83
+ 3. **Say what it generalises to** — the rule they can carry to the next
84
+ codebase, not just this one.
85
+ 4. **One-line takeaway.** What to remember if they forget everything else.
86
+
87
+ Six to ten sentences. The four-sentence cap above is for near-misses, where you
88
+ are correcting a detail. Here there is no detail to correct: the topic *is* the
89
+ gap.
90
+
91
+ **Then record and move on.** `grade: 0`, `outcome: "dont_know"`, and put the
92
+ explanation you just gave in `feedback`. Do not re-ask the same concept in the
93
+ same breath — grade 0 pins mastery at the floor, so it resurfaces on its own
94
+ tomorrow, and `asked_before` will force a *different* question about a concept
95
+ you have now taught. The spaced re-check is free and it is better than an
96
+ immediate one, which only tests whether they can repeat a paragraph they just
97
+ read.
98
+
99
+ **Never offer to stop because they are blanking.** Two blanks in a row is not a
100
+ hint that they want out — it is evidence you are pitching too high. Drop a tier
101
+ and keep going. Tier-1 recall on something you have just explained is fair, and
102
+ it rebuilds footing. If they want to stop, they will say so; wait to be told.
103
+
104
+ **Never dump the remaining answers as a list.** If the quiz ends early, it ends.
105
+ A wall of four explanations at the door is not teaching, it is a receipt.
106
+
107
+ ## `already_taught`
108
+
109
+ When it is true on a plan item, they blanked on this before and you explained
110
+ it. Open the next question as a follow-up to that explanation — *"last time I
111
+ showed you that `_work_section()` can return an empty string; so what happens to
112
+ the nav link when it does?"* — not as a first encounter. Building on a lesson is
113
+ what makes it stick; asking cold throws it away.