@erclx/aitk 0.102.0 โ†’ 0.104.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "0.102.0",
4
+ "version": "0.104.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -11,6 +11,8 @@ Without this skill, the planning docs describe the scope the session opened with
11
11
 
12
12
  A decision's verification anchor has the same shape of gap in the other direction. The reasoning keeps reading as current while the number it cites moves underneath, and an amendment written without an anchor leaves nothing recording which numbers were checked and which nobody has read since.
13
13
 
14
+ A handoff file has the same shape of gap as an unmarked outcome. A page a learning workspace produced and an operator already picked a destination for sits in gitignored scratch until something lands it, and the session that produced it is gone by the time a branch exists to carry the write. Left unfolded it reads as promoted while the destination holds nothing.
15
+
14
16
  The trigger side carries a gap of its own. "Sync the docs" names either corpus to the person saying it, so a description leaving its corpus to the opening clause alone competes with its public-facing sibling on nothing the routing field states, and the planning surface the request was about goes untouched.
15
17
 
16
18
  ## Must
@@ -24,6 +26,8 @@ The trigger side carries a gap of its own. "Sync the docs" names either corpus t
24
26
  - Retarget a closed task at the archived plan, so the reasoning behind finished work stays reachable
25
27
  - Anchor a decision entry this run writes or amends whose reasoning cites a measured number, re-reading the number against the tree before writing the marker
26
28
  - Report an anchored decision whose cited path the diff touched, since the number was read before the branch moved what it counted
29
+ - Land each block of a promotion handoff at the destination its heading names, then delete the file so a later run does not fold it twice
30
+ - Take a promotion destination as already decided, since the operator confirmed it where the page was produced
27
31
 
28
32
  ## Must not
29
33
 
@@ -32,6 +36,7 @@ The trigger side carries a gap of its own. "Sync the docs" names either corpus t
32
36
  - Widen what a writing step reads when the baseline is unusable. Widening a read is safe and widening a write stubs a surface for every file in the repository.
33
37
  - Edit `CLAUDE.md` inline. Every change there goes through a diff-and-approve gate, so this skill only flags.
34
38
  - Create a context entry or delete a plan
39
+ - Overwrite a file a promotion block routes to. A destination that already holds a page is a merge for a person, and folding over it discards work this skill never read.
35
40
  - Write an anchor onto a decision the run did not amend, or refresh one without re-reading the number. A date from a pass that measured nothing is the false confidence the marker exists to prevent.
36
41
  - Anchor an entry written before the rule, which dates it by blame rather than by a read
37
42
 
@@ -43,7 +48,8 @@ The trigger side carries a gap of its own. "Sync the docs" names either corpus t
43
48
  ## Out of scope
44
49
 
45
50
  - Creating a task file or moving one off the board, which `claude-tasks` owns
46
- - Public-facing docs, which `docs-sync` owns. This skill reconciles the `.claude/` planning surface, and both descriptions name their corpus in the trigger so a request saying only "sync the docs" lands on one of the pair rather than on either.
51
+ - Public-facing docs, which `docs-sync` owns, apart from landing a page a promotion handoff already carries a confirmed destination for. This skill reconciles the `.claude/` planning surface, and both descriptions name their corpus in the trigger so a request saying only "sync the docs" lands on one of the pair rather than on either.
52
+ - Deciding where a promoted page belongs, which is settled with the operator by the surface that produced the page
47
53
  - Regenerating the task index, owned by a hook
48
54
  - Redrawing diagrams, which `claude-diagram` owns. This skill flags staleness and leaves the re-run to the author.
49
55
  - Re-measuring an architecture claim to decide whether its number moved. The sweep keys on a cited path entering the diff, so a claim whose number moved with no cited path in the diff goes unflagged.
@@ -51,7 +51,7 @@ Read these in parallel from the current worktree root (`pwd`), not the main work
51
51
 
52
52
  Read the task board from the main worktree root instead, per Worktrees in `CLAUDE.md`. It is gitignored scratch and never commits with the branch:
53
53
 
54
- - `.claude/tasks/index.md` first, then the task files this session touched. That narrow read serves the marking step. Step 9 reads every file in the folder for its plans sweep and states that where it gives the instruction.
54
+ - `.claude/tasks/index.md` first, then the task files this session touched. That narrow read serves the marking step. The scratch sweep reads every file in the folder for its plans sweep and states that where it gives the instruction.
55
55
 
56
56
  ## Step 2: identify what changed
57
57
 
@@ -85,7 +85,7 @@ The steps that follow reach past the session, so each earns the reach separately
85
85
  - Steps 4 and 5 stub against the diff. These are why the skip is not a stop. A session that changed no docs is exactly when an uncovered surface or diagram kind goes unnoticed.
86
86
  - Step 6 reads the architecture record against the diff. A run that amended no decision is the one where an anchored number moves under a reasoning nobody reread, which is the case the marker exists to surface.
87
87
  - Step 8 rewrites context entries against the diff and against the facts `claude-memory-capture` routed. The Diff baseline section above groups its diff half with Steps 4 and 5 as a scoped-set step, so a quiet session is no different from any other for it. The routed half reads a named file and runs whatever the diff shows.
88
- - Step 9 reads the board rather than the session. Its board-wide scan exists to clear a plan an earlier run stranded, and a run that stops at Step 2 can never reach one.
88
+ - The scratch sweep reads the board rather than the session. Its board-wide scan exists to clear a plan an earlier run stranded, and a run that stops at Step 2 can never reach one.
89
89
 
90
90
  This changes which steps the skill reaches. It does not widen what any of them reads. Steps 4, 5, 6, and 8 still take the same scoped set the Diff baseline section defines, and that section's rule is about the input a step is handed rather than about which steps run.
91
91
 
@@ -184,7 +184,30 @@ Add a line naming the handoff when one was consumed:
184
184
 
185
185
  The base lint-staged config runs `aitk indexes regen` on every committed `*.md`, so `.claude/context/index.md` refreshes automatically on commit. No manual step needed.
186
186
 
187
- ## Step 9: sweep consumed scratch
187
+ ## Step 9: fold promoted pages
188
+
189
+ Derive `<slug>` per `.claude/standards/slug.md`, falling back to `latest` on an empty result, and read `.claude/.tmp/teach-promotion/<slug>.md` at the main worktree root. `claude-teach` writes it, one H2 per destination naming the path, with a source line under the heading and the page body in a fenced block below that. Read the body out of the fence rather than off the heading level, since a reference page carries headings of its own and only the fence separates them from the next destination. Skip this step silently when the file is absent, which is every run where nothing was promoted.
190
+
191
+ Each block is a page an operator already confirmed a destination for, so this step lands it rather than judging it again. Write to the destination the heading names, at `pwd` rather than at the main root, since every destination here is a tracked file that commits with the branch:
192
+
193
+ - A wiki page and a public doc arrive as a whole file. Write it as the block gives it, and stop with the block unfolded when the destination path already holds a file, since overwriting a page someone else wrote is not a fold.
194
+ - A context entry is merged into rather than created. Fold the body into the sections it belongs under, the same way the routed facts above are folded, and never add an entry the catalog does not already carry.
195
+
196
+ Then delete the handoff file so a later run does not fold it twice, and regenerate the index of any folder that carries one.
197
+
198
+ Output one line per page landed:
199
+
200
+ `โœ… Promoted: <destination path>`
201
+
202
+ Add a line naming the handoff when one was consumed:
203
+
204
+ `๐Ÿงน Folded: .claude/.tmp/teach-promotion/<slug>.md`
205
+
206
+ Report a block left unfolded rather than dropping it:
207
+
208
+ `โš  Skipped: <destination path> already exists. Merge by hand.`
209
+
210
+ ## Step 10: sweep consumed scratch
188
211
 
189
212
  Sweep reviews this session consumed, and sweep plans across the whole board. Resolve all paths at the main worktree root, not the current worktree. See Worktrees in `CLAUDE.md`.
190
213
 
@@ -13,6 +13,8 @@ It also produces one output where two are needed. A page written to be worked th
13
13
 
14
14
  A session that does record something records the wrong thing. It writes what was taught rather than what the learner retrieved, and a tally of errors carries none of the misconception a later session would work against.
15
15
 
16
+ The durable half then has nowhere to go. A reference page and a glossary carry no learner and are ordinary reference prose, so they belong wherever the project already keeps prose on that subject, and a workspace holding them is a gitignored folder one person reads. A session moving them by hand picks a destination from the reader's activity rather than from who owns the subject, drops a page into a corpus without the source line that corpus requires, and has nothing stopping it from carrying a lesson across.
17
+
16
18
  Two failures land specifically on where the folder sits. A workspace resolved against the current directory forks into a copy per linked worktree, so the learning records split and no session sees the whole history. A body naming only the destination path reports success and loses the write, because the file-editing tools refuse a main-root path from a linked worktree and offer a worktree copy instead.
17
19
 
18
20
  ## Must
@@ -26,21 +28,26 @@ Two failures land specifically on where the folder sits. A workspace resolved ag
26
28
  - Split the output by lifetime, sending the worked-through half to lessons and the looked-up half to reference pages in the format the authoring gates read
27
29
  - Record the wrong answer itself rather than the count, since that is what the next session places the learner from
28
30
  - Report progress against the mission's success lines, so a mission can be called finished
31
+ - Propose a destination for each durable page by who owns its subject, and wait for the operator rather than routing on the session's own reading
32
+ - Hand each confirmed page off through a named file of its own, so one skill owns the durable writes and no other producer's unread work is discarded with it
33
+ - Name what the destination expects that the page does not carry yet, since the workspace runs none of the gates the destination does
29
34
 
30
35
  ## Must not
31
36
 
32
37
  - Teach from recall without saying which claims rest on it
33
- - Write outside the workspace folder, which is what a promotion pass owns rather than this skill
38
+ - Write to a destination surface directly, since the skill owning that file makes the edit and two skills writing one file at one step is what the handoff exists against
34
39
  - Open a second workspace on a subject an existing one already covers
35
40
  - Renumber a workspace folder, a lesson, or a learning record, since the number is cited by files already written
41
+ - Promote a lesson, at any age and on any request, since it is generated markup carrying a quiz and a learner into a corpus every other page passed a gate to enter
36
42
 
37
43
  ## Guards
38
44
 
39
45
  - No subject named and no existing workspace matching, stop rather than opening a workspace with nothing to learn
46
+ - No wiki folder in the project and a page routing there, refuse and name the command that creates one rather than scaffolding a surface the project never chose
40
47
 
41
48
  ## Out of scope
42
49
 
43
- - Promoting a durable page out of the workspace, which is a judgment about public prose and belongs to a separate surface
50
+ - Landing a promoted page at its destination, which belongs to the skill that owns durable writes there, and creating a wiki folder a project never chose
44
51
  - What the verbs do internally, which is the CLI domain's own contract rather than this skill's, and the records backup that carries the folder off one disk
45
52
  - What a lesson renders as and how a quiz behaves once rendered, which the lesson-craft reference shapes and a rendering layer executes
46
53
  - Deciding when to fire. The skill is user-invoked through `disable-model-invocation`, so opening a workspace is the learner's call rather than a description match.
@@ -1,22 +1,23 @@
1
1
  ---
2
2
  name: claude-teach
3
- description: Opens and runs a learning workspace on one subject, holding a mission, resources, numbered lessons, reference pages, a glossary, and learning records that survive across sessions. Use when asked to "teach me X", "open a learning workspace", "I want to learn X", "quiz me on this", "continue the lesson", or "resume my workspace on X". Do NOT use to write project documentation, which belongs to the surface owning that document, and do NOT use to answer one question, which is an ordinary reply.
3
+ description: Opens and runs a learning workspace on one subject, holding a mission, resources, numbered lessons, reference pages, a glossary, and learning records that survive across sessions, and proposes where a durable page from one belongs once it outgrows the workspace. Use when asked to "teach me X", "open a learning workspace", "I want to learn X", "quiz me on this", "continue the lesson", "resume my workspace on X", or "promote this reference page". Do NOT use to write project documentation, which belongs to the surface owning that document, and do NOT use to answer one question, which is an ordinary reply.
4
4
  disable-model-invocation: true
5
- argument-hint: <subject to learn, or the topic of the workspace to resume>
5
+ argument-hint: <subject to learn, or the topic of a workspace to resume or promote>
6
6
  ---
7
7
 
8
8
  # Claude teach
9
9
 
10
10
  Run a learning workspace on one subject across sessions. The workspace holds what the learner has been through, so a session weeks later resumes from the folder rather than from the conversation.
11
11
 
12
- The shape of the workspace is fixed by `.claude/standards/teach.md`, or `${CLAUDE_SKILL_DIR}/../../standards/teach.md` when the project does not have that file. Read it before writing anything into the folder. The pedagogy sits in `${CLAUDE_SKILL_DIR}/references/pedagogy.md` and the lesson craft in `${CLAUDE_SKILL_DIR}/references/lesson-craft.md`.
12
+ The shape of the workspace is fixed by `.claude/standards/teach.md`, or `${CLAUDE_SKILL_DIR}/../../standards/teach.md` when the project does not have that file. Read it before writing anything into the folder. The glossary answers to `.claude/standards/glossary.md`, or `${CLAUDE_SKILL_DIR}/../../standards/glossary.md` when the project does not have it. The pedagogy sits in `${CLAUDE_SKILL_DIR}/references/pedagogy.md`, the lesson craft in `${CLAUDE_SKILL_DIR}/references/lesson-craft.md`, and the promotion routing in `${CLAUDE_SKILL_DIR}/references/promotion.md`.
13
13
 
14
14
  ## Guards
15
15
 
16
16
  - If the invocation names no subject and no existing workspace matches, stop: `โŒ No subject. Invoke with the subject to learn, or the topic of a workspace to resume.`
17
17
  - Never trust recall for what the subject says. Research first, cite what was read, and say what was not.
18
- - Write nothing outside the workspace folder. A durable page stays in `reference/` until a promotion pass moves it, and this skill runs no promotion.
18
+ - Write nothing outside the workspace folder, apart from the one handoff file Step 6 names. A durable page stays in `reference/` and is copied out by the skill that owns the destination, never by this one.
19
19
  - Do not open a second workspace on a subject one already covers. Resume that one.
20
+ - Never promote a lesson. It is generated markup carrying a quiz and a learner, and no request makes it promotable.
20
21
 
21
22
  ## Step 0: let the CLI resolve the workspace root
22
23
 
@@ -37,9 +38,9 @@ From a linked worktree the file-editing tools refuse every path under the main r
37
38
 
38
39
  ## Step 1: open or resume
39
40
 
40
- A topic the listing already carries is a resume, and anything else is a new workspace.
41
+ A topic the listing already carries is a resume, and anything else is a new workspace. An invocation asking to promote is neither: read the named workspace through the listing and go to Step 6, which teaches nothing and writes no lesson.
41
42
 
42
- On a resume, run `aitk teach list <topic> --json` for the files behind each count, then read `MISSION.md`, the highest-numbered learning record, and `GLOSSARY.md`. Those three carry where the learner stopped and what they got wrong. Report the mission's success lines with what is already met before teaching anything.
43
+ On a resume, run `aitk teach list <topic> --json` for the files behind each count, then read the highest-numbered learning record and `GLOSSARY.md`. Those carry where the learner stopped and what they got wrong. The listing record carries `success`, the mission's success lines, which are the exit criteria this workspace is finished against. Report them with what is already met before teaching anything.
43
44
 
44
45
  On a new workspace, settle the starting point first, by asking rather than by assuming. Difficulty with no floor under it teaches nobody, and the mission cannot be written without it.
45
46
 
@@ -79,9 +80,27 @@ Open with retrieval on what the last session got wrong, before anything new. A l
79
80
 
80
81
  Two outputs with two lifetimes, and the split decides the format.
81
82
 
82
- - A lesson goes to `lessons/<nnnn>-<slug>.html`, self-contained, carrying its own quiz and the feedback for each answer. It links the shared stylesheet under `assets/` rather than restating styles, and the first lesson in a workspace writes that stylesheet before linking it. A lesson is disposable and is never promoted.
83
+ - A lesson is a self-contained page carrying its own quiz and the feedback for each answer. It links one shared stylesheet rather than restating styles, and it is disposable and never promoted.
83
84
  - A reference page goes to `reference/<slug>.md`, written for a reader with no learner in it. This is the half that survives the workspace, so it is written in markdown to pass the authoring gates a promotion would put it through.
84
85
 
86
+ Resolve the lesson before writing it, rather than composing its name or its quiz order by hand:
87
+
88
+ ```bash
89
+ aitk teach lesson <topic> --json \
90
+ --slug <kebab slug for what this lesson covers> \
91
+ --questions <how many questions the quiz carries> \
92
+ --options <how many options each question carries>
93
+ ```
94
+
95
+ It writes nothing and reports four things:
96
+
97
+ - `lesson`, the numbered path the lesson takes. Write it there.
98
+ - `stylesheet` with `stylesheetExists` and `stylesheetHref`. Write the stylesheet only when it reports absent, which is the first lesson in the workspace. Every lesson after that links the file already on disk and adds to it rather than replacing it, since overwriting discards what the last lesson put there.
99
+ - `success`, the mission's success lines, carried here so Step 5 needs no second read.
100
+ - `quiz`, one entry per question, carrying `order` and `answer`.
101
+
102
+ Write the correct option first, then present the options in the order `order` reports, reading it as authored indices where `0` is the correct one. Take the order as given. Position drawn here rather than chosen is the whole reason the verb exists, and a lesson that reorders on its own judgment puts the answer back in the first slot.
103
+
85
104
  Add every term the lesson defines to `GLOSSARY.md` through the verb, which places the entries alphabetically in the shape the standard fixes:
86
105
 
87
106
  ```bash
@@ -100,7 +119,43 @@ Write `learning-records/<nnnn>-<slug>.md` before the session ends, carrying the
100
119
 
101
120
  Record the wrong answer rather than the count. The next session places the learner from this file, and a tally carries no misconception to work against.
102
121
 
103
- Then restate the mission's success lines with what is now met. A mission whose lines are all met is finished, and saying so is what closes a workspace.
122
+ Then restate the mission's success lines with what is now met, reading them from the `success` the lesson verb already reported rather than from `MISSION.md` by eye. Report each line as met or not met, and name what the learner did that meets it. A mission whose lines are all met is finished, and saying so is what closes a workspace.
123
+
124
+ ## Step 6: propose where the durable half belongs
125
+
126
+ Run this when the invocation asks for it, or offer it in one line when a mission finishes, since that is when the workspace stops growing and its reference pages stop changing. Never run it unasked mid-course.
127
+
128
+ Read `${CLAUDE_SKILL_DIR}/references/promotion.md` first. It carries what may be promoted, the routing test, both spellings of the wiki folder, the refusal when a project has none, and what each destination expects a page to carry.
129
+
130
+ Propose and wait. A promoted page is public prose that needs a line naming who owns its subject, which is a judgment about ownership rather than a move a session makes on its own reading. Present one block per candidate page:
131
+
132
+ ```plaintext
133
+ reference/<slug>.md โ†’ <destination path>
134
+ Subject owner: <who owns it, in a few words>
135
+ Still owed: <what the destination expects that the page does not carry yet>
136
+ ```
137
+
138
+ Then stop and let the operator strike, redirect, or confirm each block.
139
+
140
+ Write nothing to a destination here. One skill owns the durable writes, and two skills editing one file at one step is the failure that rule exists against. Record each confirmed block in `.claude/.tmp/teach-promotion/<slug>.md` at the main worktree root instead, appending when the file exists, with one H2 per destination naming its path, the source page beneath it, and the page body fenced:
141
+
142
+ ````markdown
143
+ ## <destination path>
144
+
145
+ Source: .claude/teach/<nn>-<topic>/reference/<slug>.md
146
+
147
+ ```markdown
148
+ <the page body as it should land, with the source line the destination expects>
149
+ ```
150
+ ````
151
+
152
+ The body is fenced rather than written bare because a reference page carries headings of its own, and the reader splits this file on its H2 lines. An unfenced body turns every section heading in the page into a destination naming no path. Open the body fence with four backticks so a page carrying a fenced code block of its own still closes where it should, and widen both fences together if it carries a four-backtick fence.
153
+
154
+ Derive `<slug>` per `.claude/standards/slug.md`, or `${CLAUDE_SKILL_DIR}/../../standards/slug.md` when the project does not have it. Fall back to `latest` on an empty result.
155
+
156
+ The handoff is its own file rather than a shared one. The routed-facts file another skill writes is deleted by whichever pass folds it, so a second producer's unread work goes with it, and a sibling path costs the folding skill one more read and removes the interaction.
157
+
158
+ An append is a whole-file operation, so send it as a plain single `Bash` command carrying a heredoc, per Step 0. Then tell the operator that `/claude-docs` folds the file in from a branch. The proposal costs nothing tracked and runs anywhere, while the page it describes is a tracked file, so the fold is a worktree operation and the workspace it came from is not.
104
159
 
105
160
  ## Output
106
161
 
@@ -113,3 +168,12 @@ Progress: <n> of <m> success lines met
113
168
  ```
114
169
 
115
170
  Omit the reference line where the lesson produced no durable page. Emit every path from the project root, in the form the project's instruction file sets.
171
+
172
+ A promotion pass reports its own shape instead, one line per page the operator confirmed and one naming the handoff:
173
+
174
+ ```plaintext
175
+ โžก๏ธ Promoting: .claude/teach/<nn>-<topic>/reference/<slug>.md โ†’ <destination path>
176
+ โ†’ Confirmed pages wait at .claude/.tmp/teach-promotion/<slug>.md. Run /claude-docs from a branch to fold them in.
177
+ ```
178
+
179
+ A pass where the operator confirmed nothing writes no handoff file and reports that alone.
@@ -13,6 +13,7 @@ A workspace accumulates lessons over weeks. The learner reads them as one body o
13
13
 
14
14
  - Write the shared stylesheet into the workspace assets on the first lesson, and link it from every lesson after
15
15
  - Promote anything used a second time into that stylesheet. A second use makes it a component of the course.
16
+ - Add to that stylesheet rather than replacing it. The lesson writing it is rarely the lesson that needs it changed, and a rewrite drops what every earlier lesson links it for.
16
17
  - Keep the structural furniture identical across lessons: where the title sits, where the quiz sits, what a correct answer looks like
17
18
  - Do not restate styles inside a lesson. A local override is a decision the next lesson has to either copy or contradict.
18
19
 
@@ -39,11 +40,17 @@ Every element competes with the material for attention.
39
40
  The quiz is the retrieval, so a leak in its construction turns it into a reading test.
40
41
 
41
42
  - Write every option to the same length, in words and in characters. A longer option reads as the considered one.
42
- - Vary which position holds the correct answer, and never let the order follow the order the material was taught in
43
+ - Write the correct option first and let the ordering verb place it. Position is not the author's to pick: an author who varies it by hand still varies it by judgment, and the judgment settles on the first slot.
43
44
  - Make each wrong option a misconception someone actually holds. An obviously wrong option removes itself and shrinks the question.
44
45
  - Write feedback for every option, including the correct one, saying why rather than whether
45
46
  - Give feedback after the attempt, never alongside the question
46
47
 
48
+ ## Tokens travel with the course
49
+
50
+ A lesson carries its own values in the shared stylesheet rather than reading a host project's. A workspace runs in any project and most carry no token record at all, so a lesson inheriting one is a lesson that renders unstyled wherever the record is absent, with nothing reporting it.
51
+
52
+ Pick values the material needs rather than values the project happens to hold. Inheriting is worth offering as something a learner asks for once, and it is the wrong default in every project that cannot answer.
53
+
47
54
  ## What makes a lesson worth returning to
48
55
 
49
56
  - The worked example is complete. A learner returning for the example finds the whole thing rather than a fragment they have to reconstruct.
@@ -0,0 +1,54 @@
1
+ # Promotion routing
2
+
3
+ Which surface a durable page from a learning workspace belongs to, and what to do when the destination does not exist yet.
4
+
5
+ ## What may be promoted
6
+
7
+ Only the durable half. A reference page under `reference/` and `GLOSSARY.md` carry no learner and are ordinary reference prose about a subject, so they sort under the routing test like anything else.
8
+
9
+ A lesson is never promoted, at any age and on any request. It is generated markup written to be worked through once, it carries a quiz and a second person, and nothing downstream cites it. Promoting one puts ungated markup into a corpus every other page passed a gate to enter.
10
+
11
+ A reference page that reads like a lesson is a lesson wearing the wrong extension. Where a page under `reference/` carries a quiz, a second person, or an instruction to the learner, say so and propose rewriting it in place before promotion rather than promoting it and repairing it at the destination.
12
+
13
+ ## The routing test
14
+
15
+ Sort by who owns the subject, which is the test the wiki already runs. The reader's activity decided the workspace and decides nothing here.
16
+
17
+ | The page's subject | Destination |
18
+ | -------------------------------------------------------------- | ----------------------------- |
19
+ | Owned by someone outside the project, such as a tool or vendor | the project's wiki |
20
+ | How this project itself works, for the people who build it | `.claude/context/<domain>.md` |
21
+ | Written for someone consuming what this project ships | the project's public docs |
22
+
23
+ A page a reader cannot sort by that question is a page whose subject has not been settled. Say which two destinations it sits between and let the operator choose, rather than picking the nearer one.
24
+
25
+ Within the wiki, place the page by the same owner test that decides the folder: a vendor's own subject, another tool's subject, and a subject no single vendor owns each take the folder the project's wiki index names for them.
26
+
27
+ ## Where the wiki sits
28
+
29
+ The folder has two spellings and a project carries whichever its scaffold wrote:
30
+
31
+ - `.claude/wiki/`, which is where the scaffolding verb creates one
32
+ - A folder named `wiki` at the project root, which is where a project scaffolded before the move keeps its pages
33
+
34
+ Read which one exists rather than composing either. A project carrying both keeps its pages at the root, so propose the root spelling and say the other folder is there.
35
+
36
+ A project carrying neither gets a refusal rather than a scaffold:
37
+
38
+ `โŒ No wiki folder. Run aitk wiki init to create one, then promote again.`
39
+
40
+ Creating the folder as a side effect of promoting one page hands the project a surface it never chose, and the refusal is one command from resolved.
41
+
42
+ ## What a promoted page owes its destination
43
+
44
+ Each destination gates prose the workspace does not, so name what the page still needs rather than proposing a move that fails at the gate.
45
+
46
+ - A wiki page closes its intro paragraph with a sentence naming who owns the subject, and links the canonical page when one exists. A page carrying no such sentence is not ready to move.
47
+ - A context entry is internal narrative about one domain, so a page landing there merges into an existing entry rather than arriving as a new one.
48
+ - A glossary keeps the shape `glossary.md` fixes wherever it lands, and a promoted one has to state any rule it departs from.
49
+
50
+ ## Anti-patterns
51
+
52
+ - **The page promoted into a new context entry.** Entries are created deliberately, so a promotion proposing one is proposing a domain rather than moving a page.
53
+ - **The glossary split across destinations.** Terms drawn from one subject move together or not at all, since an entry defining a term the other half uses strands both.
54
+ - **The workspace emptied on promotion.** Promotion copies rather than moves. The workspace stays readable to the learner who built it, and the page's later life belongs to its destination.
@@ -26,4 +26,4 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
26
26
  - [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, the liveness confidence field, the unresolved reasons, and what the read depends on
27
27
  - [Skill audit](skills-audit.md): Measuring both skill corpora against standards/skill.md, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
28
28
  - [Tasks](tasks.md): Selecting a shipped task by stem or pull request, recording a number and closing an outcome, the refusal reasons, the board and backlog checks validate runs, and why the board root defaults to the main worktree
29
- - [Teach](teach.md): Listing learning workspaces and the ordinal a new one takes, opening one with its required files, recording sources and glossary terms, the refusal reasons, and why every write here runs through a verb
29
+ - [Teach](teach.md): Listing learning workspaces and the ordinal a new one takes, opening one with its required files, recording sources and glossary terms, resolving what the next lesson needs before it is written, the refusal reasons, and why every write here runs through a verb
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  title: Teach
3
- description: Listing learning workspaces and the ordinal a new one takes, opening one with its required files, recording sources and glossary terms, the refusal reasons, and why every write here runs through a verb
3
+ description: Listing learning workspaces and the ordinal a new one takes, opening one with its required files, recording sources and glossary terms, resolving what the next lesson needs before it is written, the refusal reasons, and why every write here runs through a verb
4
4
  ---
5
5
 
6
6
  # Teach
7
7
 
8
- Learning workspaces sit under `.claude/teach/<nn>-<topic>/`, and `standards/teach.md` fixes their layout, naming, and file formats. Every verb here resolves that folder against the main worktree root rather than against the working directory, so a session standing in a linked worktree reaches the one workspace the learner has rather than opening a second.
8
+ Learning workspaces sit under `.claude/teach/<nn>-<topic>/`, and `standards/teach.md` fixes their layout, naming, and file formats, apart from the glossary, whose shape `standards/glossary.md` fixes so it travels with the file wherever a promotion lands it. Every verb here resolves that folder against the main worktree root rather than against the working directory, so a session standing in a linked worktree reaches the one workspace the learner has rather than opening a second.
9
9
 
10
10
  That root resolution is also why the writing verbs exist at all. The file-editing tools refuse a main-root path from a linked worktree and offer a worktree copy instead, and a caller naming only the destination reports a success that did not happen. A whole-file create still goes out as a shell heredoc. Changing a line inside a file that already exists has no shell route, because the stream editors are banned, so `resource` and `glossary` are the route for the two files a running workspace edits.
11
11
 
@@ -102,6 +102,33 @@ A term already defined is refused rather than replaced. A definition the subject
102
102
 
103
103
  The entry lands as the standard's shape, leading with the term as a bolded span. A definition not ending in sentence punctuation is terminated before the citation is appended, so a bare phrase does not run into the sentence naming where the term first appears.
104
104
 
105
+ ## Lesson
106
+
107
+ `aitk teach lesson` resolves what the next lesson needs before it is written. It reads and never writes.
108
+
109
+ ```bash
110
+ aitk teach lesson regular-expressions \
111
+ --slug capture-groups \
112
+ --questions 3 \
113
+ --options 4
114
+ ```
115
+
116
+ | Option | Behavior |
117
+ | ----------------- | --------------------------------------------------------- |
118
+ | `--slug <kebab>` | The lesson's own topic, required |
119
+ | `--questions <n>` | How many questions the quiz carries, required |
120
+ | `--options <n>` | How many options each question carries, defaulting to `4` |
121
+ | `--json` | Emit a machine-readable record on stdout |
122
+ | `--root <path>` | Teach root, defaulting to the main worktree |
123
+
124
+ It reports four things. `lesson` is the numbered path the lesson takes, derived from the highest ordinal already in `lessons/` the way an open derives a workspace ordinal. `stylesheet` names the one file every lesson in the workspace links, with `stylesheetExists` saying whether it is on disk yet and `stylesheetHref` carrying what the lesson's own link element holds. `success` carries the mission's success lines, so a session reports progress against the exit criteria without a second read of `MISSION.md`. `quiz` carries one entry per question.
125
+
126
+ The stylesheet is reported rather than written. A verb that wrote it on every lesson would discard whatever the last lesson added, and the second lesson in a workspace is the one that needs the file the first one left.
127
+
128
+ Each `quiz` entry carries `order`, the authored option indices in presentation order where index `0` is the correct answer, and `answer`, the one-based position that answer lands in. Both travel together because a caller deriving the position itself is a caller that can derive it wrongly.
129
+
130
+ The order is drawn here rather than instructed, and that is the point of the verb. An author told to vary the position still varies it by judgment, and the judgment settles on the first slot, which is the defect this design departs from. The draw is uniform over the options, so the position carries no information about which answer is correct.
131
+
105
132
  ## Refusal reasons
106
133
 
107
134
  | Reason | Raised when |
@@ -116,4 +143,4 @@ The entry lands as the standard's shape, leading with the term as a bolded span.
116
143
  | `defined` | A term already carries a glossary entry |
117
144
  | `bad-input` | The command line is malformed, before any folder is read |
118
145
 
119
- A `bad-input` refusal reports the working directory as its root rather than the resolved one, since the command line is rejected before the root is worth resolving.
146
+ A `bad-input` refusal reports the working directory as its root rather than the resolved one, since the command line is rejected before the root is worth resolving. `lesson` raises it for a slug that is not kebab-case, for a quiz carrying no question, and for a question carrying fewer than two options, since a question with one option has nothing to confuse the right answer with.
@@ -282,6 +282,8 @@ This section is the corpus the coverage claim is measured against: every name `a
282
282
 
283
283
  Every row answers a question rather than marking a point in a project's life, so a phase above would send a reader to the wrong group.
284
284
 
285
+ A learning workspace produces two halves and only one of them leaves. A lesson is worked through once and stays in the workspace, and a reference page or a glossary carries no learner, so it belongs wherever the project already keeps prose on that subject. Asking `aitk:claude-teach` to promote sorts each durable page by who owns its subject, sending an outside subject to the wiki, an internal one to the matching context entry, and consumer-facing material to the public docs. It proposes and waits, because a promoted page is public prose that needs a line naming who owns the subject, and it writes nothing to a destination: each page the operator confirms goes to a handoff file that `aitk:claude-docs` folds in from a branch. A project with no wiki folder gets a refusal naming `aitk wiki init` rather than a folder it never asked for.
286
+
285
287
  ## Feedback routing
286
288
 
287
289
  ```plaintext
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.102.0",
4
+ "version": "0.104.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -1,5 +1,6 @@
1
1
  import { relative } from 'node:path'
2
2
  import type { Command } from 'commander'
3
+ import { type LessonOutcome, planLesson } from '@/teach/lesson'
3
4
  import {
4
5
  defineTerms,
5
6
  type ListOutcome,
@@ -57,6 +58,14 @@ interface ResourceCommandOptions {
57
58
  readonly root?: string
58
59
  }
59
60
 
61
+ interface LessonCommandOptions {
62
+ readonly json?: boolean
63
+ readonly options?: string
64
+ readonly questions?: string
65
+ readonly root?: string
66
+ readonly slug?: string
67
+ }
68
+
60
69
  interface GlossaryCommandOptions {
61
70
  readonly firstSeen?: string
62
71
  readonly json?: boolean
@@ -231,6 +240,42 @@ export function register(program: Command): void {
231
240
  .action(async (topic: string, opts: GlossaryCommandOptions) => {
232
241
  process.exitCode = await runGlossary(topic, opts)
233
242
  })
243
+
244
+ teach
245
+ .command('lesson')
246
+ .description('Resolve what the next lesson needs before it is written')
247
+ .argument('<topic>', 'Workspace folder or topic, as in regular-expressions')
248
+ .helpOption('-h, --help', 'Show this help message')
249
+ .option('--slug <kebab>', "The lesson's own topic, as in capture-groups")
250
+ .option('--questions <n>', 'How many questions the quiz carries')
251
+ .option('--options <n>', 'How many options each question carries', '4')
252
+ .option('--json', 'Emit a machine-readable record on stdout')
253
+ .option('--root <path>', 'Teach root, defaulting to the main worktree')
254
+ .addHelpText(
255
+ 'after',
256
+ [
257
+ '',
258
+ 'Exit codes:',
259
+ ' 0 the lesson was planned',
260
+ ' 1 refused, with the reason on stderr or in the JSON record',
261
+ '',
262
+ 'It writes nothing. It reports the numbered path the lesson takes, the',
263
+ 'shared stylesheet with whether that file exists yet, the mission',
264
+ 'success lines to report progress against, and one option order per',
265
+ 'question.',
266
+ '',
267
+ 'Write the correct option first and present the options in the order',
268
+ 'reported. Position is drawn here rather than chosen, so an answer',
269
+ 'cannot settle into the first slot.',
270
+ '',
271
+ 'Examples:',
272
+ ' aitk teach lesson regular-expressions --slug capture-groups --questions 3 --json',
273
+ '',
274
+ ].join('\n'),
275
+ )
276
+ .action(async (topic: string, opts: LessonCommandOptions) => {
277
+ process.exitCode = await runLesson(topic, opts)
278
+ })
234
279
  }
235
280
 
236
281
  function collect(value: string, previous: string[]): string[] {
@@ -435,6 +480,43 @@ async function runGlossary(
435
480
  )
436
481
  }
437
482
 
483
+ async function runLesson(
484
+ topic: string,
485
+ opts: LessonCommandOptions,
486
+ ): Promise<number> {
487
+ const emitJson = opts.json ?? false
488
+
489
+ if (!opts.slug) {
490
+ return reportRefusal(
491
+ 'aitk teach lesson',
492
+ badInput('No lesson slug. Pass --slug <kebab>.'),
493
+ emitJson,
494
+ process.cwd(),
495
+ )
496
+ }
497
+
498
+ if (!opts.questions) {
499
+ return reportRefusal(
500
+ 'aitk teach lesson',
501
+ badInput('No question count. Pass --questions <n>.'),
502
+ emitJson,
503
+ process.cwd(),
504
+ )
505
+ }
506
+
507
+ const root = await rootFor(opts.root)
508
+
509
+ return reportLesson(
510
+ await planLesson(root, topic, {
511
+ slug: opts.slug,
512
+ questions: Number(opts.questions),
513
+ options: Number(opts.options ?? '4'),
514
+ }),
515
+ emitJson,
516
+ root,
517
+ )
518
+ }
519
+
438
520
  function reportRefusal(
439
521
  title: string,
440
522
  refused: TeachRefused,
@@ -545,6 +627,11 @@ function reportWorkspace(
545
627
  logStep(workspace.glossary.length > 0 ? 'Glossary' : 'Glossary (empty)')
546
628
  for (const entry of workspace.glossary) logInfo(entry)
547
629
 
630
+ logStep(
631
+ workspace.success.length > 0 ? 'Exit criteria' : 'Exit criteria (none)',
632
+ )
633
+ for (const line of workspace.success) logInfo(line)
634
+
548
635
  if (workspace.missing.length > 0) {
549
636
  logStep('Missing a required file')
550
637
  logWarn(`no ${workspace.missing.join(' and no ')}`)
@@ -648,3 +735,52 @@ function reportGlossary(
648
735
 
649
736
  return 0
650
737
  }
738
+
739
+ function reportLesson(
740
+ outcome: LessonOutcome,
741
+ emitJson: boolean,
742
+ root: string,
743
+ ): number {
744
+ if (!outcome.ok) {
745
+ return reportRefusal('aitk teach lesson', outcome, emitJson, root)
746
+ }
747
+
748
+ if (emitJson) {
749
+ process.stdout.write(
750
+ `${JSON.stringify({
751
+ ok: true,
752
+ root,
753
+ slug: outcome.slug,
754
+ path: outcome.path,
755
+ lesson: outcome.lesson,
756
+ stylesheet: outcome.stylesheet,
757
+ stylesheetHref: outcome.stylesheetHref,
758
+ stylesheetExists: outcome.stylesheetExists,
759
+ success: outcome.success,
760
+ quiz: outcome.quiz,
761
+ })}\n`,
762
+ )
763
+ return 0
764
+ }
765
+
766
+ intro('aitk teach lesson')
767
+ logStep('Lesson')
768
+ logInfo(outcome.lesson)
769
+
770
+ logStep(outcome.stylesheetExists ? 'Stylesheet' : 'Stylesheet (to write)')
771
+ logInfo(`${outcome.stylesheet} linked as ${outcome.stylesheetHref}`)
772
+
773
+ logStep(outcome.success.length > 0 ? 'Exit criteria' : 'Exit criteria (none)')
774
+ for (const line of outcome.success) logInfo(line)
775
+
776
+ logStep('Option order')
777
+ for (const question of outcome.quiz) {
778
+ logInfo(
779
+ `question ${question.question}: present ${question.order.join(', ')}, correct answer in position ${question.answer}`,
780
+ )
781
+ }
782
+
783
+ outro()
784
+
785
+ return 0
786
+ }
@@ -9,6 +9,7 @@ import {
9
9
  TEACH_RECORDS,
10
10
  TEACH_REFERENCE,
11
11
  TEACH_RESOURCES,
12
+ TEACH_SUCCESS_HEADING,
12
13
  WORKSPACE_NAME,
13
14
  } from '@/teach/workspace'
14
15
 
@@ -576,7 +577,6 @@ async function checkDump(dir: string, slug: string): Promise<Finding[]> {
576
577
  return [...findings, ...perCluster.flat()]
577
578
  }
578
579
 
579
- const TEACH_SUCCESS = /^##[ \t]+Success looks like[ \t]*$/
580
580
  const NUMBERED_RECORD = /^\d{4}-[a-z0-9]+(-[a-z0-9]+)*\.md$/
581
581
  /**
582
582
  * A kebab slug that does not open with an ordinal. The lookahead rejects a
@@ -705,13 +705,15 @@ async function checkWorkspace(dir: string, slug: string): Promise<Finding[]> {
705
705
  }
706
706
 
707
707
  if (
708
- !linesOutsideFences(text).some((line) => TEACH_SUCCESS.test(line.trim()))
708
+ !linesOutsideFences(text).some(
709
+ (line) => line.trim() === TEACH_SUCCESS_HEADING,
710
+ )
709
711
  ) {
710
712
  findings.push(
711
713
  finding(
712
714
  'section-missing',
713
715
  slug,
714
- '## Success looks like',
716
+ TEACH_SUCCESS_HEADING,
715
717
  'is absent, so the mission names no observable thing the learner will be able to do.',
716
718
  ),
717
719
  )
@@ -209,23 +209,35 @@ export function countMechanicalAssertions(expectation: Expectation): number {
209
209
  )
210
210
  }
211
211
 
212
+ /**
213
+ * An entry carrying `*` is matched as a glob, so an arm can assert a file whose
214
+ * name a run derives. Reports the matched path rather than the pattern, since a
215
+ * pass on `lessons/0001-*.html` says nothing until the name it found is named.
216
+ */
212
217
  function checkPaths(
213
218
  expectation: Expectation,
214
219
  sandboxDir: string,
215
220
  ): AssertionResult[] {
216
- return expectation.paths.map((path) =>
217
- existsSync(join(sandboxDir, path))
218
- ? { ok: true, message: `exists: ${path}` }
219
- : { ok: false, message: `missing: ${path}` },
220
- )
221
+ return expectation.paths.map((path) => {
222
+ const written = writtenUnder(path, sandboxDir)
223
+
224
+ return written
225
+ ? { ok: true, message: `exists: ${written}` }
226
+ : { ok: false, message: `missing: ${path}` }
227
+ })
221
228
  }
222
229
 
223
230
  /**
224
- * An entry carrying `*` is matched as a glob, which is what lets an arm forbid
225
- * a file whose name a run derives rather than fixes. Pinning one spelling of a
226
- * derived name passes vacuously against every other spelling, which reads as
227
- * coverage the arm does not have. Returns the offending path so a failure names
228
- * the file the run wrote rather than the pattern that caught it.
231
+ * The first file an entry matches, or undefined when it matches none. An entry
232
+ * carrying `*` is matched as a glob, which is what lets an arm name a file whose
233
+ * name a run derives rather than fixes. Pinning one spelling of a derived name
234
+ * passes vacuously against every other spelling, which reads as coverage the arm
235
+ * does not have.
236
+ *
237
+ * Returning the match rather than a boolean is what lets a result name the file
238
+ * the run wrote instead of the pattern that found it. A glob matching several
239
+ * files answers with one of them in no fixed order, so an arm asserting content
240
+ * through a glob seeds a folder holding one.
229
241
  */
230
242
  function writtenUnder(pattern: string, sandboxDir: string): string | undefined {
231
243
  if (!pattern.includes('*')) {
@@ -260,14 +272,18 @@ function checkAbsent(
260
272
  * throwing, since an arm may assert content without also listing the path. A
261
273
  * pattern that does not compile is a defect in the declaration, so it fails the
262
274
  * assertion it belongs to rather than aborting the whole verdict.
275
+ *
276
+ * A path carrying `*` resolves to the file it matched, and falls back to itself
277
+ * when it matched none so the miss is reported against the entry as written.
263
278
  */
264
279
  function checkContent(
265
280
  expectation: Expectation,
266
281
  sandboxDir: string,
267
282
  ): AssertionResult[] {
268
283
  return expectation.content.map(({ path, pattern }) => {
269
- const full = join(sandboxDir, path)
270
- const label = `${path} =~ ${pattern}`
284
+ const matched = writtenUnder(path, sandboxDir) ?? path
285
+ const full = join(sandboxDir, matched)
286
+ const label = `${matched} =~ ${pattern}`
271
287
 
272
288
  let matcher: RegExp
273
289
  try {
@@ -279,6 +295,7 @@ function checkContent(
279
295
  if (!existsSync(full) || !statSync(full).isFile()) {
280
296
  return { ok: false, message: `no file to match: ${path}` }
281
297
  }
298
+
282
299
  if (matcher.test(readFileSync(full, 'utf8'))) {
283
300
  return { ok: true, message: `matches: ${label}` }
284
301
  }
@@ -0,0 +1,180 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import {
4
+ readWorkspace,
5
+ refuse,
6
+ TEACH_ASSETS,
7
+ TEACH_LESSONS,
8
+ TEACH_STYLESHEET,
9
+ type TeachRefused,
10
+ } from '@/teach/workspace'
11
+
12
+ /** Four digits inside a workspace, per the standard, because one holds many. */
13
+ const LESSON_WIDTH = 4
14
+
15
+ /** The ordinal a lesson filename opens with, which fixes its read order. */
16
+ const LESSON_NUMBER = /^(\d{4})-/
17
+
18
+ const LESSON_SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/
19
+
20
+ /** A quiz needs a right answer and at least one thing to confuse it with. */
21
+ const MIN_OPTIONS = 2
22
+
23
+ export interface QuizOrder {
24
+ /** One-based, so a report reads the same way the lesson numbers them. */
25
+ readonly question: number
26
+ /**
27
+ * Authored indices in the order the lesson presents them, where the authored
28
+ * index `0` is the correct answer. The author writes the correct option first
29
+ * and reads its position back off this list.
30
+ */
31
+ readonly order: readonly number[]
32
+ /** Where the correct answer lands, one-based, so no caller derives it. */
33
+ readonly answer: number
34
+ }
35
+
36
+ export interface LessonPlanned {
37
+ readonly ok: true
38
+ readonly slug: string
39
+ /** Relative to the root, so a caller prints a path a reader can open. */
40
+ readonly path: string
41
+ readonly lesson: string
42
+ readonly stylesheet: string
43
+ /** What the lesson's own `link` element carries, resolved from `lessons/`. */
44
+ readonly stylesheetHref: string
45
+ /** False on the first lesson in a workspace, which writes the stylesheet. */
46
+ readonly stylesheetExists: boolean
47
+ /** The mission's success lines, reported as the exit criteria they are. */
48
+ readonly success: readonly string[]
49
+ readonly quiz: readonly QuizOrder[]
50
+ }
51
+
52
+ export type LessonOutcome = LessonPlanned | TeachRefused
53
+
54
+ export interface LessonRequest {
55
+ readonly slug: string
56
+ readonly questions: number
57
+ readonly options: number
58
+ /**
59
+ * Injected so a test can assert the shape of an order against a generator it
60
+ * controls. Every caller outside a test takes the default, which is what
61
+ * keeps the position of a correct answer off the author's judgment.
62
+ */
63
+ readonly random?: () => number
64
+ }
65
+
66
+ /**
67
+ * A uniform permutation of `0 .. count - 1` by Fisher-Yates.
68
+ *
69
+ * The bias this exists against is the authored order surviving into the
70
+ * lesson, which puts the correct answer first whenever the author wrote it
71
+ * first. Drawing the permutation here rather than in a prompt is what makes the
72
+ * position unguessable from the outside.
73
+ */
74
+ function shuffled(count: number, random: () => number): number[] {
75
+ const order = Array.from({ length: count }, (_, index) => index)
76
+
77
+ for (let index = count - 1; index > 0; index -= 1) {
78
+ const pick = Math.floor(random() * (index + 1))
79
+ const held = order[index]
80
+ order[index] = order[pick]
81
+ order[pick] = held
82
+ }
83
+
84
+ return order
85
+ }
86
+
87
+ /**
88
+ * One presentation order per question, each carrying where the correct answer
89
+ * landed. Both halves travel together because a caller deriving the position
90
+ * itself is a caller that can derive it wrongly.
91
+ */
92
+ export function orderQuiz(
93
+ questions: number,
94
+ options: number,
95
+ random: () => number = Math.random,
96
+ ): QuizOrder[] {
97
+ return Array.from({ length: questions }, (_, index) => {
98
+ const order = shuffled(options, random)
99
+
100
+ return {
101
+ question: index + 1,
102
+ order,
103
+ answer: order.indexOf(0) + 1,
104
+ }
105
+ })
106
+ }
107
+
108
+ /**
109
+ * The number the next lesson takes, read off the filenames already there.
110
+ *
111
+ * A file whose name carries no ordinal moves nothing, the way a workspace
112
+ * folder with no ordinal moves no workspace number. Numbering past it would
113
+ * renumber nothing and skipping it would hide it.
114
+ */
115
+ function nextLesson(files: readonly string[]): string {
116
+ const highest = files
117
+ .map((file) => LESSON_NUMBER.exec(file)?.[1])
118
+ .filter((ordinal): ordinal is string => ordinal !== undefined)
119
+ .reduce((carry, ordinal) => Math.max(carry, Number(ordinal)), 0)
120
+
121
+ return String(highest + 1).padStart(LESSON_WIDTH, '0')
122
+ }
123
+
124
+ /**
125
+ * Everything a lesson needs resolved before it is written: where it goes, which
126
+ * stylesheet it links and whether that file is already on disk, the mission's
127
+ * exit criteria, and the order each quiz presents its options in.
128
+ *
129
+ * The stylesheet is reported rather than written. Every lesson after the first
130
+ * reads the one the first wrote, so a verb that rewrote it on every lesson
131
+ * would discard whatever the last one added.
132
+ */
133
+ export async function planLesson(
134
+ root: string,
135
+ selector: string,
136
+ request: LessonRequest,
137
+ ): Promise<LessonOutcome> {
138
+ if (!LESSON_SLUG.test(request.slug)) {
139
+ return refuse('bad-input', `Not a kebab-case slug: ${request.slug}.`, [
140
+ request.slug,
141
+ ])
142
+ }
143
+
144
+ if (!Number.isInteger(request.questions) || request.questions < 1) {
145
+ return refuse(
146
+ 'bad-input',
147
+ 'A lesson carries at least one question. Pass --questions <n>.',
148
+ )
149
+ }
150
+
151
+ if (!Number.isInteger(request.options) || request.options < MIN_OPTIONS) {
152
+ return refuse(
153
+ 'bad-input',
154
+ `A question carries at least ${MIN_OPTIONS} options. Pass --options <n>.`,
155
+ )
156
+ }
157
+
158
+ const found = await readWorkspace(root, selector)
159
+ if (!found.ok) return found
160
+
161
+ const workspace = found.workspace
162
+ const lesson = join(
163
+ workspace.path,
164
+ TEACH_LESSONS,
165
+ `${nextLesson(workspace.lessonFiles)}-${request.slug}.html`,
166
+ )
167
+ const stylesheet = join(workspace.path, TEACH_ASSETS, TEACH_STYLESHEET)
168
+
169
+ return {
170
+ ok: true,
171
+ slug: workspace.slug,
172
+ path: workspace.path,
173
+ lesson,
174
+ stylesheet,
175
+ stylesheetHref: `../${TEACH_ASSETS}/${TEACH_STYLESHEET}`,
176
+ stylesheetExists: existsSync(join(root, stylesheet)),
177
+ success: workspace.success,
178
+ quiz: orderQuiz(request.questions, request.options, request.random),
179
+ }
180
+ }
@@ -32,6 +32,22 @@ export const TEACH_GLOSSARY = 'GLOSSARY.md'
32
32
  export const TEACH_REFERENCE = 'reference'
33
33
  export const TEACH_RECORDS = 'learning-records'
34
34
  export const TEACH_LESSONS = 'lessons'
35
+ export const TEACH_ASSETS = 'assets'
36
+
37
+ /**
38
+ * The one stylesheet every lesson in a workspace links. The name is fixed here
39
+ * rather than chosen per lesson, because the second lesson has to reach the
40
+ * file the first one wrote and a name composed twice is a name that can differ.
41
+ */
42
+ export const TEACH_STYLESHEET = 'course.css'
43
+
44
+ /**
45
+ * The mission heading whose list a session reads as exit criteria. The writer,
46
+ * the reader below, and the record validator all match this one spelling, so a
47
+ * heading none of them can find fails the validator rather than reading as an
48
+ * empty list.
49
+ */
50
+ export const TEACH_SUCCESS_HEADING = '## Success looks like'
35
51
 
36
52
  /**
37
53
  * A workspace folder as the standard names it, capturing the ordinal and the
@@ -79,6 +95,8 @@ export interface WorkspaceDetail extends WorkspaceSummary {
79
95
  readonly recordFiles: readonly string[]
80
96
  readonly referenceFiles: readonly string[]
81
97
  readonly glossary: readonly string[]
98
+ /** The mission's success lines, which a session reports progress against. */
99
+ readonly success: readonly string[]
82
100
  }
83
101
 
84
102
  export interface WorkspacesListed {
@@ -141,7 +159,7 @@ export interface OpenRequest {
141
159
  readonly date?: string
142
160
  }
143
161
 
144
- function refuse(
162
+ export function refuse(
145
163
  reason: TeachRefusal,
146
164
  message: string,
147
165
  detail: readonly string[] = [],
@@ -198,6 +216,30 @@ function glossaryTerms(text: string): string[] {
198
216
  .map((line) => line.trim().slice('- '.length))
199
217
  }
200
218
 
219
+ /**
220
+ * The mission's success lines, each one an observable thing the learner will be
221
+ * able to do. A session reads them as exit criteria, so a wrapped entry is
222
+ * joined back into one line rather than reported as two criteria.
223
+ *
224
+ * A mission carrying no such heading yields nothing rather than refusing. The
225
+ * record validator is what reports the absent section, and a listing that
226
+ * refused would take the whole workspace down with it.
227
+ */
228
+ function successLines(text: string): string[] {
229
+ const lines = text.split('\n')
230
+ const section = sectionRange(
231
+ unfenced(text),
232
+ TEACH_SUCCESS_HEADING,
233
+ lines.length,
234
+ )
235
+
236
+ if (!section) return []
237
+
238
+ return bulletBlocks(lines.slice(section.start, section.end)).map((block) =>
239
+ block.join(' ').trim().slice('- '.length).replace(/\s+/g, ' ').trim(),
240
+ )
241
+ }
242
+
201
243
  /** The ordinal a folder name carries, or `NaN` when it carries none. */
202
244
  function ordinalOf(slug: string): number {
203
245
  const match = WORKSPACE_NAME.exec(slug)
@@ -211,9 +253,11 @@ async function summarize(
211
253
  ): Promise<WorkspaceDetail> {
212
254
  const match = WORKSPACE_NAME.exec(slug)
213
255
  const missionPath = join(dir, TEACH_MISSION)
214
- const frontmatter = existsSync(missionPath)
215
- ? parseFrontmatter(await readFile(missionPath, 'utf8'))
256
+ const mission = existsSync(missionPath)
257
+ ? await readFile(missionPath, 'utf8')
216
258
  : undefined
259
+ const frontmatter =
260
+ mission === undefined ? undefined : parseFrontmatter(mission)
217
261
 
218
262
  const glossaryPath = join(dir, TEACH_GLOSSARY)
219
263
  const glossary = existsSync(glossaryPath)
@@ -244,6 +288,7 @@ async function summarize(
244
288
  recordFiles,
245
289
  referenceFiles,
246
290
  glossary,
291
+ success: mission === undefined ? [] : successLines(mission),
247
292
  }
248
293
  }
249
294
 
@@ -0,0 +1,75 @@
1
+ ---
2
+ title: Glossary reference
3
+ description: Frontmatter, entry shape, ordering, and the rules deciding which terms a glossary carries
4
+ ---
5
+
6
+ # Glossary reference
7
+
8
+ Applies to a glossary, the file holding one entry per term a body of material defines. It changes whenever the material names a concept a reader cannot look up yet, and it is revised in place rather than appended to.
9
+
10
+ ## Scope
11
+
12
+ Governs a glossary at `.claude/teach/<nn>-<topic>/GLOSSARY.md` and at whatever path a surface fixes for one it holds: its frontmatter, entry shape, ordering, grouping, and the rules deciding which terms it carries.
13
+
14
+ Does not govern:
15
+
16
+ - The folder a learning workspace lays out around its glossary, and the other files in it: `teach.md`
17
+ - Which surface a glossary moves to once it leaves the material that produced it, which belongs to the surface driving that move
18
+ - Voice and word choice: `prose.md`
19
+ - Headings, punctuation, and file references: `markdown.md`
20
+
21
+ ## What a working glossary looks like
22
+
23
+ A glossary works when a reader who meets a term in the material settles it here without opening the page that introduced it:
24
+
25
+ - Which word does this material use for the concept, and which words does it deliberately not use?
26
+ - What does the term mean, stated without leaning on the term itself?
27
+ - Where does the term appear, so a reader can see it used rather than only defined?
28
+ - Does every entry carry a term the material actually uses?
29
+
30
+ A glossary failing these is non-conforming even when it satisfies every shape rule below.
31
+
32
+ ## Frontmatter
33
+
34
+ - `title` (required): names the material the terms come from, in sentence case
35
+ - `description` (required): one line naming what a reader gets from the entries
36
+
37
+ ## Entries
38
+
39
+ - Write one entry per term, as a single bullet.
40
+ - Lead the bullet with the term as a bolded span, then the definition in one or two sentences.
41
+ - Define the term without using it. A definition that spends the term explains nothing to the reader who arrived not knowing it.
42
+ - Name where the term first appears, so a reader can reach one use of it in context.
43
+ - Keep an entry to the meaning. Worked detail belongs on the page that teaches the term.
44
+ - Sort entries alphabetically, so a reader who knows only the word finds it without reading the file.
45
+
46
+ ## Which terms it carries
47
+
48
+ - Add a term once the material has used it, never ahead of that. A glossary front-loaded with terms nothing has introduced is a syllabus rather than a reference.
49
+ - Pick one word per concept and use that word everywhere. A glossary carrying two words for one thing hands the reader a choice it exists to remove.
50
+ - List each rejected synonym as an alias to avoid inside the entry that won, so a reader arriving with the wrong word lands on the right one.
51
+ - Use the glossary's own terms inside other definitions. A definition reaching for a synonym of a term defined two entries down teaches the reader a word the material does not use.
52
+ - Revise an entry the material has moved under rather than adding a second one narrating the change.
53
+
54
+ ## Grouping
55
+
56
+ - Keep a short glossary as one alphabetical list under the title. Grouping a handful of entries costs a heading per category and saves no lookup.
57
+ - Group a glossary long enough that one list stops helping under `##` headings by category, sorted alphabetically within each. Roughly two screens of entries is the signal.
58
+ - Name each category so a reader picks it from the term alone. A category a reader cannot predict makes the grouping a second thing to search.
59
+ - State a departure from any rule above in the file itself, naming what it departs from and why. A glossary serving no single body of material is the case that produces one, since a term drawn from everywhere has no first appearance to name.
60
+
61
+ ## Template
62
+
63
+ ```markdown
64
+ ---
65
+ title: <Material the terms come from>
66
+ description: <one line naming what a reader gets from these entries>
67
+ ---
68
+
69
+ # <Material the terms come from>
70
+
71
+ <One line on which material these terms come from and when the file changes.>
72
+
73
+ - **<Term>**: <the meaning in one or two sentences, written without using the term>. Avoid <rejected synonym>. First appears in `<page or lesson>`.
74
+ - **<Term>**: <the meaning in one or two sentences, written without using the term>. First appears in `<page or lesson>`.
75
+ ```
@@ -11,6 +11,7 @@ Reference docs for consistent authoring across the toolkit and target projects.
11
11
  - [Context entry reference](context.md): Shape and content rules for .claude/context/<domain>.md entries
12
12
  - [Design reference](design.md): Shape and content rules for .claude/DESIGN.md
13
13
  - [Diagram reference](diagrams.md): Shape and content rules for .claude/diagrams/<kind>.md files
14
+ - [Glossary reference](glossary.md): Frontmatter, entry shape, ordering, and the rules deciding which terms a glossary carries
14
15
  - [Groundwork reference](groundwork.md): Folder layout, reserved numbering, frontmatter and dating, required file contents, and conventions for a measurement track
15
16
  - [Intake reference](intake.md): Folder layout, reserved index number, frontmatter and dating, the item template, the answer contract, and retrieval
16
17
  - [Markdown reference](markdown.md): Headings, paragraph and list structure, code spans, the date form, punctuation, emphasis, and file references
@@ -26,6 +27,6 @@ Reference docs for consistent authoring across the toolkit and target projects.
26
27
  - [Slug reference](slug.md): Transform from a git branch name to a slug, and the three responses to an empty result
27
28
  - [Standard reference](standard.md): Shape and content rules for authoring a standard
28
29
  - [Tasks reference](tasks.md): Folder layout, filename convention, readiness groups, and content rules for .claude/tasks/
29
- - [Teach reference](teach.md): Workspace layout, ordinal naming, frontmatter, and the mission, learning-record, and glossary formats for a learning workspace
30
+ - [Teach reference](teach.md): Workspace layout, ordinal naming, frontmatter, and the mission and learning-record formats for a learning workspace
30
31
  - [Versioning reference](versioning.md): Phase label vs semver discipline across tasks, PRs, reviews, issues, commits, and tags
31
32
  - [Wireframe reference](wireframes.md): Shape and content rules for .claude/wireframes/<surface>.md files
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Teach reference
3
- description: Workspace layout, ordinal naming, frontmatter, and the mission, learning-record, and glossary formats for a learning workspace
3
+ description: Workspace layout, ordinal naming, frontmatter, and the mission and learning-record formats for a learning workspace
4
4
  ---
5
5
 
6
6
  # Teach reference
@@ -11,10 +11,11 @@ The folder is gitignored. Its markdown half is written in a format the authoring
11
11
 
12
12
  ## Scope
13
13
 
14
- Governs a learning workspace under `.claude/teach/<nn>-<topic>/`: folder layout, ordinal naming, frontmatter, and the mission, learning-record, and glossary formats.
14
+ Governs a learning workspace under `.claude/teach/<nn>-<topic>/`: folder layout, ordinal naming, frontmatter, and the mission and learning-record formats.
15
15
 
16
16
  Does not govern:
17
17
 
18
+ - The frontmatter, entry shape, and ordering of the glossary the workspace holds: `glossary.md`
18
19
  - What a lesson teaches, how it sequences difficulty, and what makes one worth returning to, which belong to the surface driving the workspace
19
20
  - Where a durable page goes once it leaves the workspace, which belongs to the routing test the destination surface states
20
21
  - One question measured in depth before anyone can plan against it: `groundwork.md`
@@ -96,12 +97,9 @@ Record the wrong answer rather than the fact of an error. A wrong answer names t
96
97
 
97
98
  ## GLOSSARY.md
98
99
 
99
- One entry per term the subject defines, sorted alphabetically.
100
+ Required in every workspace, holding one entry per term the subject defines. `glossary.md` fixes what an entry looks like, how the file orders and groups them, and which terms it carries, so this standard states only that the file exists and sits at the workspace root.
100
101
 
101
- - Lead each entry with the term as a bolded span, then the definition in one or two sentences
102
- - Define the term without using it
103
- - Name the lesson or reference page where the term first appears
104
- - Keep an entry to the meaning. Worked detail belongs on a reference page.
102
+ Name the lesson or reference page a term first appears in as that standard requires. A workspace is the case it was written for, so a glossary here has a first appearance to name.
105
103
 
106
104
  ## RESOURCES.md
107
105