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.
- package/dist/assets/tutor/SKILL.md +180 -0
- package/dist/assets/tutor/references/focus-and-level.md +156 -0
- package/dist/assets/tutor/references/grading.md +113 -0
- package/dist/assets/tutor/references/writing-mcq.md +133 -0
- package/dist/cli.js +99 -18
- package/dist/cli.js.map +1 -1
- package/dist/db.js +5 -0
- package/dist/db.js.map +1 -1
- package/dist/eval/extract-json.js +81 -0
- package/dist/eval/extract-json.js.map +1 -0
- package/dist/eval/extraction-score.js +115 -0
- package/dist/eval/extraction-score.js.map +1 -0
- package/dist/eval/history-stats.js +230 -0
- package/dist/eval/history-stats.js.map +1 -0
- package/dist/eval/question-checks.js +220 -0
- package/dist/eval/question-checks.js.map +1 -0
- package/dist/hooks/checkpoint-quiz.js +1 -1
- package/dist/hooks/lib.js +38 -13
- package/dist/hooks/lib.js.map +1 -1
- package/dist/hooks/prompt-submit-nudge.js +155 -0
- package/dist/hooks/prompt-submit-nudge.js.map +1 -0
- package/dist/hooks/session-start.js +32 -14
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop-quiz-check.js +14 -3
- package/dist/hooks/stop-quiz-check.js.map +1 -1
- package/dist/hooks/subagent-start.js +87 -0
- package/dist/hooks/subagent-start.js.map +1 -0
- package/dist/packs.js +177 -0
- package/dist/packs.js.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/tutor.md +19 -5
- package/dist/plugin/hooks/CLAUDE.md +114 -2
- package/dist/plugin/hooks/hooks.json +24 -0
- package/dist/plugin/hooks/run.mjs +3 -3
- package/dist/plugin/skills/CLAUDE.md +110 -18
- package/dist/plugin/skills/pack/SKILL.md +61 -0
- package/dist/plugin/skills/setup/SKILL.md +1 -1
- package/dist/plugin/skills/tutor/SKILL.md +119 -304
- package/dist/plugin/skills/tutor/references/focus-and-level.md +156 -0
- package/dist/plugin/skills/tutor/references/grading.md +113 -0
- package/dist/plugin/skills/tutor/references/writing-mcq.md +133 -0
- package/dist/seed.js +29 -8
- package/dist/seed.js.map +1 -1
- package/dist/slug.js +63 -0
- package/dist/slug.js.map +1 -1
- package/dist/stdin.js +131 -0
- package/dist/stdin.js.map +1 -0
- package/dist/store.js +16 -2
- package/dist/store.js.map +1 -1
- package/dist/tools/get_session_quiz_plan.js +1 -1
- package/dist/tools/get_session_quiz_plan.js.map +1 -1
- package/dist/tools/record_attempt.js +15 -3
- package/dist/tools/record_attempt.js.map +1 -1
- package/dist/user-skill/eklavya/SKILL.md +1 -0
- package/package.json +1 -1
- 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
|
|
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 (
|
|
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
|
|
15
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
62
|
-
`skills/tutor/
|
|
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
|
-
##
|
|
152
|
-
|
|
153
|
-
`
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
168
|
-
cadence cap appears in `mode`, `quiz` and
|
|
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
|
|
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
|
|