eklavya 1.19.0 → 1.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/assets/dashboard.html +11 -1
- package/dist/assets/tutor/SKILL.md +5 -5
- package/dist/assets/tutor/references/focus-and-level.md +5 -5
- package/dist/assets/tutor/references/grading.md +1 -1
- package/dist/cli.js +126 -42
- package/dist/cli.js.map +1 -1
- package/dist/config.js +260 -72
- package/dist/config.js.map +1 -1
- package/dist/dashboard.js +2 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/hooks/checkpoint-quiz.js +6 -4
- package/dist/hooks/checkpoint-quiz.js.map +1 -1
- 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 +7 -6
- package/dist/hooks/prompt-submit-nudge.js.map +1 -1
- package/dist/hooks/session-start.js +76 -22
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop-quiz-check.js +32 -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/memory/sync.js +1 -1
- 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/cli/CLAUDE.md +32 -8
- package/dist/plugin/cli/eklavya-gate +80 -9
- package/dist/plugin/hooks/CLAUDE.md +40 -21
- package/dist/plugin/scripts/install-git-hook.sh +2 -1
- package/dist/plugin/skills/CLAUDE.md +12 -10
- package/dist/plugin/skills/gate/SKILL.md +1 -1
- package/dist/plugin/skills/level/SKILL.md +3 -3
- 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 +12 -12
- package/dist/plugin/skills/tutor/SKILL.md +5 -5
- package/dist/plugin/skills/tutor/references/focus-and-level.md +5 -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/tools/config_tools.js +82 -52
- 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/types.js +1 -1
- package/dist/tools/types.js.map +1 -1
- package/dist/user-skill/eklavya/SKILL.md +35 -21
- package/package.json +1 -1
package/dist/paths.js
CHANGED
|
@@ -22,4 +22,64 @@ export function migrationsDir() {
|
|
|
22
22
|
export function seedDir() {
|
|
23
23
|
return path.join(moduleDir, 'seed');
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Per-project settings, kept **outside** the checkout.
|
|
27
|
+
*
|
|
28
|
+
* Eklavya used to read `<repo>/.eklavya.json`, a file you committed. That was
|
|
29
|
+
* wrong in the ordinary case: a project's settings are how *you* want to be
|
|
30
|
+
* taught in that codebase, and a committed file makes one person's choice
|
|
31
|
+
* everybody's. It also made every clone a configuration file from a stranger,
|
|
32
|
+
* which cost a whole security boundary to contain.
|
|
33
|
+
*
|
|
34
|
+
* So project settings live beside the global ones, keyed by the checkout's
|
|
35
|
+
* absolute path, the way `~/.claude/projects/` does it:
|
|
36
|
+
*
|
|
37
|
+
* ~/.eklavya/config.json you, everywhere
|
|
38
|
+
* ~/.eklavya/projects/<slug>/config.json you, on one project
|
|
39
|
+
*
|
|
40
|
+
* Nothing Eklavya writes ever lands in a repository again — concept packs
|
|
41
|
+
* included; see `projectPacksDir` below, which moved for the same reason. The
|
|
42
|
+
* pre-move directories are still *read* so nothing breaks on upgrade, and the
|
|
43
|
+
* settings file is deleted from the checkout while a pack is not, because a
|
|
44
|
+
* committed pack is authored content somebody reviewed.
|
|
45
|
+
*/
|
|
46
|
+
export function projectsDir() {
|
|
47
|
+
return path.join(eklavyaHome(), 'projects');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The directory name for a checkout: its absolute path with the separators
|
|
51
|
+
* turned into dashes, which is what `~/.claude/projects/` does and what makes
|
|
52
|
+
* the directory identifiable at a glance. Readability is the whole point —
|
|
53
|
+
* somebody looking for "the settings for this repo" has to be able to find
|
|
54
|
+
* them without a lookup table.
|
|
55
|
+
*
|
|
56
|
+
* It is lossy, and deliberately so: `/a/b-c` and `/a-b/c` both slug to
|
|
57
|
+
* `-a-b-c`. The collision is caught rather than avoided — `projectConfigPath`'s
|
|
58
|
+
* file records the path it belongs to, and `loadConfig` ignores a file whose
|
|
59
|
+
* `project` names a different checkout. A rare wrong-directory read is worth a
|
|
60
|
+
* name you can recognise; a silent one would not be.
|
|
61
|
+
*/
|
|
62
|
+
export function projectSlug(repoRoot) {
|
|
63
|
+
return repoRoot.replace(/[/\\:]/g, '-');
|
|
64
|
+
}
|
|
65
|
+
export function projectConfigPath(repoRoot) {
|
|
66
|
+
return path.join(projectsDir(), projectSlug(repoRoot), 'config.json');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A project's concept packs, kept outside the checkout like its settings.
|
|
70
|
+
*
|
|
71
|
+
* Packs used to live at `<repo>/.eklavya/packs/`, committed, and that was the
|
|
72
|
+
* last thing Eklavya wrote into a repository. The argument for keeping them
|
|
73
|
+
* there was real — a pack is a shared concept graph, the same for everyone —
|
|
74
|
+
* but it lost to a simpler rule: Eklavya creates no files in your project.
|
|
75
|
+
*
|
|
76
|
+
* `loadPacks` still *reads* the old location, so a repository that already
|
|
77
|
+
* ships one keeps working, and nothing deletes it. That asymmetry with the
|
|
78
|
+
* settings file is deliberate: a settings file was a mistake to undo, while a
|
|
79
|
+
* committed pack is authored content somebody reviewed, and relocating it is
|
|
80
|
+
* their decision rather than ours.
|
|
81
|
+
*/
|
|
82
|
+
export function projectPacksDir(repoRoot) {
|
|
83
|
+
return path.join(projectsDir(), projectSlug(repoRoot), 'packs');
|
|
84
|
+
}
|
|
25
85
|
//# sourceMappingURL=paths.js.map
|
package/dist/paths.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,MAAM;IACpB,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,aAAa,CAAC,CAAC;AACjD,CAAC;AAED,0EAA0E;AAC1E,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE/D,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AACtC,CAAC"}
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;GAGG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,MAAM;IACpB,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,aAAa,CAAC,CAAC;AACjD,CAAC;AAED,0EAA0E;AAC1E,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE/D,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,WAAW;IACzB,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,UAAU,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,OAAO,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;AAClE,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eklavya",
|
|
3
3
|
"displayName": "Eklavya",
|
|
4
|
-
"version": "1.19.
|
|
4
|
+
"version": "1.19.2",
|
|
5
5
|
"description": "Learn while your agent works. Turns coding-agent generation time into adaptive, Socratic learning grounded in the code being written.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Ajay Kumar"
|
|
@@ -20,10 +20,22 @@ between the `# >>> eklavya gate >>>` markers, and if a `pre-commit` already
|
|
|
20
20
|
existed it moves it to `pre-commit.local` and chains it first. `--uninstall`
|
|
21
21
|
restores it.
|
|
22
22
|
|
|
23
|
-
Then the script itself only acts on a
|
|
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
|
|
@@ -34,24 +34,26 @@ else.
|
|
|
34
34
|
|
|
35
35
|
| Event | Matcher | Timeout | Script | Job |
|
|
36
36
|
|---|---|---|---|---|
|
|
37
|
-
| SessionStart | — | 10s | `session-start` | stamp this checkout's session pointer (`meta.current_session:<repo root>`), replay the spool, summarise the last session's batch, recall this project's memory,
|
|
37
|
+
| SessionStart | — | 10s | `session-start` | stamp this checkout's session pointer (`meta.current_session:<repo root>`), replay the spool, summarise the last session's batch, recall this project's memory, show the developer the profile banner (`systemMessage`) and hand the model the recall and the standing log directive (`additionalContext`) |
|
|
38
38
|
| UserPromptSubmit | — | 10s | `prompt-submit-nudge` | re-stamp this checkout's session pointer, then re-state the log directive in one line, but only for a session that has logged nothing after a grace window |
|
|
39
39
|
| SubagentStart | — | 10s | `subagent-start` | give a delegated agent the log directive the parent's SessionStart never reached it with |
|
|
40
|
-
| PreToolUse | `Bash` | 10s | `pre-tool-gate` |
|
|
40
|
+
| PreToolUse | `Bash` | 10s | `pre-tool-gate` | with `quiz.enforced` only, deny a `git commit` whose session gate has not passed |
|
|
41
41
|
| PostToolUse | — | 10s | `capture-tool` | record the tool use as memory evidence |
|
|
42
42
|
| PostToolUse | `mcp__.*log_session_concepts` | 10s | `checkpoint-quiz` | one mid-task question, `interleaved` cadence only |
|
|
43
43
|
| Stop | — | 15s | `stop-quiz-check` | block the turn and demand a quiz |
|
|
44
44
|
|
|
45
|
-
## Memory capture is not governed by `
|
|
45
|
+
## Memory capture is not governed by `quiz`
|
|
46
46
|
|
|
47
47
|
`capture-tool` has no matcher, so it runs after **every** tool call — it is the
|
|
48
48
|
hook that fires most often in a session, and it does the least: resolve
|
|
49
49
|
identity, normalise one event, one insert, exit. It never quizzes, summarises
|
|
50
50
|
or calls a provider.
|
|
51
51
|
|
|
52
|
-
Four hooks now carry memory work, and all four do it **before** their `
|
|
52
|
+
Four hooks now carry memory work, and all four do it **before** their `quiz`
|
|
53
53
|
check, because `memory.enabled` is a separate decision from the learning dials
|
|
54
|
-
(PRD CFG-01): `
|
|
54
|
+
(PRD CFG-01): `quiz.enabled: false` means no quizzes, not no project history.
|
|
55
|
+
That separation is why the `mode` dial was retired — it was always true and the
|
|
56
|
+
word `off` denied it, so `session-start` now says on screen which half stopped.
|
|
55
57
|
`session-start` replays the spool and recalls; `prompt-submit-nudge` captures
|
|
56
58
|
the prompt; `capture-tool` captures the tool use; `stop-quiz-check` closes the
|
|
57
59
|
batch at the seam. `mcp/src/hooks/memory-lib.ts` holds the shared helpers, and
|
|
@@ -70,9 +72,25 @@ the prefix depends on how the plugin was installed —
|
|
|
70
72
|
catches both; anchoring it to one spelling silently disables the checkpoint for
|
|
71
73
|
half the installs.
|
|
72
74
|
|
|
75
|
+
## Two audiences, two channels
|
|
76
|
+
|
|
77
|
+
Every line a hook writes is for the developer or for the model, and each has
|
|
78
|
+
exactly one channel: top-level `systemMessage` is rendered to the developer,
|
|
79
|
+
`hookSpecificOutput.additionalContext` is read by the model. Plain stdout is
|
|
80
|
+
never used. On `SessionStart` it is accepted, but as context only — the banner
|
|
81
|
+
went out that way for months and every session opened in apparent silence while
|
|
82
|
+
the model read three lines meant for a person. And `systemMessage` is **top
|
|
83
|
+
level**: nested inside `hookSpecificOutput` the harness drops it, which is how
|
|
84
|
+
the checkpoint's one line to the developer went unseen for just as long.
|
|
85
|
+
`docs/verified-schemas.md` has the per-hook table; `mcp/test/hooks.test.ts`
|
|
86
|
+
asserts on `shown` and `context` separately, and its `systemMessage()` helper
|
|
87
|
+
throws on the nested placement, so a new hook cannot repeat either mistake
|
|
88
|
+
without a test saying so.
|
|
89
|
+
|
|
73
90
|
## SubagentStart needs the JSON form, and skips the tutor
|
|
74
91
|
|
|
75
|
-
`SessionStart`
|
|
92
|
+
`SessionStart` accepts raw stdout as context (Eklavya no longer uses it — see
|
|
93
|
+
above). `SubagentStart` does not: it
|
|
76
94
|
reads `hookSpecificOutput.additionalContext` and drops anything else in silence,
|
|
77
95
|
so the wrong form is a hook that runs, exits 0, and does nothing. That is the
|
|
78
96
|
one thing `subagent-start.ts` cannot get wrong, and `mcp/test/hooks.test.ts`
|
|
@@ -193,20 +211,21 @@ reads rather than output anyone sees.
|
|
|
193
211
|
|
|
194
212
|
That was a bug for a while, and a bad one: `session-start` returned before
|
|
195
213
|
pushing the directive, so a developer who turned the greeting off logged
|
|
196
|
-
nothing, was never quizzed, and saw
|
|
197
|
-
time. Two tests encoded it as intended behaviour.
|
|
198
|
-
— along with its session-scoped twin
|
|
199
|
-
shorter life.
|
|
200
|
-
|
|
201
|
-
## The Stop hook blocks
|
|
202
|
-
|
|
203
|
-
Commonly got wrong.
|
|
204
|
-
returns 2
|
|
205
|
-
the `min_minutes_between_quizzes` cooldown is skipped (a cooldown could
|
|
206
|
-
commit gate unpassable — decision G5); the one-question cap under
|
|
207
|
-
is lifted, so the sweep asks for the whole remaining budget; and
|
|
208
|
-
plus `cli/eklavya-gate` start holding commits. Only
|
|
209
|
-
|
|
214
|
+
nothing, was never quizzed, and saw quizzing reported as enabled in
|
|
215
|
+
`get_config` the whole time. Two tests encoded it as intended behaviour.
|
|
216
|
+
`quiz.enabled: false` is the off switch — along with its session-scoped twin
|
|
217
|
+
below, which is the same switch with a shorter life.
|
|
218
|
+
|
|
219
|
+
## The Stop hook blocks when unenforced too
|
|
220
|
+
|
|
221
|
+
Commonly got wrong. Unenforced is not "never interrupts" — `stop-quiz-check.ts`
|
|
222
|
+
returns 2 without the gate as readily as with it. `quiz.enforced` changes three
|
|
223
|
+
things: the `min_minutes_between_quizzes` cooldown is skipped (a cooldown could
|
|
224
|
+
make a commit gate unpassable — decision G5); the one-question cap under
|
|
225
|
+
`interleaved` is lifted, so the sweep asks for the whole remaining budget; and
|
|
226
|
+
`pre-tool-gate` plus `cli/eklavya-gate` start holding commits. Only
|
|
227
|
+
`quiz.enabled: false` silences the questions — and it silences only those, not
|
|
228
|
+
the memory half.
|
|
210
229
|
|
|
211
230
|
## The per-session off switch
|
|
212
231
|
|
|
@@ -220,7 +239,7 @@ config file has quietly turned the product off for good.
|
|
|
220
239
|
|
|
221
240
|
Two rules it is easy to get wrong:
|
|
222
241
|
|
|
223
|
-
- It silences, it does not exempt. `cli/eklavya-gate` reads
|
|
242
|
+
- It silences, it does not exempt. `cli/eklavya-gate` reads the project config and
|
|
224
243
|
never sees a session id, so an enforced repo still holds the commit. Making
|
|
225
244
|
the gate honour it would turn a per-session convenience into a gate bypass.
|
|
226
245
|
`pre-tool-gate` reads `isSessionOff` for one reason only: its refusal tells
|
|
@@ -78,4 +78,5 @@ EOF
|
|
|
78
78
|
|
|
79
79
|
chmod +x "$HOOK"
|
|
80
80
|
printf 'Installed the Eklavya commit gate in %s\n' "$HOOK"
|
|
81
|
-
printf 'It only acts on
|
|
81
|
+
printf 'It only acts on projects whose config sets quiz.enforced. That config lives at\n'
|
|
82
|
+
printf '~/.eklavya/projects/<checkout>/config.json, never inside the repository.\n'
|
|
@@ -106,16 +106,16 @@ Check every one of these against the code when you touch a skill.
|
|
|
106
106
|
write it: `grep -rn 'focus' skills/ user-skill/ agents/`.
|
|
107
107
|
- **The `interleaved` one-question cap has exemptions.** In
|
|
108
108
|
`mcp/src/tools/get_session_quiz_plan.ts`:
|
|
109
|
-
`capped = cadence === 'interleaved' &&
|
|
109
|
+
`capped = cadence === 'interleaved' && !quiz.enforced && !explicitTopic`,
|
|
110
110
|
and then `max = args.max ?? (capped ? 1 : max_questions_per_task)`.
|
|
111
|
-
So `enforced`
|
|
111
|
+
So `quiz.enforced` is exempt, an explicit `domain` or `slugs` is exempt, and
|
|
112
112
|
an explicit `max` wins outright because it is read first. Read that code
|
|
113
113
|
rather than trusting prose about it — including this paragraph.
|
|
114
|
-
- **The Stop hook blocks
|
|
115
|
-
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
|
|
116
116
|
`min_minutes_between_quizzes` cooldown, takes the whole remaining budget
|
|
117
|
-
instead of one question, and gates commits. Do not write "
|
|
118
|
-
interrupts".
|
|
117
|
+
instead of one question, and gates commits. Do not write "it never
|
|
118
|
+
interrupts unless enforced".
|
|
119
119
|
- **`get_learner_profile`'s lists are capped, and `known` is ordered by score.**
|
|
120
120
|
`LIST_CAP = 8` covers `weak`, `due_for_review`, `projects`,
|
|
121
121
|
`recent_concepts` and `skipped`; `KNOWN_CAP = 30` covers `known`, with the
|
|
@@ -130,14 +130,16 @@ Check every one of these against the code when you touch a skill.
|
|
|
130
130
|
|
|
131
131
|
Until 1.14 every plan item carried `ask_header` and the tutor printed it above
|
|
132
132
|
the question: `[mode: ambient · focus: concept · level: easy · tier: 2
|
|
133
|
-
mechanism]
|
|
133
|
+
mechanism]` — in the vocabulary of the day, when a single `mode` dial still
|
|
134
|
+
existed. It existed for a real reason — on `concept` focus a deliberately
|
|
134
135
|
transferable question reads as a vague one, and on `easy` a tier-2 question
|
|
135
136
|
reads as shallow rather than as a runway — but it spent four settings' worth of
|
|
136
137
|
screen above *every* stem to say something that is true for the whole session.
|
|
137
138
|
|
|
138
139
|
Ambient state belongs somewhere ambient. `statusLine` in `mcp/src/statusline.ts`
|
|
139
|
-
composes `[EKLAVYA
|
|
140
|
-
|
|
140
|
+
composes `[EKLAVYA concept · interleaved · easy]` for `eklavya statusline`,
|
|
141
|
+
which the host's status bar runs — with `enforced` prepended only when it is
|
|
142
|
+
set, since a segment that is always there is a segment nobody reads. `askHeader` is deleted,
|
|
141
143
|
the plan no longer carries `ask_header`, and both hooks now say *ask the stem on
|
|
142
144
|
its own*.
|
|
143
145
|
|
|
@@ -287,7 +289,7 @@ renderers that lack it.
|
|
|
287
289
|
## Consistency
|
|
288
290
|
|
|
289
291
|
The same behaviour described in two skills has drifted apart before — that is
|
|
290
|
-
how `focus: project` got into three files. The
|
|
292
|
+
how `focus: project` got into three files. The dials appear in
|
|
291
293
|
`skills/mode/SKILL.md` and `user-skill/eklavya/SKILL.md`; the level bands
|
|
292
294
|
appear in `skills/level/SKILL.md` and `tutor/references/focus-and-level.md`;
|
|
293
295
|
the cadence cap appears in `mode`, `quiz` and that same reference. **When you
|
|
@@ -8,7 +8,7 @@ disable-model-invocation: true
|
|
|
8
8
|
|
|
9
9
|
Call `get_gate_status` and report it in a few lines:
|
|
10
10
|
|
|
11
|
-
-
|
|
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
|
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mode
|
|
3
|
-
description: Show or change how Eklavya teaches —
|
|
3
|
+
description: Show or change how Eklavya teaches — whether it quizzes at all and whether it gates commits (quiz.enabled, quiz.enforced), the focus (project, concept, learn), the cadence (interleaved, end) and the difficulty (auto, easy, medium, hard).
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# /eklavya:mode [project|concept|learn|
|
|
7
|
+
# /eklavya:mode [on|off|enforced|project|concept|learn|interleaved|end] [topic] [--session]
|
|
8
8
|
|
|
9
|
-
Eklavya has **
|
|
9
|
+
Eklavya has **independent switches**, and conflating them is the most common confusion. Say which one you are changing.
|
|
10
10
|
|
|
11
|
-
|
|
|
11
|
+
| Setting | Question it answers | Values |
|
|
12
12
|
|---|---|---|
|
|
13
|
-
| `
|
|
13
|
+
| `quiz.enabled` | Does it ask questions at all? | `true` (default), `false` |
|
|
14
|
+
| `quiz.enforced` | Do unanswered questions hold commits? | `false` (default), `true` |
|
|
14
15
|
| `focus` | What does it teach? | `concept` (default), `project`, `learn` |
|
|
15
16
|
| `cadence` | When does it ask? | `interleaved` (default), `end` |
|
|
16
17
|
| `difficulty` | How hard may the questions get? | `auto` (default), `easy`, `medium`, `hard` |
|
|
18
|
+
| `memory.enabled` | Is the work recorded and recalled? | `true` (default), `false` |
|
|
19
|
+
|
|
20
|
+
**`quiz` and `memory` are separate, and this is the thing to get right.** Turning the questions off does not stop Eklavya recording what the session did — that is `memory.enabled`, and it is a deliberate second decision. Someone who says "turn Eklavya off" has usually asked for silence, not amnesia: set `quiz.enabled: false` and tell them memory is still running, rather than switching both and losing their history. If they want everything off, they will say so, and then it is both.
|
|
21
|
+
|
|
22
|
+
These replaced a single `mode` dial whose values were `ambient`, `enforced` and `off`. Configs still using it keep working — `off` reads as `quiz.enabled: false`, `enforced` as `quiz.enforced: true` — but do not write it. `off` is exactly the word that made people believe their memory had stopped.
|
|
17
23
|
|
|
18
24
|
`difficulty` is the one that is normally *earned* rather than set: on `auto`, each project starts at `easy` and climbs. **`/eklavya:level` is the command for it** — send them there rather than explaining the ladder here, and only set it from this command if they explicitly asked to pin a level.
|
|
19
25
|
|
|
20
|
-
They combine freely. `enforced` + `learn` is an intern who must pass a gate on a topic they chose;
|
|
26
|
+
They combine freely. `quiz.enforced` + `learn` is an intern who must pass a gate on a topic they chose; unenforced + `project` is a gentle nudge grounded in today's diff. The default pairing is unenforced + `concept`: teach the idea, use today's code as the way in. `quiz.enabled: false` is the exception — it wins outright, `focus` is never read, and it forces `quiz.enforced` off with it, since a gate nothing asks questions for could never be passed.
|
|
21
27
|
|
|
22
28
|
## No arguments
|
|
23
29
|
|
|
24
|
-
Call `get_config` and report the effective settings in two lines:
|
|
30
|
+
Call `get_config` and report the effective settings in two lines: whether questions are on and whether they gate commits, focus and what it means, then cadence and difficulty in a clause each. **Say whether memory is on in the same breath** — it is the setting people most often assume follows the others. If `overridden_by_project` is non-empty, say which settings this project is overriding and where the file is: someone whose global focus silently stopped applying needs to be told why, not left to guess.
|
|
25
31
|
|
|
26
32
|
Then offer the three focus choices below in one line each. Do not lecture.
|
|
27
33
|
|
|
@@ -33,7 +39,7 @@ Then offer the three focus choices below in one line each. Do not lecture.
|
|
|
33
39
|
|
|
34
40
|
## The two cadence values
|
|
35
41
|
|
|
36
|
-
- **interleaved** *(default)* — one question mid-task, at the moment a concept is logged, while the code is still on screen. A quiz is capped at one question under this cadence, the end-of-task sweep included — except
|
|
42
|
+
- **interleaved** *(default)* — one question mid-task, at the moment a concept is logged, while the code is still on screen. A quiz is capped at one question under this cadence, the end-of-task sweep included — except when `quiz.enforced` is set, where the gate needs a full round, and when the developer asked for a quiz themselves — and it draws on `max_questions_per_task`, which is a session budget rather than a batch size. This is the tool working as advertised: learning while the agent builds, not a pile of questions once it stops.
|
|
37
43
|
- **end** — no mid-task questions at all. Everything waits for the end of the task. Reach for it when someone is pairing, demoing, or genuinely cannot be interrupted — and mention `min_minutes_between_checkpoints` first, since spacing the questions out is usually what they actually want.
|
|
38
44
|
|
|
39
45
|
## Setting it
|
|
@@ -43,24 +49,26 @@ Then offer the three focus choices below in one line each. Do not lecture.
|
|
|
43
49
|
Scope matters and is worth one question when it is ambiguous:
|
|
44
50
|
|
|
45
51
|
- **global** (default) — their own setting, everywhere. This is where a personal `learn` topic belongs.
|
|
46
|
-
- **
|
|
52
|
+
- **project** — writes `~/.eklavya/projects/<checkout>/config.json`, and beats global whenever they work in that codebase. This is where `quiz.enforced` or `focus: project` goes for a codebase they are new to. (`repo` is accepted as the older name for this scope.)
|
|
53
|
+
|
|
54
|
+
**Nothing is written into the repository.** Project settings are per developer: they are in the developer's own home directory, keyed by the checkout's path, and no teammate ever sees them. Say so if they ask about sharing — what a team can share is a concept pack, not settings.
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
Mention when writing `focus` at project scope that it overrides the `learn` topic they set for themselves, in that codebase only. That is sometimes exactly right, but it should be deliberate.
|
|
49
57
|
|
|
50
|
-
##
|
|
58
|
+
## Turning the gate on
|
|
51
59
|
|
|
52
|
-
Same tool, `
|
|
60
|
+
Same tool, `quiz: { enforced: true }`. Follow `/eklavya:setup` step 4 when they move **to** enforcement — the git `pre-commit` hook is what covers commits made outside Claude Code, and the gate without it only covers half the ways to commit.
|
|
53
61
|
|
|
54
62
|
## "Turn it off for this session"
|
|
55
63
|
|
|
56
|
-
**Hear this phrasing — "for now", "for this session", "I'm in the middle of something", `--session` — and use `set_config` with `scope: "session"` and `
|
|
64
|
+
**Hear this phrasing — "for now", "for this session", "I'm in the middle of something", `--session` — and use `set_config` with `scope: "session"` and `quiz: { enabled: false }`.** Not global scope. Global is a file, the file outlives the afternoon, and a developer who silenced one urgent hour in April finds out in June that they turned the questions off for good. Session scope writes nothing: every question, checkpoint, banner and status bar stops until this session ends, and it forgets by itself.
|
|
57
65
|
|
|
58
|
-
It takes `
|
|
66
|
+
It takes `quiz` and nothing else. `quiz: { enabled: true }` at that scope brings the session back — that is what "turn Eklavya back on" does — and the file-backed settings are whatever they always were. **Memory has no session switch at all**, deliberately: a day of work nobody recorded is a day nobody can look up later, and the request was for quiet, not for a hole in the history. Say so if they ask.
|
|
59
67
|
|
|
60
|
-
Say one line back — including *which* session it acted on if `set_config` reports a `session_id` you did not pass — and say the limit in it when
|
|
68
|
+
Say one line back — including *which* session it acted on if `set_config` reports a `session_id` you did not pass — and say the limit in it when `quiz.enforced` is set: the questions are silenced, but the commit gate reads the project config and never sees a session id, so a commit still waits for the quiz, and it keeps growing while you work in silence. Someone who wants that gone wants a project or global change, and that is a different, deliberate decision — `set_config` returns a `note` saying so.
|
|
61
69
|
|
|
62
70
|
Nothing is lost while a session is silent: concepts logged stay unmastered, and a later session offers them again once its own work and review debt are covered.
|
|
63
71
|
|
|
64
72
|
## Confirm
|
|
65
73
|
|
|
66
|
-
Say the new state back in one line — `
|
|
74
|
+
Say the new state back in one line — `Questions: on. Focus: learn (caching). Cadence: interleaved. Difficulty: auto (easy on this repo). Memory: on.` — and what changes next time they build something. If they set `learn`, add that `/eklavya:learn` teaches the topic on demand rather than waiting for a task to touch it.
|
|
@@ -12,16 +12,20 @@ Two places, and the difference is the whole decision:
|
|
|
12
12
|
|
|
13
13
|
| Where | Who it is for |
|
|
14
14
|
|---|---|
|
|
15
|
-
| `~/.eklavya/packs/*.json` |
|
|
16
|
-
|
|
|
15
|
+
| `~/.eklavya/packs/*.json` | them, on every project |
|
|
16
|
+
| `~/.eklavya/projects/<checkout>/packs/*.json` | them, on this codebase |
|
|
17
17
|
|
|
18
|
-
The
|
|
18
|
+
The project one is the interesting half. A pack that describes a codebase's own concepts and prerequisites is onboarding rather than quizzing. Ask which one they want before writing anything.
|
|
19
|
+
|
|
20
|
+
**Never write into the checkout.** Both directories are under the developer's home, and Eklavya creates no files in a project — not settings, not packs. A pack used to go to `<repo>/.eklavya/packs/`, committed; that directory is still **read**, so a repository that already ships one keeps working, but nothing writes there any more and nothing deletes what is there.
|
|
21
|
+
|
|
22
|
+
If they ask for a pack the whole team gets: say plainly that this no longer exists. A pack is theirs, on their machine. What a team can still do is keep the JSON in the repository as a file they each install — but Eklavya will not pick it up from there unless it is in the legacy directory, and it will not put it there.
|
|
19
23
|
|
|
20
24
|
## Build the pack in this order
|
|
21
25
|
|
|
22
26
|
1. **Read before you write.** `get_concept_graph` for the domain they named. If Eklavya already seeds it, the pack is an *extension* — new concepts, retiered old ones — not a replacement.
|
|
23
27
|
2. **Name it.** `pack` is an identifier (`eklavya-pack-rust`, `acme-billing`), `version` is a string you bump when you edit it, `domain` is the one word that groups these concepts in a learner profile.
|
|
24
|
-
3. **List the concepts, from the code.** For a
|
|
28
|
+
3. **List the concepts, from the code.** For a project pack, read the codebase: the modules, the invariants, the decisions someone new gets wrong. Each concept is `{ slug, name, tier, description }`. A slug is lowercase, hyphenated, and names the *idea* — `event-sourcing-replay`, not `fixed-the-replay-bug`.
|
|
25
29
|
4. **Set each tier honestly.** 1 is what a thing is; 3 is why this choice here; 5 is when the architecture is wrong. A pack where everything is tier 3 teaches nothing about order.
|
|
26
30
|
5. **Add the prerequisites.** `{ from, to, relation }`, relation one of `prerequisite_of`, `related_to`, `part_of`. An edge may point at a concept Eklavya already seeds — that is how a pack hangs itself off the shipped graph. `from` is the thing that comes first.
|
|
27
31
|
6. **Write the file** into the directory the scope chose, and run `eklavya doctor`. It names every pack that loaded and every one that did not, with the reason.
|
|
@@ -11,7 +11,7 @@ Render the learning map. Read-only: do not quiz, do not teach.
|
|
|
11
11
|
1. `get_learner_profile` (no domain — you want everything).
|
|
12
12
|
2. `get_concept_graph` with `include_mastery: true` only if you need a domain's
|
|
13
13
|
shape to explain something the profile already flagged. Usually you do not.
|
|
14
|
-
3. `get_gate_status` if
|
|
14
|
+
3. `get_gate_status` if `quiz.enforced` is set.
|
|
15
15
|
|
|
16
16
|
## What the report has to answer
|
|
17
17
|
|
|
@@ -22,8 +22,8 @@ The developer asked for this, so always pass `ignore_cooldown: true`. The quiz c
|
|
|
22
22
|
- `already_covered` → "Everything from this session has already been asked about. `/eklavya:quiz <topic>` to go wider."
|
|
23
23
|
- `nothing_logged` → "Nothing logged this session yet, so there's nothing grounded to ask about."
|
|
24
24
|
- `no_candidates` (topic mode) → that topic is fully mastered and nothing is due; name the closest domain that is not.
|
|
25
|
-
- `
|
|
26
|
-
- `session_off` → they
|
|
25
|
+
- `quiz_disabled` → "Questions are off for this project. Memory is still recording — `/eklavya:mode` turns the questions back on." Do not say "Eklavya is off": it is not, and saying so is what sent people hunting for a bug that was a setting.
|
|
26
|
+
- `session_off` → they silenced the questions for this session, and then asked for a quiz. Say so and offer the one step back: "Questions are off for this session — say the word and I'll turn them back on." Turn them back on with `set_config`, `scope: "session"`, `quiz: { enabled: true }` if they agree, then run the quiz.
|
|
27
27
|
- `no_topic` → focus is `learn` with nothing set; ask what they want to learn, then `/eklavya:mode learn <topic>`.
|
|
28
28
|
- `topic_unknown` → the graph has nothing matching their topic; offer the closest domain rather than inventing questions.
|
|
29
29
|
|