CLAUDE.md@skills · git:20260906.001ea72 · 2026-09-06 · sha256 91b3d479fb15fbba
CLAUDE.md@skills git:20260906.001ea72A
Immutable. This exact content is served forever at /api/v1/blob/91b3d479fb15fbba.
# Editing a skill `skills/` is where Eklavya's behaviour actually lives. The MCP server holds the data and the arithmetic; these files hold everything the model does with it. A wrong sentence here is a wrong product, and nothing fails loudly when it is. ## What a skill is, mechanically One directory, one `SKILL.md`, YAML frontmatter with `name` and `description`. **`disable-model-invocation: true` is what turns a skill into a `/eklavya:<name>` slash command** — the model can no longer load it on its own, and the developer types it. Without that line the skill is model-invocable only. Seven have it, and they are the seven slash commands: `gate`, `learn`, `level`, `mode`, `progress`, `quiz`, `setup`. `skills/tutor/SKILL.md` deliberately does not. It is the pedagogy — one question at a time, honest grading, never the same question twice — and every other skill defers to it rather than restating it. `quiz` says "follow the `tutor` skill for how to ask and grade"; keep it that way. Pedagogy duplicated into a command skill is pedagogy that drifts from the one place Cursor reads. ## The description is a trigger, not a summary For a model-invocable skill the `description` is the only part always in context; the body is read only once the model has decided to load it. So a description that summarises the workflow becomes a shortcut the model takes *instead of* reading the body — it answers from the summary and never opens the file. The body becomes documentation nobody reads. `skills/tutor/SKILL.md` had exactly that shape. It read "Use while implementing any non-trivial task (to log the concepts it touches), and whenever quizzing, grading, or explaining" — three steps named, and nowhere in it the words *one question, mid-task*. Which is the failure the interleaved cadence had to fix: questions arriving in a pile at the end of the work. So, for `tutor` and anything else without `disable-model-invocation`: **triggering conditions only**, third person, opening "Use when". Never the number of questions, never the order of the tool calls, never the grading. Those live in the body, which is where the model has to go to get them. The seven slash commands are exempt, and it is not a technicality: `disable-model-invocation: true` means the model never matches on their description at all. The developer types the command and the description is its one line of help, so those should say what they do. `agents/tutor.md` keeps one identity clause for the same kind of reason — a subagent is picked from a roster by what it is, not loaded by trigger. ## Three directories, and why they are not one - **`skills/`** ships inside the plugin. Slash commands plus `tutor`. - **`user-skill/eklavya/`** is copied to `~/.claude/skills/eklavya/` by `npx eklavya install` (`installSkill()` in `mcp/src/install.ts`). It is model-invocable, so plain chat — "Eklavya is quizzing me too much" — reaches it, and it works where the plugin is not loaded. **Never move it under `skills/`**: it would then register twice, once per surface. It drives the `eklavya` CLI and the read/write config tools; it does not teach or quiz. - **`agents/tutor.md`** is the subagent. It has the Eklavya MCP tools and read-only file access — and **no `AskUserQuestion`** — so it renders the four options as lettered text. Any change to the *Multiple choice* section of `skills/tutor/SKILL.md` has to hold for a plain-text renderer too. ## A skill is a prompt, but it is also an API client Nine tools, all in `mcp/src/tools/`: `get_learner_profile`, `log_session_concepts`, `get_session_quiz_plan`, `record_attempt`, `get_gate_status`, `upsert_concepts`, `get_concept_graph`, `get_config`, `set_config`. **Before you write "call `X` with `Y`", open `mcp/src/tools/<X>.ts`.** Check `Y` is in the `inputSchema`, and check the field you are telling the model to read is in what the handler returns. A skill that names a field the server never returns fails silently — the model improvises a plausible value and the developer sees a confident number nobody computed. `config_tools.ts` holds `get_config` and `set_config`; the rest are one file per tool name. ### Not every tool takes `session_id` `log_session_concepts`, `upsert_concepts`, `get_session_quiz_plan`, `record_attempt` and `get_gate_status` take it, optionally. `get_learner_profile`, `get_concept_graph`, `get_config` and `set_config` have no such argument at all. **Omit it everywhere.** The server resolves the current session itself, which is what lets the subagent's answers count toward the same gate. Only pass one if a hook handed you an id. ## The facts that have drifted before Check every one of these against the code when you touch a skill. - **`focus` defaults to `concept`, not `project`** (`DEFAULT_CONFIG` in `mcp/src/config.ts`). This was wrong in three skills at once. Grep before you write it: `grep -rn 'focus' skills/ user-skill/ agents/`. - **The `interleaved` one-question cap has exemptions.** In `mcp/src/tools/get_session_quiz_plan.ts`: `capped = cadence === 'interleaved' && mode !== 'enforced' && !explicitTopic`, and then `max = args.max ?? (capped ? 1 : max_questions_per_task)`. So `enforced` mode is exempt, an explicit `domain` or `slugs` is exempt, and an explicit `max` wins outright because it is read first. Read that code rather than trusting prose about it — including this paragraph. - **The Stop hook blocks in `ambient` too.** `mcp/src/hooks/stop-quiz-check.ts` blocks in both modes; what `enforced` changes is that it skips the `min_minutes_between_quizzes` cooldown, takes the whole remaining budget instead of one question, and gates commits. Do not write "ambient never interrupts". - **`get_learner_profile`'s lists are capped, and `known` is ordered by score.** `LIST_CAP = 8` covers `weak`, `due_for_review`, `projects`, `recent_concepts` and `skipped`; `KNOWN_CAP = 30` covers `known`, with the real count in `known_total`. `known` is sorted strongest-first, **not** by date. `get_concept_graph` caps at 200 nodes and sets `truncated`. - **The seed catalogue holds 87 concepts today** — 33 `web-auth`, 19 `git`, 18 `react`, 17 `node-backend`, in `mcp/src/seed/*.json`. If you state that number in a skill, recount it first; a seed file gains concepts and the sentence does not. ## The dials are in the status bar, not above the stem Until 1.14 every plan item carried `ask_header` and the tutor printed it above the question: `[mode: ambient · focus: concept · level: easy · tier: 2 mechanism]`. It existed for a real reason — on `concept` focus a deliberately transferable question reads as a vague one, and on `easy` a tier-2 question reads as shallow rather than as a runway — but it spent four settings' worth of screen above *every* stem to say something that is true for the whole session. Ambient state belongs somewhere ambient. `statusLine` in `mcp/src/statusline.ts` composes `[EKLAVYA ambient · concept · interleaved · easy]` for `eklavya statusline`, which the host's status bar runs. `askHeader` is deleted, the plan no longer carries `ask_header`, and both hooks now say *ask the stem on its own*. Two consequences for anyone editing a skill: - **Never tell the model to compose a settings line.** Not the dials, not the tier, not `question: 2 of 3`. A line a skill assembles is a line the server cannot keep consistent, which is the reason this was centralised in the first place. - **`stripAskHeader` stays, and must.** Every attempt recorded while the line existed still has it inside the stem, and `questionFingerprint` (`store.ts`) hashes that text. Delete the stripper and the entire back catalogue changes fingerprint at once, so *never the same question twice* breaks for every question ever asked. It is now a guard for history, plus a model that invents a line anyway. The tier is deliberately nowhere on screen. A status bar refreshes on the host's cadence, so a tier there would sometimes name the previous question's difficulty, and a stale readout is worse than none. `level` covers what the tier was explaining: `easy` already means tiers 1-2. ## `skills/tutor/SKILL.md` has two readers `mcp/scripts/copy-assets.mjs` bundles it to `mcp/dist/assets/tutor-skill.md`, and `eklavya export-rules` (`mcp/src/cli.ts`) strips the frontmatter and wraps it as a Cursor rules file. So that file is consumed by two editors. Consequence: **no Claude-Code-only instructions in its body.** Slash-command names, plugin paths and hook mechanics belong in the command skills, not in the pedagogy. `AskUserQuestion` is the one unavoidable exception, and `agents/tutor.md` already carries the fallback for renderers that lack it. ## Consistency The same behaviour described in two skills has drifted apart before — that is how `focus: project` got into three files. The four dials appear in `skills/mode/SKILL.md` and `user-skill/eklavya/SKILL.md`; the level bands appear in `skills/level/SKILL.md` and the *Level* section of `tutor`; the cadence cap appears in `mode`, `quiz` and `tutor`. **When you change one, grep the others for the same claim.** Where any of them disagrees with the code, `mcp/src/config.ts` and the tool file are right and the skill is wrong.