eklavya 1.14.0 → 1.16.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 (56) hide show
  1. package/dist/assets/tutor/SKILL.md +180 -0
  2. package/dist/assets/tutor/references/focus-and-level.md +156 -0
  3. package/dist/assets/tutor/references/grading.md +113 -0
  4. package/dist/assets/tutor/references/writing-mcq.md +133 -0
  5. package/dist/cli.js +99 -18
  6. package/dist/cli.js.map +1 -1
  7. package/dist/db.js +5 -0
  8. package/dist/db.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 +14 -3
  25. package/dist/hooks/stop-quiz-check.js.map +1 -1
  26. package/dist/hooks/subagent-start.js +87 -0
  27. package/dist/hooks/subagent-start.js.map +1 -0
  28. package/dist/packs.js +177 -0
  29. package/dist/packs.js.map +1 -0
  30. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  31. package/dist/plugin/agents/tutor.md +19 -5
  32. package/dist/plugin/hooks/CLAUDE.md +114 -2
  33. package/dist/plugin/hooks/hooks.json +24 -0
  34. package/dist/plugin/hooks/run.mjs +3 -3
  35. package/dist/plugin/skills/CLAUDE.md +110 -18
  36. package/dist/plugin/skills/pack/SKILL.md +61 -0
  37. package/dist/plugin/skills/setup/SKILL.md +1 -1
  38. package/dist/plugin/skills/tutor/SKILL.md +119 -304
  39. package/dist/plugin/skills/tutor/references/focus-and-level.md +156 -0
  40. package/dist/plugin/skills/tutor/references/grading.md +113 -0
  41. package/dist/plugin/skills/tutor/references/writing-mcq.md +133 -0
  42. package/dist/seed.js +29 -8
  43. package/dist/seed.js.map +1 -1
  44. package/dist/slug.js +63 -0
  45. package/dist/slug.js.map +1 -1
  46. package/dist/stdin.js +131 -0
  47. package/dist/stdin.js.map +1 -0
  48. package/dist/store.js +16 -2
  49. package/dist/store.js.map +1 -1
  50. package/dist/tools/get_session_quiz_plan.js +1 -1
  51. package/dist/tools/get_session_quiz_plan.js.map +1 -1
  52. package/dist/tools/record_attempt.js +15 -3
  53. package/dist/tools/record_attempt.js.map +1 -1
  54. package/dist/user-skill/eklavya/SKILL.md +1 -0
  55. package/package.json +1 -1
  56. package/dist/assets/tutor-skill.md +0 -365
@@ -30,11 +30,13 @@ triple in here, even in a comment, fails that test on purpose.
30
30
  `scripts/bump-version.sh` bumps `plugin.json` and `mcp/package.json`, nothing
31
31
  else.
32
32
 
33
- ## The four hooks, out of `hooks.json`
33
+ ## The six hooks, out of `hooks.json`
34
34
 
35
35
  | Event | Matcher | Timeout | Script | Job |
36
36
  |---|---|---|---|---|
37
37
  | SessionStart | — | 10s | `session-start` | stamp `meta.current_session`, print the profile banner and the standing log directive |
38
+ | UserPromptSubmit | — | 10s | `prompt-submit-nudge` | re-state the log directive in one line, but only for a session that has logged nothing after a grace window |
39
+ | SubagentStart | — | 10s | `subagent-start` | give a delegated agent the log directive the parent's SessionStart never reached it with |
38
40
  | PreToolUse | `Bash` | 10s | `pre-tool-gate` | in `enforced` mode only, deny a `git commit` whose session gate has not passed |
39
41
  | PostToolUse | `mcp__.*log_session_concepts` | 10s | `checkpoint-quiz` | one mid-task question, `interleaved` cadence only |
40
42
  | Stop | — | 15s | `stop-quiz-check` | block the turn and demand a quiz |
@@ -46,6 +48,36 @@ the prefix depends on how the plugin was installed —
46
48
  catches both; anchoring it to one spelling silently disables the checkpoint for
47
49
  half the installs.
48
50
 
51
+ ## SubagentStart needs the JSON form, and skips the tutor
52
+
53
+ `SessionStart` may print its context as raw stdout. `SubagentStart` may not: it
54
+ reads `hookSpecificOutput.additionalContext` and drops anything else in silence,
55
+ so the wrong form is a hook that runs, exits 0, and does nothing. That is the
56
+ one thing `subagent-start.ts` cannot get wrong, and `mcp/test/hooks.test.ts`
57
+ parses the envelope rather than asserting that something was printed.
58
+
59
+ It also stays silent for `eklavya-tutor`, matched as a substring so both
60
+ `eklavya-tutor` and `eklavya:eklavya-tutor` are caught. Not because the tutor
61
+ lacks `log_session_concepts` — so do `Explore` and `Plan`, and they are told
62
+ anyway. It is the directive's second sentence: *do not ask the developer
63
+ anything here* is an order not to do the only thing `agents/tutor.md` exists
64
+ for, so delivering it disables parallel tutoring in silence. An absent or
65
+ unrecognised `agent_type` fails **open** — a host that does not send the field
66
+ is a host where failing closed would turn the feature off with nothing to
67
+ report. `docs/subagent-policy.md` is the policy in full; keep the two in step.
68
+
69
+ `stop-quiz-check.ts` carries the same `agent_id` guard as `checkpoint-quiz.ts`,
70
+ and for a stronger reason: it blocks with exit 2. `Stop` is believed to be
71
+ parent-only, since `SubagentStop` is a separate event — but nothing here has
72
+ verified that, and this hook is what made the path reachable, because before it
73
+ a subagent logged nothing and the Stop hook's `logged > last_logged` predicate
74
+ could never arm.
75
+
76
+ **And it pays the stdin cost on every delegated task.** It needs `agent_type`,
77
+ so it cannot keep ponytail's stdin-independent fast path; on a host that
78
+ swallows the pipe that is `idleMs` — 2s — per subagent spawn, the same trade
79
+ `PreToolUse` makes per `Bash` call.
80
+
49
81
  ## Every failure path exits 0
50
82
 
51
83
  A hook that throws breaks the user's session, and a learning tool that breaks
@@ -54,10 +86,90 @@ stdin defensively, run the body, exit 0 silently on any throw. A new hook body
54
86
  goes inside `await run(async (input) => { ... })` and returns an exit code; it
55
87
  never calls `process.exit` itself. Helpers follow the same rule —
56
88
  `openExisting()` returns `null` rather than throwing on a missing or corrupt
57
- database, and deliberately does not migrate or seed (four hooks racing a
89
+ database, and deliberately does not migrate or seed (several hooks racing a
58
90
  migration on session start is a corruption story). The one non-zero code is the
59
91
  Stop hook's `return 2`, which is how Stop blocks; the reason goes on stderr.
60
92
 
93
+ ## A hook must never *wait*, either
94
+
95
+ Exiting 0 on a throw covers the loud failure. The quiet one is worse: a hook
96
+ that blocks never errors, never logs, and stalls the session on every tool call
97
+ that triggers it — with nothing for the developer to report except that Claude
98
+ Code got slow.
99
+
100
+ `readInput` used to be `for await (const chunk of process.stdin)`, which has
101
+ exactly one exit: EOF. Ponytail's issue #443 reports Claude Code on Windows
102
+ running a hook through a PowerShell block that swallows the piped JSON, so `end`
103
+ never fires. **Nothing here verifies that mechanism** — there is no Windows
104
+ machine in the loop — so what this defends against is the consequence, a stdin
105
+ that never ends, which the tests reproduce directly. `run.mjs` is careful about
106
+ everything else — Node version, four resolution candidates, a self-expiring heal
107
+ claim, exit 0 on every throw — and this was the one gap.
108
+
109
+ `mcp/src/stdin.ts` closes it, and `eklavya statusline` shares it: both read a
110
+ JSON blob the host pipes in, both must degrade rather than hang, and two copies
111
+ would be one copy getting the fix. Three things matter about it.
112
+
113
+ **The primary bound is on silence, not on total time.** A flat cap truncates a
114
+ payload still arriving when it fires, and truncated JSON does not fail loudly —
115
+ it fails as `{}`, so the hook runs to completion having quietly decided the
116
+ session has no cwd and no id. The idle timer resets on every chunk, so a payload
117
+ is safe as long as it keeps making progress. The total cap behind it *can* still
118
+ truncate, and claiming otherwise would be the same overclaim in the other
119
+ direction — it is a deliberate trade, and `totalMs` is generous against how long
120
+ a real payload takes.
121
+
122
+ **It pauses the stream, not just the listeners.** This is the line the rest of
123
+ it depends on. `setEncoding` puts stdin in flowing mode and a flowing stdin holds
124
+ an active libuv handle, so removing the `data` listener resolves the read and
125
+ leaves the process alive. The hooks hide that — `run()` ends in `process.exit` —
126
+ but `eklavya statusline` just returns, and under exactly the no-EOF condition
127
+ this exists for it printed the dials and then lingered forever, once per status
128
+ bar refresh. `test/stdin.test.ts` spawns the statusline for that reason: it is
129
+ the caller with no `process.exit` behind it, so it is the honest test of whether
130
+ the read lets go.
131
+
132
+ **Every timer is `unref`'d**, so a pending timer adds no latency to a hook that
133
+ has already finished. On its own that does not let the process exit — see above.
134
+
135
+ **There is an `error` handler.** A stream that errors never emits `end`, so
136
+ without one the read waits on something that is not coming. It is also
137
+ load-bearing beyond that: an unhandled `error` on `process.stdin` is an async
138
+ exception `run()`'s `try`/`catch` could not have caught.
139
+
140
+ **It costs something, and the cost is worth stating.** On a host that swallows
141
+ the pipe this turns an infinite hang into `idleMs` per invocation, and PreToolUse
142
+ matches every `Bash` call — so +2s per command until the host is fixed. Two
143
+ seconds a command is bad; a frozen session is worse.
144
+
145
+ `HOOK_STDIN` is 2s idle / 5s total, well under the 10s `hooks.json` grants (15
146
+ for Stop) — a read that outlives its host timeout is a read the developer waits
147
+ on, and `test/stdin.test.ts` asserts the relationship rather than the number.
148
+ `STATUSLINE_STDIN` is 150ms / 250ms, and the total is pinned at or below the
149
+ 250ms flat cap the inline reader had before it: splitting one budget into idle
150
+ plus total made the worst case four times worse for the one caller whose latency
151
+ a human sees, which a test now prevents.
152
+
153
+ That suite spawns a real hook, writes a payload, and **never closes stdin**;
154
+ against the old code all three cases hang until the test kills them.
155
+
156
+ `stripBom` runs before every `JSON.parse` here. Some Windows shells prepend a
157
+ byte-order mark, and `JSON.parse` throws on input that looks perfectly
158
+ well-formed in a terminal and in any editor — another silent nothing-happens.
159
+
160
+ ## `quiet` is not an off switch
161
+
162
+ It suppresses the session-start banner and the status bar — things the developer
163
+ looks at. It does **not** suppress the standing directive, and it does not gate
164
+ the `UserPromptSubmit` nudge, because both are `additionalContext` the model
165
+ reads rather than output anyone sees.
166
+
167
+ That was a bug for a while, and a bad one: `session-start` returned before
168
+ pushing the directive, so a developer who turned the greeting off logged
169
+ nothing, was never quizzed, and saw `mode: ambient` in `get_config` the whole
170
+ time. Two tests encoded it as intended behaviour. `mode: off` is the off switch,
171
+ and it is the only one.
172
+
61
173
  ## The Stop hook blocks in `ambient` too
62
174
 
63
175
  Commonly got wrong. `ambient` is not "never interrupts" — `stop-quiz-check.ts`
@@ -13,6 +13,30 @@
13
13
  ]
14
14
  }
15
15
  ],
16
+ "UserPromptSubmit": [
17
+ {
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "node",
22
+ "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "prompt-submit-nudge"],
23
+ "timeout": 10
24
+ }
25
+ ]
26
+ }
27
+ ],
28
+ "SubagentStart": [
29
+ {
30
+ "hooks": [
31
+ {
32
+ "type": "command",
33
+ "command": "node",
34
+ "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "subagent-start"],
35
+ "timeout": 10
36
+ }
37
+ ]
38
+ }
39
+ ],
16
40
  "PreToolUse": [
17
41
  {
18
42
  "matcher": "Bash",
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "<name>"]
13
13
  *
14
- * and every platform-specific decision lives here instead of in four shell
15
- * scripts.
14
+ * and every platform-specific decision lives here instead of one shell script
15
+ * per hook.
16
16
  *
17
17
  * The second job is finding the runtime. The plugin payload is source and
18
18
  * manifests only — no `dist/`, no `node_modules/`, because a git-installed
@@ -88,7 +88,7 @@ function healInBackground() {
88
88
  const version = pinnedVersion();
89
89
  if (!version) return;
90
90
 
91
- // Claim the attempt BEFORE spawning. Four hooks fire per session and the
91
+ // Claim the attempt BEFORE spawning. Every hook fires per session and the
92
92
  // check alone would let every one of them start its own npm — a spawn storm
93
93
  // on the machine of the person whose install is already struggling. An hour
94
94
  // makes the claim self-expiring, so a heal that dies still gets retried
@@ -11,9 +11,9 @@ One directory, one `SKILL.md`, YAML frontmatter with `name` and `description`.
11
11
  `/eklavya:<name>` slash command** — the model can no longer load it on its own,
12
12
  and the developer types it. Without that line the skill is model-invocable only.
13
13
 
14
- Seven have it, and they are the seven slash commands:
14
+ Eight have it, and they are the eight slash commands:
15
15
 
16
- `gate`, `learn`, `level`, `mode`, `progress`, `quiz`, `setup`.
16
+ `gate`, `learn`, `level`, `mode`, `pack`, `progress`, `quiz`, `setup`.
17
17
 
18
18
  `skills/tutor/SKILL.md` deliberately does not. It is the pedagogy — one
19
19
  question at a time, honest grading, never the same question twice — and every
@@ -40,7 +40,7 @@ So, for `tutor` and anything else without `disable-model-invocation`:
40
40
  number of questions, never the order of the tool calls, never the grading.
41
41
  Those live in the body, which is where the model has to go to get them.
42
42
 
43
- The seven slash commands are exempt, and it is not a technicality:
43
+ The eight slash commands are exempt, and it is not a technicality:
44
44
  `disable-model-invocation: true` means the model never matches on their
45
45
  description at all. The developer types the command and the description is its
46
46
  one line of help, so those should say what they do. `agents/tutor.md` keeps one
@@ -58,8 +58,9 @@ by what it is, not loaded by trigger.
58
58
  `eklavya` CLI and the read/write config tools; it does not teach or quiz.
59
59
  - **`agents/tutor.md`** is the subagent. It has the Eklavya MCP tools and
60
60
  read-only file access — and **no `AskUserQuestion`** — so it renders the four
61
- options as lettered text. Any change to the *Multiple choice* section of
62
- `skills/tutor/SKILL.md` has to hold for a plain-text renderer too.
61
+ options as lettered text. Any change to
62
+ `skills/tutor/references/writing-mcq.md` has to hold for a plain-text
63
+ renderer too.
63
64
 
64
65
  ## A skill is a prompt, but it is also an API client
65
66
 
@@ -148,23 +149,114 @@ cadence, so a tier there would sometimes name the previous question's
148
149
  difficulty, and a stale readout is worse than none. `level` covers what the tier
149
150
  was explaining: `easy` already means tiers 1-2.
150
151
 
151
- ## `skills/tutor/SKILL.md` has two readers
152
-
153
- `mcp/scripts/copy-assets.mjs` bundles it to `mcp/dist/assets/tutor-skill.md`,
154
- and `eklavya export-rules` (`mcp/src/cli.ts`) strips the frontmatter and wraps
155
- it as a Cursor rules file. So that file is consumed by two editors.
156
-
157
- Consequence: **no Claude-Code-only instructions in its body.** Slash-command
158
- names, plugin paths and hook mechanics belong in the command skills, not in
159
- the pedagogy. `AskUserQuestion` is the one unavoidable exception, and
160
- `agents/tutor.md` already carries the fallback for renderers that lack it.
152
+ ## The tutor skill is an entry point plus references
153
+
154
+ `skills/tutor/SKILL.md` was 5,296 words in one file, loaded whole whenever the
155
+ model decided a task was non-trivial. It is now the part that decides *whether
156
+ to act* — the log loop, checkpoint versus sweep, the shared budget, the tier
157
+ ladder, the plan's authoritative fields, and a Red Flags table of the
158
+ rationalizations that have each shipped a worse session — with the craft in
159
+ three siblings:
160
+
161
+ | File | Holds |
162
+ |---|---|
163
+ | `references/writing-mcq.md` | the four-option shape, `answer_position`, distractors, plain language, the second question about a concept, `prereqs_unmet`, and how to record a stem |
164
+ | `references/grading.md` | both scales, the mcq cap, feedback length, the four-step sequence a blank earns, `already_taught` |
165
+ | `references/focus-and-level.md` | the three focuses, the earned level bands, the cadence contract, the enforced-mode gate retry |
166
+
167
+ Two rules keep that split working.
168
+
169
+ **Mark a reference REQUIRED at the point of use, never as an `@`-link.** An
170
+ `@`-path is resolved eagerly by the host, which pulls the whole file into
171
+ context and undoes the split. "Read `references/grading.md` before you grade",
172
+ written where grading comes up, is what makes the model open it exactly when it
173
+ needs it.
174
+
175
+ **The entry point does not grow back, and the pointers stay honest.**
176
+ `test/packaging.test.ts` fails above 2,000 whitespace tokens, and asserts the
177
+ set of files named in SKILL.md is *equal* to the set on disk. Both directions
178
+ matter. A pointer with no file is the worse half — the model is told the rules
179
+ are elsewhere, cannot find them, and improvises, while nothing errors. A file
180
+ with no pointer is the quieter half: it ships, `export-rules` inlines it, and
181
+ Claude Code is never told to read it, so the same pedagogy differs by surface.
182
+ An earlier version of that test harvested pointers from SKILL.md *and*
183
+ `agents/tutor.md` into one list and asserted the list was non-empty — which
184
+ passed with no pointers in SKILL.md at all, the exact state it was written to
185
+ catch.
186
+
187
+ ## Match the form to the failure
188
+
189
+ Two failures need opposite wording, and using the wrong form measurably makes
190
+ things worse. superpowers A/B tested this on their own dispatch-prompt
191
+ guidance: the "don't do X" version produced **more** of the unwanted content
192
+ than the "here is the shape" version — the distributions fully separated — and
193
+ it did worse than giving no guidance at all.
194
+
195
+ - **The model knows the rule and breaks it under pressure.** Discipline. Ban
196
+ it, and name the excuse next to it: that is what the Red Flags table at the
197
+ top of `SKILL.md` is, and what the shared budget, one-question and
198
+ spent-question rules live in.
199
+ - **The model complies and produces the wrong shape.** Craft. Bans backfire
200
+ here. Describe the shape you want, in build order, and let the prohibitions
201
+ fall out of it as properties of the finished thing.
202
+
203
+ Writing a good multiple-choice question is the second kind, and
204
+ `references/writing-mcq.md` was written as the first kind — *never restate the
205
+ answer, no double negatives, avoid "which is NOT", do not number the options*.
206
+ It is now a six-part recipe in build order followed by a checklist of
207
+ properties, so the same rules arrive as "the answer appears among the options
208
+ and nowhere in the stem" rather than as separate bans to weigh.
209
+
210
+ **No nuance clauses in the recipe.** superpowers measured this separately: one
211
+ appended "unless it matters" turns a reliable recipe into a noisy one, because
212
+ it reopens the negotiation the recipe had settled. `writing-mcq.md` carried
213
+ exactly one — *"Save the precise term for when the precision is the point"* —
214
+ and it is gone. If an exception is real, it belongs in the plan's `framing`,
215
+ which is server-side and authoritative, not in a hedge the model gets to weigh.
216
+
217
+ `references/grading.md` keeps its prohibitions on purpose. Inflating a grade
218
+ and offering to stop because someone is blanking are pressure failures, not
219
+ shape failures: the model knows what honest grading is.
220
+
221
+ ## The tutor skill has two readers
222
+
223
+ `mcp/scripts/copy-assets.mjs` bundles the whole `skills/tutor/` directory to
224
+ `mcp/dist/assets/tutor/`, and `eklavya export-rules` (`mcp/src/cli.ts`) strips
225
+ the frontmatter and wraps it as a Cursor rules file. So the pedagogy is
226
+ consumed by two editors.
227
+
228
+ **Cursor has no progressive disclosure**, and that is the reason `export-rules`
229
+ concatenates SKILL.md with every `references/*.md` in alphabetical order and
230
+ says so in its preamble. A rules file is one document with `alwaysApply: true`,
231
+ so "read `references/grading.md`" there is a pointer to nothing. Had the split
232
+ shipped without the inlining, Cursor would have got the dispatch logic and none
233
+ of the craft — and every test would still have passed. `test/cli.test.ts` now
234
+ asserts a line from each reference reaches the output.
235
+
236
+ Alphabetical rather than a hand-kept order: in an always-apply document the
237
+ whole thing is in context at once, so order carries no meaning, and a listed
238
+ order is one more place a new reference gets forgotten.
239
+
240
+ **A missing reference is a hard failure there, not a warning.** `export-rules`
241
+ reads the pointers out of SKILL.md and refuses to emit anything if one of them
242
+ did not bundle, naming the file. It has to: the preamble promises the material
243
+ is further down the document, so a half-bundled export is worse than none — the
244
+ model is assured the rules are present and hunts for them instead of falling
245
+ back on what it has. `copy-assets.mjs` only warns when a copy fails, so that
246
+ state is reachable rather than hypothetical.
247
+
248
+ Consequence of the two readers: **no Claude-Code-only instructions in any of
249
+ the four files.** Slash-command names, plugin paths and hook mechanics belong
250
+ in the command skills, not in the pedagogy. `AskUserQuestion` is the one
251
+ unavoidable exception, and `agents/tutor.md` already carries the fallback for
252
+ renderers that lack it.
161
253
 
162
254
  ## Consistency
163
255
 
164
256
  The same behaviour described in two skills has drifted apart before — that is
165
257
  how `focus: project` got into three files. The four dials appear in
166
258
  `skills/mode/SKILL.md` and `user-skill/eklavya/SKILL.md`; the level bands
167
- appear in `skills/level/SKILL.md` and the *Level* section of `tutor`; the
168
- cadence cap appears in `mode`, `quiz` and `tutor`. **When you change one, grep
169
- the others for the same claim.** Where any of them disagrees with the code,
259
+ appear in `skills/level/SKILL.md` and `tutor/references/focus-and-level.md`;
260
+ the cadence cap appears in `mode`, `quiz` and that same reference. **When you
261
+ change one, grep the others for the same claim.** Where any of them disagrees with the code,
170
262
  `mcp/src/config.ts` and the tool file are right and the skill is wrong.
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: pack
3
+ description: Write or edit an Eklavya concept pack — a JSON file of concepts, tiers and prerequisites that adds a domain, or a team's own codebase, to what Eklavya can quiz on.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # /eklavya:pack [domain or repo]
8
+
9
+ Eklavya ships four graphs — git, node-backend, react, web-auth. A **pack** is the same file shape, written by someone else, merged over the shipped ones. It is how Rust or Kubernetes gets added without a pull request to Eklavya, and how a team teaches its own codebase.
10
+
11
+ Two places, and the difference is the whole decision:
12
+
13
+ | Where | Who it is for |
14
+ |---|---|
15
+ | `~/.eklavya/packs/*.json` | this developer, on every project |
16
+ | `<repo>/.eklavya/packs/*.json` | everyone who works in this repository, versioned with it |
17
+
18
+ The repo one is the interesting half. A codebase that ships its own concepts and prerequisites is describing itself to whoever joins next, and that is onboarding rather than quizzing. Ask which one they want before writing anything, and say what the repo scope means: it is committed, and it applies to every contributor.
19
+
20
+ ## Build the pack in this order
21
+
22
+ 1. **Read before you write.** `get_concept_graph` for the domain they named. If Eklavya already seeds it, the pack is an *extension* — new concepts, retiered old ones — not a replacement.
23
+ 2. **Name it.** `pack` is an identifier (`eklavya-pack-rust`, `acme-billing`), `version` is a string you bump when you edit it, `domain` is the one word that groups these concepts in a learner profile.
24
+ 3. **List the concepts, from the code.** For a repo pack, read the codebase: the modules, the invariants, the decisions someone new gets wrong. Each concept is `{ slug, name, tier, description }`. A slug is lowercase, hyphenated, and names the *idea* — `event-sourcing-replay`, not `fixed-the-replay-bug`.
25
+ 4. **Set each tier honestly.** 1 is what a thing is; 3 is why this choice here; 5 is when the architecture is wrong. A pack where everything is tier 3 teaches nothing about order.
26
+ 5. **Add the prerequisites.** `{ from, to, relation }`, relation one of `prerequisite_of`, `related_to`, `part_of`. An edge may point at a concept Eklavya already seeds — that is how a pack hangs itself off the shipped graph. `from` is the thing that comes first.
27
+ 6. **Write the file** into the directory the scope chose, and run `eklavya doctor`. It names every pack that loaded and every one that did not, with the reason.
28
+
29
+ ```json
30
+ {
31
+ "pack": "acme-billing",
32
+ "version": "1.0.0",
33
+ "domain": "acme",
34
+ "concepts": [
35
+ { "slug": "idempotency-keys", "name": "Idempotency keys", "tier": 2,
36
+ "description": "Why every write endpoint in billing/ takes one, and what a retry does without it." },
37
+ { "slug": "ledger-append-only", "name": "The ledger is append-only", "tier": 3,
38
+ "description": "Corrections are new rows. Nothing in billing/ledger updates a posted entry." }
39
+ ],
40
+ "edges": [
41
+ { "from": "idempotency-keys", "to": "ledger-append-only", "relation": "prerequisite_of" }
42
+ ]
43
+ }
44
+ ```
45
+
46
+ ## What a finished pack has
47
+
48
+ - Every slug lowercase and hyphenated, every tier between 1 and 5, no slug twice.
49
+ - A description on every concept that says something a question could be written from. `"Ownership"` is a name, not a description.
50
+ - Between about 10 and 40 concepts. A pack of 200 is a syllabus nobody reaches the end of, and Eklavya asks about what the session touched, not about the list.
51
+ - Concepts that are about the domain or the codebase, not about the tools. `webpack-config-splitting` is a concept; `we-use-webpack` is a fact.
52
+
53
+ ## Two things to say, and mean
54
+
55
+ **A pack merges over the seed; it does not replace it.** Writing `jwt-structure` into a pack retiers the shipped concept rather than adding a second one. That is deliberate — it is how a team says "this is harder here" — but check `get_concept_graph` first, and say it out loud when a repo pack does it: there is one database and no repository column, so the override follows the developer into every other project and does not come back when they leave this one. New slugs are theirs alone; overriding a shipped one is the part worth a sentence before it is committed.
56
+
57
+ **Removing a pack leaves its concepts behind.** They are what a learner's answers point at, so deleting them would delete the history with them. Uninstalling a pack stops it being re-applied; it does not undo it.
58
+
59
+ ## Then stop
60
+
61
+ Report what was written, where, and what `eklavya doctor` said about it. Do not quiz on the new concepts — nothing has been logged, and `/eklavya:learn <domain>` is how someone asks to be taught them.
@@ -8,7 +8,7 @@ disable-model-invocation: true
8
8
 
9
9
  Get Eklavya working on this machine. Be brief; this should take one exchange.
10
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.
11
+ **1. Check prerequisites.** Run `node --version`. That is the whole list — the server, the CLI and all six 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
12
 
13
13
  Optionally run `eklavya doctor`, which reports the same thing plus the runtime, the SQLite driver, the plugin's registration, the chat skill, the database and the effective config. It exits non-zero and names the repair — `eklavya install` — if any of those has broken. That is also the command to reach for later, whenever Eklavya has gone quiet: the hooks never fail loudly, so a broken install looks exactly like a quiet one.
14
14