dsh-nerd-mode 0.4.0 → 0.4.3

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/cordis.patch.yml CHANGED
@@ -6,23 +6,19 @@
6
6
  recallSessionEnabled: false
7
7
  unseenQuestionCount: 3
8
8
  unseenQuestionType: short-answer
9
- section: |
10
- Nerd Mode is on for this conversation. These rules take precedence over any earlier persona, style, or answer-shape text in this prompt, including text that asks for brevity, for a direct answer, or for no follow-up question.
11
-
12
- The `study` and `study-plus` skills are available through the skill tool. An explicit `/study-plus` means the user chose tracked study: load `study-plus` directly with the topic and supplied material, without a route question. Otherwise, load `study` by default for one concept or one learning task and use chat output. Follow `study` for lecture-writing standards. Save other artifacts only when the user asks for a path. Do not let `study` read or write tracker files in `study/<topic>/`. Do not grade, score, or log in `study`.
13
-
14
- Every other request keeps the base mode's behavior: produce the artifact, run the command, make the edit. Answer in plain prose, without a teaching detour and without a closing question the learner did not ask for.
15
-
16
- Study-plus loads `study` before Teach. It owns tracker updates, automatic lecture files for tracked sessions, and book assembly. Direct `study` may save a user-requested lecture or other artifact to the requested path. The four tracker files remain study-plus-only. Do not grade in `study`.
17
-
18
- Teaching level: run calibration once per new topic, after graph approval and before any tracker file is written. Ask it as one question-tool call with three items: the goal, the prior exposure, and a multi-select skip list. Ask the goal and background items only when the current request and the supplied material do not already state them. Derive `beginner`, `standard`, or `advanced`. Answer `beginner` when most prerequisites are unknown, or when the goal is to learn the topic from zero. Answer `standard` when some prerequisites are known and the goal is to understand the principles or pass an exam. Answer `advanced` when most prerequisites are known and the goal is review, reference, or use in a project. When the two answers clearly conflict, you may choose an adjacent level and state the reason in one line. Write `Goal`, `Background`, and `Teaching level` in `graph.md`. When the learner declines or gives no usable answer, write `Teaching level: standard` and omit the other two lines. Pass the lines that exist verbatim to `study`, without compressing or rewriting them. A missing `Teaching level` line means `standard`. Never ask for calibration on a topic that already has a `graph.md`. A mid-topic change of depth lasts for this session only. Only an explicit "from now on" rewrites the `Teaching level` line.
19
-
20
- Routing order: an explicit `/study-plus` loads `study-plus` directly, even for multiple independent concepts, and never asks a route question. Otherwise, for multiple independent concepts, ask exactly one question-tool choice before loading either skill. This includes default Nerd Mode and explicit `/study` entry. Offer only one-off complete Book, Chapter, and Section content in this conversation, or tracked study-plus with one concept per session. If the learner chooses one-off, load `study` with the current topic and supplied material. If the learner chooses tracked, load `study-plus` with the current topic and supplied material. Never ask again after the learner chooses. For one concept or one learning task without explicit `/study-plus`, load `study` by default.
21
-
22
- `study` defaults to chat output. Do not apply a blanket ban on files under `study/`: study-plus owns its tracker files and automatic tracked-session lectures. Direct `study` may save user-requested lectures, diagrams, and other artifacts. Only study-plus may read or write `graph.md`, `state.md`, `log.md`, and `plan.md`.
23
9
  - id: study-plus-skills
24
10
  name: '@deepseek-ai/dsh-skill-filesystem'
25
11
  config:
26
12
  providerName: study-plus
27
13
  includeDefaultRoots: false
28
14
  bundledSkillDir: !!js process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:path').dirname(process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('dsh-nerd-mode/package.json')), 'skills')
15
+
16
+ # Goal mode stays off. The built-in create_goal description tells the model to
17
+ # arm a goal for any long-running request, which turns one study answer into an
18
+ # auto-continuing run. Without the three packages below, no goal can be created.
19
+ - id: tool-goal
20
+ disabled: true
21
+ - id: command-goal
22
+ disabled: true
23
+ - id: goal-round-driver
24
+ disabled: true
package/lib/index.js CHANGED
@@ -1,213 +1,223 @@
1
- /**
2
- * Nerd Mode: a per-session switch. While it is on, one prompt section joins
3
- * every request and the `study` skill becomes the default answer shape. State
4
- * folds from the session log, so it survives compaction, resume, and fork. The
5
- * tool set never changes.
6
- *
7
- * The section registers at the profile level, not inside a preset, so it
8
- * reaches every agent in every preset. See docs/adr/0008.
9
- */
10
-
11
- import { COMMAND, KEY, fold, init } from './fold.js'
12
- import z from '@deepseek-ai/schemastery'
13
-
14
- export const name = 'dsh-nerd-mode'
15
- export const PACKAGE_NAME = 'dsh-nerd-mode'
16
- export const inject = ['systemPrompt', 'sessionProjections']
17
-
18
- export const Config = z.object({
19
- section: z.string().pattern(/\S/).required(),
20
- recallSessionEnabled: z.boolean().default(false).volatile(),
21
- unseenQuestionCount: z.number().step(1).min(0).max(10).default(3).volatile(),
22
- unseenQuestionType: z.union([
23
- z.const('short-answer'),
24
- z.const('multiple-choice'),
25
- z.const('mixed'),
26
- ]).default('short-answer').volatile(),
27
- })
28
-
29
- const SECTION = 'study:policy'
30
- // Between PLAN_POLICY (500) and TEAM_POLICY (600).
31
- const SECTION_ORDER = 550
32
- const studyPlusDefaultsBySession = new WeakMap()
33
- const directStudyPlusBySession = new WeakMap()
34
- // A PTC skill load leaves no message in the history, so keep its id for later
35
- // turns the way an injected skill message would.
36
- const ptcStudyPlusBySession = new WeakMap()
37
- const SKILL_COMMAND = /(^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?=\s|$)/g
38
-
39
- // shortcut: parse-only schemas, no zod. The projection registry calls `parse`
40
- // and nothing else. Upgrade to zod if the host starts introspecting schemas.
41
- const stateSchema = {
42
- parse: (value) => ({
43
- active: value?.active === true,
44
- running:
45
- value?.running && typeof value.running.commandId === 'string'
46
- ? { commandId: value.running.commandId, wanted: value.running.wanted === true }
47
- : null,
48
- }),
49
- }
50
-
51
- const viewSchema = { parse: (value) => ({ active: value?.active === true }) }
52
-
53
- function studyPlusSettings(config, session, invocationId) {
54
- let defaults = studyPlusDefaultsBySession.get(session)
55
- if (defaults?.invocationId !== invocationId) {
56
- defaults = {
57
- invocationId,
58
- recallSessionEnabled: config.recallSessionEnabled?.get?.() ?? false,
59
- unseenQuestionCount: config.unseenQuestionCount?.get?.() ?? 3,
60
- unseenQuestionType: config.unseenQuestionType?.get?.() ?? 'short-answer',
61
- }
62
- studyPlusDefaultsBySession.set(session, defaults)
63
- }
64
-
65
- return [
66
- '',
67
- '',
68
- 'Study-plus profile defaults for this tracked session:',
69
- `- Recall Session: ${defaults.recallSessionEnabled ? 'enabled' : 'disabled'}.`,
70
- `- Post-lecture questions: ${defaults.unseenQuestionCount}; type: ${defaults.unseenQuestionType}.`,
71
- 'Apply these defaults only during tracked study-plus sessions. Ignore them for ordinary study and all other requests.',
72
- 'An explicit preference in the current /study-plus request overrides its profile default for this session and does not update the profile.',
73
- ].join('\n')
74
- }
75
-
76
- /** Return the message id when the message is a direct /study-plus command, else undefined. */
77
- function directStudyPlusId(message) {
78
- if (message?.source?.kind !== 'user') return undefined
79
- const text = (message.content ?? [])
80
- .filter((block) => block.type === 'text')
81
- .map((block) => block.text)
82
- .join('\n')
83
- return [...text.matchAll(SKILL_COMMAND)].at(-1)?.[2] === 'study-plus' ? message.id : undefined
84
- }
85
-
86
- function studyPlusInvocation(session) {
87
- const directInvocation = directStudyPlusBySession.get(session)
88
- if (directInvocation !== undefined) return directInvocation
89
- const ptcInvocation = ptcStudyPlusBySession.get(session)
90
- if (ptcInvocation !== undefined) return ptcInvocation
91
-
92
- // The host returns the full surface history here, not just this turn. See docs/agents/dsh-session-api.md.
93
- const invocation = session.deriveMessages().findLast((message) => message.source?.kind === 'skill-invocation')
94
- if (invocation?.source.name === 'study-plus') return invocation.id
95
-
96
- const completedCalls = new Set()
97
- let loadedId
98
- let directId
99
- let skillCall
100
- let ptcSkillCall
101
- for (let seq = session.seq - 1; seq >= 0; seq -= 1) {
102
- const event = session.eventAt(seq)
103
- if (event?.type === 'turn/start') break
104
- // A resumed, forked, or seeded log may carry a partial event payload, so
105
- // every nested read here is guarded. One bad record must not fail a turn.
106
- if (event?.type === 'tool/result') {
107
- const result = event.data?.message
108
- if (result !== undefined && result.isError !== true && result.source?.callId !== undefined) {
109
- completedCalls.add(result.source.callId)
110
- }
111
- } else if (event?.type === 'tool/call' && event.data?.name === 'skill') {
112
- try {
113
- if (JSON.parse(event.data.arguments).name === 'study-plus') skillCall = event.data.callId
114
- } catch {}
115
- } else if (event?.type === 'tool/ptc-dispatch' && event.data?.name === 'skill') {
116
- // PTC records a skill call made inside run_code as a dispatch, not as a
117
- // `tool/call`, and the dispatch event itself carries the outcome.
118
- if (event.data.isError !== true && event.data.arguments?.name === 'study-plus') {
119
- ptcSkillCall = event.data.subCallId
120
- }
121
- } else if (event?.type === 'user/message') {
122
- // `user/message` stores the message fields directly in `data`; only
123
- // `tool/result` and `system/message` wrap them under `data.message`.
124
- const message = event.data
125
- if (message?.source?.kind === 'skill-invocation') {
126
- if (message.source.name === 'study-plus') loadedId = message.id
127
- } else {
128
- directId = directStudyPlusId(message) ?? directId
129
- }
130
- }
131
- }
132
-
133
- // A skill load counts only when its tool call completed. A typed command counts at once.
134
- if (skillCall !== undefined) {
135
- return completedCalls.has(skillCall) ? loadedId ?? directId ?? skillCall : undefined
136
- }
137
- if (ptcSkillCall !== undefined) {
138
- ptcStudyPlusBySession.set(session, ptcSkillCall)
139
- return loadedId ?? directId ?? ptcSkillCall
140
- }
141
- return loadedId ?? directId
142
- }
143
-
144
- const projection = {
145
- key: KEY,
146
- stateVersion: 1,
147
- stateSchema,
148
- init,
149
- apply: fold,
150
- wire: {
151
- viewSchema,
152
- view: (state) => ({ active: state.active }),
153
- },
154
- }
155
-
156
- /**
157
- * @param config - `section` is the overlay text, required and non-empty.
158
- */
159
- export function apply(ctx, config) {
160
- const section = config?.section
161
- if (typeof section !== 'string' || section.trim() === '') {
162
- throw new Error('dsh-nerd-mode needs a non-empty string `section`')
163
- }
164
-
165
- ctx.sessionProjections.register(projection)
166
-
167
- ctx.effect(
168
- () => ctx.on('agent/inbox/claimed', ({ agent, message }) => {
169
- directStudyPlusBySession.set(agent.session, directStudyPlusId(message))
170
- }),
171
- 'dsh-nerd-mode: study-plus claim',
172
- )
173
-
174
- ctx.effect(
175
- () =>
176
- ctx.systemPrompt.section({
177
- name: SECTION,
178
- order: SECTION_ORDER,
179
- text: (context) => {
180
- if (context.agent === undefined) return ''
181
- if (!ctx.sessionProjections.stateOf(context.agent.session, KEY).active) return ''
182
- const session = context.agent.session
183
- const invocationId = studyPlusInvocation(session)
184
- if (invocationId === undefined) return section
185
- return section + studyPlusSettings(config ?? {}, session, invocationId)
186
- },
187
- }),
188
- 'dsh-nerd-mode: section',
189
- )
190
-
191
- ctx.inject(['commands'], (commandCtx) => {
192
- commandCtx.commands.register({
193
- name: COMMAND,
194
- description: 'Turn Nerd Mode on or off for this conversation',
195
- input: { hint: '[on|off]' },
196
- handler: ({ agent, rawInput }) => {
197
- const arg = rawInput.trim().toLowerCase()
198
- const active = ctx.sessionProjections.stateOf(agent.session, KEY).active
199
-
200
- if (arg === 'off') {
201
- return { kind: 'success', text: active ? 'Nerd Mode off.' : 'Nerd Mode is already off.' }
202
- }
203
- if (arg !== '' && arg !== 'on') {
204
- return { kind: 'error', text: `Unknown argument "${arg}". Use /nerd-mode on or /nerd-mode off.` }
205
- }
206
- return {
207
- kind: 'success',
208
- text: active ? 'Nerd Mode is already on.' : 'Nerd Mode on. /study guides a concept or a task.',
209
- }
210
- },
211
- })
212
- })
213
- }
1
+ /**
2
+ * Nerd Mode: a per-session switch. While it is on, one prompt section joins
3
+ * every request and the `study` skill becomes the default answer shape. State
4
+ * folds from the session log, so it survives compaction, resume, and fork. The
5
+ * tool set never changes.
6
+ *
7
+ * The section registers at the profile level, not inside a preset, so it
8
+ * reaches every agent in every preset. See docs/adr/0008.
9
+ */
10
+
11
+ import { readFileSync } from 'node:fs'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { COMMAND, KEY, fold, init } from './fold.js'
14
+ import z from '@deepseek-ai/schemastery'
15
+
16
+ export const name = 'dsh-nerd-mode'
17
+ export const PACKAGE_NAME = 'dsh-nerd-mode'
18
+ export const inject = ['systemPrompt', 'sessionProjections']
19
+
20
+ // The prompt text ships with the package. Keeping it out of the config stops
21
+ // every profile patch from carrying a stale copy of it.
22
+ const BUNDLED_SECTION = readFileSync(
23
+ fileURLToPath(new URL('../assets/section.md', import.meta.url)),
24
+ 'utf8',
25
+ ).trim()
26
+
27
+ export const Config = z.object({
28
+ section: z.string().pattern(/\S/).default(BUNDLED_SECTION),
29
+ recallSessionEnabled: z.boolean().default(false).volatile(),
30
+ unseenQuestionCount: z.number().step(1).min(0).max(10).default(3).volatile(),
31
+ unseenQuestionType: z.union([
32
+ z.const('short-answer'),
33
+ z.const('multiple-choice'),
34
+ z.const('mixed'),
35
+ ]).default('short-answer').volatile(),
36
+ })
37
+
38
+ const SECTION = 'study:policy'
39
+ // Between PLAN_POLICY (500) and TEAM_POLICY (600).
40
+ const SECTION_ORDER = 550
41
+ const studyPlusDefaultsBySession = new WeakMap()
42
+ const directStudyPlusBySession = new WeakMap()
43
+ // A PTC skill load leaves no message in the history, so keep its id for later
44
+ // turns the way an injected skill message would.
45
+ const ptcStudyPlusBySession = new WeakMap()
46
+ const SKILL_COMMAND = /(^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?=\s|$)/g
47
+
48
+ // shortcut: parse-only schemas, no zod. The projection registry calls `parse`
49
+ // and nothing else. Upgrade to zod if the host starts introspecting schemas.
50
+ const stateSchema = {
51
+ parse: (value) => ({
52
+ active: value?.active === true,
53
+ running:
54
+ value?.running && typeof value.running.commandId === 'string'
55
+ ? { commandId: value.running.commandId, wanted: value.running.wanted === true }
56
+ : null,
57
+ }),
58
+ }
59
+
60
+ const viewSchema = { parse: (value) => ({ active: value?.active === true }) }
61
+
62
+ function studyPlusSettings(config, session, invocationId) {
63
+ let defaults = studyPlusDefaultsBySession.get(session)
64
+ if (defaults?.invocationId !== invocationId) {
65
+ defaults = {
66
+ invocationId,
67
+ recallSessionEnabled: config.recallSessionEnabled?.get?.() ?? false,
68
+ unseenQuestionCount: config.unseenQuestionCount?.get?.() ?? 3,
69
+ unseenQuestionType: config.unseenQuestionType?.get?.() ?? 'short-answer',
70
+ }
71
+ studyPlusDefaultsBySession.set(session, defaults)
72
+ }
73
+
74
+ return [
75
+ '',
76
+ '',
77
+ 'Study-plus profile defaults for this tracked session:',
78
+ `- Recall Session: ${defaults.recallSessionEnabled ? 'enabled' : 'disabled'}.`,
79
+ `- Post-lecture questions: ${defaults.unseenQuestionCount}; type: ${defaults.unseenQuestionType}.`,
80
+ 'Apply these defaults only during tracked study-plus sessions. Ignore them for ordinary study and all other requests.',
81
+ 'An explicit preference in the current /study-plus request overrides its profile default for this session and does not update the profile.',
82
+ ].join('\n')
83
+ }
84
+
85
+ /** Return the message id when the message is a direct /study-plus command, else undefined. */
86
+ function directStudyPlusId(message) {
87
+ if (message?.source?.kind !== 'user') return undefined
88
+ const text = (message.content ?? [])
89
+ .filter((block) => block.type === 'text')
90
+ .map((block) => block.text)
91
+ .join('\n')
92
+ return [...text.matchAll(SKILL_COMMAND)].at(-1)?.[2] === 'study-plus' ? message.id : undefined
93
+ }
94
+
95
+ function studyPlusInvocation(session) {
96
+ const directInvocation = directStudyPlusBySession.get(session)
97
+ if (directInvocation !== undefined) return directInvocation
98
+ const ptcInvocation = ptcStudyPlusBySession.get(session)
99
+ if (ptcInvocation !== undefined) return ptcInvocation
100
+
101
+ // The host returns the full surface history here, not just this turn. See docs/agents/dsh-session-api.md.
102
+ const invocation = session.deriveMessages().findLast((message) => message.source?.kind === 'skill-invocation')
103
+ if (invocation?.source.name === 'study-plus') return invocation.id
104
+
105
+ const completedCalls = new Set()
106
+ let loadedId
107
+ let directId
108
+ let skillCall
109
+ let ptcSkillCall
110
+ for (let seq = session.seq - 1; seq >= 0; seq -= 1) {
111
+ const event = session.eventAt(seq)
112
+ if (event?.type === 'turn/start') break
113
+ // A resumed, forked, or seeded log may carry a partial event payload, so
114
+ // every nested read here is guarded. One bad record must not fail a turn.
115
+ if (event?.type === 'tool/result') {
116
+ const result = event.data?.message
117
+ if (result !== undefined && result.isError !== true && result.source?.callId !== undefined) {
118
+ completedCalls.add(result.source.callId)
119
+ }
120
+ } else if (event?.type === 'tool/call' && event.data?.name === 'skill') {
121
+ try {
122
+ if (JSON.parse(event.data.arguments).name === 'study-plus') skillCall = event.data.callId
123
+ } catch {}
124
+ } else if (event?.type === 'tool/ptc-dispatch' && event.data?.name === 'skill') {
125
+ // PTC records a skill call made inside run_code as a dispatch, not as a
126
+ // `tool/call`, and the dispatch event itself carries the outcome.
127
+ if (event.data.isError !== true && event.data.arguments?.name === 'study-plus') {
128
+ ptcSkillCall = event.data.subCallId
129
+ }
130
+ } else if (event?.type === 'user/message') {
131
+ // `user/message` stores the message fields directly in `data`; only
132
+ // `tool/result` and `system/message` wrap them under `data.message`.
133
+ const message = event.data
134
+ if (message?.source?.kind === 'skill-invocation') {
135
+ if (message.source.name === 'study-plus') loadedId = message.id
136
+ } else {
137
+ directId = directStudyPlusId(message) ?? directId
138
+ }
139
+ }
140
+ }
141
+
142
+ // A skill load counts only when its tool call completed. A typed command counts at once.
143
+ if (skillCall !== undefined) {
144
+ return completedCalls.has(skillCall) ? loadedId ?? directId ?? skillCall : undefined
145
+ }
146
+ if (ptcSkillCall !== undefined) {
147
+ ptcStudyPlusBySession.set(session, ptcSkillCall)
148
+ return loadedId ?? directId ?? ptcSkillCall
149
+ }
150
+ return loadedId ?? directId
151
+ }
152
+
153
+ const projection = {
154
+ key: KEY,
155
+ stateVersion: 1,
156
+ stateSchema,
157
+ init,
158
+ apply: fold,
159
+ wire: {
160
+ viewSchema,
161
+ view: (state) => ({ active: state.active }),
162
+ },
163
+ }
164
+
165
+ /**
166
+ * @param config - `section` is an optional overlay text; a missing value uses
167
+ * the bundled `assets/section.md`.
168
+ */
169
+ export function apply(ctx, config) {
170
+ const section = config?.section
171
+ if (typeof section !== 'string' || section.trim() === '') {
172
+ throw new Error('dsh-nerd-mode needs a non-empty string `section`')
173
+ }
174
+
175
+ ctx.sessionProjections.register(projection)
176
+
177
+ ctx.effect(
178
+ () => ctx.on('agent/inbox/claimed', ({ agent, message }) => {
179
+ directStudyPlusBySession.set(agent.session, directStudyPlusId(message))
180
+ }),
181
+ 'dsh-nerd-mode: study-plus claim',
182
+ )
183
+
184
+ ctx.effect(
185
+ () =>
186
+ ctx.systemPrompt.section({
187
+ name: SECTION,
188
+ order: SECTION_ORDER,
189
+ text: (context) => {
190
+ if (context.agent === undefined) return ''
191
+ if (!ctx.sessionProjections.stateOf(context.agent.session, KEY).active) return ''
192
+ const session = context.agent.session
193
+ const invocationId = studyPlusInvocation(session)
194
+ if (invocationId === undefined) return section
195
+ return section + studyPlusSettings(config ?? {}, session, invocationId)
196
+ },
197
+ }),
198
+ 'dsh-nerd-mode: section',
199
+ )
200
+
201
+ ctx.inject(['commands'], (commandCtx) => {
202
+ commandCtx.commands.register({
203
+ name: COMMAND,
204
+ description: 'Turn Nerd Mode on or off for this conversation',
205
+ input: { hint: '[on|off]' },
206
+ handler: ({ agent, rawInput }) => {
207
+ const arg = rawInput.trim().toLowerCase()
208
+ const active = ctx.sessionProjections.stateOf(agent.session, KEY).active
209
+
210
+ if (arg === 'off') {
211
+ return { kind: 'success', text: active ? 'Nerd Mode off.' : 'Nerd Mode is already off.' }
212
+ }
213
+ if (arg !== '' && arg !== 'on') {
214
+ return { kind: 'error', text: `Unknown argument "${arg}". Use /nerd-mode on or /nerd-mode off.` }
215
+ }
216
+ return {
217
+ kind: 'success',
218
+ text: active ? 'Nerd Mode is already on.' : 'Nerd Mode on. /study guides a concept or a task.',
219
+ }
220
+ },
221
+ })
222
+ })
223
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-nerd-mode",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "A study switch and study skills for DeepSeek Harness.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,7 +48,7 @@ For `advanced`:
48
48
 
49
49
  Accuracy rules outrank the level. No level removes a symbol definition, a justification for a derivation step, a stated assumption or approximation, or a bound. No level adds filler, repeated explanation, or a section the concept does not need. The level affects the lecture only.
50
50
 
51
- For a one-off request with no graph, derive the level from the request and the supplied material. When neither states a goal or a background, ask one combined question before teaching: what the learner wants to reach and whether they have met the topic before. State the derived level in one line, then teach. Write no file.
51
+ For a one-off request with no graph, derive the level from the brief the upper layer supplies and from the request and the supplied material. A brief from an upper layer is final: never ask again for a goal or a background it leaves out. A bare topic, or "I want to learn X", states no goal. When no brief arrives and neither the request nor the supplied material states a goal or a background, ask one combined question before teaching: what the learner wants to reach and whether they have met the topic before. State the derived level in one line, then teach. Write no file.
52
52
 
53
53
  ## Format the answer
54
54
 
@@ -73,6 +73,6 @@ The upper layer may hand you an approved concept graph, the relevant supplied ma
73
73
 
74
74
  Default to chat output. If the learner asks to save a lecture or other artifact, save it to the requested path and show the same content in chat when practical.
75
75
 
76
- When an SVG would materially clarify a relationship, create it by default at `diagrams/<topic>/<name>.svg`. Load `diagram` for its visual authoring rules, but use this study-specific path instead of its generic default. Sanitize topic and name as single lowercase kebab-case path segments with no slashes or dot segments. If either is unclear, choose a safe fallback. Before writing, confirm the resolved parent stays inside the intended `diagrams/` root and the target is not `study/<topic>/graph.md`, `state.md`, `log.md`, or `plan.md`. Compute plotted points rather than placing them by eye. Before replacing an SVG with the same name, copy the old file to `<name>.svg.bak`, replacing any older backup.
76
+ When an SVG would materially clarify a relationship, create it by default. When the upper layer scopes a Course, write `study/<course>/diagrams/<name>.svg`. When the guide runs alone, write `diagrams/<name>.svg` and create no folder under `study/`. Load `diagram` for its visual authoring rules, but use this study-specific path instead of its generic default. Sanitize `course` and `name` as single lowercase kebab-case path segments with no slashes or dot segments. If either is unclear, choose a safe fallback. Before writing, confirm the resolved parent stays inside the intended `diagrams/` root and the target is not `study/<course>/graph.md`, `state.md`, `log.md`, or `plan.md`. Compute plotted points rather than placing them by eye. Before replacing an SVG with the same name, copy the old file to `<name>.svg.bak`, replacing any older backup.
77
77
 
78
- Never read or write `study/<topic>/graph.md`, `state.md`, `log.md`, or `plan.md`. Do not read or change tracker state. Study-plus owns the choice of lecture paths and the automatic lecture save during tracked sessions. Study may read or write ordinary artifacts, including lectures, drafts, and figures, at paths the user or the upper layer gives it.
78
+ Never read or write `study/<course>/graph.md`, `state.md`, `log.md`, or `plan.md`. Do not read or change tracker state. Study-plus owns the choice of lecture paths and the automatic lecture save during tracked sessions. Study may read or write ordinary artifacts, including lectures, drafts, and figures, at paths the user or the upper layer gives it.