eklavya 1.7.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 (95) hide show
  1. package/README.md +69 -0
  2. package/dist/ask.js +58 -0
  3. package/dist/ask.js.map +1 -0
  4. package/dist/assets/dashboard.html +407 -0
  5. package/dist/assets/tokens.css +135 -0
  6. package/dist/assets/tutor-skill.md +345 -0
  7. package/dist/cli.js +223 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/concurrency.js +41 -0
  10. package/dist/concurrency.js.map +1 -0
  11. package/dist/config.js +158 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/dashboard.js +214 -0
  14. package/dist/dashboard.js.map +1 -0
  15. package/dist/db.js +26 -0
  16. package/dist/db.js.map +1 -0
  17. package/dist/hooks/checkpoint-quiz.js +152 -0
  18. package/dist/hooks/checkpoint-quiz.js.map +1 -0
  19. package/dist/hooks/lib.js +177 -0
  20. package/dist/hooks/lib.js.map +1 -0
  21. package/dist/hooks/pre-tool-gate.js +53 -0
  22. package/dist/hooks/pre-tool-gate.js.map +1 -0
  23. package/dist/hooks/session-start.js +128 -0
  24. package/dist/hooks/session-start.js.map +1 -0
  25. package/dist/hooks/stop-quiz-check.js +147 -0
  26. package/dist/hooks/stop-quiz-check.js.map +1 -0
  27. package/dist/install.js +313 -0
  28. package/dist/install.js.map +1 -0
  29. package/dist/mcq.js +34 -0
  30. package/dist/mcq.js.map +1 -0
  31. package/dist/migrate.js +45 -0
  32. package/dist/migrate.js.map +1 -0
  33. package/dist/migrations/001_init.sql +71 -0
  34. package/dist/migrations/002_stop_markers.sql +14 -0
  35. package/dist/migrations/003_gate_repo.sql +5 -0
  36. package/dist/migrations/004_attempt_outcome.sql +15 -0
  37. package/dist/migrations/005_session_concept_origin.sql +24 -0
  38. package/dist/migrations/006_attempt_format.sql +31 -0
  39. package/dist/migrations/007_checkpoints.sql +23 -0
  40. package/dist/migrations/008_difficulty_levels.sql +57 -0
  41. package/dist/paths.js +25 -0
  42. package/dist/paths.js.map +1 -0
  43. package/dist/plugin/.claude-plugin/marketplace.json +14 -0
  44. package/dist/plugin/.claude-plugin/plugin.json +17 -0
  45. package/dist/plugin/.mcp.json +11 -0
  46. package/dist/plugin/agents/tutor.md +63 -0
  47. package/dist/plugin/cli/eklavya-gate +67 -0
  48. package/dist/plugin/hooks/hooks.json +57 -0
  49. package/dist/plugin/hooks/run.mjs +169 -0
  50. package/dist/plugin/scripts/install-git-hook.sh +81 -0
  51. package/dist/plugin/skills/gate/SKILL.md +17 -0
  52. package/dist/plugin/skills/learn/SKILL.md +28 -0
  53. package/dist/plugin/skills/level/SKILL.md +53 -0
  54. package/dist/plugin/skills/mode/SKILL.md +56 -0
  55. package/dist/plugin/skills/progress/SKILL.md +72 -0
  56. package/dist/plugin/skills/quiz/SKILL.md +31 -0
  57. package/dist/plugin/skills/setup/SKILL.md +49 -0
  58. package/dist/plugin/skills/tutor/SKILL.md +345 -0
  59. package/dist/seed/git.json +49 -0
  60. package/dist/seed/node-backend.json +43 -0
  61. package/dist/seed/react.json +46 -0
  62. package/dist/seed/web-auth.json +78 -0
  63. package/dist/seed.js +112 -0
  64. package/dist/seed.js.map +1 -0
  65. package/dist/server.js +52 -0
  66. package/dist/server.js.map +1 -0
  67. package/dist/session.js +24 -0
  68. package/dist/session.js.map +1 -0
  69. package/dist/slug.js +75 -0
  70. package/dist/slug.js.map +1 -0
  71. package/dist/srs.js +206 -0
  72. package/dist/srs.js.map +1 -0
  73. package/dist/store.js +450 -0
  74. package/dist/store.js.map +1 -0
  75. package/dist/tools/config_tools.js +119 -0
  76. package/dist/tools/config_tools.js.map +1 -0
  77. package/dist/tools/get_concept_graph.js +104 -0
  78. package/dist/tools/get_concept_graph.js.map +1 -0
  79. package/dist/tools/get_gate_status.js +20 -0
  80. package/dist/tools/get_gate_status.js.map +1 -0
  81. package/dist/tools/get_learner_profile.js +142 -0
  82. package/dist/tools/get_learner_profile.js.map +1 -0
  83. package/dist/tools/get_session_quiz_plan.js +393 -0
  84. package/dist/tools/get_session_quiz_plan.js.map +1 -0
  85. package/dist/tools/index.js +45 -0
  86. package/dist/tools/index.js.map +1 -0
  87. package/dist/tools/log_session_concepts.js +103 -0
  88. package/dist/tools/log_session_concepts.js.map +1 -0
  89. package/dist/tools/record_attempt.js +158 -0
  90. package/dist/tools/record_attempt.js.map +1 -0
  91. package/dist/tools/types.js +4 -0
  92. package/dist/tools/types.js.map +1 -0
  93. package/dist/tools/upsert_concepts.js +89 -0
  94. package/dist/tools/upsert_concepts.js.map +1 -0
  95. package/package.json +66 -0
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The one entry point every Eklavya hook and the MCP server go through.
4
+ *
5
+ * Why this file exists at all: hook commands run under `sh -c` on macOS and
6
+ * Linux, but on Windows under Git Bash, or PowerShell, or — if WSL is installed
7
+ * — WSL's bash, which cannot see Windows paths. Shell hooks are a documented
8
+ * minefield there (claude-code#18610, #21847, #23556, #73971). The hooks
9
+ * reference names the one portable form: `node` plus a script path, because
10
+ * `node.exe` is a real executable and needs no shell. So hooks.json says
11
+ *
12
+ * "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "<name>"]
13
+ *
14
+ * and every platform-specific decision lives here instead of in four shell
15
+ * scripts.
16
+ *
17
+ * The second job is finding the runtime. The plugin payload is source and
18
+ * manifests only — no `dist/`, no `node_modules/`, because a git-installed
19
+ * plugin has neither. The compiled server, the hooks and their one native
20
+ * dependency live in the runtime directory that `eklavya install` populates.
21
+ * Resolution order, most specific first:
22
+ *
23
+ * 1. EKLAVYA_RUNTIME — tests, and anyone pinning a build
24
+ * 2. the plugin's own mcp/dist — a development checkout, so edits take effect
25
+ * 3. ~/.eklavya/runtime — what `npx eklavya install` writes
26
+ * 4. npx eklavya@<pinned> — the plugin was installed via /plugin and the
27
+ * runtime is not there yet
28
+ *
29
+ * Rule 4 is what keeps the two install routes interchangeable: `/plugin install`
30
+ * alone gets you a working MCP server immediately, and the first session heals
31
+ * the rest in the background.
32
+ *
33
+ * Hard rule (PRD §9.1): a hook must never break a session. Everything here
34
+ * fails to exit 0 in silence.
35
+ */
36
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
37
+ import { homedir } from 'node:os';
38
+ import path from 'node:path';
39
+ import { fileURLToPath, pathToFileURL } from 'node:url';
40
+ import { spawn } from 'node:child_process';
41
+
42
+ const name = process.argv[2];
43
+ // Not `import.meta.dirname`: it is undefined before Node 20.11, and this file
44
+ // has to survive being run by an old Node long enough to say so.
45
+ const hereDir = path.dirname(fileURLToPath(import.meta.url));
46
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ?? path.dirname(hereDir);
47
+ const runtimeHome = process.env.EKLAVYA_RUNTIME ?? path.join(homedir(), '.eklavya', 'runtime');
48
+
49
+ /** Node 22 is where better-sqlite3 ships a prebuilt binary for every platform
50
+ * Eklavya supports. Below it, `npm install` falls through to node-gyp and needs
51
+ * a C++ toolchain — which is exactly the Windows install failure we removed. */
52
+ const MIN_NODE_MAJOR = 22;
53
+
54
+ function pinnedVersion() {
55
+ try {
56
+ const manifest = path.join(pluginRoot, '.claude-plugin', 'plugin.json');
57
+ return JSON.parse(readFileSync(manifest, 'utf8')).version ?? null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /** The compiled entry point for `name`, or null if no build is reachable. */
64
+ function resolveEntry() {
65
+ const relative = name === 'server' ? ['dist', 'server.js'] : ['dist', 'hooks', `${name}.js`];
66
+
67
+ const candidates = [
68
+ process.env.EKLAVYA_RUNTIME ? path.join(process.env.EKLAVYA_RUNTIME, ...relative) : null,
69
+ // A development checkout: the plugin root is the repo, so mcp/dist is the
70
+ // build under edit. Requires node_modules too, or better-sqlite3 is missing.
71
+ existsSync(path.join(pluginRoot, 'mcp', 'node_modules', 'better-sqlite3'))
72
+ ? path.join(pluginRoot, 'mcp', ...relative)
73
+ : null,
74
+ path.join(runtimeHome, 'node_modules', 'eklavya', ...relative),
75
+ ].filter(Boolean);
76
+
77
+ return candidates.find((file) => existsSync(file)) ?? null;
78
+ }
79
+
80
+ /**
81
+ * Installs the runtime in the background, at most once.
82
+ *
83
+ * This is the self-heal for the `/plugin install` route, which puts the plugin
84
+ * on disk but cannot run npm for us. Detached and fully ignored: the session
85
+ * that triggers it is never blocked, never waits, and never sees the output.
86
+ */
87
+ function healInBackground() {
88
+ const version = pinnedVersion();
89
+ if (!version) return;
90
+
91
+ // Claim the attempt BEFORE spawning. Four hooks fire per session and the
92
+ // check alone would let every one of them start its own npm — a spawn storm
93
+ // on the machine of the person whose install is already struggling. An hour
94
+ // makes the claim self-expiring, so a heal that dies still gets retried
95
+ // tomorrow instead of wedging the runtime as missing forever.
96
+ const stamp = path.join(runtimeHome, '.installing');
97
+ try {
98
+ const age = Date.now() - statSync(stamp).mtimeMs;
99
+ if (age < 60 * 60 * 1000) return;
100
+ } catch {
101
+ /* no stamp: this is the first attempt */
102
+ }
103
+
104
+ try {
105
+ mkdirSync(runtimeHome, { recursive: true });
106
+ writeFileSync(stamp, new Date().toISOString());
107
+ } catch {
108
+ // Cannot even write the stamp, so we cannot bound the retries. Doing
109
+ // nothing is the safe failure: the explicit installer still works.
110
+ return;
111
+ }
112
+
113
+ try {
114
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
115
+ const child = spawn(
116
+ npm,
117
+ ['install', `eklavya@${version}`, '--prefix', runtimeHome, '--omit=dev', '--no-audit', '--no-fund'],
118
+ { detached: true, stdio: 'ignore', shell: process.platform === 'win32' },
119
+ );
120
+ child.unref();
121
+ } catch {
122
+ /* A failed heal is a slow install, not a broken session. */
123
+ }
124
+ }
125
+
126
+ async function main() {
127
+ if (!name) process.exit(0);
128
+
129
+ const major = Number(process.versions.node.split('.')[0]);
130
+ if (major < MIN_NODE_MAJOR) {
131
+ // Only the server is worth complaining about — a hook that says this on
132
+ // every tool call is noise. The installer already refuses loudly.
133
+ if (name === 'server') {
134
+ process.stderr.write(
135
+ `[eklavya] Node ${MIN_NODE_MAJOR}+ is required (found ${process.versions.node}).\n`,
136
+ );
137
+ }
138
+ process.exit(0);
139
+ }
140
+
141
+ const entry = resolveEntry();
142
+
143
+ if (entry) {
144
+ await import(pathToFileURL(entry).href);
145
+ return;
146
+ }
147
+
148
+ if (name === 'server') {
149
+ // No runtime yet: fetch it on demand so `/plugin install` alone gives a
150
+ // working server. stdout is the MCP transport, so npx's chatter must not
151
+ // land there.
152
+ const version = pinnedVersion();
153
+ if (!version) process.exit(0);
154
+ const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
155
+ const child = spawn(npx, ['--yes', `eklavya@${version}`, 'serve'], {
156
+ stdio: ['inherit', 'inherit', 'ignore'],
157
+ shell: process.platform === 'win32',
158
+ });
159
+ child.on('exit', (code) => process.exit(code ?? 0));
160
+ return;
161
+ }
162
+
163
+ // A hook with no runtime has nothing to say. Start the heal and get out of
164
+ // the way — the next session will have it.
165
+ healInBackground();
166
+ process.exit(0);
167
+ }
168
+
169
+ main().catch(() => process.exit(0));
@@ -0,0 +1,81 @@
1
+ #!/bin/sh
2
+ # Installs (or removes) the git pre-commit hook that enforces Eklavya's gate
3
+ # outside Claude Code. Chains to any pre-commit hook already present rather than
4
+ # replacing it.
5
+ #
6
+ # scripts/install-git-hook.sh [--uninstall] [repo-path]
7
+
8
+ set -eu
9
+
10
+ MARKER="# >>> eklavya gate >>>"
11
+ UNINSTALL=0
12
+ REPO_ARG=""
13
+
14
+ for arg in "$@"; do
15
+ case "$arg" in
16
+ --uninstall) UNINSTALL=1 ;;
17
+ -h|--help) sed -n '2,8p' "$0"; exit 0 ;;
18
+ *) REPO_ARG=$arg ;;
19
+ esac
20
+ done
21
+
22
+ CLI_PATH=$(cd "$(dirname "$0")/../cli" && pwd)/eklavya-gate
23
+ REPO=${REPO_ARG:-$(git rev-parse --show-toplevel 2>/dev/null || true)}
24
+
25
+ if [ -z "$REPO" ]; then
26
+ printf 'Not inside a git repository, and no path given.\n' >&2
27
+ exit 1
28
+ fi
29
+
30
+ HOOK_DIR="$REPO/.git/hooks"
31
+ HOOK="$HOOK_DIR/pre-commit"
32
+ CHAINED="$HOOK_DIR/pre-commit.local"
33
+
34
+ if [ "$UNINSTALL" -eq 1 ]; then
35
+ if [ -f "$HOOK" ] && grep -q "$MARKER" "$HOOK" 2>/dev/null; then
36
+ rm -f "$HOOK"
37
+ if [ -f "$CHAINED" ]; then
38
+ mv "$CHAINED" "$HOOK"
39
+ printf 'Removed the Eklavya gate and restored your previous pre-commit hook.\n'
40
+ else
41
+ printf 'Removed the Eklavya gate.\n'
42
+ fi
43
+ else
44
+ printf 'No Eklavya gate installed here.\n'
45
+ fi
46
+ exit 0
47
+ fi
48
+
49
+ mkdir -p "$HOOK_DIR"
50
+
51
+ if [ -f "$HOOK" ] && grep -q "$MARKER" "$HOOK" 2>/dev/null; then
52
+ printf 'Eklavya gate already installed in %s\n' "$HOOK"
53
+ exit 0
54
+ fi
55
+
56
+ # Preserve whatever was there. The existing hook keeps running, first.
57
+ if [ -f "$HOOK" ]; then
58
+ if [ -f "$CHAINED" ]; then
59
+ printf 'Refusing to overwrite: both %s and %s already exist.\n' "$HOOK" "$CHAINED" >&2
60
+ exit 1
61
+ fi
62
+ mv "$HOOK" "$CHAINED"
63
+ printf 'Moved your existing pre-commit hook to %s; it will still run first.\n' "$CHAINED"
64
+ fi
65
+
66
+ cat > "$HOOK" <<EOF
67
+ #!/bin/sh
68
+ $MARKER
69
+ # Installed by Eklavya. Remove with scripts/install-git-hook.sh --uninstall
70
+
71
+ if [ -x "\$(dirname "\$0")/pre-commit.local" ]; then
72
+ "\$(dirname "\$0")/pre-commit.local" "\$@" || exit \$?
73
+ fi
74
+
75
+ exec "$CLI_PATH"
76
+ # <<< eklavya gate <<<
77
+ EOF
78
+
79
+ chmod +x "$HOOK"
80
+ printf 'Installed the Eklavya commit gate in %s\n' "$HOOK"
81
+ printf 'It only acts on repositories whose .eklavya.json sets "mode": "enforced".\n'
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: gate
3
+ description: Show the Eklavya commit-gate status for this session.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # /eklavya:gate
8
+
9
+ Call `get_gate_status` and report it in a few lines:
10
+
11
+ - Mode. If it is not `enforced`, say that nothing is gated and stop.
12
+ - `answered` of `required` concepts, and the `pass_threshold` they need to clear.
13
+ - Passed or not.
14
+
15
+ If it has not passed, say exactly what remains and offer to run the quiz now. Do not run it without being asked.
16
+
17
+ If it has passed, say so in one line. Don't celebrate.
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: learn
3
+ description: Run a structured Eklavya teaching session on a topic, ordered by prerequisites and calibrated to what the developer already knows.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # /eklavya:learn <topic>
8
+
9
+ Teach `$ARGUMENTS` properly. This is a lesson, not a quiz — but it is still a conversation, and it still records what was learned.
10
+
11
+ **First, offer to make it stick.** A one-off lesson ends when the conversation does. If they want this topic to keep coming back as they work, `set_config` with `focus: "learn"` and `focus_topic: "$ARGUMENTS"` — from then on quizzes plan from this topic, and use their real code as the example wherever the work touches it. Ask once, in one line; if they decline or say nothing, just teach the lesson. Do not set it silently: it changes what every later session asks about.
12
+
13
+ 1. `get_learner_profile` for the topic's domain. Note `suggested_tier`, and note `known` — those concepts are already theirs. Reference them; do not teach them.
14
+ 2. `get_concept_graph` with `include_mastery: true` and `unmastered_only: true`. The order returned **is** the teaching order: prerequisites come before what depends on them.
15
+ 3. If the topic isn't a known domain, ask which of the existing domains is closest, or teach from first principles and `upsert_concepts` as you go.
16
+
17
+ Then, for each concept in order, and **at most 5 per session**:
18
+
19
+ - Start from what they already know — the profile tells you what that is. Never re-explain a mastered prerequisite; reference it in a clause and move on.
20
+ - Explain in a short paragraph, then show it in code from *this repo* if it appears here. Concrete beats general.
21
+ - Ask one question to check it landed, and grade it with `record_attempt`. For the tier and — crucially — for what has already been asked, call `get_session_quiz_plan` with `slugs: [...]` for the concepts in this lesson and `ignore_cooldown: true`. Its `tier_to_ask` beats `suggested_tier`, and its `asked_before` is what keeps a lesson from re-asking a question from a past one.
22
+ - If they miss it, that concept is not done. Come at it from a different angle before moving on.
23
+
24
+ Stop when you hit five concepts or they've had enough. Close with what to review next and when — `next_review` from the recorded attempts tells you.
25
+
26
+ If they set `focus: "learn"` at the top, say so in the closing line, and mention `/eklavya:mode` as the way back out. A standing setting they forgot they made is a setting that feels like a bug later.
27
+
28
+ Never lecture for more than two exchanges without asking something.
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: level
3
+ description: Show or pin Eklavya's difficulty level for this project — the easy/medium/hard band the questions are drawn from, and how far into it the developer is.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # /eklavya:level [auto|easy|medium|hard]
8
+
9
+ Difficulty is a band, held **per project** and normally earned rather than chosen.
10
+
11
+ | Level | Tier band | The questions it asks |
12
+ |---|---|---|
13
+ | `easy` | 1–2 | what a thing is, and what the machine does with it |
14
+ | `medium` | 2–4 | why this choice here, and what breaks it |
15
+ | `hard` | 3–5 | judgement, failure modes, when the architecture is wrong |
16
+
17
+ Everyone starts a codebase at `easy`. That is the point: the developer has been *watching* the agent work, and tiers 1–2 are the questions that state can answer honestly. The band moves up on evidence — passing answers, at a real accuracy, spread across enough concepts — so week ten does not feel like week one.
18
+
19
+ It is per project because "how hard should this be" is a question about a codebase, not about a person. The same developer is a different learner on their fourth Node service and their first Rust one.
20
+
21
+ ## No arguments
22
+
23
+ 1. `get_learner_profile` — the `level` block has everything: the band, the next one, `passed` of `needed`, the accuracy so far and the concept spread.
24
+ 2. `get_config` if you need to say whether it is pinned, and by which file.
25
+
26
+ Report it in three lines or fewer:
27
+
28
+ - **Where they are** — the level, and one clause on what that band asks for.
29
+ - **The runway** — `passed`/`needed` passing answers, plus whichever other condition is still short (accuracy, or distinct concepts). Say the number; a progress bar nobody can total is not progress.
30
+ - **What is next** — the band above, and what changes about the questions when they reach it. If they are on `hard`, say that this is the top and the tier ladder keeps working inside it.
31
+
32
+ If `pinned` is set, say who pinned it (global config or this repo's `.eklavya.json`) and that nothing will promote while it stands. Do not report a runway toward a level a pin makes unreachable.
33
+
34
+ ## With an argument
35
+
36
+ `set_config` with `difficulty`.
37
+
38
+ - **`auto`** — back to earning it. The project's own record is still there; it resumes where its evidence left it.
39
+ - **`easy` / `medium` / `hard`** — pins the band and stops progression. A hard set, not a floor.
40
+
41
+ Scope, and ask when it is ambiguous:
42
+
43
+ - **global** (default) — their own setting on every project. `hard` here is a senior saying they do not want the runway.
44
+ - **repo** — `.eklavya.json` at the root, and it beats global for **everyone** working in that project. `easy` here is an onboarding codebase that stays gentle whoever opens it.
45
+
46
+ Two things to say plainly before writing:
47
+
48
+ - Pinning at repo scope overrides every contributor's personal setting in that project.
49
+ - Pinning `hard` on a codebase someone has just met is the failure levels exist to prevent. If they are pinning up because the questions feel trivial, check the level first — they may simply be near a promotion.
50
+
51
+ ## Confirm
52
+
53
+ One line: the new setting, the band it means, and what the next question will look like. Then stop — do not run a quiz unless they ask. `/eklavya:quiz` is for that.
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: mode
3
+ description: Show or change how Eklavya teaches — the focus (project, concept, learn), the enforcement mode (ambient, enforced, off), the cadence (interleaved, end) and the difficulty (auto, easy, medium, hard).
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # /eklavya:mode [project|concept|learn|ambient|enforced|off|interleaved|end] [topic]
8
+
9
+ Eklavya has **four independent dials**, and conflating them is the most common confusion. Say which one you are changing.
10
+
11
+ | Dial | Question it answers | Values |
12
+ |---|---|---|
13
+ | `mode` | How hard does Eklavya push? | `ambient`, `enforced`, `off` |
14
+ | `focus` | What does it teach? | `concept` (default), `project`, `learn` |
15
+ | `cadence` | When does it ask? | `interleaved` (default), `end` |
16
+ | `difficulty` | How hard may the questions get? | `auto` (default), `easy`, `medium`, `hard` |
17
+
18
+ `difficulty` is the one that is normally *earned* rather than set: on `auto`, each project starts at `easy` and climbs. **`/eklavya:level` is the command for it** — send them there rather than explaining the ladder here, and only set it from this command if they explicitly asked to pin a level.
19
+
20
+ They combine freely. `enforced` + `learn` is an intern who must pass a gate on a topic they chose; `ambient` + `project` is a gentle nudge grounded in today's diff. The default pairing is `ambient` + `concept`: teach the idea, use today's code as the way in. `off` is the exception — it wins outright and `focus` is never read.
21
+
22
+ ## No arguments
23
+
24
+ Call `get_config` and report the effective settings in two lines: mode and what it means, focus and what it means, then cadence and difficulty in a clause each. If `overridden_by_repo` is non-empty, say which settings this repo is overriding and where the file is — someone whose personal focus silently stopped applying needs to be told why, not left to guess.
25
+
26
+ Then offer the three focus choices below in one line each. Do not lecture.
27
+
28
+ ## The three focus values
29
+
30
+ - **project** *(default)* — quizzes the code just written. Questions name the file, the line, the decision. Best when you are learning a codebase.
31
+ - **concept** — same subject matter, asked so the answer transfers. The diff becomes the motivation rather than the subject: *"we cached this for 60s — what class of problem does a TTL solve, and when is it the wrong tool?"* Best when you are learning a stack rather than a repo.
32
+ - **learn** — you name a topic; Eklavya teaches it in prerequisite order, whether or not today's work touches it. When the work *does* touch it, your real code becomes the worked example.
33
+
34
+ ## The two cadence values
35
+
36
+ - **interleaved** *(default)* — one question mid-task, at the moment a concept is logged, while the code is still on screen. The end-of-task quiz then only asks for what is left of `max_questions_per_task`, which is a session budget rather than a batch size. This is the tool working as advertised: learning while the agent builds, not a pile of questions once it stops.
37
+ - **end** — no mid-task questions at all. Everything waits for the end of the task. Reach for it when someone is pairing, demoing, or genuinely cannot be interrupted — and mention `min_minutes_between_checkpoints` first, since spacing the questions out is usually what they actually want.
38
+
39
+ ## Setting it
40
+
41
+ `set_config` with `focus`, and `focus_topic` when they chose `learn`. Ask for the topic if they picked `learn` without naming one — a `learn` focus with no topic teaches nothing, and the planner will refuse with `reason: "no_topic"`.
42
+
43
+ Scope matters and is worth one question when it is ambiguous:
44
+
45
+ - **global** (default) — their own setting, everywhere. This is where a personal `learn` topic belongs.
46
+ - **repo** — writes `.eklavya.json` at the repo root, and beats global for everyone working in it. This is where a lead pins `enforced` or `project` for onboarding.
47
+
48
+ Warn before writing `focus` at repo scope: it overrides every contributor's personal focus in that project, including a `learn` topic they set for themselves. That is sometimes exactly right, but it should be deliberate.
49
+
50
+ ## Changing mode
51
+
52
+ Same tool, `mode` key. Follow `/eklavya:setup` step 4 if they move **to** `enforced` — the git `pre-commit` hook is what covers commits made outside Claude Code, and enforced mode without it only gates half the ways to commit.
53
+
54
+ ## Confirm
55
+
56
+ Say the new state back in one line — `Mode: ambient. Focus: learn (caching). Cadence: interleaved. Difficulty: auto (easy on this repo).` — and what changes next time they build something. If they set `learn`, add that `/eklavya:learn` teaches the topic on demand rather than waiting for a task to touch it.
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: progress
3
+ description: Show the Eklavya mastery map — which projects you have been learning on, what those sessions actually taught, what you skipped, and what is due for review.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # /eklavya:progress
8
+
9
+ Render the learning map. Read-only: do not quiz, do not teach.
10
+
11
+ 1. `get_learner_profile` (no domain — you want everything).
12
+ 2. `get_concept_graph` with `include_mastery: true` only if you need a domain's
13
+ shape to explain something the profile already flagged. Usually you do not.
14
+ 3. `get_gate_status` if the mode is `enforced`.
15
+
16
+ ## What the report has to answer
17
+
18
+ In this order, because this is the order a learner asks them:
19
+
20
+ 1. **Which project has this been happening on?** `projects` — repo, `answers`,
21
+ `passed`, `last_active`. Show the basename, not the full path, unless two
22
+ basenames collide. Compute accuracy yourself and show it as a percentage.
23
+ A repo of `null` anywhere means work recorded before the repo column
24
+ existed: label that row `unattributed`, never drop it and never guess.
25
+ 2. **What have I actually learned?** `recent_concepts` — and lead with the
26
+ `context` line, not the slug. `mason-lifecycle-hooks` means nothing;
27
+ "moved `flutter create` into pre_gen so brick files overlay it" is the
28
+ memory that makes the slug mean something. Slug second, in parentheses.
29
+ 3. **What did I skip?** `skipped` — `declined` means they chose to pass,
30
+ `dont_know` means they blanked and were taught. Say which. This is the
31
+ actionable list: those come back a tier lower, so it is a backlog, not a
32
+ scolding. If it is empty, say so in one clause; it is good news.
33
+ 4. **What is due?** `due_for_review` — the count, then the slugs with their
34
+ `tier_to_ask`. Already clamped to the level, so do not re-explain tiers.
35
+ 5. **What is shaky?** `weak` — each with a one-clause gloss of what the concept
36
+ actually is. A bare slug list is not a report.
37
+ 6. **What is mastered?** `known_total`, then the most recent few. If it is 0,
38
+ name the concepts closest to it instead (one more correct rep, not a first)
39
+ — a zero with no runway attached reads as failure and it is not.
40
+
41
+ Finish with **one** concrete next step: `/eklavya:quiz` when anything is due,
42
+ `/eklavya:learn <topic>` when a domain the developer clearly works in is
43
+ mostly unseen.
44
+
45
+ Then one line pointing at `eklavya dashboard` — the same data as a local page,
46
+ with the history and the full concept list this report deliberately truncates.
47
+ Mention it once, at the end; it is the deeper view, not a replacement for
48
+ answering the question they just asked.
49
+
50
+ ## How to render it
51
+
52
+ Keep it under 20 lines. A wall of statistics is not a progress report.
53
+
54
+ - **Lead with movement, not inventory.** The first line is the level, the
55
+ project, and what changed. Counts of things that have not happened yet go
56
+ last or nowhere.
57
+ - **Never draw a bar whose biggest segment is `unseen`.** A three-part
58
+ known/learning/unseen bar on a new learner is a chart of absence: every row
59
+ looks identical and the one number that moved is invisible. Per domain, one
60
+ line: `general — 21 in progress, 0 mastered, 46 not yet seen`. Collapse
61
+ domains with no activity at all into a single trailing line naming them.
62
+ - **Tables only when every column is populated for every row.** Two columns
63
+ of zeros is prose pretending to be data.
64
+ - The seed catalogue is large on purpose. Never present the unseen count as a
65
+ gap to close — nobody is meant to learn all 174 concepts, and framing it
66
+ that way makes the honest numbers feel like failure.
67
+
68
+ ## When there is nothing yet
69
+
70
+ If `projects`, `recent_concepts` and `attempts` are all empty, say so plainly
71
+ in two lines and suggest building something first — the tool has nothing to
72
+ show until there is work to ground it in. Do not render empty sections.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: quiz
3
+ description: Start an Eklavya quiz now, on this session's work or on a named topic.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # /eklavya:quiz [topic]
8
+
9
+ Quiz the developer now. Follow the `tutor` skill for how to ask and grade — this command only decides *what* to ask about.
10
+
11
+ The developer asked for this, so always pass `ignore_cooldown: true`. The quiz cadence exists to stop Eklavya nagging; it must never refuse a request.
12
+
13
+ **Topic given** (`$ARGUMENTS` is non-empty): treat it as a domain, or as one or more concept slugs.
14
+ 1. `get_learner_profile` with that domain — `known` is the list you must not ask about.
15
+ 2. `get_session_quiz_plan` with `domain: "<topic>"` (or `slugs: [...]` if they named specific concepts) and `ignore_cooldown: true`. The plan already drops what they have mastered, orders prerequisites first, sets each `tier_to_ask`, and hands you `asked_before` so nothing repeats.
16
+ 3. If the topic is not a known domain, `get_concept_graph` to see what domains exist, and offer the closest one rather than inventing questions.
17
+
18
+ **No topic**: quiz this session's work.
19
+ 1. `get_learner_profile`.
20
+ 2. `get_session_quiz_plan` with `ignore_cooldown: true`.
21
+ 3. If `questions_needed` is 0, say why in one line and stop. The `reason` tells you which:
22
+ - `already_covered` → "Everything from this session has already been asked about. `/eklavya:quiz <topic>` to go wider."
23
+ - `nothing_logged` → "Nothing logged this session yet, so there's nothing grounded to ask about."
24
+ - `no_candidates` (topic mode) → that topic is fully mastered and nothing is due; name the closest domain that is not.
25
+ - `mode_off` → "Eklavya is off. `/eklavya:setup` to turn it back on."
26
+ - `no_topic` → focus is `learn` with nothing set; ask what they want to learn, then `/eklavya:mode learn <topic>`.
27
+ - `topic_unknown` → the graph has nothing matching their topic; offer the closest domain rather than inventing questions.
28
+
29
+ **A named topic overrides the configured focus for this quiz only.** Someone who types `/eklavya:quiz caching` wants caching now; it is not a request to change their standing setting. Do not call `set_config` — `/eklavya:mode` is for that.
30
+
31
+ Then ask one question at a time at each concept's `tier_to_ask`, following the plan's `framing` field, never repeating anything in `asked_before`, grade every answer with `record_attempt`, and close with one line: what moved, and what comes back for review when.
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: setup
3
+ description: First-run Eklavya setup — check prerequisites, create the knowledge database, and choose a mode.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # /eklavya:setup
8
+
9
+ Get Eklavya working on this machine. Be brief; this should take one exchange.
10
+
11
+ **1. Check prerequisites.** Run `node --version`. That is the whole list — the server, the CLI and all four hooks are Node, so nothing else has to be on `PATH`. Node must be 22+; below that the SQLite driver has no prebuilt binary and would need a C++ toolchain to install. If it is older, say so and how to upgrade on this platform, and stop: the rest of setup will not work.
12
+
13
+ Optionally run `eklavya doctor`, which reports the same thing plus the database and the effective config.
14
+
15
+ **2. Confirm the database.** Call `get_config` and report `global_path`, then check the DB:
16
+
17
+ ```bash
18
+ sqlite3 ~/.eklavya/knowledge.db 'select domain, count(*) from concepts group by domain'
19
+ ```
20
+
21
+ The server creates and seeds it on first start, and `npx eklavya install` creates it up front, so this should already show `web-auth`, `react`, `node-backend` and `git`. If the file is missing, the MCP server is not running — check `/plugin` for errors, and check that the runtime is installed (`npx eklavya install` finishes it in one step).
22
+
23
+ **3. Choose a mode.** Ask which fits, and explain in one line each:
24
+
25
+ - **ambient** (default) — questions after tasks, always skippable
26
+ - **enforced** — the session quiz must pass before a commit goes through; for interns and onboarding
27
+ - **off** — installed but dormant
28
+
29
+ Then `set_config` with their choice. Use `scope: "repo"` if they want it to apply to this project only — that writes `.eklavya.json` at the repo root, which is how a lead pins enforced mode for one codebase. Otherwise global.
30
+
31
+ **3b. Choose a focus.** One more question, because it changes every quiz. Mode is how hard Eklavya pushes; focus is what it teaches.
32
+
33
+ - **project** (default) — asks about the code just written. Best when learning a codebase.
34
+ - **concept** — asks the transferable version of the same ideas. Best when learning a stack.
35
+ - **learn** — you name a topic and it teaches that, using your real code as the example where the work touches it.
36
+
37
+ `set_config` with `focus`, plus `focus_topic` if they chose `learn` — that combination is useless without one. If they have no preference, say project is the default and move on; this should not become an interview. `/eklavya:mode` changes it later.
38
+
39
+ **4. If they chose enforced, install the git hook.** The `PreToolUse` hook only covers commits made inside Claude Code. The git `pre-commit` hook covers every other path — a bare terminal, VS Code, Cursor:
40
+
41
+ ```bash
42
+ "${CLAUDE_PLUGIN_ROOT}"/scripts/install-git-hook.sh
43
+ ```
44
+
45
+ It chains to any existing `pre-commit` hook rather than replacing it, and only acts on repos whose `.eklavya.json` sets `"mode": "enforced"` — so installing it is safe even if they later switch to ambient. Mention `--uninstall` restores the previous hook.
46
+
47
+ Skip this step for ambient or off.
48
+
49
+ **5. Say what happens next.** In ambient mode: build something, and questions will follow. Point at `/eklavya:progress` and `/eklavya:quiz`. In enforced mode, add that commits are held until the session quiz passes, and `/eklavya:gate` shows what is outstanding.