@erclx/aitk 0.101.0 → 0.103.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.
@@ -9,6 +9,7 @@ import {
9
9
  } from '@/tasks/archive'
10
10
 
11
11
  const ORDERING_FILE = 'priority.md'
12
+ const BACKLOG_FILE = 'backlog.md'
12
13
 
13
14
  /**
14
15
  * The readiness headings `.claude/standards/tasks.md` fixes. The names are the
@@ -60,6 +61,16 @@ export interface Untested {
60
61
  readonly message: string
61
62
  }
62
63
 
64
+ /**
65
+ * A backlog line, which carries a pointer and nothing else. The backlog is
66
+ * explicitly unordered, so a line has no position to read and no columns to
67
+ * resolve.
68
+ */
69
+ export interface BacklogRow {
70
+ readonly label: string
71
+ readonly stem: string | undefined
72
+ }
73
+
63
74
  export interface BoardRow {
64
75
  readonly group: BoardGroup
65
76
  readonly label: string
@@ -74,6 +85,7 @@ export interface BoardRow {
74
85
  export interface ValidateReport {
75
86
  readonly ok: true
76
87
  readonly rows: number
88
+ readonly backlog: number
77
89
  readonly tasks: number
78
90
  readonly findings: readonly Finding[]
79
91
  readonly untested: readonly Untested[]
@@ -91,6 +103,10 @@ export function orderingPath(root: string): string {
91
103
  return join(tasksDir(root), ORDERING_FILE)
92
104
  }
93
105
 
106
+ export function backlogPath(root: string): string {
107
+ return join(tasksDir(root), BACKLOG_FILE)
108
+ }
109
+
94
110
  /**
95
111
  * Pulls the target out of a markdown link, which is how both the `Task` and the
96
112
  * `Plan` column spell their pointer. A cell carrying prose instead of a link
@@ -225,6 +241,39 @@ export function readBoard(text: string): {
225
241
  return { rows, groups }
226
242
  }
227
243
 
244
+ /**
245
+ * Parses the backlog into one row per bullet carrying a link. The backlog is a
246
+ * flat list rather than a table, so there is no header to resolve and no group
247
+ * to sit under, and a bullet holding prose instead of a pointer is skipped.
248
+ *
249
+ * Skipping it rather than reporting it is what keeps the intro paragraph and
250
+ * any explanatory bullet out of the findings. The task that bullet meant to
251
+ * name is still accounted for, because a stem no line reaches is reported by
252
+ * `checkMapping` as carrying no row on either surface.
253
+ */
254
+ export function readBacklog(text: string): readonly BacklogRow[] {
255
+ const rows: BacklogRow[] = []
256
+
257
+ for (const line of text.split('\n')) {
258
+ if (!/^\s*[-*]\s/.test(line)) continue
259
+
260
+ const target = linkTarget(line)
261
+ if (!target) continue
262
+
263
+ // A pointer carrying a directory names something other than a sibling task,
264
+ // the way `citedStem` reads the same shape on the board.
265
+ const path = target.split('#')[0] ?? ''
266
+ if (path.includes('/')) continue
267
+
268
+ const stem = stemOf(path)
269
+ if (stem && isReservedStem(stem)) continue
270
+
271
+ rows.push({ label: linkText(line) || line.trim(), stem })
272
+ }
273
+
274
+ return rows
275
+ }
276
+
228
277
  async function listTaskStems(dir: string): Promise<string[]> {
229
278
  const entries = await readdir(dir)
230
279
 
@@ -247,13 +296,44 @@ function resolves(target: string, dir: string, root: string): boolean {
247
296
  return existsSync(resolve(dir, path)) || existsSync(resolve(root, path))
248
297
  }
249
298
 
299
+ /**
300
+ * Accounts every task file against both surfaces the board spans. A task sits
301
+ * on `priority.md` when it would plausibly be planned soon and on `backlog.md`
302
+ * otherwise, so a file reached by neither is the dropped one this reports and a
303
+ * file reached by both claims two contradictory things about itself.
304
+ */
250
305
  function checkMapping(
251
306
  rows: readonly BoardRow[],
307
+ backlog: readonly BacklogRow[],
252
308
  stems: readonly string[],
253
309
  dir: string,
254
310
  ): Finding[] {
255
311
  const findings: Finding[] = []
256
312
  const seen = new Map<string, number>()
313
+ const listed = new Set<string>()
314
+
315
+ for (const row of backlog) {
316
+ if (!row.stem) {
317
+ findings.push({
318
+ kind: 'task-unresolved',
319
+ group: undefined,
320
+ subject: row.label,
321
+ message: 'is a backlog line naming no task file.',
322
+ })
323
+ continue
324
+ }
325
+
326
+ listed.add(row.stem)
327
+
328
+ if (!existsSync(join(dir, `${row.stem}.md`))) {
329
+ findings.push({
330
+ kind: 'task-unresolved',
331
+ group: undefined,
332
+ subject: row.stem,
333
+ message: 'has a backlog line and no task file.',
334
+ })
335
+ }
336
+ }
257
337
 
258
338
  for (const row of rows) {
259
339
  if (!row.stem) {
@@ -287,15 +367,26 @@ function checkMapping(
287
367
  message: `carries ${count} rows. A task belongs to exactly one group.`,
288
368
  })
289
369
  }
370
+
371
+ if (listed.has(stem)) {
372
+ findings.push({
373
+ kind: 'row-duplicated',
374
+ group: undefined,
375
+ subject: stem,
376
+ message:
377
+ 'carries a row on the board and a line on the backlog. A task sits on one surface.',
378
+ })
379
+ }
290
380
  }
291
381
 
292
382
  for (const stem of stems) {
293
- if (!seen.has(stem)) {
383
+ if (!seen.has(stem) && !listed.has(stem)) {
294
384
  findings.push({
295
385
  kind: 'row-missing',
296
386
  group: undefined,
297
387
  subject: stem,
298
- message: 'is a task file with no row on the board.',
388
+ message:
389
+ 'is a task file with no row on the board and no line on the backlog.',
299
390
  })
300
391
  }
301
392
  }
@@ -547,11 +638,19 @@ export async function validateBoard(root: string): Promise<ValidateOutcome> {
547
638
  )
548
639
  }
549
640
 
641
+ // An absent backlog reads as empty rather than refusing. A project that has
642
+ // never needed the second surface keeps every task on the board, which is the
643
+ // one-to-one mapping this check ran before the backlog existed.
644
+ const backlogFile = backlogPath(root)
645
+ const backlog = existsSync(backlogFile)
646
+ ? readBacklog(await readFile(backlogFile, 'utf8'))
647
+ : []
648
+
550
649
  const stems = await listTaskStems(dir)
551
650
  const parked = await checkParked(rows, root)
552
651
 
553
652
  const findings = [
554
- ...checkMapping(rows, stems, dir),
653
+ ...checkMapping(rows, backlog, stems, dir),
555
654
  ...checkPlans(rows, dir, root),
556
655
  ...checkCollisions(rows),
557
656
  ...parked.findings,
@@ -560,6 +659,7 @@ export async function validateBoard(root: string): Promise<ValidateOutcome> {
560
659
  return {
561
660
  ok: true,
562
661
  rows: rows.length,
662
+ backlog: backlog.length,
563
663
  tasks: stems.length,
564
664
  findings,
565
665
  untested: parked.untested,
@@ -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
@@ -51,8 +51,6 @@ Does not govern:
51
51
 
52
52
  The character bans sit in `markdown.md` under `## Punctuation` rather than here, because an em dash and a semicolon are typography and the bans here reach the words a sentence chooses and the claims it makes. A surface applying both reads both files.
53
53
 
54
- Illustrate a pattern ban with a multi-word phrase. An audit reading this section harvests the single lowercase backticked words out of every `- Do not use ` bullet into a literal ban set, so a one-word example bans that word everywhere it appears rather than banning the pattern it stands for. Both the toolkit command and the audit hook a project installs parse that shape, so the constraint holds wherever this file lands.
55
-
56
54
  ## Frontmatter descriptions
57
55
 
58
56
  When frontmatter carries a short `title` or `description` used for catalog display:
@@ -13,7 +13,7 @@ The folder is gitignored. Board state changes when work ships rather than when a
13
13
 
14
14
  ## Scope
15
15
 
16
- Governs the task board under `.claude/tasks/`: folder layout, filenames, frontmatter, file format, origin lines, execution ordering, and archiving.
16
+ Governs the task board under `.claude/tasks/`: folder layout, filenames, frontmatter, file format, origin lines, execution ordering, the backlog beside it, and archiving.
17
17
 
18
18
  Does not govern:
19
19
 
@@ -30,6 +30,7 @@ Does not govern:
30
30
  .claude/tasks/
31
31
  ├── index.md ← generated, never hand-edited
32
32
  ├── priority.md ← hand-maintained execution order
33
+ ├── backlog.md ← unordered, what is not being scheduled
33
34
  ├── session-<slug>.md ← optional, what a compaction is about to destroy
34
35
  ├── v09.0-sync-paths.md
35
36
  └── v13.0-toolkit-drift.md
@@ -37,7 +38,9 @@ Does not govern:
37
38
 
38
39
  One file per task is what keeps the board safe under parallel sessions. Two sessions working different tasks never write the same file, which matters because a gitignored board has no history to recover a clobbered write from.
39
40
 
40
- Siblings sit in the folder without being tasks, and each earns its place by being governed somewhere. `index.md` and `priority.md` are governed here. Every `session-` file is a pre-compaction handoff governed by `session.md`, and each is optional: a project whose sessions never approach a compaction carries none. Anything filtering the folder to tasks skips all of them, so a name outside the set is a task whatever it holds.
41
+ Siblings sit in the folder without being tasks, and each earns its place by being governed somewhere. `index.md`, `priority.md`, and `backlog.md` are governed here. Every `session-` file is a pre-compaction handoff governed by `session.md`, and each is optional: a project whose sessions never approach a compaction carries none. Anything filtering the folder to tasks skips all of them, so a name outside the set is a task whatever it holds.
42
+
43
+ `backlog.md` is optional too. A project small enough that every task it holds would be planned soon carries none, and the ordering rules below then describe the whole board.
41
44
 
42
45
  The handoff takes one file per session for the reason a task does. A single shared path puts two sessions closing near each other on one file that neither can watch the other write, and the loser leaves no trace on a board with no history behind it.
43
46
 
@@ -49,7 +52,7 @@ The `claude-tasks` skill creates and archives task files. `claude-docs` marks ou
49
52
 
50
53
  ## Ordering
51
54
 
52
- `priority.md` carries execution order and what each task is waiting on. The generated index sorts by filename and says nothing about order, so without this file board state gets reconstructed by hand every session. Why the order is what it is belongs in `.claude/ROADMAP.md`, which is committed because that rationale has no substitute record.
55
+ `priority.md` carries execution order and what each task is waiting on. The generated index sorts by filename and says nothing about order, so without this file board state gets reconstructed by hand every session. Why one version sequences against another belongs in `.claude/ROADMAP.md`, which is committed because that rationale has no substitute record. Why a row sits where it does inside its group is stated on the row itself, in the column that already carries what the task is waiting on.
53
56
 
54
57
  Group tasks by readiness rather than by status, one row per task, under the columns each group fixes below. Keep it to links and blockers: tables, plus at most one sentence per section. A paragraph in `priority.md` is a defect whatever it says. Stating the shape this way is what lets a single diff fail, since a size cap only trips after the fact and every addition looks defensible on its own.
55
58
 
@@ -61,11 +64,15 @@ Readiness is three groups under fixed headings, `## Run now`, `## Up next`, and
61
64
 
62
65
  Each group fixes its own columns, which follow from the test above it rather than from preference. Neither half of the `## Run now` test is checkable without the file set and the plan sitting beside the task.
63
66
 
67
+ Row position inside `## Needs a plan` is the order those tasks get planned in, top first. The three tests answer whether a task can start, which is mechanical, and none of them answers which task is worth starting, which is a judgment no column holds. Position is where that judgment is recorded, so the top row is the answer to what to plan next and a reader needs no other surface to get it. The other two groups take the same reading, and it costs them little, since a group holding what is already planned is short by construction.
68
+
69
+ Position alone carries it, and no rank column exists. A number beside each row is a second thing to keep in step with the order it duplicates, and the file is edited by one session at a time, so the order the rows are written in is already unambiguous. State on each row why it sits where it does, in the same cell that carries what it is waiting on. A position with no stated reason is re-derived from memory by the next session, which is the failure the ordering replaces rather than moves.
70
+
64
71
  The `Waiting on` column under `## Up next` carries that reason in one of three forms. `## Needs a plan` states no file set at all, because a task with no plan has no bounded one to state. A group with no rows keeps its heading and its header row.
65
72
 
66
73
  Under `## Up next` a collision names the file held by the task already running, a sibling task names that task, and an external condition names both the condition and what would satisfy it. Naming what would satisfy it is what separates a blocked row from one nobody has examined, so a cell stating a condition with no way out of it fails the test. The header text is the contract the way the group names are, because anything reading the cell resolves the column by header rather than by position.
67
74
 
68
- `aitk tasks validate` reads those columns and reports where a row's claim and the tree disagree: a plan pointer resolving to no file, a row and a task file that do not map one to one, a task in two groups, and two `## Run now` rows touching a path in common. It also re-takes the two blocker kinds a command can settle, reporting a parked row whose cited task is archived or has closed every outcome and one whose cited file nothing under `## Run now` still holds. Both halves read a citation out of the cell rather than parsing it into fields, and a row citing neither is reported as untested, which is where the three kinds resting on a person's judgment land. Run it when the readiness claim is made rather than on a schedule, since the board is gitignored per-machine scratch and no shared moment exists to hang it on. It reports and never writes, so a session fixes the row it names.
75
+ Under `## Needs a plan` the cell carries two halves and each takes one clause: what the task needs before it can be planned, then why it sits at this position. A cell running past that is the paragraph this file already deletes, arriving one row at a time rather than all at once, and the group is where it costs the most, since it holds the rows nobody has read recently and is the longest group on any board that needs a backlog at all.
69
76
 
70
77
  ```markdown
71
78
  ---
@@ -88,13 +95,47 @@ description: One line on what the board covers
88
95
 
89
96
  ## Needs a plan
90
97
 
91
- | Task | Waiting on |
92
- | ------------------------------- | ---------------------------------------------- |
93
- | [vXX.Y <slug>](vXX.Y-<slug>.md) | <what the task needs before it can be planned> |
98
+ | Task | Waiting on |
99
+ | ------------------------------- | -------------------------------------------------------------- |
100
+ | [vXX.Y <slug>](vXX.Y-<slug>.md) | <what it needs before it can be planned, and why it sits here> |
94
101
  ```
95
102
 
96
103
  The tests live here so the board does not carry them. Writing them as a sentence under each heading produces the paragraph the rule above deletes, and a criterion with no home gets restated from memory every time the board is touched.
97
104
 
105
+ ## The backlog
106
+
107
+ A task sits on the board when it would plausibly be planned within the next few waves, and on `backlog.md` otherwise. The call is a judgment, so restate it whenever the board is swept rather than making it once: a backlogged task rises when the work in front of it lands or the world changes under it, and a board row falls to the backlog when it stops being near-term.
108
+
109
+ A mechanical test over age or origin was the alternative and neither predicts what gets picked next, which is the judgment the ordering exists to carry. Leaving everything on the board is the other alternative, and it is what produces a group too long to rank, where the ordering means nothing because nobody can hold the whole list in one reading.
110
+
111
+ What the split buys is that the board is short enough for its order to be read, and what it costs is a second surface to keep. That trade only pays while the backlog stays honest about what it is, which is why it carries no order, no groups, and no readiness claim.
112
+
113
+ Nothing is deleted. A backlogged task keeps its file, its findings, and its frontmatter, and the backlog row is a pointer at that file. The folder is gitignored and has no history behind it, so a row dropped without landing somewhere readable is gone with nothing to recover it from.
114
+
115
+ The backlog is a flat list of links under one heading, sorted by filename. Sorting mechanically is what keeps it from reading as a queue: the order is the same order the index already sorts in, so no position on it means anything.
116
+
117
+ ```markdown
118
+ ---
119
+ title: Backlog
120
+ description: One line on what the backlog holds
121
+ ---
122
+
123
+ # Backlog
124
+
125
+ Unordered. Nothing here is scheduled, and a task rises to `priority.md` when it becomes near-term.
126
+
127
+ - [vXX.Y <slug>](vXX.Y-<slug>.md)
128
+ - [vXX.Y <slug>](vXX.Y-<slug>.md)
129
+ ```
130
+
131
+ Add no fourth readiness group in place of this file. The three group names are the contract, and a backlog is a separate surface rather than a group because it makes no readiness claim at all: it says nobody has scheduled the task, which is a fact about attention rather than about whether the work can start.
132
+
133
+ ## Validation
134
+
135
+ `aitk tasks validate` reads the columns above and reports where a row's claim and the tree disagree: a plan pointer resolving to no file, a task file reached by neither surface, a task on both surfaces or in two groups, and two `## Run now` rows touching a path in common. It also re-takes the two blocker kinds a command can settle, reporting a parked row whose cited task is archived or has closed every outcome and one whose cited file nothing under `## Run now` still holds. Both halves read a citation out of the cell rather than parsing it into fields, and a row citing neither is reported as untested, which is where the three kinds resting on a person's judgment land. Run it when the readiness claim is made rather than on a schedule, since the board is gitignored per-machine scratch and no shared moment exists to hang it on. It reports and never writes, so a session fixes the row it names.
136
+
137
+ A task file is accounted for when a row on `priority.md` or a line on `backlog.md` names it, and reported when neither does. One check across both surfaces is what lets a task move between them without the move looking like a dropped file, and a task named by both is reported for the same reason a task in two groups is: it claims two things about itself and only one of them can hold. A project carrying no `backlog.md` is read as an empty backlog rather than refused, which leaves the one-to-one mapping this check ran before the second surface existed.
138
+
98
139
  ## Filenames
99
140
 
100
141
  `vXX.Y-<slug>.md`, where the version is the phase label zero-padded to two digits and the slug is kebab-case.
@@ -195,7 +236,7 @@ The line is what lets a merge close its own task. Every merge on `main` is a squ
195
236
  - Architectural reasoning that outlives the task. A finding explains why this task is shaped as it is. A decision the system keeps after the task closes belongs in `.claude/ARCHITECTURE.md`.
196
237
  - Narrative of the session that produced the task. A finding states what constrains the task, so what was probed, what it cost, and who decided belongs in the groundwork folder the `Groundwork:` line names. A task with no groundwork folder cuts the narrative rather than relocating it, since the board is not the fallback destination for it.
197
238
  - "In progress" or "Blocked" headings. Note status inline on the outcome instead.
198
- - Sequencing rationale or which version is active. Those belong in `.claude/ROADMAP.md`, which is committed because that reasoning has no substitute record.
239
+ - Sequencing rationale or which version is active. Why this task is planned before its neighbors goes on its row in `priority.md`, in the cell that already carries what it is waiting on. Why one version sequences against another belongs in `.claude/ROADMAP.md`, which is committed because that reasoning has no substitute record.
199
240
 
200
241
  ## Archiving
201
242
 
@@ -205,6 +246,8 @@ Two callers reach that command. The `claude-tasks` skill runs it inside a sessio
205
246
 
206
247
  One destination rather than a per-project choice is what lets the move happen without asking. It mirrors the plans archive at `.claude/plans-archive/` and stays gitignored, so an archived task does not start appearing in diffs. The cost is that the folder is unbacked, which is the same cost the plans archive already carries.
207
248
 
249
+ The archive clears a row from `priority.md` and reads no other surface, which holds because a task reaches a merge by being planned and handed out, and both steps move it onto the board first. A task archived straight off the backlog therefore leaves its line standing, and the validator reports that line as naming a file that is gone rather than the board losing it silently.
250
+
208
251
  Archiving a task does not archive its plan. `claude-docs` owns the plans sweep and moves a plan only when the closing task is its last live citation. The archive clears the task's row from `priority.md` itself, since a shipped task left in the ordering reads as ready to hand a worker. It leaves prose naming the task alone for a person to resolve.
209
252
 
210
253
  The row is matched by the link in its first cell rather than by a pattern against the whole line. A row names the task it is about in the first cell, so a link anywhere after that is a reference, such as a blocker pointing at what it waits on. Matching the line would delete the referring task's row too, on a board that is gitignored and has nothing to recover it from.
@@ -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
 
@@ -24,66 +24,62 @@ esac
24
24
 
25
25
  [ -f "$file" ] || exit 0
26
26
 
27
- # Read the closed-set word bans from the standard so the hook never carries a
28
- # second copy. Every "Do not use ... (`a`, `b`)" bullet contributes its
29
- # single-word backticked terms, which skips the multi-word and punctuation bans.
27
+ # The audit verb owns the ban sets, so this hook carries no copy of them. The
28
+ # awk this replaces parsed the word bans out of the project's own prose.md,
29
+ # hardcoded the em-dash and semicolon, and reached none of the spellings, so a
30
+ # British spelling passed at edit time and a corpus check caught it later with
31
+ # nothing in between explaining the difference. A ban class added to the verb
32
+ # now reaches this hook without an edit here.
30
33
  #
31
- # The word bans sit in prose.md and the em-dash and semicolon bans in
32
- # markdown.md, so this parses the first and hardcodes the second. A "Do not use"
33
- # bullet added to markdown.md is parsed by nothing and enforces silently.
34
- standard="${CLAUDE_PROJECT_DIR:-.}/.claude/standards/prose.md"
35
- words=""
34
+ # The verb resolves its own paths under the cwd, so the project root is named
35
+ # rather than inherited. The payload carries an absolute file path, which is
36
+ # what lets the runner move without taking the argument out of reach.
37
+ root="${CLAUDE_PROJECT_DIR:-.}"
38
+
39
+ # A machine without the binary reports that nothing ran rather than exiting
40
+ # clean. An edit nobody checked and an edit carrying no violation are the same
41
+ # silence to a reader, so the enforcement a machine lacks is reported.
42
+ #
43
+ # A completed run always writes the record, and a refusal writes nothing, so an
44
+ # empty one means the verb declined to measure rather than measured and found
45
+ # nothing. It refuses outside a git repository, which is a project this hook can
46
+ # be installed into, and reading the findings alone reports that as a clean file.
36
47
  unread=""
37
- if [ -f "$standard" ]; then
38
- words=$(grep '^- Do not use ' "$standard" |
39
- grep -o '`[^`]*`' |
40
- tr -d '`' |
41
- grep -x '[a-z][a-z]*' |
42
- sort -u |
43
- paste -sd '|' -)
48
+ record=""
49
+ if command -v aitk >/dev/null 2>&1; then
50
+ record=$(cd "$root" 2>/dev/null && aitk markdown audit "$file" --json 2>/dev/null) || true
51
+ [ -n "$record" ] || unread="record"
44
52
  else
45
- # An absent standard empties the word list, and the awk below reads an empty
46
- # list as nothing to look for rather than as nothing found. The path is kept
47
- # so the report names what could not be read, since a file carrying no banned
48
- # word and a file nobody checked produce the same silence otherwise.
49
- unread="$standard"
53
+ unread="runner"
50
54
  fi
51
55
 
52
- hits=$(awk -v words="$words" '
53
- BEGIN { if (words != "") banned = "(^|[^a-z])(" words ")([^a-z]|$)" }
54
- /^```/ { in_code = !in_code; next }
55
- in_code { next }
56
- /—/ { print NR ": em-dash: " $0 }
57
- /;/ { print NR ": semicolon: " $0 }
58
- banned != "" {
59
- prose = tolower($0)
60
- gsub(/`[^`]*`/, "", prose)
61
- found = ""
62
- delete seen
63
- while (match(prose, banned)) {
64
- word = substr(prose, RSTART, RLENGTH)
65
- gsub(/[^a-z]/, "", word)
66
- if (word != "" && !(word in seen)) {
67
- seen[word] = 1
68
- found = found (found == "" ? "" : ", ") word
69
- }
70
- prose = substr(prose, RSTART + RLENGTH)
71
- }
72
- if (found != "") print NR ": banned word (" found "): " $0
73
- }
74
- ' "$file")
56
+ # The record decides rather than the exit code, so a binary predating a ban
57
+ # class still reports what it does measure. A refusal and an unparseable payload
58
+ # both yield nothing, which is the same silence a clean file produces.
59
+ hits=$(printf '%s' "$record" |
60
+ jq -r '.entries[]?.bans[]? | ":\(.line):\(.column + 1) \(.kind) \(.term)"' 2>/dev/null)
61
+
62
+ # A set the verb shipped empty measures nothing and would report a clean file.
63
+ # Reading the findings alone turns that narrowed check into a pass, so the field
64
+ # is read beside them.
65
+ empty=$(printf '%s' "$record" |
66
+ jq -r '[.bans.emptySets[]?] | join(", ")' 2>/dev/null)
75
67
 
76
- [ -z "$hits" ] && [ -z "$unread" ] && exit 0
68
+ [ -z "$hits" ] && [ -z "$unread" ] && [ -z "$empty" ] && exit 0
77
69
 
78
70
  nl=$'\n'
79
71
  msg=""
80
72
 
81
- if [ -n "$unread" ]; then
82
- msg=$(printf 'Standards-audit: no word ban checked in %s. Found no standard at %s, so only the character bans ran. Restore it with `aitk standards install`.' "$file" "$unread")
73
+ if [ "$unread" = "runner" ]; then
74
+ msg=$(printf 'Standards-audit: nothing checked in %s. Found no `aitk` binary on PATH. Install one with `bun add -g @erclx/aitk`.' "$file")
75
+ elif [ "$unread" = "record" ]; then
76
+ msg=$(printf 'Standards-audit: nothing checked in %s. `aitk markdown audit` returned no record, which it does when it declines to measure. It needs a git repository to build its corpus.' "$file")
77
+ elif [ -n "$empty" ]; then
78
+ msg=$(printf 'Standards-audit: the shipped ban set is empty for %s, so %s was checked against a narrowed set. Reinstall the toolkit with `bun add -g @erclx/aitk`.' "$empty" "$file")
83
79
  fi
84
80
 
85
81
  if [ -n "$hits" ]; then
86
- found=$(printf 'Standards-audit: prose.md and markdown.md violations in %s. Rewrite or restructure (do not lazy-swap).\n%s' "$file" "$hits")
82
+ found=$(printf 'Standards-audit: prose.md and markdown.md violations in %s. Rewrite the sentence (do not lazy-swap). A code span is the answer only where the token is genuinely an identifier under discussion.\n%s' "$file" "$hits")
87
83
  msg="${msg:+$msg$nl}$found"
88
84
  fi
89
85