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.
- package/README.md +1 -1
- package/dist/assets/dashboard.html +570 -15
- package/dist/assets/tutor/SKILL.md +5 -5
- package/dist/assets/tutor/references/focus-and-level.md +34 -5
- package/dist/assets/tutor/references/grading.md +1 -1
- package/dist/cli.js +753 -41
- package/dist/cli.js.map +1 -1
- package/dist/config-path.js +156 -0
- package/dist/config-path.js.map +1 -0
- package/dist/config.js +448 -31
- package/dist/config.js.map +1 -1
- package/dist/dashboard.js +357 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/eval/retrieval-score.js +77 -0
- package/dist/eval/retrieval-score.js.map +1 -0
- package/dist/hooks/capture-tool.js +107 -0
- package/dist/hooks/capture-tool.js.map +1 -0
- package/dist/hooks/checkpoint-quiz.js +2 -2
- package/dist/hooks/checkpoint-quiz.js.map +1 -1
- package/dist/hooks/lib.js +4 -30
- package/dist/hooks/lib.js.map +1 -1
- package/dist/hooks/memory-lib.js +181 -0
- package/dist/hooks/memory-lib.js.map +1 -0
- package/dist/hooks/pre-tool-gate.js +2 -2
- package/dist/hooks/pre-tool-gate.js.map +1 -1
- package/dist/hooks/prompt-submit-nudge.js +57 -20
- package/dist/hooks/prompt-submit-nudge.js.map +1 -1
- package/dist/hooks/session-start.js +110 -80
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop-quiz-check.js +58 -38
- package/dist/hooks/stop-quiz-check.js.map +1 -1
- package/dist/hooks/subagent-start.js +2 -2
- package/dist/hooks/subagent-start.js.map +1 -1
- package/dist/install.js +37 -0
- package/dist/install.js.map +1 -1
- package/dist/memory/capture.js +123 -0
- package/dist/memory/capture.js.map +1 -0
- package/dist/memory/code.js +186 -0
- package/dist/memory/code.js.map +1 -0
- package/dist/memory/collections.js +87 -0
- package/dist/memory/collections.js.map +1 -0
- package/dist/memory/embed.js +81 -0
- package/dist/memory/embed.js.map +1 -0
- package/dist/memory/hosts.js +71 -0
- package/dist/memory/hosts.js.map +1 -0
- package/dist/memory/identity.js +74 -0
- package/dist/memory/identity.js.map +1 -0
- package/dist/memory/import.js +916 -0
- package/dist/memory/import.js.map +1 -0
- package/dist/memory/learning.js +148 -0
- package/dist/memory/learning.js.map +1 -0
- package/dist/memory/notify.js +220 -0
- package/dist/memory/notify.js.map +1 -0
- package/dist/memory/privacy.js +117 -0
- package/dist/memory/privacy.js.map +1 -0
- package/dist/memory/provider.js +155 -0
- package/dist/memory/provider.js.map +1 -0
- package/dist/memory/recall.js +327 -0
- package/dist/memory/recall.js.map +1 -0
- package/dist/memory/replay.js +174 -0
- package/dist/memory/replay.js.map +1 -0
- package/dist/memory/search.js +143 -0
- package/dist/memory/search.js.map +1 -0
- package/dist/memory/spool.js +93 -0
- package/dist/memory/spool.js.map +1 -0
- package/dist/memory/store.js +399 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/summarize.js +178 -0
- package/dist/memory/summarize.js.map +1 -0
- package/dist/memory/sync.js +595 -0
- package/dist/memory/sync.js.map +1 -0
- package/dist/memory/tokens.js +55 -0
- package/dist/memory/tokens.js.map +1 -0
- package/dist/memory/worker.js +207 -0
- package/dist/memory/worker.js.map +1 -0
- package/dist/migrations/009_memory.sql +226 -0
- package/dist/migrations/010_import.sql +26 -0
- package/dist/migrations/011_sync.sql +76 -0
- package/dist/migrations/012_job_backoff.sql +13 -0
- package/dist/migrations/013_batch_provenance.sql +18 -0
- package/dist/migrations/014_batch_events_index.sql +18 -0
- package/dist/packs.js +26 -12
- package/dist/packs.js.map +1 -1
- package/dist/paths.js +60 -0
- package/dist/paths.js.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/tutor.md +3 -1
- package/dist/plugin/cli/CLAUDE.md +32 -8
- package/dist/plugin/cli/eklavya-gate +80 -9
- package/dist/plugin/hooks/CLAUDE.md +44 -19
- package/dist/plugin/hooks/hooks.json +10 -0
- package/dist/plugin/scripts/install-git-hook.sh +2 -1
- package/dist/plugin/skills/CLAUDE.md +29 -17
- package/dist/plugin/skills/gate/SKILL.md +1 -1
- package/dist/plugin/skills/level/SKILL.md +3 -3
- package/dist/plugin/skills/memory/SKILL.md +88 -0
- package/dist/plugin/skills/mode/SKILL.md +24 -16
- package/dist/plugin/skills/pack/SKILL.md +8 -4
- package/dist/plugin/skills/progress/SKILL.md +1 -1
- package/dist/plugin/skills/quiz/SKILL.md +2 -2
- package/dist/plugin/skills/setup/SKILL.md +25 -11
- package/dist/plugin/skills/tutor/SKILL.md +5 -5
- package/dist/plugin/skills/tutor/references/focus-and-level.md +34 -5
- package/dist/plugin/skills/tutor/references/grading.md +1 -1
- package/dist/session.js +1 -1
- package/dist/statusline.js +20 -6
- package/dist/statusline.js.map +1 -1
- package/dist/store.js +2 -2
- package/dist/store.js.map +1 -1
- package/dist/surface.js +1 -1
- package/dist/time.js +41 -0
- package/dist/time.js.map +1 -0
- package/dist/tools/code_tools.js +73 -0
- package/dist/tools/code_tools.js.map +1 -0
- package/dist/tools/collection_tools.js +92 -0
- package/dist/tools/collection_tools.js.map +1 -0
- package/dist/tools/config_tools.js +165 -32
- package/dist/tools/config_tools.js.map +1 -1
- package/dist/tools/get_learner_profile.js +2 -1
- package/dist/tools/get_learner_profile.js.map +1 -1
- package/dist/tools/get_session_quiz_plan.js +15 -9
- package/dist/tools/get_session_quiz_plan.js.map +1 -1
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory_read_tools.js +277 -0
- package/dist/tools/memory_read_tools.js.map +1 -0
- package/dist/tools/memory_write_tools.js +115 -0
- package/dist/tools/memory_write_tools.js.map +1 -0
- package/dist/tools/types.js +1 -1
- package/dist/tools/types.js.map +1 -1
- package/dist/user-skill/eklavya/SKILL.md +95 -25
- 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
|
|
12
|
-
*
|
|
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
|
|
15
|
-
* and prerequisites is
|
|
16
|
-
*
|
|
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
|
|
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,
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"packs.js","sourceRoot":"","sources":["../src/packs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,YAAY,EAAkB,MAAM,WAAW,CAAC;AAsB5F;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAc;IAC/C,OAAO,qBAAqB,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AAC/G,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IAClD,MAAM,IAAI,GAA6C;QACrD,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE;KAC5D,CAAC;IACF,MAAM,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5E,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,aAAa,CAAC,GAAW;IAChC,IAAI,CAAC;QACH,OAAO,EAAE;aACN,WAAW,CAAC,GAAG,CAAC;aAChB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;aAClC,IAAI,EAAE;aACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;QAC9D,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IACnD,MAAM,GAAG,GAAiB,EAAE,CAAC;IAC7B,KAAK,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3C,KAAK,MAAM,IAAI,IAAI,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAS,CAAC;gBACjE,IAAI,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;oBACpD,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;gBAC/E,CAAC;gBACD,sEAAsE;gBACtE,uEAAuE;gBACvE,wEAAwE;gBACxE,kEAAkE;gBAClE,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC7E,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YAC1C,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACrF,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,KAAe,EAAE,cAAsB,YAAY;IACjF,MAAM,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACtC,2EAA2E;IAC3E,4EAA4E;IAC5E,2EAA2E;IAC3E,0EAA0E;IAC1E,0EAA0E;IAC1E,mBAAmB;IACnB,CAAC,CAAC,MAAM,CAAC,QAAQ,WAAW,IAAI,CAAC,CAAC;IAClC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,GAAoB,IAAI,CAAC;QACjC,IAAI,CAAC;YACH,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,gEAAgE;QAClE,CAAC;QACD,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,IAAI,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE,OAAO,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzB,CAAC;AAUD,yEAAyE;AACzE,MAAM,UAAU,UAAU,CAAC,EAAY,EAAE,MAAc,OAAO,CAAC,GAAG,EAAE;IAClE,OAAO,WAAW,CAAC,EAAE,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED,SAAS,WAAW,CAAC,EAAY,EAAE,MAAoB,EAAE,IAAc;IACrE,MAAM,OAAO,GAAgB,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAEhG,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAChB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,YAAY,EAAE,CAAC,CAAC;YAC9E,SAAS;QACX,CAAC;QACD,MAAM,CAAC,GAAG,cAAc,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACjD,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;QACrB,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC;QAC/B,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC;QACzB,OAAO,CAAC,YAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC;IACpE,CAAC;IAED,EAAE,CAAC,OAAO,CACR;2DACuD,CACxD,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAE5E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAY,EAAE,MAAc,OAAO,CAAC,GAAG,EAAE;IAC1E,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAC9B,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAE9E,CAAC;IACd,IAAI,GAAG,EAAE,KAAK,KAAK,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3E,OAAO,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC"}
|
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.
|
|
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
|
|
24
|
-
`"mode": "enforced"
|
|
25
|
-
`~/.eklavya/config.json` —
|
|
26
|
-
|
|
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
|
|
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
|
-
- `
|
|
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.
|
|
55
|
-
|
|
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
|
-
#
|
|
13
|
-
# That is deliberate: a git hook installed once must not
|
|
14
|
-
# because someone flipped a global setting. Gating a
|
|
15
|
-
#
|
|
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
|
-
#
|
|
26
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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
|
|
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` |
|
|
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
|
-
|
|
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
|
|
175
|
-
time. Two tests encoded it as intended behaviour.
|
|
176
|
-
— along with its session-scoped twin
|
|
177
|
-
shorter life.
|
|
178
|
-
|
|
179
|
-
## The Stop hook blocks
|
|
180
|
-
|
|
181
|
-
Commonly got wrong.
|
|
182
|
-
returns 2
|
|
183
|
-
the `min_minutes_between_quizzes` cooldown is skipped (a cooldown could
|
|
184
|
-
commit gate unpassable — decision G5); the one-question cap under
|
|
185
|
-
is lifted, so the sweep asks for the whole remaining budget; and
|
|
186
|
-
plus `cli/eklavya-gate` start holding commits. Only
|
|
187
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
`
|
|
69
|
-
`get_gate_status`, `upsert_concepts`, `get_concept_graph`,
|
|
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
|
|
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' &&
|
|
109
|
+
`capped = cadence === 'interleaved' && !quiz.enforced && !explicitTopic`,
|
|
100
110
|
and then `max = args.max ?? (capped ? 1 : max_questions_per_task)`.
|
|
101
|
-
So `enforced`
|
|
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
|
|
105
|
-
blocks
|
|
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 "
|
|
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]
|
|
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
|
|
130
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
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
|