eklavya 1.18.3 → 1.19.1

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 (132) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/dashboard.html +570 -15
  3. package/dist/assets/tutor/SKILL.md +5 -5
  4. package/dist/assets/tutor/references/focus-and-level.md +34 -5
  5. package/dist/assets/tutor/references/grading.md +1 -1
  6. package/dist/cli.js +753 -41
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config-path.js +156 -0
  9. package/dist/config-path.js.map +1 -0
  10. package/dist/config.js +448 -31
  11. package/dist/config.js.map +1 -1
  12. package/dist/dashboard.js +357 -1
  13. package/dist/dashboard.js.map +1 -1
  14. package/dist/eval/retrieval-score.js +77 -0
  15. package/dist/eval/retrieval-score.js.map +1 -0
  16. package/dist/hooks/capture-tool.js +107 -0
  17. package/dist/hooks/capture-tool.js.map +1 -0
  18. package/dist/hooks/checkpoint-quiz.js +2 -2
  19. package/dist/hooks/checkpoint-quiz.js.map +1 -1
  20. package/dist/hooks/lib.js +4 -30
  21. package/dist/hooks/lib.js.map +1 -1
  22. package/dist/hooks/memory-lib.js +181 -0
  23. package/dist/hooks/memory-lib.js.map +1 -0
  24. package/dist/hooks/pre-tool-gate.js +2 -2
  25. package/dist/hooks/pre-tool-gate.js.map +1 -1
  26. package/dist/hooks/prompt-submit-nudge.js +57 -20
  27. package/dist/hooks/prompt-submit-nudge.js.map +1 -1
  28. package/dist/hooks/session-start.js +110 -80
  29. package/dist/hooks/session-start.js.map +1 -1
  30. package/dist/hooks/stop-quiz-check.js +58 -38
  31. package/dist/hooks/stop-quiz-check.js.map +1 -1
  32. package/dist/hooks/subagent-start.js +2 -2
  33. package/dist/hooks/subagent-start.js.map +1 -1
  34. package/dist/install.js +37 -0
  35. package/dist/install.js.map +1 -1
  36. package/dist/memory/capture.js +123 -0
  37. package/dist/memory/capture.js.map +1 -0
  38. package/dist/memory/code.js +186 -0
  39. package/dist/memory/code.js.map +1 -0
  40. package/dist/memory/collections.js +87 -0
  41. package/dist/memory/collections.js.map +1 -0
  42. package/dist/memory/embed.js +81 -0
  43. package/dist/memory/embed.js.map +1 -0
  44. package/dist/memory/hosts.js +71 -0
  45. package/dist/memory/hosts.js.map +1 -0
  46. package/dist/memory/identity.js +74 -0
  47. package/dist/memory/identity.js.map +1 -0
  48. package/dist/memory/import.js +916 -0
  49. package/dist/memory/import.js.map +1 -0
  50. package/dist/memory/learning.js +148 -0
  51. package/dist/memory/learning.js.map +1 -0
  52. package/dist/memory/notify.js +220 -0
  53. package/dist/memory/notify.js.map +1 -0
  54. package/dist/memory/privacy.js +117 -0
  55. package/dist/memory/privacy.js.map +1 -0
  56. package/dist/memory/provider.js +155 -0
  57. package/dist/memory/provider.js.map +1 -0
  58. package/dist/memory/recall.js +327 -0
  59. package/dist/memory/recall.js.map +1 -0
  60. package/dist/memory/replay.js +174 -0
  61. package/dist/memory/replay.js.map +1 -0
  62. package/dist/memory/search.js +143 -0
  63. package/dist/memory/search.js.map +1 -0
  64. package/dist/memory/spool.js +93 -0
  65. package/dist/memory/spool.js.map +1 -0
  66. package/dist/memory/store.js +399 -0
  67. package/dist/memory/store.js.map +1 -0
  68. package/dist/memory/summarize.js +178 -0
  69. package/dist/memory/summarize.js.map +1 -0
  70. package/dist/memory/sync.js +595 -0
  71. package/dist/memory/sync.js.map +1 -0
  72. package/dist/memory/tokens.js +55 -0
  73. package/dist/memory/tokens.js.map +1 -0
  74. package/dist/memory/worker.js +207 -0
  75. package/dist/memory/worker.js.map +1 -0
  76. package/dist/migrations/009_memory.sql +226 -0
  77. package/dist/migrations/010_import.sql +26 -0
  78. package/dist/migrations/011_sync.sql +76 -0
  79. package/dist/migrations/012_job_backoff.sql +13 -0
  80. package/dist/migrations/013_batch_provenance.sql +18 -0
  81. package/dist/migrations/014_batch_events_index.sql +18 -0
  82. package/dist/packs.js +26 -12
  83. package/dist/packs.js.map +1 -1
  84. package/dist/paths.js +60 -0
  85. package/dist/paths.js.map +1 -1
  86. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  87. package/dist/plugin/agents/tutor.md +3 -1
  88. package/dist/plugin/cli/CLAUDE.md +32 -8
  89. package/dist/plugin/cli/eklavya-gate +80 -9
  90. package/dist/plugin/hooks/CLAUDE.md +44 -19
  91. package/dist/plugin/hooks/hooks.json +10 -0
  92. package/dist/plugin/scripts/install-git-hook.sh +2 -1
  93. package/dist/plugin/skills/CLAUDE.md +29 -17
  94. package/dist/plugin/skills/gate/SKILL.md +1 -1
  95. package/dist/plugin/skills/level/SKILL.md +3 -3
  96. package/dist/plugin/skills/memory/SKILL.md +88 -0
  97. package/dist/plugin/skills/mode/SKILL.md +24 -16
  98. package/dist/plugin/skills/pack/SKILL.md +8 -4
  99. package/dist/plugin/skills/progress/SKILL.md +1 -1
  100. package/dist/plugin/skills/quiz/SKILL.md +2 -2
  101. package/dist/plugin/skills/setup/SKILL.md +25 -11
  102. package/dist/plugin/skills/tutor/SKILL.md +5 -5
  103. package/dist/plugin/skills/tutor/references/focus-and-level.md +34 -5
  104. package/dist/plugin/skills/tutor/references/grading.md +1 -1
  105. package/dist/session.js +1 -1
  106. package/dist/statusline.js +20 -6
  107. package/dist/statusline.js.map +1 -1
  108. package/dist/store.js +2 -2
  109. package/dist/store.js.map +1 -1
  110. package/dist/surface.js +1 -1
  111. package/dist/time.js +41 -0
  112. package/dist/time.js.map +1 -0
  113. package/dist/tools/code_tools.js +73 -0
  114. package/dist/tools/code_tools.js.map +1 -0
  115. package/dist/tools/collection_tools.js +92 -0
  116. package/dist/tools/collection_tools.js.map +1 -0
  117. package/dist/tools/config_tools.js +165 -32
  118. package/dist/tools/config_tools.js.map +1 -1
  119. package/dist/tools/get_learner_profile.js +2 -1
  120. package/dist/tools/get_learner_profile.js.map +1 -1
  121. package/dist/tools/get_session_quiz_plan.js +15 -9
  122. package/dist/tools/get_session_quiz_plan.js.map +1 -1
  123. package/dist/tools/index.js +15 -0
  124. package/dist/tools/index.js.map +1 -1
  125. package/dist/tools/memory_read_tools.js +277 -0
  126. package/dist/tools/memory_read_tools.js.map +1 -0
  127. package/dist/tools/memory_write_tools.js +115 -0
  128. package/dist/tools/memory_write_tools.js.map +1 -0
  129. package/dist/tools/types.js +1 -1
  130. package/dist/tools/types.js.map +1 -1
  131. package/dist/user-skill/eklavya/SKILL.md +95 -25
  132. package/package.json +2 -1
package/dist/packs.js CHANGED
@@ -8,23 +8,31 @@
8
8
  * mid-session. A pack is the same file shape, loaded from two places Eklavya
9
9
  * does not own:
10
10
  *
11
- * ~/.eklavya/packs/*.json the learner's own, and anything installed
12
- * <repo>/.eklavya/packs/*.json a team's, versioned with the codebase
11
+ * ~/.eklavya/packs/*.json yours, everywhere
12
+ * ~/.eklavya/projects/<slug>/packs/*.json yours, on one codebase
13
+ * <repo>/.eklavya/packs/*.json read-only, the pre-move home
13
14
  *
14
- * The second is the interesting one. A repository that ships its own concepts
15
- * and prerequisites is describing itself to a new joiner, which is onboarding
16
- * rather than quizzing.
15
+ * The second is the interesting one. A pack that describes a codebase's own
16
+ * concepts and prerequisites is onboarding rather than quizzing.
17
+ *
18
+ * It used to be the third, committed alongside the code, and that was the last
19
+ * thing Eklavya wrote into a repository. The argument for it was real — unlike
20
+ * settings, a concept graph genuinely is the same for everyone — but it lost to
21
+ * a simpler rule: Eklavya creates no files in your project. The old directory is
22
+ * still **read**, so a repository that already ships a pack keeps working, and
23
+ * nothing deletes it: a committed pack is authored content somebody reviewed,
24
+ * which is not ours to relocate. Only the writers moved.
17
25
  *
18
26
  * Packs are applied AFTER the seed and merged over it, so a pack may retier or
19
- * rename a shipped concept, and a repo pack wins over a global one. Nothing
27
+ * rename a shipped concept, and a project pack wins over a global one. Nothing
20
28
  * here touches `mastery`: the seed's rule is the pack's rule, and a learner's
21
29
  * history survives any pack being installed, edited or deleted.
22
30
  */
23
31
  import fs from 'node:fs';
24
32
  import path from 'node:path';
25
33
  import crypto from 'node:crypto';
26
- import { eklavyaHome } from './paths.js';
27
- import { findRepoConfig } from './config.js';
34
+ import { eklavyaHome, projectPacksDir } from './paths.js';
35
+ import { findRepoConfig, mainRepoRoot } from './config.js';
28
36
  import { applySeedGraph, validateSeedGraph, SEED_VERSION } from './seed.js';
29
37
  /**
30
38
  * One row per set of pack directories, not one row for the install.
@@ -38,17 +46,23 @@ export function packFingerprintKey(dirs) {
38
46
  return `packs_fingerprint:${crypto.createHash('sha256').update(dirs.join('\n')).digest('hex').slice(0, 16)}`;
39
47
  }
40
48
  /**
41
- * Global first, repo second, because later wins. `packs/` under both, so the
42
- * rule is one sentence: packs live in a `packs/` directory next to Eklavya's
43
- * state, whether that state is yours or the repository's.
49
+ * Global first, then the project's, because later wins.
50
+ *
51
+ * The legacy in-repo directory sits between them: after global, so a pack a
52
+ * repository already ships still overrides your own, and before the project
53
+ * directory, so moving a pack out of the checkout takes effect rather than
54
+ * being silently outranked by the copy left behind.
44
55
  */
45
56
  export function packDirs(cwd = process.cwd()) {
46
57
  const dirs = [
47
58
  { dir: path.join(eklavyaHome(), 'packs'), scope: 'global' },
48
59
  ];
49
60
  const { repoRoot } = findRepoConfig(cwd);
50
- if (repoRoot)
61
+ if (repoRoot) {
62
+ const project = mainRepoRoot(repoRoot);
51
63
  dirs.push({ dir: path.join(repoRoot, '.eklavya', 'packs'), scope: 'repo' });
64
+ dirs.push({ dir: projectPacksDir(project), scope: 'project' });
65
+ }
52
66
  return dirs;
53
67
  }
54
68
  function listPackFiles(dir) {
package/dist/packs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"packs.js","sourceRoot":"","sources":["../src/packs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"packs.js","sourceRoot":"","sources":["../src/packs.ts"],"names":[],"mappings":"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"}
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.18.3",
4
+ "version": "1.19.1",
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"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eklavya-tutor
3
3
  description: Teaches the developer the concepts behind work being done, using the Eklavya knowledge graph. Use when you want tutoring to run alongside implementation rather than after it, or when the main thread is busy building and someone should be explaining.
4
- tools: Read, Grep, Glob, mcp__plugin_eklavya_eklavya__get_learner_profile, mcp__plugin_eklavya_eklavya__get_session_quiz_plan, mcp__plugin_eklavya_eklavya__record_attempt, mcp__plugin_eklavya_eklavya__get_concept_graph, mcp__plugin_eklavya_eklavya__upsert_concepts, mcp__plugin_eklavya_eklavya__get_gate_status, mcp__eklavya__get_learner_profile, mcp__eklavya__get_session_quiz_plan, mcp__eklavya__record_attempt, mcp__eklavya__get_concept_graph, mcp__eklavya__upsert_concepts, mcp__eklavya__get_gate_status
4
+ tools: Read, Grep, Glob, mcp__plugin_eklavya_eklavya__get_learner_profile, mcp__plugin_eklavya_eklavya__get_session_quiz_plan, mcp__plugin_eklavya_eklavya__record_attempt, mcp__plugin_eklavya_eklavya__get_concept_graph, mcp__plugin_eklavya_eklavya__upsert_concepts, mcp__plugin_eklavya_eklavya__get_gate_status, mcp__plugin_eklavya_eklavya__memory_search, mcp__plugin_eklavya_eklavya__memory_get, mcp__plugin_eklavya_eklavya__memory_file_history, mcp__eklavya__get_learner_profile, mcp__eklavya__get_session_quiz_plan, mcp__eklavya__record_attempt, mcp__eklavya__get_concept_graph, mcp__eklavya__upsert_concepts, mcp__eklavya__get_gate_status, mcp__eklavya__memory_search, mcp__eklavya__memory_get, mcp__eklavya__memory_file_history
5
5
  ---
6
6
 
7
7
  You are Eklavya's tutor, running as a separate agent while implementation happens elsewhere.
@@ -10,6 +10,8 @@ Follow the `tutor` skill for all pedagogy — profile first, one question at a t
10
10
 
11
11
  Note that "grounded in real code" is `project` focus, not a universal rule: in `concept` focus the plan hands you `context: null` deliberately, and quoting the diff back defeats the point. Read `framing` before writing a question.
12
12
 
13
+ You also have the read-only memory tools, and in parallel they matter more than they do in the main thread: you cannot see the conversation the developer is having, so the project's recorded history is the only way you know what this codebase has actually done. `references/focus-and-level.md` has the rule for using it — briefly, and only when the plan left you without a concrete example.
14
+
13
15
  ## Why the tool list is spelled twice
14
16
 
15
17
  An MCP server provided by a plugin is namespaced: its tools resolve as
@@ -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
@@ -30,18 +30,42 @@ 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 six hooks, out of `hooks.json`
33
+ ## The seven hooks, out of `hooks.json`
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>`), 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, print the profile banner and the standing log directive |
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
+ | PostToolUse | — | 10s | `capture-tool` | record the tool use as memory evidence |
41
42
  | PostToolUse | `mcp__.*log_session_concepts` | 10s | `checkpoint-quiz` | one mid-task question, `interleaved` cadence only |
42
43
  | Stop | — | 15s | `stop-quiz-check` | block the turn and demand a quiz |
43
44
 
44
- The PostToolUse matcher is a regex over the MCP tool name, not a literal, because
45
+ ## Memory capture is not governed by `quiz`
46
+
47
+ `capture-tool` has no matcher, so it runs after **every** tool call — it is the
48
+ hook that fires most often in a session, and it does the least: resolve
49
+ identity, normalise one event, one insert, exit. It never quizzes, summarises
50
+ or calls a provider.
51
+
52
+ Four hooks now carry memory work, and all four do it **before** their `quiz`
53
+ check, because `memory.enabled` is a separate decision from the learning dials
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.
57
+ `session-start` replays the spool and recalls; `prompt-submit-nudge` captures
58
+ the prompt; `capture-tool` captures the tool use; `stop-quiz-check` closes the
59
+ batch at the seam. `mcp/src/hooks/memory-lib.ts` holds the shared helpers, and
60
+ every one of them swallows its own failures — a capture path that throws is a
61
+ throw on every tool call.
62
+
63
+ The seam never waits on inference. With `providers.observer` configured,
64
+ `flushAtSeam` queues the batch and returns; the next session start, or
65
+ `eklavya memory process`, drains it. A Stop hook that waits on an API call is
66
+ exactly what PRD LRN-04 forbids.
67
+
68
+ The PostToolUse matcher on `checkpoint-quiz` is a regex over the MCP tool name, not a literal, because
45
69
  the prefix depends on how the plugin was installed —
46
70
  `mcp__eklavya__log_session_concepts` standalone,
47
71
  `mcp__plugin_eklavya_eklavya__log_session_concepts` via `/plugin`. `mcp__.*`
@@ -171,20 +195,21 @@ reads rather than output anyone sees.
171
195
 
172
196
  That was a bug for a while, and a bad one: `session-start` returned before
173
197
  pushing the directive, so a developer who turned the greeting off logged
174
- nothing, was never quizzed, and saw `mode: ambient` in `get_config` the whole
175
- time. Two tests encoded it as intended behaviour. `mode: off` is the off switch
176
- — along with its session-scoped twin below, which is the same switch with a
177
- shorter life.
178
-
179
- ## The Stop hook blocks in `ambient` too
180
-
181
- Commonly got wrong. `ambient` is not "never interrupts" — `stop-quiz-check.ts`
182
- returns 2 in ambient as readily as in enforced. `enforced` changes three things:
183
- the `min_minutes_between_quizzes` cooldown is skipped (a cooldown could make a
184
- commit gate unpassable — decision G5); the one-question cap under `interleaved`
185
- is lifted, so the sweep asks for the whole remaining budget; and `pre-tool-gate`
186
- plus `cli/eklavya-gate` start holding commits. Only `mode === 'off'` silences
187
- everything.
198
+ nothing, was never quizzed, and saw quizzing reported as enabled in
199
+ `get_config` the whole time. Two tests encoded it as intended behaviour.
200
+ `quiz.enabled: false` is the off switch — along with its session-scoped twin
201
+ below, which is the same switch with a shorter life.
202
+
203
+ ## The Stop hook blocks when unenforced too
204
+
205
+ Commonly got wrong. Unenforced is not "never interrupts" — `stop-quiz-check.ts`
206
+ returns 2 without the gate as readily as with it. `quiz.enforced` changes three
207
+ things: the `min_minutes_between_quizzes` cooldown is skipped (a cooldown could
208
+ make a commit gate unpassable — decision G5); the one-question cap under
209
+ `interleaved` is lifted, so the sweep asks for the whole remaining budget; and
210
+ `pre-tool-gate` plus `cli/eklavya-gate` start holding commits. Only
211
+ `quiz.enabled: false` silences the questions — and it silences only those, not
212
+ the memory half.
188
213
 
189
214
  ## The per-session off switch
190
215
 
@@ -198,7 +223,7 @@ config file has quietly turned the product off for good.
198
223
 
199
224
  Two rules it is easy to get wrong:
200
225
 
201
- - It silences, it does not exempt. `cli/eklavya-gate` reads `.eklavya.json` and
226
+ - It silences, it does not exempt. `cli/eklavya-gate` reads the project config and
202
227
  never sees a session id, so an enforced repo still holds the commit. Making
203
228
  the gate honour it would turn a per-session convenience into a gate bypass.
204
229
  `pre-tool-gate` reads `isSessionOff` for one reason only: its refusal tells
@@ -51,6 +51,16 @@
51
51
  }
52
52
  ],
53
53
  "PostToolUse": [
54
+ {
55
+ "hooks": [
56
+ {
57
+ "type": "command",
58
+ "command": "node",
59
+ "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/run.mjs", "capture-tool"],
60
+ "timeout": 10
61
+ }
62
+ ]
63
+ },
54
64
  {
55
65
  "matcher": "mcp__.*log_session_concepts",
56
66
  "hooks": [
@@ -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'
@@ -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
- Eight have it, and they are the eight slash commands:
14
+ Nine have it, and they are the nine slash commands:
15
15
 
16
- `gate`, `learn`, `level`, `mode`, `pack`, `progress`, `quiz`, `setup`.
16
+ `gate`, `learn`, `level`, `memory`, `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
@@ -64,10 +64,18 @@ by what it is, not loaded by trigger.
64
64
 
65
65
  ## A skill is a prompt, but it is also an API client
66
66
 
67
- Nine tools, all in `mcp/src/tools/`: `get_learner_profile`,
68
- `log_session_concepts`, `get_session_quiz_plan`, `record_attempt`,
69
- `get_gate_status`, `upsert_concepts`, `get_concept_graph`, `get_config`,
70
- `set_config`.
67
+ Twenty tools, all in `mcp/src/tools/`. Nine for learning:
68
+ `get_learner_profile`, `log_session_concepts`, `get_session_quiz_plan`,
69
+ `record_attempt`, `get_gate_status`, `upsert_concepts`, `get_concept_graph`,
70
+ `get_config`, `set_config`. Eleven for memory: `memory_search`, `memory_get`,
71
+ `memory_timeline`, `memory_file_history`, `memory_status`, `memory_write`,
72
+ `memory_correct`, `memory_delete`, `memory_collections`, `code_outline`,
73
+ `code_find_symbol`.
74
+
75
+ The memory tools are two-stage on purpose: the index tools return identifiers
76
+ and titles, and `memory_get` is the only one that returns a narrative. A skill
77
+ that tells the model to hydrate everything a search returned spends exactly
78
+ the context the feature exists to save. Say "search, choose, then get".
71
79
 
72
80
  **Before you write "call `X` with `Y`", open `mcp/src/tools/<X>.ts`.** Check
73
81
  `Y` is in the `inputSchema`, and check the field you are telling the model to
@@ -75,7 +83,9 @@ read is in what the handler returns. A skill that names a field the server
75
83
  never returns fails silently — the model improvises a plausible value and the
76
84
  developer sees a confident number nobody computed.
77
85
 
78
- `config_tools.ts` holds `get_config` and `set_config`; the rest are one file
86
+ `config_tools.ts` holds `get_config` and `set_config`, and the memory tools are
87
+ grouped the same way — `memory_read_tools.ts`, `memory_write_tools.ts`,
88
+ `code_tools.ts`, `collection_tools.ts`. The nine learning tools are one file
79
89
  per tool name.
80
90
 
81
91
  ### Not every tool takes `session_id`
@@ -96,16 +106,16 @@ Check every one of these against the code when you touch a skill.
96
106
  write it: `grep -rn 'focus' skills/ user-skill/ agents/`.
97
107
  - **The `interleaved` one-question cap has exemptions.** In
98
108
  `mcp/src/tools/get_session_quiz_plan.ts`:
99
- `capped = cadence === 'interleaved' && mode !== 'enforced' && !explicitTopic`,
109
+ `capped = cadence === 'interleaved' && !quiz.enforced && !explicitTopic`,
100
110
  and then `max = args.max ?? (capped ? 1 : max_questions_per_task)`.
101
- 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
102
112
  an explicit `max` wins outright because it is read first. Read that code
103
113
  rather than trusting prose about it — including this paragraph.
104
- - **The Stop hook blocks in `ambient` too.** `mcp/src/hooks/stop-quiz-check.ts`
105
- 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
106
116
  `min_minutes_between_quizzes` cooldown, takes the whole remaining budget
107
- instead of one question, and gates commits. Do not write "ambient never
108
- interrupts".
117
+ instead of one question, and gates commits. Do not write "it never
118
+ interrupts unless enforced".
109
119
  - **`get_learner_profile`'s lists are capped, and `known` is ordered by score.**
110
120
  `LIST_CAP = 8` covers `weak`, `due_for_review`, `projects`,
111
121
  `recent_concepts` and `skipped`; `KNOWN_CAP = 30` covers `known`, with the
@@ -120,14 +130,16 @@ Check every one of these against the code when you touch a skill.
120
130
 
121
131
  Until 1.14 every plan item carried `ask_header` and the tutor printed it above
122
132
  the question: `[mode: ambient · focus: concept · level: easy · tier: 2
123
- 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
124
135
  transferable question reads as a vague one, and on `easy` a tier-2 question
125
136
  reads as shallow rather than as a runway — but it spent four settings' worth of
126
137
  screen above *every* stem to say something that is true for the whole session.
127
138
 
128
139
  Ambient state belongs somewhere ambient. `statusLine` in `mcp/src/statusline.ts`
129
- composes `[EKLAVYA ambient · concept · interleaved · easy]` for
130
- `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,
131
143
  the plan no longer carries `ask_header`, and both hooks now say *ask the stem on
132
144
  its own*.
133
145
 
@@ -277,7 +289,7 @@ renderers that lack it.
277
289
  ## Consistency
278
290
 
279
291
  The same behaviour described in two skills has drifted apart before — that is
280
- how `focus: project` got into three files. The four dials appear in
292
+ how `focus: project` got into three files. The dials appear in
281
293
  `skills/mode/SKILL.md` and `user-skill/eklavya/SKILL.md`; the level bands
282
294
  appear in `skills/level/SKILL.md` and `tutor/references/focus-and-level.md`;
283
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