@erclx/aitk 0.30.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,7 +55,7 @@ Each domain has a canonical source in this repo and a thin install or sync CLI o
55
55
  - [Design system](.claude/context/design.md): `DESIGN.md` token shape, extract skill and its two paths, render command
56
56
  - [Slides](.claude/context/slides.md): `SLIDES.md` source, layout catalog, render command, draft skill
57
57
  - [Transcripts](.claude/context/transcripts.md): fetch a YouTube transcript with metadata frontmatter via `aitk transcripts`
58
- - [Sandbox](.claude/context/sandbox.md): scenario-based scaffolds for verifying each domain flow
58
+ - [Sandbox](.claude/context/sandbox/index.md): scenario-based scaffolds for verifying each domain flow
59
59
 
60
60
  ## Documentation
61
61
 
@@ -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.30.1",
4
+ "version": "0.32.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: migration-context
3
+ description: Scope boundary for the docs audience split and its ordering against the other two migrations
4
+ ---
5
+
6
+ # Migration context requirement
7
+
8
+ ## Gap
9
+
10
+ Without this skill, a session judges a `docs/` file agent-facing, moves it, and rewrites the inbound links in the same pass, so a classification the user rejects is no longer one command to undo.
11
+
12
+ Three more failures share a cause. The skill reads a folder two other surfaces also write, and a proposal blind to them destroys work. A seed-derived `development.md` moved out of `docs/` comes back on the next seed sync. A move onto an existing `.claude/context/` filename overwrites the entry sitting there. A proposal drafted while `migration-claude-md` is proposing entries into the same folder cannot see those targets, and running the two in the wrong order turns a legitimate move into a skipped conflict.
13
+
14
+ ## Must
15
+
16
+ - Classify by audience signal in the file rather than by its name or its folder position
17
+ - Propose and stop. The `git mv` commands and the link fixes stay the user's to run.
18
+ - Report every inbound reference to a moved path as a TODO line, leaving the reference itself untouched
19
+ - Defer a seed-derived file to the seed sync path rather than proposing a move for it
20
+ - Skip a move whose destination already exists and report it as a conflict
21
+
22
+ ## Must not
23
+
24
+ - Sort a file carrying both audiences into either bucket. Flag it for a manual split, since a whole-file move and a silent split are both wrong.
25
+ - Descend into `docs/` subfolders. List them for manual review and leave them alone.
26
+
27
+ ## Guards
28
+
29
+ - A missing `docs/` directory, or one holding no top-level markdown, stops before any classification runs
30
+
31
+ ## Out of scope
32
+
33
+ - Classifying `CLAUDE.md` sections, which `migration-claude-md` proposes into this same `.claude/context/` folder. Run this skill first when both apply, so that skill's append-or-create check reads a folder the moves have already populated.
34
+ - Relocating root `standards/` and `snippets/`: `migration-standards`
35
+ - Regenerating `.claude/context/index.md`, which `aitk indexes regen` does once the user has applied the moves
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: setup-gov
3
+ description: Scope boundary for governance rule install and the authoring it refuses to do in a target
4
+ ---
5
+
6
+ # Setup gov requirement
7
+
8
+ ## Gap
9
+
10
+ Without this skill, a session picks a stack name from memory and installs rules written for a framework the project does not use, passes an extra the catalog does not carry so the install warns on a file it cannot resolve, and adds a rule the picked stack already pulls in.
11
+
12
+ The two costly failures are quieter. An unmatched technology gets the nearest rule instead of a stop, so the project carries governance authored for something adjacent and nothing marks it as a guess. A rule written directly into the target lands where no sync will ever reach it, so it drifts from the toolkit copy that owns it.
13
+
14
+ ## Must
15
+
16
+ - Read the catalog at run time and resolve both the stack and every extra against it
17
+ - Name the evidence file behind each detected technology in the preview, so a wrong match is visible before the install rather than after
18
+ - Dedupe extras against the rules the picked stack already resolves
19
+ - Run the install non-interactively, since the CLI picker blocks where no terminal is attached
20
+
21
+ ## Must not
22
+
23
+ - Author a rule inside the target project. Rules are authored in the toolkit and reach a project by install or sync.
24
+ - Install a stack or extra whose name did not come from the catalog read
25
+
26
+ ## Guards
27
+
28
+ - An unmatched technology stops and presents the options rather than resolving to a nearby rule. Authoring the missing rule in the toolkit, installing the matching layers without the technology-specific one, and aborting are the three, and this skill picks none of them.
29
+
30
+ ## Out of scope
31
+
32
+ - The rest of the onboarding chain. This skill installs governance and nothing else, and `setup-init` is the one-shot path that installs governance beside tooling, standards, snippets, and the seeds.
33
+ - Scaffolding the rule file an unmatched technology needs. `create-rule` writes one into a target project, and a rule the toolkit should ship is authored under `governance/rules/` against `.claude/standards/rule.md`.
34
+ - Updating rules a project already has, which `aitk gov sync` owns
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: setup-init
3
+ description: Scope boundary for the one-shot onboarding chain and the setup steps it leaves to a caller
4
+ ---
5
+
6
+ # Setup init requirement
7
+
8
+ ## Gap
9
+
10
+ Without this skill, a session installs a stack whose name it recalled rather than read, hand-writes configs the tooling sync already ships as golden files, and re-runs the chain over a project that owns its configs already.
11
+
12
+ Two failures are the expensive ones because both return success. A monorepo run drops husky into every subtree, git honors one `core.hooksPath`, and the extra hook directories go quiet with nothing reporting it. A caller reads the chain as onboarding complete when index bootstrap and plugin provisioning never ran, since no step states whether they belong to the chain.
13
+
14
+ ## Must
15
+
16
+ - Resolve every domain argument against a catalog read at run time, so a stack, rule, snippet, or tooling name the catalog does not carry cannot reach the command line
17
+ - Preview the resolved chain before the first command runs, naming the evidence file behind each detected technology
18
+ - Pass `--skip base` on every subtree past the first in a monorepo, so the shared hook layer lands once
19
+ - State which onboarding steps the chain does not run, so a caller knows what is left rather than inferring completeness from a clean report
20
+
21
+ ## Must not
22
+
23
+ - Generate a config that tooling sync ships as a golden file. Generating from prose duplicates the installed file and the two drift.
24
+ - Author a rule, stack, or snippet inside the target project when detection finds no match. Authoring happens in the toolkit and reaches a project by install or sync.
25
+ - Grow an existing-project branch, a mode switch, or a clobber guard. The chain runs once against a fresh scaffold and the per-domain paths cover everything after that.
26
+
27
+ ## Guards
28
+
29
+ - An unmatched technology stops for a decision. Surface the gap and either defer to rule authoring or proceed on the matched layer with the gap named in the report, rather than resolving to the nearest rule.
30
+
31
+ ## Out of scope
32
+
33
+ - Installing governance rules alone: `setup-gov`. This skill installs governance beside tooling, standards, snippets, and the seeds.
34
+ - Bootstrapping the `index.md` system: `setup-indexes`. Neither `aitk init` nor this chain performs it.
35
+ - Provisioning Claude Code plugins: `setup-plugins`. Those install once per machine rather than into a project, so no project-scoped chain can carry them.
36
+ - Running the verification scripts, which `setup-verify` owns and this chain invokes as its last step
37
+ - Re-installing into a project that already has the files, which the per-domain `aitk <domain> install` and `aitk sync` commands own
package/docs/agents.md CHANGED
@@ -391,11 +391,13 @@ Exit codes are `0` for a clean run, `1` for a refusal, and `2` for an unresolved
391
391
 
392
392
  ### What each check reports
393
393
 
394
- Length and depth quote their checkpoints from `.claude/standards/context.md`: roughly 150 lines for an entry, roughly 40 for a run of lines no heading breaks. Depth measures the longest such run rather than everything under one `##`, skips fenced blocks so a markdown example does not read as three headings, and exempts a run whose lines are all list items at one indent. Runs count blank lines, which the standard leaves open, so a hand reader who drops them lands a line or two lower. The report states the convention on every run.
394
+ Length and depth quote their checkpoints from `.claude/standards/context.md`: roughly 150 rendered lines for an entry, roughly 40 for a run no heading breaks. Depth measures the longest such run rather than everything under one `##`, skips fenced blocks so a markdown example does not read as three headings, and exempts a run whose lines are all list items at one indent averaging under 130 characters. The weight condition is what separates a scannable catalog of one-liners from a stack of paragraph-bullets, which reach the same count and read nothing alike.
395
+
396
+ Both checks count rendered lines rather than source lines, wrapping each line at 80 columns and summing the heights. Entries here are authored one line per bullet, so a block of fifteen paragraph-bullets occupies fifteen source lines and renders past sixty, which source counting cannot see. Measuring one checkpoint in each unit would put an entry length beside a run length that mean different things. Their exclusions still differ: the file measure counts fenced blocks and frontmatter, while the run measure skips a fence so an example cannot break the run around it. A reference-heavy entry therefore ranks by its examples, which the length legend states on every run. Runs count blank lines, which the standard leaves open, so a hand reader who drops them lands a line or two lower. Both sections state the width on every run, since a number in rendered lines cannot be reproduced without it.
395
397
 
396
398
  The table check reports a catalog that grows a row per shipped thing, not a table count. A fixed comparison table never reflows, so its size costs nothing. A table qualifies at six or more body rows whose first column mostly carries a path, command, or link, which is what separates a catalog from a comparison without reading the prose.
397
399
 
398
- The provenance check reports the markers narrating how a domain reached its shape rather than describing what it is: a date, a change number, or a release label. The standard admits a rejected alternative and the reasoning that killed it while refusing the provenance attached to it, so a marker names a line to read rather than a line to delete. Findings group by entry and sort left to right within a line, since what a reader acts on is which file to open. Fenced blocks are excluded, which keeps a pinned version in an install command from reading as a claim the entry makes. Frontmatter is excluded with them, since every check reads the body alone, and that is what keeps a diagram entry's dated `verified` stamp a record of its last check rather than a marker to settle.
400
+ The provenance check reports the markers narrating how a domain reached its shape rather than describing what it is: a date, a change number, or a release label. The standard admits a rejected alternative and the reasoning that killed it while refusing the provenance attached to it, so a marker names a line to read rather than a line to delete. Findings group by entry and sort left to right within a line, since what a reader acts on is which file to open. Fenced blocks are excluded, which keeps a pinned version in an install command from reading as a claim the entry makes. Frontmatter is excluded with them, since the content checks read the body alone, and that is what keeps a diagram entry's dated `verified` stamp a record of its last check rather than a marker to settle. Length is the exception, counting the whole file, so a reader applying the 150-rendered-line checkpoint against the body alone lands a few lines under what the tool reports.
399
401
 
400
402
  Index drift compares an index against its siblings in both directions. An entry the index does not link is invisible to a session choosing what to open, and a linked name resolving to nothing sends one to a path that opens nothing.
401
403
 
@@ -127,7 +127,7 @@ Use a targeted entry point when only one surface moved upstream. Use the catch-a
127
127
 
128
128
  ## Verify a sync
129
129
 
130
- Before running a sync against a real project, run the relevant sandbox scenario. The sandbox provisions a representative project state and routes `SANDBOX_SCENARIO=sync` through the domain flow. See [sandbox](../.claude/context/sandbox.md) for the scenario catalog and routing patterns.
130
+ Before running a sync against a real project, run the relevant sandbox scenario. The sandbox provisions a representative project state and routes `SANDBOX_SCENARIO=sync` through the domain flow. See [sandbox](../.claude/context/sandbox/index.md) for the scenario catalog and routing patterns.
131
131
 
132
132
  ## Scenarios
133
133
 
@@ -182,4 +182,4 @@ Sync also refuses a target whose working tree is dirty, so commit or stash befor
182
182
  - [agents](agents.md): CLI flags, exit codes, and JSON output shapes
183
183
  - [AI workflow](ai-workflow.md): feature-development loop inside a toolkit-managed project
184
184
  - [tooling](../.claude/context/tooling.md), [governance](../.claude/context/governance.md), [claude plugin](../.claude/context/claude-plugin/index.md), [indexes](../.claude/context/indexes.md), [snippets](../.claude/context/snippets.md), [standards](../.claude/context/standards.md): per-domain mechanics
185
- - [sandbox](../.claude/context/sandbox.md): scenario catalog for verifying domain flows
185
+ - [sandbox](../.claude/context/sandbox/index.md): scenario catalog for verifying domain flows
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.30.1",
4
+ "version": "0.32.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -4,6 +4,8 @@ import {
4
4
  type EntryReport,
5
5
  LENGTH_CHECKPOINT,
6
6
  measureFolders,
7
+ PEER_BULLET_CHECKPOINT,
8
+ RENDER_WIDTH,
7
9
  RUN_CHECKPOINT,
8
10
  } from '@/context/audit'
9
11
  import { auditCitations, type CitationReport } from '@/context/citations'
@@ -160,6 +162,8 @@ async function runAudit(
160
162
  lines: LENGTH_CHECKPOINT,
161
163
  run: RUN_CHECKPOINT,
162
164
  runCountsBlankLines: true,
165
+ renderWidth: RENDER_WIDTH,
166
+ peerBullet: PEER_BULLET_CHECKPOINT,
163
167
  },
164
168
  })}\n`,
165
169
  )
@@ -241,6 +245,12 @@ function reportCitations(report: ScannedCitations): void {
241
245
 
242
246
  function reportLength(entries: readonly EntryReport[]): void {
243
247
  logStep('Length')
248
+ logInfo(
249
+ `Entries measure rendered lines at ${RENDER_WIDTH} columns, counting frontmatter and fenced blocks.`,
250
+ )
251
+ logInfo(
252
+ 'A reference-heavy entry therefore ranks by its examples, which the depth check excludes.',
253
+ )
244
254
 
245
255
  const over = entries
246
256
  .filter((entry) => entry.lines > LENGTH_CHECKPOINT)
@@ -253,20 +263,29 @@ function reportLength(entries: readonly EntryReport[]): void {
253
263
 
254
264
  logWarn(`${over.length} past the ${LENGTH_CHECKPOINT}-line checkpoint`)
255
265
  pipeOutput(
256
- over.map((entry) => `${entry.rel} ${entry.lines} lines`).join('\n'),
266
+ over
267
+ .map((entry) => `${entry.rel} ${entry.lines} rendered lines`)
268
+ .join('\n'),
257
269
  )
258
270
  }
259
271
 
260
272
  /**
261
- * Names the blank-line convention on every run.
273
+ * Names the render width and the blank-line convention on every run.
262
274
  *
263
275
  * The standard settles heading level and fenced blocks and stops there, so a
264
276
  * hand reader who drops blank lines lands a line or two below this number.
265
- * Stating it is what keeps the two measurements reconcilable.
277
+ * Stating both is what keeps the two measurements reconcilable, and the width
278
+ * matters more than the blank lines because a number counted in rendered lines
279
+ * cannot be reproduced without it.
266
280
  */
267
281
  function reportDepth(entries: readonly EntryReport[]): void {
268
282
  logStep('Depth')
269
- logInfo('Runs count blank lines. Fenced blocks and peer lists are excluded.')
283
+ logInfo(
284
+ `Runs measure rendered lines at ${RENDER_WIDTH} columns and count blank lines.`,
285
+ )
286
+ logInfo(
287
+ `Fenced blocks are excluded, and so are peer lists averaging under ${PEER_BULLET_CHECKPOINT} characters a bullet.`,
288
+ )
270
289
 
271
290
  const over = entries
272
291
  .filter((entry) => entry.longestRun > RUN_CHECKPOINT)
@@ -282,7 +301,7 @@ function reportDepth(entries: readonly EntryReport[]): void {
282
301
  over
283
302
  .map(
284
303
  (entry) =>
285
- `${entry.rel}:${entry.longestRunLine} ${entry.longestRun} lines unbroken`,
304
+ `${entry.rel}:${entry.longestRunLine} ${entry.longestRun} rendered lines unbroken`,
286
305
  )
287
306
  .join('\n'),
288
307
  )
@@ -6,6 +6,27 @@ import type { AuditedFolder } from '@/context/folders'
6
6
  export const LENGTH_CHECKPOINT = 150
7
7
  export const RUN_CHECKPOINT = 40
8
8
 
9
+ /**
10
+ * Columns a source line wraps at when rendered.
11
+ *
12
+ * Nothing in this repository sets a line width and entries are authored one
13
+ * line per bullet, so the rendered width is the viewer's rather than the file's.
14
+ * The common terminal and diff width is the reproducible choice, and the report
15
+ * legend states it so a reader can arrive at the same number by hand.
16
+ */
17
+ export const RENDER_WIDTH = 80
18
+
19
+ /**
20
+ * Characters a bullet averages before its list stops reading as a set of peers.
21
+ *
22
+ * The exemption below covers a flat catalog of one-liners and a stack of
23
+ * paragraphs equally, and only the first is navigable. Measured across this
24
+ * corpus the two shapes separate with nothing between roughly 100 and 170
25
+ * characters a bullet, so the midpoint splits the population rather than a
26
+ * continuum. It is a checkpoint like the two above, not a cap.
27
+ */
28
+ export const PEER_BULLET_CHECKPOINT = 130
29
+
9
30
  /**
10
31
  * A table this size or larger whose first column mostly names artifacts reads
11
32
  * as a catalog that grows a row per shipped thing, which is the shape the
@@ -56,7 +77,20 @@ export interface ProvenanceFinding {
56
77
 
57
78
  export interface EntryReport {
58
79
  readonly rel: string
80
+ /**
81
+ * Rendered lines across the whole file, counting frontmatter and fenced
82
+ * blocks. Both this and `longestRun` measure in the same unit, since the two
83
+ * checkpoints they feed sit in one section of the standard and a reader
84
+ * compares them.
85
+ *
86
+ * The exclusions differ on purpose. `longestRun` skips a fence so an example
87
+ * cannot break the run around it, and a file measure has no run to protect.
88
+ * Excluding fences here would change which entries report by one and would
89
+ * not reach the case that motivates it: the most fenced entry in the corpus
90
+ * runs 20 percent fenced and sits past the checkpoint either way.
91
+ */
59
92
  readonly lines: number
93
+ /** Rendered lines at `RENDER_WIDTH`, not source lines. */
60
94
  readonly longestRun: number
61
95
  /** First line of the longest run, or 0 when the entry has no run at all. */
62
96
  readonly longestRunLine: number
@@ -90,24 +124,44 @@ function bodyLines(source: string): BodyLine[] {
90
124
  *
91
125
  * Every non-blank line has to be a list item at one indent. Prose mixed into
92
126
  * the run or a nested level inside it ends the exemption, because either one
93
- * means the block is no longer a flat set a reader can skim.
127
+ * means the block is no longer a flat set a reader can skim. Bullet count says
128
+ * nothing on its own, since a catalog of one-liners and a wall of paragraphs
129
+ * reach the same count and read nothing alike, so the average bullet is what
130
+ * decides whether the set is still skimmable.
94
131
  */
95
- function isPeerList(run: readonly BodyLine[]): boolean {
132
+ function isScannablePeerList(run: readonly BodyLine[]): boolean {
96
133
  const indents = new Set<number>()
134
+ let items = 0
135
+ let characters = 0
97
136
 
98
137
  for (const line of run) {
99
- if (line.text.trim() === '') continue
138
+ const text = line.text.trim()
139
+ if (text === '') continue
100
140
 
101
141
  const match = line.text.match(LIST_ITEM)
102
142
  if (!match) return false
103
143
  indents.add(match[1].length)
144
+ items++
145
+ characters += text.length
104
146
  }
105
147
 
106
- return indents.size === 1
148
+ if (indents.size !== 1) return false
149
+
150
+ return characters / items < PEER_BULLET_CHECKPOINT
107
151
  }
108
152
 
109
153
  /**
110
- * Measures the longest run of lines no heading breaks.
154
+ * Height a source line occupies once wrapped.
155
+ *
156
+ * A blank line renders as the gap it is rather than as nothing, which keeps it
157
+ * the distance the source measure already counted it as.
158
+ */
159
+ function renderedHeight(text: string): number {
160
+ return Math.max(1, Math.ceil(text.length / RENDER_WIDTH))
161
+ }
162
+
163
+ /**
164
+ * Measures the longest run of lines no heading breaks, in rendered lines.
111
165
  *
112
166
  * Fenced blocks are skipped rather than treated as breaks, per the standard:
113
167
  * they leave the count without ending the run, so prose either side of an
@@ -116,6 +170,11 @@ function isPeerList(run: readonly BodyLine[]): boolean {
116
170
  * between signposts and a blank line is distance like any other. A hand reader
117
171
  * measuring without them lands one or two lines lower, which the report legend
118
172
  * states.
173
+ *
174
+ * Height is what a reader travels, and source lines only stand in for it while
175
+ * lines stay short. An entry authored one line per bullet puts a paragraph on
176
+ * each, so a block of fifteen bullets measures as fifteen and renders past
177
+ * sixty. Wrapping every line at a stated width is what closes that gap.
119
178
  */
120
179
  function longestRun(lines: readonly BodyLine[]): {
121
180
  length: number
@@ -132,9 +191,16 @@ function longestRun(lines: readonly BodyLine[]): {
132
191
  // two headings rather than a stretch a reader travels, so it never counts.
133
192
  const first = run.find((line) => line.text.trim() !== '')
134
193
 
135
- if (first && run.length > longest && !isPeerList(run)) {
136
- longest = run.length
137
- longestLine = first.number
194
+ if (first && !isScannablePeerList(run)) {
195
+ const height = run.reduce(
196
+ (sum, line) => sum + renderedHeight(line.text),
197
+ 0,
198
+ )
199
+
200
+ if (height > longest) {
201
+ longest = height
202
+ longestLine = first.number
203
+ }
138
204
  }
139
205
  run = []
140
206
  }
@@ -260,7 +326,10 @@ export function measureEntry(rel: string, source: string): EntryReport {
260
326
 
261
327
  return {
262
328
  rel,
263
- lines: source.replace(/\n$/, '').split('\n').length,
329
+ lines: source
330
+ .replace(/\n$/, '')
331
+ .split('\n')
332
+ .reduce((sum, text) => sum + renderedHeight(text), 0),
264
333
  longestRun: run.length,
265
334
  longestRunLine: run.line,
266
335
  catalogTables: catalogTables(lines),
@@ -69,7 +69,7 @@ export function listSkills(root: string): string[] {
69
69
  /**
70
70
  * Maps a scenario to the skill it drives, trying two spellings in order.
71
71
  *
72
- * `<category>-<command>` is the rule `.claude/context/sandbox.md` states, and it
72
+ * `<category>-<command>` is the rule `.claude/context/sandbox/coverage.md` states, and it
73
73
  * alone pairs 29 of 55 skills. The bare `<command>` fallback is what reaches the
74
74
  * rest: `claude/setup-init.sh` drives the `setup-init` skill, not a
75
75
  * `claude-setup-init` that does not exist. Stating one spelling and shipping two
@@ -100,9 +100,10 @@ Only the `development` entry carries this section. It is not a general-purpose h
100
100
  ## Length
101
101
 
102
102
  - Aim for one entry per domain. There is no hard cap. Length is a symptom, not the defect.
103
- - Past roughly 150 lines, check three things before adding more: whether the entry still covers a single domain, whether it has filled with content `ls` or `--help` reproduces, and whether it has accumulated the history of its own changes. Fix whichever is true rather than trimming to hit a number.
104
- - Past roughly 40 lines with no heading of any level breaking them, add a subheading at the seam. Measure the longest such run rather than everything under one `##`, and exclude fenced code blocks. The number is a checkpoint like the 150 above, not a cap.
105
- - Exempt a block whose lines are all list items at one level. A long list of peers is already navigable, and a subheading dropped into it splits a set that belongs together. Mixing prose with the list, or nesting levels inside it, ends the exemption.
103
+ - Past roughly 150 rendered lines, check three things before adding more: whether the entry still covers a single domain, whether it has filled with content `ls` or `--help` reproduces, and whether it has accumulated the history of its own changes. Fix whichever is true rather than trimming to hit a number.
104
+ - Past roughly 40 rendered lines with no heading of any level breaking them, add a subheading at the seam. Measure the longest such run rather than everything under one `##`, and exclude fenced code blocks. The number is a checkpoint like the 150 above, not a cap.
105
+ - Both checkpoints count rendered lines, so wrap each source line at 80 columns and sum the heights. Source lines undercount an entry authored one line per bullet, where a block of fifteen paragraph-bullets occupies fifteen lines and renders past sixty. Counting the two checkpoints in different units would put a file measured one way beside a run measured another.
106
+ - Exempt a block whose lines are all list items at one level averaging under roughly 130 characters. A flat list of short peers is already navigable, and a subheading dropped into it splits a set that belongs together. Bullet count says nothing on its own, since a catalog of one-liners and a stack of paragraphs reach the same count and read nothing alike, so weight is what decides. Mixing prose with the list, or nesting levels inside it, ends the exemption at any weight.
106
107
  - Never cut a `## Decisions` or `## Gotchas` entry to shorten a file. Cut a `## Layout` or `## CLI` section instead.
107
108
  - Split into a folder (`.claude/context/<domain>/<sub-area>.md`) when a domain has three or more sub-areas that do not fit cleanly in one file. That split is the natural ceiling.
108
109
  - Keep a split domain's `index.md` generated. The catalog body is rewritten on every regen, so the domain's own overview and layout belong in a sibling file rather than in it, and the `subtitle` is what names the file to start with.