eklavya 1.19.0 → 1.19.2

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 (60) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/dashboard.html +11 -1
  3. package/dist/assets/tutor/SKILL.md +5 -5
  4. package/dist/assets/tutor/references/focus-and-level.md +5 -5
  5. package/dist/assets/tutor/references/grading.md +1 -1
  6. package/dist/cli.js +126 -42
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.js +260 -72
  9. package/dist/config.js.map +1 -1
  10. package/dist/dashboard.js +2 -1
  11. package/dist/dashboard.js.map +1 -1
  12. package/dist/hooks/checkpoint-quiz.js +6 -4
  13. package/dist/hooks/checkpoint-quiz.js.map +1 -1
  14. package/dist/hooks/pre-tool-gate.js +2 -2
  15. package/dist/hooks/pre-tool-gate.js.map +1 -1
  16. package/dist/hooks/prompt-submit-nudge.js +7 -6
  17. package/dist/hooks/prompt-submit-nudge.js.map +1 -1
  18. package/dist/hooks/session-start.js +76 -22
  19. package/dist/hooks/session-start.js.map +1 -1
  20. package/dist/hooks/stop-quiz-check.js +32 -38
  21. package/dist/hooks/stop-quiz-check.js.map +1 -1
  22. package/dist/hooks/subagent-start.js +2 -2
  23. package/dist/hooks/subagent-start.js.map +1 -1
  24. package/dist/memory/sync.js +1 -1
  25. package/dist/packs.js +26 -12
  26. package/dist/packs.js.map +1 -1
  27. package/dist/paths.js +60 -0
  28. package/dist/paths.js.map +1 -1
  29. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  30. package/dist/plugin/cli/CLAUDE.md +32 -8
  31. package/dist/plugin/cli/eklavya-gate +80 -9
  32. package/dist/plugin/hooks/CLAUDE.md +40 -21
  33. package/dist/plugin/scripts/install-git-hook.sh +2 -1
  34. package/dist/plugin/skills/CLAUDE.md +12 -10
  35. package/dist/plugin/skills/gate/SKILL.md +1 -1
  36. package/dist/plugin/skills/level/SKILL.md +3 -3
  37. package/dist/plugin/skills/mode/SKILL.md +24 -16
  38. package/dist/plugin/skills/pack/SKILL.md +8 -4
  39. package/dist/plugin/skills/progress/SKILL.md +1 -1
  40. package/dist/plugin/skills/quiz/SKILL.md +2 -2
  41. package/dist/plugin/skills/setup/SKILL.md +12 -12
  42. package/dist/plugin/skills/tutor/SKILL.md +5 -5
  43. package/dist/plugin/skills/tutor/references/focus-and-level.md +5 -5
  44. package/dist/plugin/skills/tutor/references/grading.md +1 -1
  45. package/dist/session.js +1 -1
  46. package/dist/statusline.js +20 -6
  47. package/dist/statusline.js.map +1 -1
  48. package/dist/store.js +2 -2
  49. package/dist/store.js.map +1 -1
  50. package/dist/surface.js +1 -1
  51. package/dist/tools/config_tools.js +82 -52
  52. package/dist/tools/config_tools.js.map +1 -1
  53. package/dist/tools/get_learner_profile.js +2 -1
  54. package/dist/tools/get_learner_profile.js.map +1 -1
  55. package/dist/tools/get_session_quiz_plan.js +15 -9
  56. package/dist/tools/get_session_quiz_plan.js.map +1 -1
  57. package/dist/tools/types.js +1 -1
  58. package/dist/tools/types.js.map +1 -1
  59. package/dist/user-skill/eklavya/SKILL.md +35 -21
  60. package/package.json +1 -1
package/dist/paths.js CHANGED
@@ -22,4 +22,64 @@ export function migrationsDir() {
22
22
  export function seedDir() {
23
23
  return path.join(moduleDir, 'seed');
24
24
  }
25
+ /**
26
+ * Per-project settings, kept **outside** the checkout.
27
+ *
28
+ * Eklavya used to read `<repo>/.eklavya.json`, a file you committed. That was
29
+ * wrong in the ordinary case: a project's settings are how *you* want to be
30
+ * taught in that codebase, and a committed file makes one person's choice
31
+ * everybody's. It also made every clone a configuration file from a stranger,
32
+ * which cost a whole security boundary to contain.
33
+ *
34
+ * So project settings live beside the global ones, keyed by the checkout's
35
+ * absolute path, the way `~/.claude/projects/` does it:
36
+ *
37
+ * ~/.eklavya/config.json you, everywhere
38
+ * ~/.eklavya/projects/<slug>/config.json you, on one project
39
+ *
40
+ * Nothing Eklavya writes ever lands in a repository again — concept packs
41
+ * included; see `projectPacksDir` below, which moved for the same reason. The
42
+ * pre-move directories are still *read* so nothing breaks on upgrade, and the
43
+ * settings file is deleted from the checkout while a pack is not, because a
44
+ * committed pack is authored content somebody reviewed.
45
+ */
46
+ export function projectsDir() {
47
+ return path.join(eklavyaHome(), 'projects');
48
+ }
49
+ /**
50
+ * The directory name for a checkout: its absolute path with the separators
51
+ * turned into dashes, which is what `~/.claude/projects/` does and what makes
52
+ * the directory identifiable at a glance. Readability is the whole point —
53
+ * somebody looking for "the settings for this repo" has to be able to find
54
+ * them without a lookup table.
55
+ *
56
+ * It is lossy, and deliberately so: `/a/b-c` and `/a-b/c` both slug to
57
+ * `-a-b-c`. The collision is caught rather than avoided — `projectConfigPath`'s
58
+ * file records the path it belongs to, and `loadConfig` ignores a file whose
59
+ * `project` names a different checkout. A rare wrong-directory read is worth a
60
+ * name you can recognise; a silent one would not be.
61
+ */
62
+ export function projectSlug(repoRoot) {
63
+ return repoRoot.replace(/[/\\:]/g, '-');
64
+ }
65
+ export function projectConfigPath(repoRoot) {
66
+ return path.join(projectsDir(), projectSlug(repoRoot), 'config.json');
67
+ }
68
+ /**
69
+ * A project's concept packs, kept outside the checkout like its settings.
70
+ *
71
+ * Packs used to live at `<repo>/.eklavya/packs/`, committed, and that was the
72
+ * last thing Eklavya wrote into a repository. The argument for keeping them
73
+ * there was real — a pack is a shared concept graph, the same for everyone —
74
+ * but it lost to a simpler rule: Eklavya creates no files in your project.
75
+ *
76
+ * `loadPacks` still *reads* the old location, so a repository that already
77
+ * ships one keeps working, and nothing deletes it. That asymmetry with the
78
+ * settings file is deliberate: a settings file was a mistake to undo, while a
79
+ * committed pack is authored content somebody reviewed, and relocating it is
80
+ * their decision rather than ours.
81
+ */
82
+ export function projectPacksDir(repoRoot) {
83
+ return path.join(projectsDir(), projectSlug(repoRoot), 'packs');
84
+ }
25
85
  //# sourceMappingURL=paths.js.map
package/dist/paths.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,MAAM;IACpB,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,aAAa,CAAC,CAAC;AACjD,CAAC;AAED,0EAA0E;AAC1E,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE/D,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AACtC,CAAC"}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,MAAM;IACpB,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,aAAa,CAAC,CAAC;AACjD,CAAC;AAED,0EAA0E;AAC1E,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE/D,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,UAAU,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,OAAO,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;AAClE,CAAC"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eklavya",
3
3
  "displayName": "Eklavya",
4
- "version": "1.19.0",
4
+ "version": "1.19.2",
5
5
  "description": "Learn while your agent works. Turns coding-agent generation time into adaptive, Socratic learning grounded in the code being written.",
6
6
  "author": {
7
7
  "name": "Ajay Kumar"
@@ -20,10 +20,22 @@ between the `# >>> eklavya gate >>>` markers, and if a `pre-commit` already
20
20
  existed it moves it to `pre-commit.local` and chains it first. `--uninstall`
21
21
  restores it.
22
22
 
23
- Then the script itself only acts on a repo whose `.eklavya.json` sets
24
- `"mode": "enforced"`. Note that it reads **only** the repo file — not
25
- `~/.eklavya/config.json` — so a globally enforced mode does not gate a bare
26
- terminal. Any doc claiming a terminal commit is gated has to attach the installer
23
+ Then the script itself only acts on a project whose config sets `quiz.enforced`
24
+ (or the retired `"mode": "enforced"`, which it still reads). That config is at
25
+ `~/.eklavya/projects/<slug>/config.json` — the script rebuilds the slug itself,
26
+ in shell, from `git rev-parse --show-toplevel` folded to the main checkout via
27
+ `--git-common-dir`, because a worktree shares its parent's settings. Note that it
28
+ reads enforcement from **only** the project file — not `~/.eklavya/config.json` —
29
+ so a globally set gate does not hold a bare terminal. The global file is read for
30
+ one thing: a `quiz.enabled: false` (or `mode: off`) there, not overridden by the
31
+ project, releases the gate, exactly as `coerce()` does — otherwise a terminal
32
+ commit is held by a gate no quiz will ever run to clear. The slug path is built
33
+ with `pwd -P` so a checkout reached through a symlink still finds its file.
34
+
35
+ It reads `<repo>/.eklavya.json` as a fallback and does **not** migrate it. The
36
+ node half moves that file out automatically; a git pre-commit hook is the wrong
37
+ place to start rewriting somebody's working tree, so it reads the old location
38
+ and lets the next ordinary session do the move. Any doc claiming a terminal commit is gated has to attach the installer
27
39
  step and the repo config; `web/src/content/docs/docs/commit-gate.mdx` and
28
40
  `skills/setup/SKILL.md` are where that lives.
29
41
 
@@ -33,10 +45,12 @@ A learning tool that bricks commits gets uninstalled, and an unpassable gate
33
45
  teaches nothing. Every one of these exits 0:
34
46
 
35
47
  - not inside a git repo, or `git rev-parse` fails;
36
- - no `.eklavya.json` in the repo root;
48
+ - no config for this project, at either the current or the legacy location;
37
49
  - `jq` not on PATH (warns on stderr) — **or** `sqlite3` not on PATH (warns too).
38
50
  It needs both, not just `jq`;
39
- - `mode` is anything but `enforced`;
51
+ - the config does not ask for enforcement — `quiz.enforced` false or absent with
52
+ no `mode: enforced` behind it, or `quiz.enabled` false — in the project file,
53
+ or in the global one when the project does not say;
40
54
  - the database file does not exist;
41
55
  - the `sqlite3` query errors, or returns no gate row for this repo.
42
56
 
@@ -51,8 +65,18 @@ SQL and duplicates things it cannot import. Keep it in step with:
51
65
  - the `gates` table shape — it selects `passed, required, answered` and picks the
52
66
  repo's most recent row by `updated_at`. `syncGate` in `mcp/src/store.ts` is the
53
67
  only writer; a renamed or added column lands here in the same commit.
54
- - `DEFAULT_CONFIG.mode` in `mcp/src/config.ts` — the script hardcodes `"ambient"`
55
- as jq's fallback for a `.eklavya.json` with no `mode`.
68
+ - `DEFAULT_CONFIG.quiz` in `mcp/src/config.ts` — the script defaults a config
69
+ with neither `quiz` nor `mode` to unenforced.
70
+ - `projectSlug` in `mcp/src/paths.ts` — the script rebuilds it with
71
+ `tr '/\\:' '-'`, and `test/gate.test.ts` runs both parsers over one set of
72
+ configs so the two cannot drift apart silently.
73
+ - the `mode` → `quiz` alias in `normalizeLegacyKeys`, and the rule that
74
+ `quiz.enabled: false` forces `enforced` off. Both are duplicated here in jq,
75
+ spelled out with `if`/`elif` rather than `//`: jq's `//` is an alternative
76
+ operator, so `.quiz.enforced // (.mode == "enforced")` reads an explicit
77
+ `false` as unset and falls back to a stale `mode` in the same file — a gate
78
+ that keeps holding commits after somebody switched it off.
79
+ `test/gate.test.ts` runs both parsers over the same five configs.
56
80
 
57
81
  The pass/fail arithmetic itself (`PASSING_GRADE`, `pass_threshold`,
58
82
  `ceil(required * pass_threshold)`) is **not** duplicated here — the script trusts
@@ -9,10 +9,24 @@
9
9
  # bricks commits when a dependency is absent gets uninstalled, and an unpassable
10
10
  # gate teaches nothing.
11
11
  #
12
- # It reads the repo's .eklavya.json and nothing else -- never ~/.eklavya/config.json.
13
- # That is deliberate: a git hook installed once must not start blocking commits
14
- # because someone flipped a global setting. Gating a repo is a per-repo decision,
15
- # stated in a file the repo can see.
12
+ # Enforcement is read from this project's config only -- never from
13
+ # ~/.eklavya/config.json. That is deliberate: a git hook installed once must not
14
+ # start blocking commits because someone flipped a global setting. Gating a
15
+ # codebase is a per-project decision, stated in that project's own file. The
16
+ # global file is consulted for one thing: a `quiz.enabled: false` there switches
17
+ # the questions off, and a gate nothing can pass must not hold.
18
+ #
19
+ # That file used to be <repo>/.eklavya.json, committed. It is not any more: a
20
+ # project's settings say how *you* want to be taught in that codebase, and a
21
+ # committed file made one person's choice everybody's. They now live at
22
+ # ~/.eklavya/projects/<checkout path with separators as dashes>/config.json,
23
+ # alongside the global config and keyed the way ~/.claude/projects/ keys its own.
24
+ # Nothing Eklavya writes lands in a checkout.
25
+ #
26
+ # The node half migrates a leftover .eklavya.json out of the repo automatically.
27
+ # This script does not -- a git pre-commit hook is the wrong place to start
28
+ # rewriting somebody's working tree -- so it reads the legacy file as a fallback
29
+ # and lets the next ordinary session do the move.
16
30
 
17
31
  set -u
18
32
 
@@ -22,8 +36,37 @@ eklavya_db() { printf '%s' "${EKLAVYA_DB:-$(eklavya_home)/knowledge.db}"; }
22
36
  REPO=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
23
37
  [ -n "$REPO" ] || exit 0
24
38
 
25
- # Only repos that explicitly opt in are gated.
26
- CONFIG="$REPO/.eklavya.json"
39
+ # A linked worktree is a branch of the same codebase, not a project to configure
40
+ # again -- the same fold `mainRepoRoot` applies on the node side. `--git-common-dir`
41
+ # points at the main checkout's .git for a worktree and at this one's otherwise,
42
+ # so stripping the trailing /.git yields the checkout that owns the settings.
43
+ #
44
+ # `--path-format=absolute` would be the obvious way to ask, and is deliberately
45
+ # not used: it needs git >= 2.31, and on anything older the whole `rev-parse`
46
+ # fails, the slug misses, and the gate fails open in every linked worktree --
47
+ # enforcement somebody was promised, silently not happening. Plain
48
+ # `--git-common-dir` has been there for years and answers relative to the
49
+ # current directory, so the relative case is resolved by hand.
50
+ #
51
+ # Resolved with `pwd -P`, never `$(pwd)/...`: in a hook `pwd` echoes the `PWD`
52
+ # the shell handed down, which is the symlinked spelling when the repo was
53
+ # reached through one (macOS's /tmp, a symlinked ~/code). The node side keys on
54
+ # the real path, so a symlinked prefix misses the slug and the gate fails open.
55
+ PROJECT="$REPO"
56
+ COMMON=$(git rev-parse --git-common-dir 2>/dev/null) || COMMON=''
57
+ [ -n "$COMMON" ] && COMMON=$(cd "$COMMON" 2>/dev/null && pwd -P) || COMMON=''
58
+ case "$COMMON" in
59
+ */.git) CANDIDATE=${COMMON%/.git}; [ -d "$CANDIDATE" ] && PROJECT="$CANDIDATE" ;;
60
+ esac
61
+
62
+ # The slug `projectSlug` builds: the absolute path with separators as dashes.
63
+ SLUG=$(printf '%s' "$PROJECT" | tr '/\\:' '-')
64
+ CONFIG="$(eklavya_home)/projects/$SLUG/config.json"
65
+
66
+ # The pre-move location, read only while a checkout still has one.
67
+ [ -f "$CONFIG" ] || CONFIG="$REPO/.eklavya.json"
68
+
69
+ # Only projects that explicitly opt in are gated.
27
70
  [ -f "$CONFIG" ] || exit 0
28
71
 
29
72
  if ! command -v jq >/dev/null 2>&1; then
@@ -35,8 +78,36 @@ if ! command -v sqlite3 >/dev/null 2>&1; then
35
78
  exit 0
36
79
  fi
37
80
 
38
- MODE=$(jq -r '.mode // "ambient"' "$CONFIG" 2>/dev/null) || exit 0
39
- [ "$MODE" = "enforced" ] || exit 0
81
+ # `quiz.enforced`, with the retired `mode` dial honoured as a fallback -- this
82
+ # script meets configs written years before it.
83
+ #
84
+ # Spelled out with if/elif rather than jq's `//`, which is an alternative
85
+ # operator and not a null-coalescing one: `false // X` evaluates to X, so the
86
+ # obvious `.quiz.enforced // (.mode == "enforced")` would read an explicit
87
+ # `"enforced": false` as unset and fall through to a stale `mode` left in the
88
+ # same file. That is a gate holding commits after somebody switched it off.
89
+ #
90
+ ENFORCED=$(jq -r '
91
+ if .quiz.enforced != null then .quiz.enforced
92
+ else (.mode == "enforced") end
93
+ ' "$CONFIG" 2>/dev/null) || exit 0
94
+ [ "$ENFORCED" = "true" ] || exit 0
95
+
96
+ # `quiz.enabled`, resolved the way `coerce()` resolves it: project over global,
97
+ # `mode` per file. A gate with no questions behind it can never be passed, so
98
+ # it must not hold -- and the switch that silences the questions may well be in
99
+ # the global file. This is the one thing read from there: the global config can
100
+ # release this gate, never impose one.
101
+ QUIZ_ENABLED='
102
+ if .quiz.enabled != null then .quiz.enabled
103
+ elif .mode == "off" then false
104
+ elif .mode == "ambient" or .mode == "enforced" then true
105
+ else null end'
106
+ ENABLED=$(jq -r "$QUIZ_ENABLED" "$CONFIG" 2>/dev/null) || exit 0
107
+ if [ "$ENABLED" = "null" ] && [ -f "$(eklavya_home)/config.json" ]; then
108
+ ENABLED=$(jq -r "$QUIZ_ENABLED" "$(eklavya_home)/config.json" 2>/dev/null) || exit 0
109
+ fi
110
+ [ "$ENABLED" = "false" ] && exit 0
40
111
 
41
112
  DB=$(eklavya_db)
42
113
  [ -f "$DB" ] || exit 0
@@ -70,7 +141,7 @@ cat >&2 <<EOF
70
141
  Open Claude Code in this repo and run /eklavya:quiz to finish, then commit again.
71
142
 
72
143
  To see what is outstanding: /eklavya:gate
73
- To turn this off for the repo, set "mode" to "ambient" in .eklavya.json
144
+ To turn this off: eklavya config set quiz.enforced false --project
74
145
 
75
146
  EOF
76
147
  exit 1
@@ -34,24 +34,26 @@ else.
34
34
 
35
35
  | Event | Matcher | Timeout | Script | Job |
36
36
  |---|---|---|---|---|
37
- | SessionStart | — | 10s | `session-start` | stamp this checkout's session pointer (`meta.current_session:<repo root>`), replay the spool, summarise the last session's batch, recall this project's memory, print the profile banner and the standing log directive |
37
+ | SessionStart | — | 10s | `session-start` | stamp this checkout's session pointer (`meta.current_session:<repo root>`), replay the spool, summarise the last session's batch, recall this project's memory, show the developer the profile banner (`systemMessage`) and hand the model the recall and the standing log directive (`additionalContext`) |
38
38
  | UserPromptSubmit | — | 10s | `prompt-submit-nudge` | re-stamp this checkout's session pointer, then re-state the log directive in one line, but only for a session that has logged nothing after a grace window |
39
39
  | SubagentStart | — | 10s | `subagent-start` | give a delegated agent the log directive the parent's SessionStart never reached it with |
40
- | PreToolUse | `Bash` | 10s | `pre-tool-gate` | in `enforced` mode only, deny a `git commit` whose session gate has not passed |
40
+ | PreToolUse | `Bash` | 10s | `pre-tool-gate` | with `quiz.enforced` only, deny a `git commit` whose session gate has not passed |
41
41
  | PostToolUse | — | 10s | `capture-tool` | record the tool use as memory evidence |
42
42
  | PostToolUse | `mcp__.*log_session_concepts` | 10s | `checkpoint-quiz` | one mid-task question, `interleaved` cadence only |
43
43
  | Stop | — | 15s | `stop-quiz-check` | block the turn and demand a quiz |
44
44
 
45
- ## Memory capture is not governed by `mode`
45
+ ## Memory capture is not governed by `quiz`
46
46
 
47
47
  `capture-tool` has no matcher, so it runs after **every** tool call — it is the
48
48
  hook that fires most often in a session, and it does the least: resolve
49
49
  identity, normalise one event, one insert, exit. It never quizzes, summarises
50
50
  or calls a provider.
51
51
 
52
- Four hooks now carry memory work, and all four do it **before** their `mode`
52
+ Four hooks now carry memory work, and all four do it **before** their `quiz`
53
53
  check, because `memory.enabled` is a separate decision from the learning dials
54
- (PRD CFG-01): `mode: off` means no quizzes, not no project history.
54
+ (PRD CFG-01): `quiz.enabled: false` means no quizzes, not no project history.
55
+ That separation is why the `mode` dial was retired — it was always true and the
56
+ word `off` denied it, so `session-start` now says on screen which half stopped.
55
57
  `session-start` replays the spool and recalls; `prompt-submit-nudge` captures
56
58
  the prompt; `capture-tool` captures the tool use; `stop-quiz-check` closes the
57
59
  batch at the seam. `mcp/src/hooks/memory-lib.ts` holds the shared helpers, and
@@ -70,9 +72,25 @@ the prefix depends on how the plugin was installed —
70
72
  catches both; anchoring it to one spelling silently disables the checkpoint for
71
73
  half the installs.
72
74
 
75
+ ## Two audiences, two channels
76
+
77
+ Every line a hook writes is for the developer or for the model, and each has
78
+ exactly one channel: top-level `systemMessage` is rendered to the developer,
79
+ `hookSpecificOutput.additionalContext` is read by the model. Plain stdout is
80
+ never used. On `SessionStart` it is accepted, but as context only — the banner
81
+ went out that way for months and every session opened in apparent silence while
82
+ the model read three lines meant for a person. And `systemMessage` is **top
83
+ level**: nested inside `hookSpecificOutput` the harness drops it, which is how
84
+ the checkpoint's one line to the developer went unseen for just as long.
85
+ `docs/verified-schemas.md` has the per-hook table; `mcp/test/hooks.test.ts`
86
+ asserts on `shown` and `context` separately, and its `systemMessage()` helper
87
+ throws on the nested placement, so a new hook cannot repeat either mistake
88
+ without a test saying so.
89
+
73
90
  ## SubagentStart needs the JSON form, and skips the tutor
74
91
 
75
- `SessionStart` may print its context as raw stdout. `SubagentStart` may not: it
92
+ `SessionStart` accepts raw stdout as context (Eklavya no longer uses it — see
93
+ above). `SubagentStart` does not: it
76
94
  reads `hookSpecificOutput.additionalContext` and drops anything else in silence,
77
95
  so the wrong form is a hook that runs, exits 0, and does nothing. That is the
78
96
  one thing `subagent-start.ts` cannot get wrong, and `mcp/test/hooks.test.ts`
@@ -193,20 +211,21 @@ reads rather than output anyone sees.
193
211
 
194
212
  That was a bug for a while, and a bad one: `session-start` returned before
195
213
  pushing the directive, so a developer who turned the greeting off logged
196
- nothing, was never quizzed, and saw `mode: ambient` in `get_config` the whole
197
- time. Two tests encoded it as intended behaviour. `mode: off` is the off switch
198
- — along with its session-scoped twin below, which is the same switch with a
199
- shorter life.
200
-
201
- ## The Stop hook blocks in `ambient` too
202
-
203
- Commonly got wrong. `ambient` is not "never interrupts" — `stop-quiz-check.ts`
204
- returns 2 in ambient as readily as in enforced. `enforced` changes three things:
205
- the `min_minutes_between_quizzes` cooldown is skipped (a cooldown could make a
206
- commit gate unpassable — decision G5); the one-question cap under `interleaved`
207
- is lifted, so the sweep asks for the whole remaining budget; and `pre-tool-gate`
208
- plus `cli/eklavya-gate` start holding commits. Only `mode === 'off'` silences
209
- everything.
214
+ nothing, was never quizzed, and saw quizzing reported as enabled in
215
+ `get_config` the whole time. Two tests encoded it as intended behaviour.
216
+ `quiz.enabled: false` is the off switch — along with its session-scoped twin
217
+ below, which is the same switch with a shorter life.
218
+
219
+ ## The Stop hook blocks when unenforced too
220
+
221
+ Commonly got wrong. Unenforced is not "never interrupts" — `stop-quiz-check.ts`
222
+ returns 2 without the gate as readily as with it. `quiz.enforced` changes three
223
+ things: the `min_minutes_between_quizzes` cooldown is skipped (a cooldown could
224
+ make a commit gate unpassable — decision G5); the one-question cap under
225
+ `interleaved` is lifted, so the sweep asks for the whole remaining budget; and
226
+ `pre-tool-gate` plus `cli/eklavya-gate` start holding commits. Only
227
+ `quiz.enabled: false` silences the questions — and it silences only those, not
228
+ the memory half.
210
229
 
211
230
  ## The per-session off switch
212
231
 
@@ -220,7 +239,7 @@ config file has quietly turned the product off for good.
220
239
 
221
240
  Two rules it is easy to get wrong:
222
241
 
223
- - It silences, it does not exempt. `cli/eklavya-gate` reads `.eklavya.json` and
242
+ - It silences, it does not exempt. `cli/eklavya-gate` reads the project config and
224
243
  never sees a session id, so an enforced repo still holds the commit. Making
225
244
  the gate honour it would turn a per-session convenience into a gate bypass.
226
245
  `pre-tool-gate` reads `isSessionOff` for one reason only: its refusal tells
@@ -78,4 +78,5 @@ EOF
78
78
 
79
79
  chmod +x "$HOOK"
80
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'
81
+ printf 'It only acts on projects whose config sets quiz.enforced. That config lives at\n'
82
+ printf '~/.eklavya/projects/<checkout>/config.json, never inside the repository.\n'
@@ -106,16 +106,16 @@ Check every one of these against the code when you touch a skill.
106
106
  write it: `grep -rn 'focus' skills/ user-skill/ agents/`.
107
107
  - **The `interleaved` one-question cap has exemptions.** In
108
108
  `mcp/src/tools/get_session_quiz_plan.ts`:
109
- `capped = cadence === 'interleaved' && mode !== 'enforced' && !explicitTopic`,
109
+ `capped = cadence === 'interleaved' && !quiz.enforced && !explicitTopic`,
110
110
  and then `max = args.max ?? (capped ? 1 : max_questions_per_task)`.
111
- So `enforced` mode is exempt, an explicit `domain` or `slugs` is exempt, and
111
+ So `quiz.enforced` is exempt, an explicit `domain` or `slugs` is exempt, and
112
112
  an explicit `max` wins outright because it is read first. Read that code
113
113
  rather than trusting prose about it — including this paragraph.
114
- - **The Stop hook blocks in `ambient` too.** `mcp/src/hooks/stop-quiz-check.ts`
115
- blocks in both modes; what `enforced` changes is that it skips the
114
+ - **The Stop hook blocks when unenforced too.** `mcp/src/hooks/stop-quiz-check.ts`
115
+ blocks either way; what `quiz.enforced` changes is that it skips the
116
116
  `min_minutes_between_quizzes` cooldown, takes the whole remaining budget
117
- instead of one question, and gates commits. Do not write "ambient never
118
- interrupts".
117
+ instead of one question, and gates commits. Do not write "it never
118
+ interrupts unless enforced".
119
119
  - **`get_learner_profile`'s lists are capped, and `known` is ordered by score.**
120
120
  `LIST_CAP = 8` covers `weak`, `due_for_review`, `projects`,
121
121
  `recent_concepts` and `skipped`; `KNOWN_CAP = 30` covers `known`, with the
@@ -130,14 +130,16 @@ Check every one of these against the code when you touch a skill.
130
130
 
131
131
  Until 1.14 every plan item carried `ask_header` and the tutor printed it above
132
132
  the question: `[mode: ambient · focus: concept · level: easy · tier: 2
133
- mechanism]`. It existed for a real reason — on `concept` focus a deliberately
133
+ mechanism]` — in the vocabulary of the day, when a single `mode` dial still
134
+ existed. It existed for a real reason — on `concept` focus a deliberately
134
135
  transferable question reads as a vague one, and on `easy` a tier-2 question
135
136
  reads as shallow rather than as a runway — but it spent four settings' worth of
136
137
  screen above *every* stem to say something that is true for the whole session.
137
138
 
138
139
  Ambient state belongs somewhere ambient. `statusLine` in `mcp/src/statusline.ts`
139
- composes `[EKLAVYA ambient · concept · interleaved · easy]` for
140
- `eklavya statusline`, which the host's status bar runs. `askHeader` is deleted,
140
+ composes `[EKLAVYA concept · interleaved · easy]` for `eklavya statusline`,
141
+ which the host's status bar runs — with `enforced` prepended only when it is
142
+ set, since a segment that is always there is a segment nobody reads. `askHeader` is deleted,
141
143
  the plan no longer carries `ask_header`, and both hooks now say *ask the stem on
142
144
  its own*.
143
145
 
@@ -287,7 +289,7 @@ renderers that lack it.
287
289
  ## Consistency
288
290
 
289
291
  The same behaviour described in two skills has drifted apart before — that is
290
- how `focus: project` got into three files. The four dials appear in
292
+ how `focus: project` got into three files. The dials appear in
291
293
  `skills/mode/SKILL.md` and `user-skill/eklavya/SKILL.md`; the level bands
292
294
  appear in `skills/level/SKILL.md` and `tutor/references/focus-and-level.md`;
293
295
  the cadence cap appears in `mode`, `quiz` and that same reference. **When you
@@ -8,7 +8,7 @@ disable-model-invocation: true
8
8
 
9
9
  Call `get_gate_status` and report it in a few lines:
10
10
 
11
- - Mode. If it is not `enforced`, say that nothing is gated and stop.
11
+ - Whether `quiz.enforced` is set. If it is not, say that nothing is gated and stop.
12
12
  - `passed_count` of `needed` passing answers, out of `required` concepts.
13
13
  - Passed or not.
14
14
 
@@ -29,7 +29,7 @@ Report it in three lines or fewer:
29
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
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
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.
32
+ If `pinned` is set, say where it was pinned (their global config or their settings for this project) and that nothing will promote while it stands. Do not report a runway toward a level a pin makes unreachable.
33
33
 
34
34
  ## With an argument
35
35
 
@@ -41,11 +41,11 @@ If `pinned` is set, say who pinned it (global config or this repo's `.eklavya.js
41
41
  Scope, and ask when it is ambiguous:
42
42
 
43
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.
44
+ - **project** — `~/.eklavya/projects/<checkout>/config.json`, and it beats global whenever *they* work in that codebase. `easy` here is a codebase they are new to and want kept gentle. It is in their home directory, not the repository, so it reaches nobody else.
45
45
 
46
46
  Two things to say plainly before writing:
47
47
 
48
- - Pinning at repo scope overrides every contributor's personal setting in that project.
48
+ - Pinning at project scope overrides their own global setting in that codebase, and nobody else's.
49
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
50
 
51
51
  ## Confirm
@@ -1,27 +1,33 @@
1
1
  ---
2
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).
3
+ description: Show or change how Eklavya teaches — whether it quizzes at all and whether it gates commits (quiz.enabled, quiz.enforced), the focus (project, concept, learn), the cadence (interleaved, end) and the difficulty (auto, easy, medium, hard).
4
4
  disable-model-invocation: true
5
5
  ---
6
6
 
7
- # /eklavya:mode [project|concept|learn|ambient|enforced|off|interleaved|end] [topic] [--session]
7
+ # /eklavya:mode [on|off|enforced|project|concept|learn|interleaved|end] [topic] [--session]
8
8
 
9
- Eklavya has **four independent dials**, and conflating them is the most common confusion. Say which one you are changing.
9
+ Eklavya has **independent switches**, and conflating them is the most common confusion. Say which one you are changing.
10
10
 
11
- | Dial | Question it answers | Values |
11
+ | Setting | Question it answers | Values |
12
12
  |---|---|---|
13
- | `mode` | How hard does Eklavya push? | `ambient`, `enforced`, `off` |
13
+ | `quiz.enabled` | Does it ask questions at all? | `true` (default), `false` |
14
+ | `quiz.enforced` | Do unanswered questions hold commits? | `false` (default), `true` |
14
15
  | `focus` | What does it teach? | `concept` (default), `project`, `learn` |
15
16
  | `cadence` | When does it ask? | `interleaved` (default), `end` |
16
17
  | `difficulty` | How hard may the questions get? | `auto` (default), `easy`, `medium`, `hard` |
18
+ | `memory.enabled` | Is the work recorded and recalled? | `true` (default), `false` |
19
+
20
+ **`quiz` and `memory` are separate, and this is the thing to get right.** Turning the questions off does not stop Eklavya recording what the session did — that is `memory.enabled`, and it is a deliberate second decision. Someone who says "turn Eklavya off" has usually asked for silence, not amnesia: set `quiz.enabled: false` and tell them memory is still running, rather than switching both and losing their history. If they want everything off, they will say so, and then it is both.
21
+
22
+ These replaced a single `mode` dial whose values were `ambient`, `enforced` and `off`. Configs still using it keep working — `off` reads as `quiz.enabled: false`, `enforced` as `quiz.enforced: true` — but do not write it. `off` is exactly the word that made people believe their memory had stopped.
17
23
 
18
24
  `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
25
 
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.
26
+ They combine freely. `quiz.enforced` + `learn` is an intern who must pass a gate on a topic they chose; unenforced + `project` is a gentle nudge grounded in today's diff. The default pairing is unenforced + `concept`: teach the idea, use today's code as the way in. `quiz.enabled: false` is the exception — it wins outright, `focus` is never read, and it forces `quiz.enforced` off with it, since a gate nothing asks questions for could never be passed.
21
27
 
22
28
  ## No arguments
23
29
 
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.
30
+ Call `get_config` and report the effective settings in two lines: whether questions are on and whether they gate commits, focus and what it means, then cadence and difficulty in a clause each. **Say whether memory is on in the same breath** — it is the setting people most often assume follows the others. If `overridden_by_project` is non-empty, say which settings this project is overriding and where the file is: someone whose global focus silently stopped applying needs to be told why, not left to guess.
25
31
 
26
32
  Then offer the three focus choices below in one line each. Do not lecture.
27
33
 
@@ -33,7 +39,7 @@ Then offer the three focus choices below in one line each. Do not lecture.
33
39
 
34
40
  ## The two cadence values
35
41
 
36
- - **interleaved** *(default)* — one question mid-task, at the moment a concept is logged, while the code is still on screen. A quiz is capped at one question under this cadence, the end-of-task sweep included — except in `enforced` mode, where the gate needs a full round, and when the developer asked for a quiz themselves — and it draws on `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.
42
+ - **interleaved** *(default)* — one question mid-task, at the moment a concept is logged, while the code is still on screen. A quiz is capped at one question under this cadence, the end-of-task sweep included — except when `quiz.enforced` is set, where the gate needs a full round, and when the developer asked for a quiz themselves — and it draws on `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
43
  - **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
44
 
39
45
  ## Setting it
@@ -43,24 +49,26 @@ Then offer the three focus choices below in one line each. Do not lecture.
43
49
  Scope matters and is worth one question when it is ambiguous:
44
50
 
45
51
  - **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.
52
+ - **project** — writes `~/.eklavya/projects/<checkout>/config.json`, and beats global whenever they work in that codebase. This is where `quiz.enforced` or `focus: project` goes for a codebase they are new to. (`repo` is accepted as the older name for this scope.)
53
+
54
+ **Nothing is written into the repository.** Project settings are per developer: they are in the developer's own home directory, keyed by the checkout's path, and no teammate ever sees them. Say so if they ask about sharing — what a team can share is a concept pack, not settings.
47
55
 
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.
56
+ Mention when writing `focus` at project scope that it overrides the `learn` topic they set for themselves, in that codebase only. That is sometimes exactly right, but it should be deliberate.
49
57
 
50
- ## Changing mode
58
+ ## Turning the gate on
51
59
 
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.
60
+ Same tool, `quiz: { enforced: true }`. Follow `/eklavya:setup` step 4 when they move **to** enforcement — the git `pre-commit` hook is what covers commits made outside Claude Code, and the gate without it only covers half the ways to commit.
53
61
 
54
62
  ## "Turn it off for this session"
55
63
 
56
- **Hear this phrasing — "for now", "for this session", "I'm in the middle of something", `--session` — and use `set_config` with `scope: "session"` and `mode: "off"`.** Not global scope. Global is a file, the file outlives the afternoon, and a developer who silenced one urgent hour in April finds out in June that they turned the tool off for good. Session scope writes nothing: every question, checkpoint, banner and status bar stops until this session ends, and it forgets by itself.
64
+ **Hear this phrasing — "for now", "for this session", "I'm in the middle of something", `--session` — and use `set_config` with `scope: "session"` and `quiz: { enabled: false }`.** Not global scope. Global is a file, the file outlives the afternoon, and a developer who silenced one urgent hour in April finds out in June that they turned the questions off for good. Session scope writes nothing: every question, checkpoint, banner and status bar stops until this session ends, and it forgets by itself.
57
65
 
58
- It takes `mode` and nothing else. Any other value of `mode` at that scope brings the session back — that is what "turn Eklavya back on" does — and the file-backed dials are whatever they always were.
66
+ It takes `quiz` and nothing else. `quiz: { enabled: true }` at that scope brings the session back — that is what "turn Eklavya back on" does — and the file-backed settings are whatever they always were. **Memory has no session switch at all**, deliberately: a day of work nobody recorded is a day nobody can look up later, and the request was for quiet, not for a hole in the history. Say so if they ask.
59
67
 
60
- Say one line back — including *which* session it acted on if `set_config` reports a `session_id` you did not pass — and say the limit in it when the effective mode is `enforced`: the questions are silenced, but the commit gate reads `.eklavya.json` and never sees a session id, so a commit still waits for the quiz, and it keeps growing while you work in silence. Someone who wants that gone wants a repo or global `mode` change, and that is a different, deliberate decision — `set_config` returns a `note` saying so.
68
+ Say one line back — including *which* session it acted on if `set_config` reports a `session_id` you did not pass — and say the limit in it when `quiz.enforced` is set: the questions are silenced, but the commit gate reads the project config and never sees a session id, so a commit still waits for the quiz, and it keeps growing while you work in silence. Someone who wants that gone wants a project or global change, and that is a different, deliberate decision — `set_config` returns a `note` saying so.
61
69
 
62
70
  Nothing is lost while a session is silent: concepts logged stay unmastered, and a later session offers them again once its own work and review debt are covered.
63
71
 
64
72
  ## Confirm
65
73
 
66
- 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.
74
+ Say the new state back in one line — `Questions: on. Focus: learn (caching). Cadence: interleaved. Difficulty: auto (easy on this repo). Memory: on.` — 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.
@@ -12,16 +12,20 @@ Two places, and the difference is the whole decision:
12
12
 
13
13
  | Where | Who it is for |
14
14
  |---|---|
15
- | `~/.eklavya/packs/*.json` | this developer, on every project |
16
- | `<repo>/.eklavya/packs/*.json` | everyone who works in this repository, versioned with it |
15
+ | `~/.eklavya/packs/*.json` | them, on every project |
16
+ | `~/.eklavya/projects/<checkout>/packs/*.json` | them, on this codebase |
17
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.
18
+ The project one is the interesting half. A pack that describes a codebase's own concepts and prerequisites is onboarding rather than quizzing. Ask which one they want before writing anything.
19
+
20
+ **Never write into the checkout.** Both directories are under the developer's home, and Eklavya creates no files in a project — not settings, not packs. A pack used to go to `<repo>/.eklavya/packs/`, committed; that directory is still **read**, so a repository that already ships one keeps working, but nothing writes there any more and nothing deletes what is there.
21
+
22
+ If they ask for a pack the whole team gets: say plainly that this no longer exists. A pack is theirs, on their machine. What a team can still do is keep the JSON in the repository as a file they each install — but Eklavya will not pick it up from there unless it is in the legacy directory, and it will not put it there.
19
23
 
20
24
  ## Build the pack in this order
21
25
 
22
26
  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
27
  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`.
28
+ 3. **List the concepts, from the code.** For a project 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
29
  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
30
  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
31
  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.
@@ -11,7 +11,7 @@ Render the learning map. Read-only: do not quiz, do not teach.
11
11
  1. `get_learner_profile` (no domain — you want everything).
12
12
  2. `get_concept_graph` with `include_mastery: true` only if you need a domain's
13
13
  shape to explain something the profile already flagged. Usually you do not.
14
- 3. `get_gate_status` if the mode is `enforced`.
14
+ 3. `get_gate_status` if `quiz.enforced` is set.
15
15
 
16
16
  ## What the report has to answer
17
17
 
@@ -22,8 +22,8 @@ The developer asked for this, so always pass `ignore_cooldown: true`. The quiz c
22
22
  - `already_covered` → "Everything from this session has already been asked about. `/eklavya:quiz <topic>` to go wider."
23
23
  - `nothing_logged` → "Nothing logged this session yet, so there's nothing grounded to ask about."
24
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
- - `session_off` → they turned Eklavya off for this session, and then asked for a quiz. Say so and offer the one step back: "Eklavya is off for this session — say the word and I'll turn it back on." Turn it back on with `set_config`, `scope: "session"`, `mode: "ambient"` if they agree, then run the quiz.
25
+ - `quiz_disabled` → "Questions are off for this project. Memory is still recording — `/eklavya:mode` turns the questions back on." Do not say "Eklavya is off": it is not, and saying so is what sent people hunting for a bug that was a setting.
26
+ - `session_off` → they silenced the questions for this session, and then asked for a quiz. Say so and offer the one step back: "Questions are off for this session — say the word and I'll turn them back on." Turn them back on with `set_config`, `scope: "session"`, `quiz: { enabled: true }` if they agree, then run the quiz.
27
27
  - `no_topic` → focus is `learn` with nothing set; ask what they want to learn, then `/eklavya:mode learn <topic>`.
28
28
  - `topic_unknown` → the graph has nothing matching their topic; offer the closest domain rather than inventing questions.
29
29