eklavya 1.59.0 → 1.61.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 +2 -2
- package/dist/assets/dashboard.html +131 -56
- package/dist/assets/tutor/SKILL.md +49 -44
- package/dist/assets/tutor/references/focus-and-level.md +2 -1
- package/dist/assets/tutor/references/grading.md +101 -104
- package/dist/assets/tutor/references/panel.md +5 -2
- package/dist/assets/tutor/references/writing-mcq.md +28 -20
- package/dist/eval/conversation-score.js +98 -0
- package/dist/eval/conversation-score.js.map +1 -0
- package/dist/eval/question-checks.js +31 -0
- package/dist/eval/question-checks.js.map +1 -1
- package/dist/hooks/ask-label.js +37 -14
- package/dist/hooks/ask-label.js.map +1 -1
- package/dist/migrations/027_invalid_questions.sql +19 -0
- package/dist/panel.js +17 -0
- package/dist/panel.js.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/tutor.md +26 -23
- package/dist/plugin/hooks/CLAUDE.md +1 -1
- package/dist/plugin/skills/CLAUDE.md +1 -1
- package/dist/plugin/skills/tutor/SKILL.md +49 -44
- package/dist/plugin/skills/tutor/references/focus-and-level.md +2 -1
- package/dist/plugin/skills/tutor/references/grading.md +101 -104
- package/dist/plugin/skills/tutor/references/panel.md +5 -2
- package/dist/plugin/skills/tutor/references/writing-mcq.md +28 -20
- package/dist/store.js +15 -1
- package/dist/store.js.map +1 -1
- package/dist/tools/panel_tools.js +1 -1
- package/dist/tools/panel_tools.js.map +1 -1
- package/dist/tools/record_attempt.js +26 -8
- package/dist/tools/record_attempt.js.map +1 -1
- package/dist/user-skill/eklavya/SKILL.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ page in the dashboard, where the server grades the new pick, and `option_notes`
|
|
|
61
61
|
(the note under each option, in order), which that page shows under each option. A `correct` that is not one of
|
|
62
62
|
the options, or notes of the wrong length, is stored as nothing and reported
|
|
63
63
|
back (`correct_mismatch`, `option_notes_mismatch`); the answer is still kept.
|
|
64
|
-
An `outcome` of `declined` or `dont_know` with a grade of 3 or more is
|
|
64
|
+
An `outcome` of `invalid` means the question itself was at fault (it assumed context the learner never saw, or two options both answered it): it is stored in `invalid_questions`, grades nothing, returns no `attempt_id`, and changes no mastery, level, review date or gate; only the repeat check reads it. An `outcome` of `declined` or `dont_know` with a grade of 3 or more is
|
|
65
65
|
contradictory: it returns `error: "outcome_grade_conflict"` and records
|
|
66
66
|
nothing, so that call can never update mastery, clear a gate or count toward a
|
|
67
67
|
level.
|
|
@@ -72,7 +72,7 @@ level.
|
|
|
72
72
|
right one, 2 for a near miss, 1 for a misconception), the right one in the
|
|
73
73
|
plan's `answer_position` slot, and an explanation. It stores the question and
|
|
74
74
|
returns at once; it writes no attempt. The panel records the answer through
|
|
75
|
-
`panel_answer` with the same code `record_attempt` uses, exactly once. On a miss with `explain_on_wrong` on, its reply carries the same `explain` block `record_attempt` returns, and the panel starts the explainer agent from it. `present_question` returns `panel_disabled` while `quiz.panel` is off, and
|
|
75
|
+
`panel_answer` with the same code `record_attempt` uses, exactly once. On a miss with `explain_on_wrong` on, its reply carries the same `explain` block `record_attempt` returns, and the panel starts the explainer agent from it. `present_question` sends a question back once with `conspicuous_options` (nothing stored) when one option's label plus description outruns the next by a few words or a description explains itself, and presents the same stem as written on the second call. It returns `panel_disabled` while `quiz.panel` is off, and
|
|
76
76
|
`panel_sync` returns `{disabled: true}` and expires the session's open question without recording an attempt, so turning the panel back on cannot revive it; `panel_answer` answers only a question
|
|
77
77
|
that already exists. A round the learner asked for (a topic, `max`) is remembered per session: `present_question` takes each question off it and sets the last one's `more` false, and `get_session_quiz_plan` with `resume_round: true` plans what is left. This is
|
|
78
78
|
a Claude Code mod feature: a standalone MCP client has the tools but not the
|
|
@@ -813,7 +813,6 @@
|
|
|
813
813
|
.set__ctl input[type="number"], .set__ctl textarea { font:inherit; font-size:13.5px; background:var(--bg); color:var(--ink);
|
|
814
814
|
border:1px solid var(--line-2); border-radius:0; padding:7px var(--space-3); width:100%; }
|
|
815
815
|
.set__ctl textarea { font-family:var(--font-mono); font-size:12.5px; resize:vertical; min-height:64px; }
|
|
816
|
-
.set__ctl input[type="checkbox"] { width:18px; height:18px; accent-color:var(--spot); margin:4px 0; cursor:pointer; }
|
|
817
816
|
.set__ctl :disabled { opacity:.55; cursor:not-allowed; }
|
|
818
817
|
.set__rule { color:var(--faint); }
|
|
819
818
|
.set__err { margin:var(--space-2) 0 0; color:var(--warning); font-size:12.5px; overflow-wrap:anywhere; }
|
|
@@ -821,6 +820,58 @@
|
|
|
821
820
|
.set__btn { font-family:var(--font-mono); font-size:11px; letter-spacing:.06em; text-transform:uppercase;
|
|
822
821
|
background:none; border:1px solid var(--line-2); color:var(--dim); padding:2px 8px; cursor:pointer; }
|
|
823
822
|
.set__btn:hover { color:var(--ink); border-color:var(--spot); }
|
|
823
|
+
/* ---------- Settings switchboard: scopes on the right, one tab of settings at a time ---------- */
|
|
824
|
+
.sw { display:grid; grid-template-columns:minmax(0,1fr) 208px; gap:var(--space-5); align-items:stretch; min-height:calc(100vh - 168px); }
|
|
825
|
+
.sw__scopes { align-self:stretch; display:flex; flex-direction:column; gap:2px; align-content:flex-start;
|
|
826
|
+
background:var(--panel); border:1px solid var(--line-2); padding:var(--space-2); }
|
|
827
|
+
.sw__cap { margin:var(--space-3) var(--space-2) var(--space-1); font-family:var(--font-mono); font-size:10.5px;
|
|
828
|
+
letter-spacing:var(--tracking-caps); text-transform:uppercase; color:var(--faint); }
|
|
829
|
+
.sw__cap:first-child { margin-top:var(--space-1); }
|
|
830
|
+
.sw__projects { display:flex; flex-direction:column; gap:2px; overflow-y:auto; }
|
|
831
|
+
.sw__scope { display:flex; align-items:center; gap:10px; padding:8px 10px; color:var(--dim); text-decoration:none;
|
|
832
|
+
font-size:13.5px; min-width:0; transition:background var(--duration-fast) var(--ease), color var(--duration-fast) var(--ease); }
|
|
833
|
+
.sw__scope svg { width:16px; height:16px; flex:none; }
|
|
834
|
+
.sw__scope span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
835
|
+
.sw__scope:hover { background:var(--mass); color:var(--ink); }
|
|
836
|
+
.sw__scope[aria-current="page"] { background:var(--mass); color:var(--ink); box-shadow:inset 2px 0 0 var(--spot); }
|
|
837
|
+
.sw__scope[aria-current="page"] svg { color:var(--spot); }
|
|
838
|
+
.sw__none { margin:var(--space-1) var(--space-2) var(--space-2); color:var(--faint); font-size:12.5px; }
|
|
839
|
+
.sw__pane { min-width:0; display:flex; flex-direction:column; gap:var(--space-5); }
|
|
840
|
+
.sw__tabs { display:flex; gap:0; border-bottom:1px solid var(--line-2); overflow-x:auto; scrollbar-width:none; }
|
|
841
|
+
.sw__tabs::-webkit-scrollbar { display:none; }
|
|
842
|
+
.sw__tab { display:flex; align-items:center; gap:6px; padding:10px 9px; margin-bottom:-1px; white-space:nowrap;
|
|
843
|
+
font-family:var(--font-mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase;
|
|
844
|
+
color:var(--faint); text-decoration:none; border-bottom:2px solid transparent;
|
|
845
|
+
transition:color var(--duration-fast) var(--ease); }
|
|
846
|
+
.sw__tab:hover { color:var(--ink); }
|
|
847
|
+
.sw__tab[aria-current="page"] { color:var(--ink); border-bottom-color:var(--spot); }
|
|
848
|
+
.sw__tab i { font-style:normal; color:var(--spot); }
|
|
849
|
+
.card.sw__list { padding:0; }
|
|
850
|
+
.sw__list .set, .sw__list .set:first-child, .sw__list .set:last-child { padding:var(--space-4) var(--space-5); border-bottom:1px solid var(--line); }
|
|
851
|
+
.sw__list .set:last-child { border-bottom:none; }
|
|
852
|
+
.set--bool { align-items:center; flex-wrap:nowrap; }
|
|
853
|
+
.set--bool .set__ctl { flex:0 0 auto; }
|
|
854
|
+
/* A switch: a square track and thumb, the accent when on. It is still a checkbox underneath. */
|
|
855
|
+
.set__ctl input[type="checkbox"] { appearance:none; -webkit-appearance:none; width:44px; height:24px; margin:0; position:relative;
|
|
856
|
+
background:var(--mass); border:1px solid var(--line-2); border-radius:0; cursor:pointer;
|
|
857
|
+
transition:background var(--duration-fast) var(--ease), border-color var(--duration-fast) var(--ease); }
|
|
858
|
+
.set__ctl input[type="checkbox"]::after { content:""; position:absolute; top:2px; left:2px; width:18px; height:18px;
|
|
859
|
+
background:var(--dim); transition:transform var(--duration-fast) var(--ease), background var(--duration-fast) var(--ease); }
|
|
860
|
+
.set__ctl input[type="checkbox"]:checked { background:var(--spot); border-color:var(--spot); }
|
|
861
|
+
.set__ctl input[type="checkbox"]:checked::after { transform:translateX(20px); background:var(--spot-ink); }
|
|
862
|
+
@media (max-width:900px) {
|
|
863
|
+
.sw { grid-template-columns:minmax(0,1fr); gap:var(--space-4); min-height:0; }
|
|
864
|
+
.sw__scopes { order:-1; align-self:auto; flex-direction:row; align-items:center; overflow-x:auto; scrollbar-width:none; }
|
|
865
|
+
.sw__scopes::-webkit-scrollbar { display:none; }
|
|
866
|
+
.sw__cap { display:none; }
|
|
867
|
+
.sw__projects { flex-direction:row; max-height:none; overflow:visible; }
|
|
868
|
+
.sw__scope { white-space:nowrap; }
|
|
869
|
+
.sw__none { margin:0; white-space:nowrap; }
|
|
870
|
+
}
|
|
871
|
+
@media (max-width:560px) {
|
|
872
|
+
.sw__list .set, .sw__list .set:first-child, .sw__list .set:last-child { padding:var(--space-4); }
|
|
873
|
+
.set--bool { flex-wrap:wrap; }
|
|
874
|
+
}
|
|
824
875
|
/* A config path is one unbroken word; on a phone it has to break anywhere. */
|
|
825
876
|
.counts .mono, .lines .mono { overflow-wrap:anywhere; }
|
|
826
877
|
/* ---------- feature tips: Driver.js hints restyled, vendor CSS untouched ---------- */
|
|
@@ -3118,8 +3169,8 @@ const TIPS = [
|
|
|
3118
3169
|
when: () => (S.artifacts ?? []).some((a) => a.correction === 'open') },
|
|
3119
3170
|
{ id: 'artifact-tabs', where: ['artifacts/view'], el: '#view [role="tablist"]', title: 'Open pages',
|
|
3120
3171
|
text: 'Pages you open stay here as tabs, up to eight. All artifacts takes you back.' },
|
|
3121
|
-
{ id: 'settings-scope', where: ['settings/dashboard'], el: '
|
|
3122
|
-
text: 'User settings apply everywhere.
|
|
3172
|
+
{ id: 'settings-scope', where: ['settings/dashboard'], el: '.sw__scopes', side: 'left', title: 'User or project',
|
|
3173
|
+
text: 'User settings apply everywhere. Pick a project to override them for that project only.' },
|
|
3123
3174
|
{ id: 'ground', where: ['settings/dashboard'], el: '.ground[aria-label="Colour ground"]', side: 'right', title: 'Ink or paper',
|
|
3124
3175
|
text: 'Switch the dashboard between a dark and a light ground.' },
|
|
3125
3176
|
];
|
|
@@ -3492,7 +3543,7 @@ function settingRow(f, d, scope) {
|
|
|
3492
3543
|
? (Object.hasOwn(d.user.set, f.key) ? 'inherited from user settings' : 'default')
|
|
3493
3544
|
: 'default'}</span>`;
|
|
3494
3545
|
}
|
|
3495
|
-
return `<div class="set">
|
|
3546
|
+
return `<div class="set${f.type === 'bool' ? ' set--bool' : ''}">
|
|
3496
3547
|
<div class="set__main"><label for="${id}">${esc(f.label)}</label>
|
|
3497
3548
|
<p id="${id}-h">${esc(f.help)}${ruleHint(f) ? ` <span class="set__rule">${esc(ruleHint(f))}</span>` : ''}</p>
|
|
3498
3549
|
<p class="set__src"><code>${esc(f.key)}</code>${src}<span data-msg="${esc(f.key)}" role="status"></span></p></div>
|
|
@@ -3500,37 +3551,86 @@ function settingRow(f, d, scope) {
|
|
|
3500
3551
|
</div>`;
|
|
3501
3552
|
}
|
|
3502
3553
|
|
|
3554
|
+
/** A category's tab id: `This machine` is `this-machine`. */
|
|
3555
|
+
const swTab = (group) => group.toLowerCase().replace(/\W+/g, '-');
|
|
3556
|
+
const SW_ICON = {
|
|
3557
|
+
user: '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
|
|
3558
|
+
project: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
|
3559
|
+
};
|
|
3560
|
+
|
|
3561
|
+
/**
|
|
3562
|
+
* The switchboard. On the left, the scope: you (user settings, the default) or
|
|
3563
|
+
* one project. On the right, that scope's settings one category at a time, each
|
|
3564
|
+
* category a tab in the URL (`#/settings/dashboard/memory`). Picking another scope
|
|
3565
|
+
* keeps the tab. `Terminal` lists what stays a typed command; `Changed` lists
|
|
3566
|
+
* what this scope's file actually sets.
|
|
3567
|
+
*/
|
|
3503
3568
|
function drawSettings(d, scope) {
|
|
3504
3569
|
SETS = d;
|
|
3505
|
-
const
|
|
3570
|
+
const tabParam = (id) => (id === swTab(d.fields[0].group) ? null : id);
|
|
3571
|
+
const page = scope === 'project' ? 'project' : 'dashboard';
|
|
3572
|
+
const file = scope === 'project' ? d.project : d.user;
|
|
3573
|
+
const groups = [...new Set(d.fields.map((f) => f.group))];
|
|
3574
|
+
const tabs = groups.map((g) => {
|
|
3575
|
+
const fields = d.fields.filter((f) => f.group === g);
|
|
3576
|
+
return { id: swTab(g), label: g, fields, n: file ? fields.filter((f) => Object.hasOwn(file.set, f.key)).length : 0 };
|
|
3577
|
+
});
|
|
3578
|
+
tabs.push({ id: 'terminal', label: 'Terminal', n: 0 }, { id: 'changed', label: 'Changed', n: file ? Object.keys(file.set).length : 0 });
|
|
3579
|
+
const here = tabs.find((t) => t.id === CUR.param) ?? tabs[0];
|
|
3580
|
+
|
|
3581
|
+
const scopeLink = (label, icon, to, current) =>
|
|
3582
|
+
`<a class="sw__scope" href="${esc(to)}"${current ? ' aria-current="page"' : ''}>${svg(icon)}<span>${esc(label)}</span></a>`;
|
|
3583
|
+
const rail = `<nav class="sw__scopes" aria-label="Settings scope">
|
|
3584
|
+
<p class="sw__cap">Everywhere</p>
|
|
3585
|
+
${scopeLink('User', SW_ICON.user, SH('dashboard', tabParam(here.id)), scope === 'user')}
|
|
3586
|
+
<p class="sw__cap">Projects</p>
|
|
3587
|
+
<div class="sw__projects">${d.projects.length
|
|
3588
|
+
? d.projects.map((p) => scopeLink(p.name, SW_ICON.project, SH('project', tabParam(here.id), { project: p.inventory }),
|
|
3589
|
+
scope === 'project' && d.project?.inventory === p.inventory)).join('')
|
|
3590
|
+
: '<p class="sw__none">No project with a checkout on this machine yet.</p>'}</div>
|
|
3591
|
+
</nav>`;
|
|
3592
|
+
|
|
3506
3593
|
const about = `User settings apply everywhere; a project setting wins in that project only, and <b>Inherit</b>
|
|
3507
3594
|
removes it so the user setting applies again. Every change here is the same write as
|
|
3508
3595
|
<code>eklavya config set <key> <value></code> (with <code>--project</code> for a project), and
|
|
3509
3596
|
<b>Inherit</b> / <b>Reset</b> is <code>eklavya config unset</code>. Nothing is written inside a repository.`;
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
return [
|
|
3513
|
-
pageHead(
|
|
3597
|
+
|
|
3598
|
+
if (!file) {
|
|
3599
|
+
return `<div class="sw"><div class="sw__pane">${[
|
|
3600
|
+
pageHead('Project settings', [], about),
|
|
3514
3601
|
nextStep(PROJECT
|
|
3515
|
-
? `<strong>${esc(scopeName())}</strong> has no git checkout on this machine, so there is no project file to change. Pick
|
|
3516
|
-
: 'Pick a project to change its settings
|
|
3517
|
-
|
|
3518
|
-
go: SH('project', null, { project: p.inventory }) })), 'No project with a checkout on this machine yet.')),
|
|
3519
|
-
].join('');
|
|
3602
|
+
? `<strong>${esc(scopeName())}</strong> has no git checkout on this machine, so there is no project file to change. Pick another project on the right.`
|
|
3603
|
+
: 'Pick a project on the right to change its settings.'),
|
|
3604
|
+
].join('')}</div>${rail}</div>`;
|
|
3520
3605
|
}
|
|
3521
|
-
|
|
3522
|
-
const
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
...groups.map((g) => card(g, null, d.fields.filter((f) => f.group === g).map((f) => settingRow(f, d, scope)).join(''))),
|
|
3529
|
-
fold(`settings:${scope}:cli`, 'Changed from the terminal', plural(d.cli_only.length, 'setting'),
|
|
3606
|
+
|
|
3607
|
+
const tabBar = `<nav class="sw__tabs" aria-label="Setting categories">${tabs.map((t) =>
|
|
3608
|
+
`<a class="sw__tab" href="${esc(SH(page, tabParam(t.id)))}"${t.id === here.id ? ' aria-current="page"' : ''}>${esc(t.label)}${t.n ? `<i title="${t.n} set in this scope">${t.n}</i>` : ''}</a>`).join('')}</nav>`;
|
|
3609
|
+
|
|
3610
|
+
let body;
|
|
3611
|
+
if (here.id === 'terminal') {
|
|
3612
|
+
body = card('Changed from the terminal', 'These send work off this machine or run a command, so they stay a typed command: eklavya config set <key> <value>.',
|
|
3530
3613
|
lines(d.cli_only.map((c) => ({ t: `<code>${esc(c.key)}</code>`, s: esc(c.why),
|
|
3531
|
-
v: `<span class="mono">${esc(showValue(file.effective[c.key]))}</span>` })))
|
|
3532
|
-
|
|
3533
|
-
|
|
3614
|
+
v: `<span class="mono">${esc(showValue(file.effective[c.key]))}</span>` }))));
|
|
3615
|
+
} else if (here.id === 'changed') {
|
|
3616
|
+
const tabOf = (k) => { const f = d.fields.find((x) => x.key === k); return f ? swTab(f.group) : 'terminal'; };
|
|
3617
|
+
body = card(`Set ${scope === 'project' ? 'for this project' : 'by you'}`,
|
|
3618
|
+
scope === 'project' ? 'Everything else inherits from user settings.' : 'Everything else is the default.',
|
|
3619
|
+
lines(Object.keys(file.set).map((k) => ({ t: `<code>${esc(k)}</code>`,
|
|
3620
|
+
s: scope === 'project' ? esc(`user: ${showValue(d.user.effective[k])}`) : '',
|
|
3621
|
+
v: `<span class="mono">${esc(showValue(file.set[k]))}</span>`, go: SH(page, tabParam(tabOf(k))) })),
|
|
3622
|
+
scope === 'project' ? 'This project overrides nothing; user settings apply.' : 'Nothing set: every setting is at its default.'));
|
|
3623
|
+
} else {
|
|
3624
|
+
body = `<section class="card sw__list" aria-label="${esc(here.label)} settings">${here.fields.map((f) => settingRow(f, d, scope)).join('')}</section>`;
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
return `<div class="sw"><div class="sw__pane">${[
|
|
3628
|
+
pageHead(scope === 'project' ? `${esc(d.project.name)} settings` : 'User settings',
|
|
3629
|
+
[`<span class="mono">${esc(file.path)}</span>`, `${Object.keys(file.set).length} set here`], about),
|
|
3630
|
+
d.problem ? nextStep(`<strong>A config file is not being read.</strong> ${esc(d.problem)}`) : '',
|
|
3631
|
+
tabBar,
|
|
3632
|
+
body,
|
|
3633
|
+
].join('')}</div>${rail}</div>`;
|
|
3534
3634
|
}
|
|
3535
3635
|
|
|
3536
3636
|
/* ============================================================
|
|
@@ -3736,30 +3836,7 @@ function fbDelete(btn) {
|
|
|
3736
3836
|
|
|
3737
3837
|
function viewSettings(scope) {
|
|
3738
3838
|
fill('settings', settingsUrl(), (d) => drawSettings(d, scope));
|
|
3739
|
-
return `<div
|
|
3740
|
-
}
|
|
3741
|
-
|
|
3742
|
-
function viewSettingsHome() {
|
|
3743
|
-
fill('settings', settingsUrl(), (d) => {
|
|
3744
|
-
SETS = d;
|
|
3745
|
-
const proj = d.project;
|
|
3746
|
-
const overrides = proj ? Object.keys(proj.set).filter((k) => !d.global_only.includes(k)) : [];
|
|
3747
|
-
return [
|
|
3748
|
-
pageHead('Settings', [plural(Object.keys(d.user.set).length, 'user setting'),
|
|
3749
|
-
proj ? `${plural(overrides.length, 'project setting')} in ${esc(proj.name)}` : 'no project selected'],
|
|
3750
|
-
`Two files, one precedence: the project's file wins over yours, key by key, and anything neither sets is the
|
|
3751
|
-
default. The terminal writes the same files — <code>eklavya config get</code> shows what is in force.`),
|
|
3752
|
-
card(null, null, lines([
|
|
3753
|
-
{ t: 'User settings', s: `<span class="mono">${esc(d.user.path)}</span>`, v: esc(`${Object.keys(d.user.set).length} set`), go: SH('user') },
|
|
3754
|
-
{ t: 'Project settings', s: proj ? `<span class="mono">${esc(proj.path)}</span>` : 'choose a project',
|
|
3755
|
-
v: proj ? esc(`${overrides.length} set`) : '', go: SH('project') },
|
|
3756
|
-
])),
|
|
3757
|
-
proj && overrides.length ? card(`What ${proj.name} changes`, null, lines(overrides.map((k) => ({
|
|
3758
|
-
t: `<code>${esc(k)}</code>`, s: esc(`user: ${showValue(d.user.effective[k])}`),
|
|
3759
|
-
v: `<span class="mono">${esc(showValue(proj.set[k]))}</span>`, go: SH('project') })))) : '',
|
|
3760
|
-
].join('');
|
|
3761
|
-
});
|
|
3762
|
-
return `<div class="stack" id="settings">${loader('Reading your settings…')}</div>`;
|
|
3839
|
+
return `<div id="settings">${loader('Reading your settings…')}</div>`;
|
|
3763
3840
|
}
|
|
3764
3841
|
|
|
3765
3842
|
/** Reads a control back into the value its field stores. */
|
|
@@ -3914,13 +3991,11 @@ const WORKFLOWS = {
|
|
|
3914
3991
|
},
|
|
3915
3992
|
settings: {
|
|
3916
3993
|
name: 'Settings',
|
|
3917
|
-
groups: [
|
|
3918
|
-
{ id: 'scopes', label: 'Scopes', items: ['user', 'project'] },
|
|
3919
|
-
],
|
|
3994
|
+
groups: [],
|
|
3920
3995
|
pages: {
|
|
3921
|
-
dashboard: { label: '
|
|
3922
|
-
user: {
|
|
3923
|
-
project: {
|
|
3996
|
+
dashboard: { label: 'Preferences', view: () => viewSettings('user') },
|
|
3997
|
+
user: { parent: 'dashboard', view: () => viewSettings('user') },
|
|
3998
|
+
project: { parent: 'dashboard', view: () => viewSettings('project') },
|
|
3924
3999
|
},
|
|
3925
4000
|
},
|
|
3926
4001
|
};
|
|
@@ -9,8 +9,16 @@ You are teaching a real person, not generating a lesson. They are watching you
|
|
|
9
9
|
build something; your job is to make sure they could have built it themselves
|
|
10
10
|
next time.
|
|
11
11
|
|
|
12
|
-
Everything you learn about them persists in the Eklavya MCP server
|
|
13
|
-
|
|
12
|
+
Everything you learn about them persists in the Eklavya MCP server: use it to
|
|
13
|
+
never ask the same question twice.
|
|
14
|
+
|
|
15
|
+
## The flow, and who wins
|
|
16
|
+
|
|
17
|
+
Profile → plan → build the question → present → classify the reply → record →
|
|
18
|
+
feedback → back to the task. Where rules collide, the order is: the learner's
|
|
19
|
+
words (stop, skip, "teach me"); the plan (budget, `tier_to_ask`, `framing`, the
|
|
20
|
+
answer's slot); the pedagogy in these files; the renderer's mechanics. An
|
|
21
|
+
exception sits beside the rule it overrides.
|
|
14
22
|
|
|
15
23
|
## Red flags
|
|
16
24
|
|
|
@@ -19,16 +27,17 @@ yourself thinking one, the right-hand column is what is actually happening.
|
|
|
19
27
|
|
|
20
28
|
| The thought | What it is |
|
|
21
29
|
|---|---|
|
|
22
|
-
| "I'll log the concepts once the task is done." | A checkpoint can only fire
|
|
23
|
-
| "One more question while they're engaged." | The plan said one. Two is the batch,
|
|
30
|
+
| "I'll log the concepts once the task is done." | A checkpoint can only fire mid-work. End-of-task logging restores the pile it replaced. |
|
|
31
|
+
| "One more question while they're engaged." | The plan said one. Two is the batch, early. |
|
|
24
32
|
| "The plan returned one item but there's more worth asking." | Calling the plan again refills a budget the server deliberately spent. |
|
|
25
33
|
| "They got it, near enough." | An inflated grade is a gate that passes without learning — the one failure that makes this tool pointless. |
|
|
26
|
-
| "Two blanks in a row; I should offer to stop." |
|
|
27
|
-
| "I'll
|
|
28
|
-
| "
|
|
29
|
-
| "
|
|
30
|
-
| "
|
|
31
|
-
| "
|
|
34
|
+
| "Two blanks in a row; I should offer to stop." | The planner already lowers the tier. Keep going at `tier_to_ask`; wait to be told to stop. |
|
|
35
|
+
| "They said it lacks context; I'll record a blank." | That grades them 0 for your mistake. Record `invalid`. |
|
|
36
|
+
| "I'll reword the question they got wrong." | A question in `asked_before` is spent. |
|
|
37
|
+
| "A definition is the transferable version of this." | `concept` focus wants the general rule; "What is X" is tier-1 recall. |
|
|
38
|
+
| "Too easy to be worth asking — I'll add a 'why'." | `easy` is tiers 1–2 and was earned. A smuggled "why" cannot be answered honestly. |
|
|
39
|
+
| "They know what 'Task 6' means." | They saw none of it. Define it or drop it. |
|
|
40
|
+
| "I'll put the dials above the stem for context." | They are in the status bar; a line you assemble cannot stay consistent. |
|
|
32
41
|
| "Grade 5 — they picked the right option." | Multiple choice caps at 4. One in four is a coin. |
|
|
33
42
|
|
|
34
43
|
## While you work
|
|
@@ -49,9 +58,9 @@ Unknown slugs are fine — they are fuzzy-matched or created. Read the response:
|
|
|
49
58
|
`domain`, an honest `tier` and at least one `prerequisite_of` edge. Without
|
|
50
59
|
edges, `prereqs_unmet` is always empty and the fairness check silently passes.
|
|
51
60
|
|
|
52
|
-
Logging is silent — never narrate it
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
Logging is silent — never narrate it or pause the work to announce it. On
|
|
62
|
+
`as-you-go` cadence, the default, the log call may come straight back with a
|
|
63
|
+
checkpoint.
|
|
55
64
|
|
|
56
65
|
## Which one is asking
|
|
57
66
|
|
|
@@ -74,18 +83,18 @@ too. Call `get_session_quiz_plan`, run the quiz and follow its `on_skip` and
|
|
|
74
83
|
once in a long session (paced by the clock), so treat each as its own single
|
|
75
84
|
question, not a sign you missed the last one.
|
|
76
85
|
|
|
77
|
-
**Your task answer goes last.**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
**Your task answer goes last.** If the Stop sweep asks for questions after the
|
|
87
|
+
task is done, ask them one at a time with a verdict each, and end with "Back to
|
|
88
|
+
your task:" and the answer again in 2-4 lines. Never end a turn on a question
|
|
89
|
+
or a verdict.
|
|
81
90
|
|
|
82
91
|
**While an agent builds.** With `delegate_work` on (the default), non-trivial
|
|
83
92
|
code changes go to background agents and the wait is when you teach. A hook
|
|
84
93
|
repeats this once, when you edit a second file yourself. Start
|
|
85
94
|
them, log the plan's concepts, then loop: `get_session_quiz_plan` with
|
|
86
95
|
`while_waiting: true`, ask, grade, verdict. Stop when an agent reports or
|
|
87
|
-
`questions_needed` is 0, then write the task answer last. Agents cannot ask,
|
|
88
|
-
|
|
96
|
+
`questions_needed` is 0, then write the task answer last. Agents cannot ask, and hooks
|
|
97
|
+
do not quiz inside them. Keep small fixes and lookups inline.
|
|
89
98
|
|
|
90
99
|
**How many questions is not your call — it is the plan's.** Under `as-you-go`
|
|
91
100
|
the plan returns one item, the sweep included. Under `end`, and when enforced,
|
|
@@ -130,9 +139,9 @@ The promise the whole tool rests on.
|
|
|
130
139
|
|
|
131
140
|
- **A question in `asked_before` is spent** — not "reword it", spent. Ask a
|
|
132
141
|
different thing about the same concept.
|
|
133
|
-
- **Never ask about a slug in `known`** unless it is also in `due_for_review
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
- **Never ask about a slug in `known`** unless it is also in `due_for_review`
|
|
143
|
+
or its plan item is `reason: "gate_work"` (an enforced gate lists it even when
|
|
144
|
+
mastered; skipping it can make the gate impossible to pass).
|
|
136
145
|
- `record_attempt` returns `repeat_question: true` if you broke this. Treat it as
|
|
137
146
|
a mistake you just made.
|
|
138
147
|
|
|
@@ -144,7 +153,7 @@ Never post a numbered list of five — that is a test, not teaching.
|
|
|
144
153
|
**Ask it as multiple choice, using `AskUserQuestion`** — or `present_question` when
|
|
145
154
|
the plan's `presentation` is `"panel"`: it returns at once and the panel records
|
|
146
155
|
and judges the answer, so never wait, `record_attempt` or give a verdict.
|
|
147
|
-
|
|
156
|
+
**Read
|
|
148
157
|
`references/writing-mcq.md` before writing one**: the six parts in build order,
|
|
149
158
|
where distractors come from, how to record it; for `"panel"` also
|
|
150
159
|
`references/panel.md`.
|
|
@@ -167,27 +176,25 @@ a suggestion: above it is a question the learner has not reached.
|
|
|
167
176
|
|
|
168
177
|
Definitions are tier 1 **only**. "What is X" at tier 3 is a bad question.
|
|
169
178
|
|
|
170
|
-
**The stem is the whole question
|
|
171
|
-
sign it.** The dials live in the
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
follow `ask_attribution` rather than deciding.
|
|
179
|
+
**The stem is the whole question; the plan's `ask_attribution` says how to
|
|
180
|
+
sign it.** The dials live in the status bar, so they never go in the stem.
|
|
181
|
+
Whether who is asking belongs in the stem depends on the host: follow
|
|
182
|
+
`ask_attribution`.
|
|
175
183
|
|
|
176
184
|
## Grading, and blanks
|
|
177
185
|
|
|
178
|
-
`record_attempt` for **every** answer
|
|
179
|
-
`
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
tool exists to prevent. **Read `references/grading.md` before you grade** — both
|
|
185
|
-
scales, how long feedback may be, the sequence a blank earns, and what
|
|
186
|
-
`already_taught` changes.
|
|
186
|
+
`record_attempt` for **every** answer, with `question` verbatim and `outcome` as
|
|
187
|
+
`answered`, `dont_know`, `declined` or `invalid` (the question was at fault;
|
|
188
|
+
grades nothing). Multiple choice caps at 4. "I don't know" is not a skip: it is a
|
|
189
|
+
request for teaching. **Read `references/grading.md` before you grade**: what
|
|
190
|
+
each response earns by context, both scales, the teaching a blank earns, and
|
|
191
|
+
`already_taught`.
|
|
187
192
|
|
|
188
193
|
**A missed answer can come back with `explain`.** That is `explain_on_wrong` at
|
|
189
194
|
work: follow its `instruction` exactly. Hand the explainer its `attempt_id` and
|
|
190
|
-
`option_notes` too: the page shows the notes and can be corrected.
|
|
195
|
+
`option_notes` too: the page shows the notes and can be corrected. A parallel
|
|
196
|
+
tutor's reply ends with a handoff block (`agents/tutor.md`): start the
|
|
197
|
+
explainer from it, every field included. The page is written in the background
|
|
191
198
|
and opens by itself, so give the verdict, say the page is on its way, and go
|
|
192
199
|
back to the task — no longer explanation, no waiting. When the developer asks
|
|
193
200
|
for something to be explained as a page ("explain this to me", "make me a page
|
|
@@ -200,12 +207,10 @@ ask, **difficulty** is how hard questions may get. They are independent and
|
|
|
200
207
|
every combination is coherent.
|
|
201
208
|
|
|
202
209
|
You choose none of them — the plan and the hooks do. **Read
|
|
203
|
-
`references/focus-and-level.md` before you quiz**: the three focuses
|
|
204
|
-
|
|
205
|
-
enforced gate retry that is the only route out of a blocked commit.
|
|
210
|
+
`references/focus-and-level.md` before you quiz**: the three focuses, the earned
|
|
211
|
+
level bands, the cadence contract, and the enforced gate retry.
|
|
206
212
|
|
|
207
213
|
## The bar
|
|
208
214
|
|
|
209
|
-
The developer should finish a quiz thinking *"I understand what we just built"
|
|
210
|
-
|
|
211
|
-
wrong question.
|
|
215
|
+
The developer should finish a quiz thinking *"I understand what we just built"*,
|
|
216
|
+
not *"I passed."* A question that only proves they read the diff was the wrong one.
|
|
@@ -204,7 +204,8 @@ into the batch it replaced.
|
|
|
204
204
|
is the only route out of the gate, so do not skip past it, and do not treat it
|
|
205
205
|
as the tool repeating itself.
|
|
206
206
|
|
|
207
|
-
A concept they explicitly **declined** is not offered again
|
|
207
|
+
A concept they explicitly **declined** is not offered again by this session's
|
|
208
|
+
retry (a later session's review may bring it back once it is due). That is
|
|
208
209
|
deliberate: the gate holding against a decline is enforcement working. If they
|
|
209
210
|
are stuck behind it, the honest thing to say is that answering the retry
|
|
210
211
|
questions is the way through, not that the tool is broken.
|