eklavya 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/ask.js +58 -0
- package/dist/ask.js.map +1 -0
- package/dist/assets/dashboard.html +407 -0
- package/dist/assets/tokens.css +135 -0
- package/dist/assets/tutor-skill.md +345 -0
- package/dist/cli.js +223 -0
- package/dist/cli.js.map +1 -0
- package/dist/concurrency.js +41 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/config.js +158 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard.js +214 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/db.js +26 -0
- package/dist/db.js.map +1 -0
- package/dist/hooks/checkpoint-quiz.js +152 -0
- package/dist/hooks/checkpoint-quiz.js.map +1 -0
- package/dist/hooks/lib.js +177 -0
- package/dist/hooks/lib.js.map +1 -0
- package/dist/hooks/pre-tool-gate.js +53 -0
- package/dist/hooks/pre-tool-gate.js.map +1 -0
- package/dist/hooks/session-start.js +128 -0
- package/dist/hooks/session-start.js.map +1 -0
- package/dist/hooks/stop-quiz-check.js +147 -0
- package/dist/hooks/stop-quiz-check.js.map +1 -0
- package/dist/install.js +313 -0
- package/dist/install.js.map +1 -0
- package/dist/mcq.js +34 -0
- package/dist/mcq.js.map +1 -0
- package/dist/migrate.js +45 -0
- package/dist/migrate.js.map +1 -0
- package/dist/migrations/001_init.sql +71 -0
- package/dist/migrations/002_stop_markers.sql +14 -0
- package/dist/migrations/003_gate_repo.sql +5 -0
- package/dist/migrations/004_attempt_outcome.sql +15 -0
- package/dist/migrations/005_session_concept_origin.sql +24 -0
- package/dist/migrations/006_attempt_format.sql +31 -0
- package/dist/migrations/007_checkpoints.sql +23 -0
- package/dist/migrations/008_difficulty_levels.sql +57 -0
- package/dist/paths.js +25 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugin/.claude-plugin/marketplace.json +14 -0
- package/dist/plugin/.claude-plugin/plugin.json +17 -0
- package/dist/plugin/.mcp.json +11 -0
- package/dist/plugin/agents/tutor.md +63 -0
- package/dist/plugin/cli/eklavya-gate +67 -0
- package/dist/plugin/hooks/hooks.json +57 -0
- package/dist/plugin/hooks/run.mjs +169 -0
- package/dist/plugin/scripts/install-git-hook.sh +81 -0
- package/dist/plugin/skills/gate/SKILL.md +17 -0
- package/dist/plugin/skills/learn/SKILL.md +28 -0
- package/dist/plugin/skills/level/SKILL.md +53 -0
- package/dist/plugin/skills/mode/SKILL.md +56 -0
- package/dist/plugin/skills/progress/SKILL.md +72 -0
- package/dist/plugin/skills/quiz/SKILL.md +31 -0
- package/dist/plugin/skills/setup/SKILL.md +49 -0
- package/dist/plugin/skills/tutor/SKILL.md +345 -0
- package/dist/seed/git.json +49 -0
- package/dist/seed/node-backend.json +43 -0
- package/dist/seed/react.json +46 -0
- package/dist/seed/web-auth.json +78 -0
- package/dist/seed.js +112 -0
- package/dist/seed.js.map +1 -0
- package/dist/server.js +52 -0
- package/dist/server.js.map +1 -0
- package/dist/session.js +24 -0
- package/dist/session.js.map +1 -0
- package/dist/slug.js +75 -0
- package/dist/slug.js.map +1 -0
- package/dist/srs.js +206 -0
- package/dist/srs.js.map +1 -0
- package/dist/store.js +450 -0
- package/dist/store.js.map +1 -0
- package/dist/tools/config_tools.js +119 -0
- package/dist/tools/config_tools.js.map +1 -0
- package/dist/tools/get_concept_graph.js +104 -0
- package/dist/tools/get_concept_graph.js.map +1 -0
- package/dist/tools/get_gate_status.js +20 -0
- package/dist/tools/get_gate_status.js.map +1 -0
- package/dist/tools/get_learner_profile.js +142 -0
- package/dist/tools/get_learner_profile.js.map +1 -0
- package/dist/tools/get_session_quiz_plan.js +393 -0
- package/dist/tools/get_session_quiz_plan.js.map +1 -0
- package/dist/tools/index.js +45 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/log_session_concepts.js +103 -0
- package/dist/tools/log_session_concepts.js.map +1 -0
- package/dist/tools/record_attempt.js +158 -0
- package/dist/tools/record_attempt.js.map +1 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/upsert_concepts.js +89 -0
- package/dist/tools/upsert_concepts.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
3
|
+
import { clampToLevel, decayedScore, isDue, isKnown, nextTierToAsk } from '../srs.js';
|
|
4
|
+
import { answerPosition } from '../mcq.js';
|
|
5
|
+
import { askFooter } from '../ask.js';
|
|
6
|
+
import { resolveSessionId } from '../session.js';
|
|
7
|
+
import { attemptedConceptIds, domainSiblings, gateRetryConcepts, gateRow, prereqsOf, resolveTopic, lastAttempt, lastAttemptAt, levelStanding, masteryFor, recentQuestions, sessionConcepts, unmetPrereqs, wasEverTaught, } from '../store.js';
|
|
8
|
+
import { CWD_HINT, SESSION_HINT } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* What the tutor must do with the questions, stated per focus so the pedagogy
|
|
11
|
+
* cannot drift from the setting. Returned with every plan.
|
|
12
|
+
*/
|
|
13
|
+
const FRAMING = {
|
|
14
|
+
project: 'Ground every question in the diff just written: name the file, the line, the decision. The code is the subject.',
|
|
15
|
+
concept: 'Ask the transferable version. The diff is the motivation, not the subject: open from what was just written, then ask for the general rule, the class of problem, or where else it applies. A correct answer must be usable on a different codebase. Do not ask for a definition -- that is tier 1 recall, not generalisation.',
|
|
16
|
+
learn: 'Teach the declared topic, in prerequisite order. Where an item carries bridge_context, the session touched that concept: use that real code as the worked example instead of a hypothetical. Where it does not, teach it on its own terms -- do not force a link to unrelated work.',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* What the project's level permits, stated per level for the same reason
|
|
20
|
+
* `FRAMING` is stated per focus: the tier number alone does not stop a tutor
|
|
21
|
+
* writing a judgement question and labelling it tier 2.
|
|
22
|
+
*/
|
|
23
|
+
const LEVEL_FRAMING = {
|
|
24
|
+
easy: 'Level easy (tiers 1-2). Ask what a thing is and what the machine does with it. No judgement questions, no failure modes, no design. They have been watching the agent work, and every question must be answerable from that -- this is the runway that makes the habit stick, not a warm-up to hurry through.',
|
|
25
|
+
medium: 'Level medium (tiers 2-4). Mechanism, then why this choice rather than the obvious alternative, then what breaks it. Definitions are spent at this level.',
|
|
26
|
+
hard: 'Level hard (tiers 3-5). Judgement, failure modes and design: when is this the wrong approach entirely, and how would they notice in production. A definition question is a wasted question here.',
|
|
27
|
+
};
|
|
28
|
+
const ASKED_HISTORY = 3;
|
|
29
|
+
function minutesSince(iso, now) {
|
|
30
|
+
if (!iso)
|
|
31
|
+
return Number.POSITIVE_INFINITY;
|
|
32
|
+
// SQLite datetime('now') is UTC without a zone marker; make that explicit.
|
|
33
|
+
const t = Date.parse(iso.includes('T') ? iso : `${iso.replace(' ', 'T')}Z`);
|
|
34
|
+
if (!Number.isFinite(t))
|
|
35
|
+
return Number.POSITIVE_INFINITY;
|
|
36
|
+
return (now.getTime() - t) / 60_000;
|
|
37
|
+
}
|
|
38
|
+
export const getSessionQuizPlan = {
|
|
39
|
+
name: 'get_session_quiz_plan',
|
|
40
|
+
title: 'Get session quiz plan',
|
|
41
|
+
description: 'What to quiz on right now and at what difficulty tier, chosen from this session\'s concepts and whatever is due for review. Pass a domain to plan a topic quiz instead. Every item carries asked_before (questions this learner has already been asked — never repeat one), already_taught (they blanked and you explained it, so the next question is a follow-up) and prereqs_unmet. In enforced mode, once everything else is exhausted and the gate is still unpassed, it re-offers concepts that were blanked on and taught, a tier lower, with reason "gate_retry". Honours the configured focus: "project" plans from the diff, "concept" widens to prerequisites and domain siblings, "learn" plans from focus_topic and marks overlaps with the session\'s work as bridge_context. Every plan carries focus and framing — follow framing, it is what the setting means. Each item carries format_to_use, currently always "mcq": ask it with AskUserQuestion as four options, never as a blank prompt. Each item also carries answer_position (1-4) — put the correct option in exactly that slot, or the right answer ends up first every time and the learner stops reading the options. Every tier is clamped to this project\'s difficulty level (easy 1-2, medium 2-4, hard 3-5), which is earned per project and returned as level with level_framing — obey it: a tier-4 question at level easy is the failure this exists to prevent. Each item carries ask_footer, the line naming the settings that asked; print it under the stem, and never pass it back in record_attempt. Returns questions_needed: 0 when there is nothing worth asking.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
session_id: z.string().optional().describe(SESSION_HINT),
|
|
44
|
+
cwd: z.string().optional().describe(CWD_HINT),
|
|
45
|
+
max: z.number().int().min(1).max(10).optional().describe('Cap on questions. Defaults to the configured max_questions_per_task.'),
|
|
46
|
+
domain: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Plan a topic quiz on this domain instead of this session\'s work, e.g. "web-auth". Prerequisites are ordered first.'),
|
|
50
|
+
slugs: z
|
|
51
|
+
.array(z.string())
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Plan around these specific concepts. Use when the developer named a concept rather than a domain.'),
|
|
54
|
+
ignore_cooldown: z
|
|
55
|
+
.boolean()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('Set true when the developer asked to be quizzed. The cadence limit exists to stop nagging, not to refuse a request.'),
|
|
58
|
+
focus: z
|
|
59
|
+
.enum(['project', 'concept', 'learn'])
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('Override the configured focus for this one plan. Omit to use the config.'),
|
|
62
|
+
},
|
|
63
|
+
handler: (args, { db }) => {
|
|
64
|
+
const now = new Date();
|
|
65
|
+
const { config, repoRoot } = loadConfig(args.cwd);
|
|
66
|
+
const sessionId = resolveSessionId(db, args.session_id);
|
|
67
|
+
const max = args.max ?? config.max_questions_per_task;
|
|
68
|
+
const focus = args.focus ?? config.focus;
|
|
69
|
+
// The band this project is on. Every tier below is clamped into it, so a
|
|
70
|
+
// learner three sessions in cannot be handed a tier-4 question by an
|
|
71
|
+
// escalation rule that only knows about one concept at a time.
|
|
72
|
+
const standing = levelStanding(db, config, repoRoot);
|
|
73
|
+
// `learn` focus turns the configured topic into the same domain/slug
|
|
74
|
+
// selection an explicit topic quiz uses, so it goes through one engine with
|
|
75
|
+
// one set of tier and repeat rules. An explicit domain or slugs still win:
|
|
76
|
+
// the developer naming a topic outranks a standing setting.
|
|
77
|
+
let effDomain = args.domain;
|
|
78
|
+
let effSlugs = args.slugs;
|
|
79
|
+
let topicUnresolved = false;
|
|
80
|
+
const explicitTopic = Boolean(args.domain || (args.slugs && args.slugs.length > 0));
|
|
81
|
+
if (!explicitTopic && focus === 'learn') {
|
|
82
|
+
if (!config.focus_topic) {
|
|
83
|
+
return {
|
|
84
|
+
session_id: sessionId,
|
|
85
|
+
questions_needed: 0,
|
|
86
|
+
concepts: [],
|
|
87
|
+
focus,
|
|
88
|
+
reason: 'no_topic',
|
|
89
|
+
detail: 'Focus is "learn" but no focus_topic is set. Ask what they want to learn, then set_config focus_topic.',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const match = resolveTopic(db, config.focus_topic);
|
|
93
|
+
if (match.domain)
|
|
94
|
+
effDomain = match.domain;
|
|
95
|
+
else if (match.slugs.length > 0)
|
|
96
|
+
effSlugs = match.slugs;
|
|
97
|
+
else
|
|
98
|
+
topicUnresolved = true;
|
|
99
|
+
}
|
|
100
|
+
const topicMode = Boolean(effDomain || (effSlugs && effSlugs.length > 0));
|
|
101
|
+
if (config.mode === 'off') {
|
|
102
|
+
return { session_id: sessionId, questions_needed: 0, concepts: [], reason: 'mode_off' };
|
|
103
|
+
}
|
|
104
|
+
// Cooldown keeps ambient mode from nagging. It must never apply in enforced
|
|
105
|
+
// mode, or a cooldown could make a commit gate unpassable (decision G5), and
|
|
106
|
+
// it must never override an explicit request (`ignore_cooldown`).
|
|
107
|
+
if (config.mode === 'ambient' && !args.ignore_cooldown) {
|
|
108
|
+
const since = minutesSince(lastAttemptAt(db, sessionId), now);
|
|
109
|
+
if (since < config.min_minutes_between_quizzes) {
|
|
110
|
+
return {
|
|
111
|
+
session_id: sessionId,
|
|
112
|
+
questions_needed: 0,
|
|
113
|
+
concepts: [],
|
|
114
|
+
reason: 'cooldown',
|
|
115
|
+
minutes_remaining: Math.ceil(config.min_minutes_between_quizzes - since),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// The graph has nothing on this topic yet. Saying so is the useful answer;
|
|
120
|
+
// inventing questions about concepts that do not exist is not.
|
|
121
|
+
if (topicUnresolved) {
|
|
122
|
+
return {
|
|
123
|
+
session_id: sessionId,
|
|
124
|
+
questions_needed: 0,
|
|
125
|
+
concepts: [],
|
|
126
|
+
focus,
|
|
127
|
+
topic: config.focus_topic,
|
|
128
|
+
reason: 'topic_unknown',
|
|
129
|
+
detail: `Nothing in the graph matches "${config.focus_topic}". Offer the closest domain from get_concept_graph, or teach from first principles and upsert_concepts as you go.`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const picked = [];
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
// Where the session's own work touched a concept. In `learn` focus this is
|
|
135
|
+
// what turns an abstract topic question into one about code they just saw.
|
|
136
|
+
const sessionContext = new Map();
|
|
137
|
+
for (const c of sessionConcepts(db, sessionId)) {
|
|
138
|
+
if (c.context)
|
|
139
|
+
sessionContext.set(c.slug, c.context);
|
|
140
|
+
}
|
|
141
|
+
const domains = new Set();
|
|
142
|
+
// Concepts already answered in this session have had their turn. Re-offering
|
|
143
|
+
// them is how a learner gets asked the same thing twice in five minutes.
|
|
144
|
+
const alreadyAsked = attemptedConceptIds(db, sessionId);
|
|
145
|
+
let skippedAsked = 0;
|
|
146
|
+
const add = (concept, context, reason,
|
|
147
|
+
// The gate-retry pass is the one caller allowed past the already-asked
|
|
148
|
+
// filter, and it drops a tier because it is re-opening a concept the
|
|
149
|
+
// learner has just been taught rather than testing a fresh one.
|
|
150
|
+
opts = {}) => {
|
|
151
|
+
if (seen.has(concept.slug) || picked.length >= max)
|
|
152
|
+
return;
|
|
153
|
+
if (!opts.retry && alreadyAsked.has(concept.id)) {
|
|
154
|
+
seen.add(concept.slug);
|
|
155
|
+
skippedAsked += 1;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const m = masteryFor(db, concept.id);
|
|
159
|
+
const score = decayedScore(m.score, m.next_review, now);
|
|
160
|
+
const last = lastAttempt(db, concept.id);
|
|
161
|
+
const asked = recentQuestions(db, concept.id, ASKED_HISTORY);
|
|
162
|
+
const tier = clampToLevel(nextTierToAsk({
|
|
163
|
+
conceptTier: concept.tier,
|
|
164
|
+
lastDifficulty: last?.difficulty ?? null,
|
|
165
|
+
lastGrade: last?.grade ?? null,
|
|
166
|
+
score,
|
|
167
|
+
}), standing.level);
|
|
168
|
+
// A retry drops a tier because the concept has just been taught rather than
|
|
169
|
+
// tested -- but never below the band, or `easy` would ask tier 0.
|
|
170
|
+
const tierToAsk = opts.retry ? clampToLevel(tier - 1, standing.level) : tier;
|
|
171
|
+
const footer = askFooter({
|
|
172
|
+
config,
|
|
173
|
+
level: standing.level,
|
|
174
|
+
pinned: standing.pinned,
|
|
175
|
+
tier: tierToAsk,
|
|
176
|
+
});
|
|
177
|
+
seen.add(concept.slug);
|
|
178
|
+
picked.push({
|
|
179
|
+
slug: concept.slug,
|
|
180
|
+
name: concept.name,
|
|
181
|
+
domain: concept.domain,
|
|
182
|
+
description: concept.description,
|
|
183
|
+
tier_to_ask: tierToAsk,
|
|
184
|
+
context,
|
|
185
|
+
...(footer ? { ask_footer: footer } : {}),
|
|
186
|
+
// Only in `learn` focus, and only when the topic concept really did turn
|
|
187
|
+
// up in this session's work. Absent means "teach it on its own terms",
|
|
188
|
+
// which is different from "no context available".
|
|
189
|
+
...(focus === 'learn' && sessionContext.has(concept.slug)
|
|
190
|
+
? { bridge_context: sessionContext.get(concept.slug) }
|
|
191
|
+
: {}),
|
|
192
|
+
last_grade: last?.grade ?? null,
|
|
193
|
+
asked_before: asked,
|
|
194
|
+
already_taught: wasEverTaught(db, concept.id),
|
|
195
|
+
prereqs_unmet: unmetPrereqs(db, concept.id, now),
|
|
196
|
+
format_to_use: 'mcq',
|
|
197
|
+
answer_position: answerPosition(concept.slug, asked.length),
|
|
198
|
+
reason,
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
if (topicMode) {
|
|
202
|
+
// Topic quizzes go through the same engine as session quizzes, so a named
|
|
203
|
+
// topic gets the same tier escalation and the same repeat protection.
|
|
204
|
+
const clauses = [];
|
|
205
|
+
const params = [];
|
|
206
|
+
if (effDomain) {
|
|
207
|
+
clauses.push('c.domain = ?');
|
|
208
|
+
params.push(effDomain);
|
|
209
|
+
}
|
|
210
|
+
if (effSlugs && effSlugs.length > 0) {
|
|
211
|
+
clauses.push(`c.slug IN (${effSlugs.map(() => '?').join(',')})`);
|
|
212
|
+
params.push(...effSlugs);
|
|
213
|
+
}
|
|
214
|
+
const rows = db
|
|
215
|
+
.prepare(`SELECT c.* FROM concepts c
|
|
216
|
+
LEFT JOIN mastery m ON m.concept_id = c.id
|
|
217
|
+
WHERE (${clauses.join(' OR ')})
|
|
218
|
+
ORDER BY c.tier ASC, c.slug ASC`)
|
|
219
|
+
.all(...params);
|
|
220
|
+
// Weakest and due-for-review first, prerequisites before dependents; a
|
|
221
|
+
// concept they have mastered is only worth asking when review says so.
|
|
222
|
+
const candidates = rows
|
|
223
|
+
.map((c) => {
|
|
224
|
+
const m = masteryFor(db, c.id);
|
|
225
|
+
const score = decayedScore(m.score, m.next_review, now);
|
|
226
|
+
return { c, score, known: isKnown({ score, reps: m.reps }), due: isDue(m.next_review, now) };
|
|
227
|
+
})
|
|
228
|
+
.filter((r) => !r.known || r.due)
|
|
229
|
+
.sort((a, b) => {
|
|
230
|
+
if (a.due !== b.due)
|
|
231
|
+
return a.due ? -1 : 1;
|
|
232
|
+
if (a.score !== b.score)
|
|
233
|
+
return a.score - b.score;
|
|
234
|
+
return a.c.tier - b.c.tier;
|
|
235
|
+
});
|
|
236
|
+
// `learn_topic` and `topic` run the same selection; they differ only in
|
|
237
|
+
// why the topic was chosen, which is what the tutor needs in order to
|
|
238
|
+
// frame the question.
|
|
239
|
+
const topicReason = !explicitTopic && focus === 'learn' ? 'learn_topic' : 'topic';
|
|
240
|
+
for (const cand of candidates) {
|
|
241
|
+
add(cand.c, sessionContext.get(cand.c.slug) ?? null, topicReason);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const touched = sessionConcepts(db, sessionId);
|
|
246
|
+
for (const c of touched)
|
|
247
|
+
domains.add(c.domain);
|
|
248
|
+
// (a) concepts this session touched that the learner has not mastered
|
|
249
|
+
for (const c of touched) {
|
|
250
|
+
const m = masteryFor(db, c.id);
|
|
251
|
+
const score = decayedScore(m.score, m.next_review, now);
|
|
252
|
+
if (!isKnown({ score, reps: m.reps }))
|
|
253
|
+
add(c, c.context, 'unmastered');
|
|
254
|
+
}
|
|
255
|
+
// (b) concepts this session touched that are due for review
|
|
256
|
+
for (const c of touched) {
|
|
257
|
+
const m = masteryFor(db, c.id);
|
|
258
|
+
if (isDue(m.next_review, now))
|
|
259
|
+
add(c, c.context, 'due_review');
|
|
260
|
+
}
|
|
261
|
+
// `concept` focus: reach past what the task happened to touch. The ideas
|
|
262
|
+
// the diff is an instance of live in its prerequisites and its domain
|
|
263
|
+
// siblings, and asking only about what the diff contains is how a quiz
|
|
264
|
+
// stays stuck at "what does this line do".
|
|
265
|
+
//
|
|
266
|
+
// Deliberately before (c): a prerequisite of today's work is more use than
|
|
267
|
+
// an unrelated concept that happens to be due.
|
|
268
|
+
if (focus === 'concept' && picked.length < max) {
|
|
269
|
+
const touchedIds = touched.map((c) => c.id);
|
|
270
|
+
const exclude = new Set(touchedIds);
|
|
271
|
+
for (const c of prereqsOf(db, touchedIds)) {
|
|
272
|
+
const m = masteryFor(db, c.id);
|
|
273
|
+
if (!isKnown({ score: decayedScore(m.score, m.next_review, now), reps: m.reps })) {
|
|
274
|
+
// Null context on purpose. In concept focus the diff is the
|
|
275
|
+
// motivation, not the subject, and handing over a line of code
|
|
276
|
+
// invites exactly the grounded-in-the-file question this focus
|
|
277
|
+
// exists to avoid.
|
|
278
|
+
add(c, null, 'concept_widening');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const c of domainSiblings(db, [...domains], exclude)) {
|
|
282
|
+
if (picked.length >= max)
|
|
283
|
+
break;
|
|
284
|
+
const m = masteryFor(db, c.id);
|
|
285
|
+
if (!isKnown({ score: decayedScore(m.score, m.next_review, now), reps: m.reps })) {
|
|
286
|
+
add(c, null, 'concept_widening');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// (c) anything else due in the same domains, so review debt gets paid down
|
|
291
|
+
if (picked.length < max && domains.size > 0) {
|
|
292
|
+
const placeholders = [...domains].map(() => '?').join(',');
|
|
293
|
+
const rows = db
|
|
294
|
+
.prepare(`SELECT c.* FROM concepts c
|
|
295
|
+
JOIN mastery m ON m.concept_id = c.id
|
|
296
|
+
WHERE c.domain IN (${placeholders}) AND m.next_review IS NOT NULL AND m.next_review <= ?
|
|
297
|
+
ORDER BY m.next_review ASC`)
|
|
298
|
+
.all(...domains, now.toISOString());
|
|
299
|
+
for (const c of rows)
|
|
300
|
+
add(c, null, 'domain_review');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Enforced mode only, and only once everything else is exhausted: a session
|
|
304
|
+
// answered entirely with "I don't know" grades every concept 0, and a graded
|
|
305
|
+
// concept is filtered from the plan above -- so `required` can never be met,
|
|
306
|
+
// the planner returns nothing, and pre-tool-gate.sh denies the commit while
|
|
307
|
+
// telling the developer to run a quiz that has nothing left to ask. That is
|
|
308
|
+
// a dead end with no route out inside the session.
|
|
309
|
+
//
|
|
310
|
+
// Ambient mode is deliberately left alone. It has no gate to deadlock, and
|
|
311
|
+
// re-offering a concept there would be the nagging the cooldown exists to
|
|
312
|
+
// prevent.
|
|
313
|
+
if (!topicMode && picked.length === 0 && config.mode === 'enforced') {
|
|
314
|
+
const gate = gateRow(db, sessionId);
|
|
315
|
+
if (gate && gate.required > 0 && !gate.passed) {
|
|
316
|
+
// `seen` carries two meanings: "already picked" and "considered and
|
|
317
|
+
// rejected". Only the second is in it here -- nothing was picked, or we
|
|
318
|
+
// would not be in this branch -- so clearing it drops exactly the
|
|
319
|
+
// rejections the retry pass exists to reconsider, and cannot lose a pick.
|
|
320
|
+
seen.clear();
|
|
321
|
+
for (const c of gateRetryConcepts(db, sessionId)) {
|
|
322
|
+
add(c, c.context, 'gate_retry', { retry: true });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Foundations before what rests on them, so a quiz never leads with a
|
|
327
|
+
// question the learner has no way to answer.
|
|
328
|
+
picked.sort((a, b) => {
|
|
329
|
+
if (a.prereqs_unmet.length !== b.prereqs_unmet.length) {
|
|
330
|
+
return a.prereqs_unmet.length - b.prereqs_unmet.length;
|
|
331
|
+
}
|
|
332
|
+
return a.tier_to_ask - b.tier_to_ask;
|
|
333
|
+
});
|
|
334
|
+
// Numbered after the sort, so `q 2/3` is the order they will actually be
|
|
335
|
+
// asked in. Only when there is more than one -- `q 1/1` is noise.
|
|
336
|
+
if (picked.length > 1) {
|
|
337
|
+
picked.forEach((item, i) => {
|
|
338
|
+
const numbered = askFooter({
|
|
339
|
+
config,
|
|
340
|
+
level: standing.level,
|
|
341
|
+
pinned: standing.pinned,
|
|
342
|
+
tier: item.tier_to_ask,
|
|
343
|
+
position: { index: i + 1, total: picked.length },
|
|
344
|
+
});
|
|
345
|
+
if (numbered)
|
|
346
|
+
item.ask_footer = numbered;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (picked.length === 0) {
|
|
350
|
+
// Distinguish "nothing left to ask" from "nothing to ask about" — they
|
|
351
|
+
// need different things said to the developer.
|
|
352
|
+
return skippedAsked > 0
|
|
353
|
+
? {
|
|
354
|
+
session_id: sessionId,
|
|
355
|
+
questions_needed: 0,
|
|
356
|
+
concepts: [],
|
|
357
|
+
focus,
|
|
358
|
+
reason: 'already_covered',
|
|
359
|
+
detail: `${skippedAsked} concept(s) were already asked about in this session.`,
|
|
360
|
+
}
|
|
361
|
+
: {
|
|
362
|
+
session_id: sessionId,
|
|
363
|
+
questions_needed: 0,
|
|
364
|
+
concepts: [],
|
|
365
|
+
focus,
|
|
366
|
+
reason: topicMode ? 'no_candidates' : 'nothing_logged',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
session_id: sessionId,
|
|
371
|
+
questions_needed: picked.length,
|
|
372
|
+
focus,
|
|
373
|
+
// Stated with every plan so the pedagogy cannot drift from the setting:
|
|
374
|
+
// the tutor should not have to remember what `concept` implies.
|
|
375
|
+
framing: FRAMING[focus],
|
|
376
|
+
...(focus === 'learn' && config.focus_topic ? { topic: config.focus_topic } : {}),
|
|
377
|
+
level: standing.level,
|
|
378
|
+
level_framing: LEVEL_FRAMING[standing.level],
|
|
379
|
+
level_progress: {
|
|
380
|
+
passed: standing.counts.passed,
|
|
381
|
+
needed: standing.needed.answers,
|
|
382
|
+
concepts: standing.counts.concepts,
|
|
383
|
+
needed_concepts: standing.needed.concepts,
|
|
384
|
+
accuracy: standing.accuracy,
|
|
385
|
+
min_accuracy: standing.needed.accuracy,
|
|
386
|
+
next: standing.next,
|
|
387
|
+
...(standing.pinned ? { pinned: true } : {}),
|
|
388
|
+
},
|
|
389
|
+
concepts: picked,
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
//# sourceMappingURL=get_session_quiz_plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get_session_quiz_plan.js","sourceRoot":"","sources":["../../src/tools/get_session_quiz_plan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { retryOnBusy } from '../concurrency.js';
|
|
2
|
+
import { getLearnerProfile } from './get_learner_profile.js';
|
|
3
|
+
import { logSessionConcepts } from './log_session_concepts.js';
|
|
4
|
+
import { getSessionQuizPlan } from './get_session_quiz_plan.js';
|
|
5
|
+
import { recordAttempt } from './record_attempt.js';
|
|
6
|
+
import { getGateStatus } from './get_gate_status.js';
|
|
7
|
+
import { upsertConcepts } from './upsert_concepts.js';
|
|
8
|
+
import { getConceptGraph } from './get_concept_graph.js';
|
|
9
|
+
import { getConfig, setConfig } from './config_tools.js';
|
|
10
|
+
export const TOOLS = [
|
|
11
|
+
getLearnerProfile,
|
|
12
|
+
logSessionConcepts,
|
|
13
|
+
getSessionQuizPlan,
|
|
14
|
+
recordAttempt,
|
|
15
|
+
getGateStatus,
|
|
16
|
+
upsertConcepts,
|
|
17
|
+
getConceptGraph,
|
|
18
|
+
getConfig,
|
|
19
|
+
setConfig,
|
|
20
|
+
];
|
|
21
|
+
/** Tool results are JSON text: compact, and small enough to live in context (decision G7). */
|
|
22
|
+
function toResult(payload) {
|
|
23
|
+
const text = JSON.stringify(payload);
|
|
24
|
+
const isError = typeof payload === 'object' && payload !== null && 'error' in payload;
|
|
25
|
+
return { content: [{ type: 'text', text }], isError };
|
|
26
|
+
}
|
|
27
|
+
export function registerTools(server, db) {
|
|
28
|
+
for (const tool of TOOLS) {
|
|
29
|
+
server.registerTool(tool.name, { title: tool.title, description: tool.description, inputSchema: tool.inputSchema }, async (args) => {
|
|
30
|
+
try {
|
|
31
|
+
// Safe to retry: a busy transaction has already rolled back.
|
|
32
|
+
return toResult(retryOnBusy(() => tool.handler(args, { db })));
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
// A tool throwing must not take the server down mid-session.
|
|
36
|
+
return toResult({
|
|
37
|
+
error: 'tool_failed',
|
|
38
|
+
tool: tool.name,
|
|
39
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,CAAC,MAAM,KAAK,GAAc;IAC9B,iBAAiB;IACjB,kBAAkB;IAClB,kBAAkB;IAClB,aAAa;IACb,aAAa;IACb,cAAc;IACd,eAAe;IACf,SAAS;IACT,SAAS;CACV,CAAC;AAEF,8FAA8F;AAC9F,SAAS,QAAQ,CAAC,OAAgB;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACrC,MAAM,OAAO,GAAG,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,IAAI,OAAO,CAAC;IACtF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAiB,EAAE,EAAM;IACrD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,YAAY,CACjB,IAAI,CAAC,IAAI,EACT,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,EACnF,KAAK,EAAE,IAAa,EAAE,EAAE;YACtB,IAAI,CAAC;gBACH,6DAA6D;gBAC7D,OAAO,QAAQ,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;YACjE,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,6DAA6D;gBAC7D,OAAO,QAAQ,CAAC;oBACd,KAAK,EAAE,aAAa;oBACpB,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;iBACzD,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CACF,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
3
|
+
import { normalizeSlug, findFuzzyMatch } from '../slug.js';
|
|
4
|
+
import { decayedScore, isKnown } from '../srs.js';
|
|
5
|
+
import { resolveSessionId } from '../session.js';
|
|
6
|
+
import { allConceptSlugs, conceptBySlug, insertConcept, logSessionConcept, masteryFor, newConceptsThisSession, sessionConcepts, syncGate, } from '../store.js';
|
|
7
|
+
import { CWD_HINT, SESSION_HINT } from './types.js';
|
|
8
|
+
const DEFAULT_DOMAIN = 'general';
|
|
9
|
+
const DEFAULT_TIER = 2;
|
|
10
|
+
export const logSessionConcepts = {
|
|
11
|
+
name: 'log_session_concepts',
|
|
12
|
+
title: 'Log session concepts',
|
|
13
|
+
description: 'Record the concepts the current task genuinely exercises, each with a one-line context pointing at the real code you wrote. Call this while implementing, batched, 3-8 concepts per task. Unknown slugs are created automatically, but bare — when the response carries next_action, do what it says before quizzing.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
session_id: z.string().optional().describe(SESSION_HINT),
|
|
16
|
+
cwd: z.string().optional().describe(CWD_HINT),
|
|
17
|
+
concepts: z
|
|
18
|
+
.array(z.object({
|
|
19
|
+
slug: z.string().describe('kebab-case, e.g. "httponly-cookies"'),
|
|
20
|
+
name: z.string().optional(),
|
|
21
|
+
domain: z.string().optional(),
|
|
22
|
+
tier: z.number().int().min(1).max(5).optional(),
|
|
23
|
+
context: z.string().optional().describe('One line naming the actual code, e.g. "set httpOnly on the refresh cookie in auth.ts".'),
|
|
24
|
+
}))
|
|
25
|
+
.min(1),
|
|
26
|
+
},
|
|
27
|
+
handler: (args, { db }) => {
|
|
28
|
+
const now = new Date();
|
|
29
|
+
const { config, repoRoot } = loadConfig(args.cwd);
|
|
30
|
+
const sessionId = resolveSessionId(db, args.session_id);
|
|
31
|
+
const created = [];
|
|
32
|
+
const matched = [];
|
|
33
|
+
const logged = [];
|
|
34
|
+
let capped = 0;
|
|
35
|
+
const apply = db.transaction(() => {
|
|
36
|
+
let budget = Math.max(0, config.max_new_concepts_per_session - newConceptsThisSession(db, sessionId));
|
|
37
|
+
for (const input of args.concepts) {
|
|
38
|
+
const slug = normalizeSlug(input.slug);
|
|
39
|
+
if (!slug)
|
|
40
|
+
continue;
|
|
41
|
+
let concept = conceptBySlug(db, slug);
|
|
42
|
+
if (!concept) {
|
|
43
|
+
const near = findFuzzyMatch(slug, allConceptSlugs(db));
|
|
44
|
+
if (near) {
|
|
45
|
+
concept = conceptBySlug(db, near.slug);
|
|
46
|
+
if (concept)
|
|
47
|
+
matched.push({ requested: slug, resolved: concept.slug });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!concept) {
|
|
51
|
+
if (budget <= 0) {
|
|
52
|
+
capped += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
concept = insertConcept(db, {
|
|
56
|
+
slug,
|
|
57
|
+
name: input.name ?? slug.replace(/-/g, ' '),
|
|
58
|
+
domain: input.domain ?? DEFAULT_DOMAIN,
|
|
59
|
+
tier: input.tier ?? DEFAULT_TIER,
|
|
60
|
+
source: 'llm',
|
|
61
|
+
});
|
|
62
|
+
created.push(concept.slug);
|
|
63
|
+
budget -= 1;
|
|
64
|
+
}
|
|
65
|
+
logSessionConcept(db, sessionId, concept.id, input.context ?? null, 'work');
|
|
66
|
+
logged.push(concept.slug);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
apply();
|
|
70
|
+
// The gate's bar rises with the work: how many touched concepts are still
|
|
71
|
+
// unmastered, capped at the configured questions per task.
|
|
72
|
+
// 'work' only: `passedCount` counts nothing else, so a review-debt concept
|
|
73
|
+
// raising the bar would raise it past anything the learner could clear.
|
|
74
|
+
const unmastered = sessionConcepts(db, sessionId, 'work').filter((c) => {
|
|
75
|
+
const m = masteryFor(db, c.id);
|
|
76
|
+
return !isKnown({ score: decayedScore(m.score, m.next_review, now), reps: m.reps });
|
|
77
|
+
}).length;
|
|
78
|
+
const gate = syncGate(db, sessionId, config, {
|
|
79
|
+
requiredHint: Math.min(unmastered, config.max_questions_per_task),
|
|
80
|
+
repo: repoRoot,
|
|
81
|
+
});
|
|
82
|
+
// Said in the response, not only in the tutor skill. A bare concept is
|
|
83
|
+
// tier 2, domain "general", no edges -- and `prereqs_unmet` is computed from
|
|
84
|
+
// edges, so a concept with none can never be reported as unfair to ask
|
|
85
|
+
// about, however far out of its depth the learner is. The skill says to fix
|
|
86
|
+
// that, but it is model-invoked and may never load; the same gap is why the
|
|
87
|
+
// concept-logging directive had to move into the SessionStart banner. This
|
|
88
|
+
// lands in context whatever loaded.
|
|
89
|
+
const nextAction = created.length > 0
|
|
90
|
+
? `Call upsert_concepts for ${created.join(', ')} — each was created bare (tier 2, domain "general", no edges). Give each a real domain, an honest tier, and at least one prerequisite_of edge, or prereqs_unmet stays empty and the fairness check silently passes.`
|
|
91
|
+
: undefined;
|
|
92
|
+
return {
|
|
93
|
+
session_id: sessionId,
|
|
94
|
+
logged,
|
|
95
|
+
created,
|
|
96
|
+
matched,
|
|
97
|
+
capped,
|
|
98
|
+
gate,
|
|
99
|
+
...(nextAction ? { next_action: nextAction } : {}),
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=log_session_concepts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log_session_concepts.js","sourceRoot":"","sources":["../../src/tools/log_session_concepts.ts"],"names":[],"mappings":"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"}
|