@erclx/canon 4.110.0 → 4.112.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.
Files changed (32) hide show
  1. package/assets/brand/mark.svg +10 -0
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
  4. package/claude/skills/auto-ship/SKILL.md +12 -2
  5. package/claude/skills/canon-operator/REQUIREMENT.md +2 -2
  6. package/claude/skills/canon-operator/SKILL.md +3 -4
  7. package/claude/skills/git-pr/REQUIREMENT.md +2 -0
  8. package/claude/skills/git-pr/SKILL.md +21 -3
  9. package/claude/skills/target-check/references/domains.md +2 -2
  10. package/claude/skills/teach-workspace/SKILL.md +1 -1
  11. package/docs/agents/install-and-sync.md +4 -13
  12. package/docs/agents/teach.md +3 -1
  13. package/docs/target-projects.md +0 -2
  14. package/docs/workflow/ai-workflow.md +7 -11
  15. package/package.json +2 -1
  16. package/scripts/core/capture-home.sh +59 -0
  17. package/src/claude/cases/setup.ts +2 -23
  18. package/src/commands/sync.ts +0 -16
  19. package/src/design/components.ts +153 -152
  20. package/src/design/favicon.ts +8 -0
  21. package/src/design/tokens.ts +1 -1
  22. package/src/shipped/references.ts +1 -2
  23. package/src/sync/reverse.ts +3 -74
  24. package/src/teach/nav.ts +55 -13
  25. package/claude/skills/migration-claude-md/REQUIREMENT.md +0 -40
  26. package/claude/skills/migration-claude-md/SKILL.md +0 -79
  27. package/claude/skills/migration-context/REQUIREMENT.md +0 -36
  28. package/claude/skills/migration-context/SKILL.md +0 -97
  29. package/claude/skills/migration-standards-drop/REQUIREMENT.md +0 -55
  30. package/claude/skills/migration-standards-drop/SKILL.md +0 -113
  31. package/claude/skills/migration-superseded/REQUIREMENT.md +0 -44
  32. package/claude/skills/migration-superseded/SKILL.md +0 -115
@@ -31,3 +31,11 @@ export function renderFavicon(mark: string, colors: FaviconColors): string {
31
31
  </svg>
32
32
  `
33
33
  }
34
+
35
+ /**
36
+ * The `<link rel="icon">` a page carries the rendered favicon in, inlined so a
37
+ * generated page needs no sibling file and no href that varies with its depth.
38
+ */
39
+ export function faviconLink(svg: string): string {
40
+ return `<link rel="icon" href="data:image/svg+xml,${encodeURIComponent(svg)}">`
41
+ }
@@ -381,7 +381,7 @@ export const TOKENS: DesignTokens = {
381
381
  iconography: [
382
382
  'No icon library is installed. `assets/brand/mark.svg` is the one authored icon, embedded inline in the hero topbar, and the surfaces otherwise draw literal glyph characters: `│ ├ ✓ ! ✗ + - ◆ ◇ ❯` for the terminal framing.',
383
383
  '',
384
- 'The same mark ships in six independently-maintained copies that carry different colors by design rather than by drift, each fitted to the chrome it renders on: the dark accent for a dark-chrome surface, the light accent for a light-chrome one, and the pair the web favicon takes from `src/design/favicon.ts`. Unifying them or repairing the one that looks drifted would break the fit each was chosen for. `canon/context/design.md` carries which file holds each copy.',
384
+ 'The same mark ships in five independently-maintained copies that carry different colors by design rather than by drift, each fitted to the chrome it renders on: the dark accent for a dark-chrome surface, the light accent for a light-chrome one, and the pair the web favicon takes from `src/design/favicon.ts`, which teach pages also take by rendering the live mark. Unifying them or repairing the one that looks drifted would break the fit each was chosen for. `canon/context/design.md` carries which file holds each copy.',
385
385
  ].join('\n'),
386
386
  }
387
387
 
@@ -172,8 +172,7 @@ const DOCS_PATH = /(?<![\w./-])docs\/[^\s`)\]]*\.md\b/g
172
172
  * deliberate placeholder rather than a real citation. A body illustrating
173
173
  * the shape a project's own `standards/<name>.md` takes writes a bracketed
174
174
  * token this pattern also matches, such as `standards/<slug>.md` in
175
- * `create-standard/SKILL.md` or `standards/<name>.md` in
176
- * `migration-standards-drop/SKILL.md`, and neither names a real file. A bare,
175
+ * `create-standard/SKILL.md`, which names no real file. A bare,
177
176
  * non-bracketed match now reports whether or not the file exists, unlike
178
177
  * `DOCS_PATH`, which still gates on existence: a same-repository citation
179
178
  * under this corpus names no target project's own tree the way a `docs/`
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, statSync } from 'node:fs'
1
+ import { existsSync, statSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { execaSync } from 'execa'
4
4
  import { gitEnv } from '@/git-env'
@@ -11,15 +11,6 @@ import {
11
11
 
12
12
  const CLAUDE_DIR = '.claude'
13
13
 
14
- /**
15
- * A `CLAUDE.md` past this many lines has more in it than the always-load tier
16
- * is for. The figure is the checkpoint `standards/context.md` sets for
17
- * a context entry, raised because `CLAUDE.md` legitimately carries more than
18
- * one domain. It gates a proposal rather than a failure, so being approximate
19
- * costs a line an operator can ignore.
20
- */
21
- const CLAUDE_MD_LINES = 250
22
-
23
14
  /**
24
15
  * Who put an unclaimed folder in the target. A dropped folder and one the
25
16
  * project wrote are the same bytes at the same path, so this is traced from the
@@ -45,26 +36,14 @@ export interface UnclaimedFolder {
45
36
  readonly since?: string
46
37
  }
47
38
 
48
- /**
49
- * A proposal-only skill with a live case in this target. The skills propose and
50
- * never execute, so the report names one rather than queueing a change.
51
- */
52
- export interface MigrationCandidate {
53
- readonly skill: string
54
- /** What was measured, so the proposal is checkable before it is run. */
55
- readonly reason: string
56
- }
57
-
58
39
  export interface ReverseReport {
59
40
  readonly unclaimed: readonly UnclaimedFolder[]
60
- readonly migrations: readonly MigrationCandidate[]
61
41
  /** Set when the walk needed history to run and this toolkit has none. */
62
42
  readonly historyUnavailable: boolean
63
43
  }
64
44
 
65
45
  const EMPTY_REVERSE: ReverseReport = {
66
46
  unclaimed: [],
67
- migrations: [],
68
47
  historyUnavailable: false,
69
48
  }
70
49
 
@@ -89,10 +68,9 @@ export function buildReverseReport(
89
68
  target: string,
90
69
  ): ReverseReport {
91
70
  const roots = readDroppedRoots(toolkitRoot)
92
- const migrations = detectMigrations(target)
93
71
 
94
72
  if (roots === undefined) {
95
- return { unclaimed: [], migrations, historyUnavailable: true }
73
+ return { unclaimed: [], historyUnavailable: true }
96
74
  }
97
75
 
98
76
  const unclaimed: UnclaimedFolder[] = []
@@ -106,7 +84,7 @@ export function buildReverseReport(
106
84
  }
107
85
  }
108
86
 
109
- return { unclaimed, migrations, historyUnavailable: false }
87
+ return { unclaimed, historyUnavailable: false }
110
88
  }
111
89
 
112
90
  export function emptyReverseReport(): ReverseReport {
@@ -206,55 +184,6 @@ function covers(index: HistoryIndex, sourceRel: string): boolean {
206
184
  return index.get(sourceRel) !== undefined
207
185
  }
208
186
 
209
- /**
210
- * Live cases for the two proposal-only skills no other report field names.
211
- * Without this, both are documented and unreachable.
212
- *
213
- * Both tests read what the skill itself acts on, so a proposal the report makes
214
- * is one the skill has work to do for. A context tier that already holds files
215
- * settles the `docs/` split, whether or not the toolkit is what made it.
216
- */
217
- export function detectMigrations(target: string): MigrationCandidate[] {
218
- const found: MigrationCandidate[] = []
219
- const claudeMd = join(target, 'CLAUDE.md')
220
-
221
- if (existsSync(claudeMd)) {
222
- const lines = countLines(readFileSync(claudeMd, 'utf8'))
223
- if (lines > CLAUDE_MD_LINES) {
224
- found.push({
225
- skill: 'migration-claude-md',
226
- reason: `CLAUDE.md carries ${lines} lines, past the ${CLAUDE_MD_LINES} the three-tier split is for`,
227
- })
228
- }
229
- }
230
-
231
- const docs = listFiles(join(target, 'docs')).filter(isMarkdown).length
232
- const context = listFiles(join(target, CLAUDE_DIR, 'context')).length
233
-
234
- if (docs > 0 && context === 0) {
235
- found.push({
236
- skill: 'migration-context',
237
- reason: `${docs} markdown files under docs/ with no canon/context/ tier`,
238
- })
239
- }
240
-
241
- return found
242
- }
243
-
244
- function isMarkdown(rel: string): boolean {
245
- return rel.endsWith('.md')
246
- }
247
-
248
- /**
249
- * Lines as `wc -l` counts them, so the figure in the reason matches what an
250
- * operator gets from the shell. Splitting alone overstates a file ending in a
251
- * newline by one, and the reason states a measurement rather than a rank.
252
- */
253
- function countLines(content: string): number {
254
- const parts = content.split('\n')
255
- return parts.at(-1) === '' ? parts.length - 1 : parts.length
256
- }
257
-
258
187
  /**
259
188
  * Every file under a directory, dotfiles included. A dropped root can hold any
260
189
  * extension, so this does not filter to markdown the way the domain walk does.
package/src/teach/nav.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs'
1
+ import { existsSync, readFileSync } from 'node:fs'
2
2
  import { readFile, writeFile } from 'node:fs/promises'
3
3
  import { join, relative } from 'node:path'
4
4
  import {
@@ -6,7 +6,9 @@ import {
6
6
  TEACH_STYLESHEET_COMPONENTS,
7
7
  } from '@/design/components'
8
8
  import { buildDesignCss } from '@/design/css'
9
+ import { FAVICON_COLORS, faviconLink, renderFavicon } from '@/design/favicon'
9
10
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
11
+ import { PROJECT_ROOT } from '@/project-root'
10
12
  import { TEACH_FONT_FACES } from '@/teach/fonts'
11
13
  import {
12
14
  listWorkspaces,
@@ -23,7 +25,33 @@ import {
23
25
  writeStylesheet,
24
26
  } from '@/teach/workspace'
25
27
 
26
- const FAVICON = `<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='10 10 80 80'%3E%3Cpath d='M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z' fill='rgb(224,114,75)' /%3E%3Crect x='44' y='15' width='12' height='70' rx='2' fill='rgb(224,114,75)' /%3E%3C/svg%3E" />`
28
+ const BRAND_MARK = 'assets/brand/mark.svg'
29
+
30
+ /**
31
+ * The link `teach-workspace` once had a session write by hand into every page.
32
+ * A lesson keeps whatever sits outside its marked regions, so the old icon
33
+ * would stay beside the spliced one. Matching the exact string the skill
34
+ * mandated removes it without touching a link an author chose on purpose.
35
+ */
36
+ const HAND_WRITTEN_ICON = `<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='10 10 80 80'%3E%3Cpath d='M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z' fill='rgb(224,114,75)' /%3E%3Crect x='44' y='15' width='12' height='70' rx='2' fill='rgb(224,114,75)' /%3E%3C/svg%3E" />`
37
+
38
+ function dropHandWrittenIcon(html: string): string {
39
+ return html
40
+ .split('\n')
41
+ .filter((line) => line.trim() !== HAND_WRITTEN_ICON)
42
+ .join('\n')
43
+ .replaceAll(HAND_WRITTEN_ICON, '')
44
+ }
45
+
46
+ /**
47
+ * Built from the brand mark and the favicon's own pair, the same two inputs
48
+ * `web/public/favicon.svg` is generated from, so every teach page carries the
49
+ * icon the landing page does rather than a copy of its own.
50
+ */
51
+ function teachFavicon(): string {
52
+ const mark = readFileSync(join(PROJECT_ROOT, BRAND_MARK), 'utf8')
53
+ return faviconLink(renderFavicon(mark, FAVICON_COLORS))
54
+ }
27
55
 
28
56
  /**
29
57
  * Narrower and steeper than the mark it replaces, with round caps and joins.
@@ -164,6 +192,7 @@ const GLOSSARY_FILTER_SCRIPT = `<script>
164
192
  (function () {
165
193
  var input = document.getElementById("gfilter");
166
194
  var list = document.getElementById("gloss");
195
+ var count = document.getElementById("gloss-count");
167
196
  if (!input || !list) return;
168
197
  function updateGroups() {
169
198
  list.querySelectorAll(".gloss-group").forEach(function (heading) {
@@ -186,6 +215,10 @@ const GLOSSARY_FILTER_SCRIPT = `<script>
186
215
  });
187
216
  list.classList.toggle("none", n === 0);
188
217
  updateGroups();
218
+ if (count) {
219
+ var total = list.querySelectorAll(".gterm").length;
220
+ count.textContent = (n === total ? total : n + " of " + total) + (total === 1 ? " term" : " terms");
221
+ }
189
222
  });
190
223
  var clear = list.querySelector(".clear");
191
224
  if (clear) {
@@ -517,14 +550,16 @@ interface CrumbSegment {
517
550
 
518
551
  /**
519
552
  * Every ancestor renders as a link, and the current page renders as plain
520
- * text, whether or not it still carries a jump widget of its own.
553
+ * text, whether or not it still carries a jump widget of its own. A link's
554
+ * label sits in its own span because `text-box` is ignored on the
555
+ * `inline-flex` link, so the cap trim needs a plain box to land on.
521
556
  */
522
557
  function renderBreadcrumb(segments: readonly CrumbSegment[]): string {
523
558
  return segments
524
559
  .map((segment, index) => {
525
560
  const text =
526
561
  segment.href !== undefined
527
- ? `<a class="crumb" href="${segment.href}">${escapeHtml(segment.label)}</a>`
562
+ ? `<a class="crumb" href="${segment.href}"><span class="crumb-t">${escapeHtml(segment.label)}</span></a>`
528
563
  : `<span class="crumb crumb-here">${escapeHtml(segment.label)}</span>`
529
564
 
530
565
  const jump = segment.jump
@@ -655,7 +690,7 @@ function pageHead(
655
690
  <meta charset="utf-8">
656
691
  <meta name="viewport" content="width=device-width, initial-scale=1">
657
692
  <title>${escapeHtml(title)}</title>
658
- ${FAVICON}
693
+ ${teachFavicon()}
659
694
  ${style}
660
695
  ${headScript('index')}
661
696
  </head>
@@ -859,6 +894,12 @@ function renderGlossaryGroup(group: GlossaryGroup): string {
859
894
  return `<h3 class="gloss-group">${escapeHtml(group.heading)}</h3>${entries}`
860
895
  }
861
896
 
897
+ function termCount(shown: number, total: number): string {
898
+ const noun = total === 1 ? 'term' : 'terms'
899
+
900
+ return shown === total ? `${total} ${noun}` : `${shown} of ${total} ${noun}`
901
+ }
902
+
862
903
  function renderGlossarySection(
863
904
  entries: readonly string[],
864
905
  metas: readonly LessonMeta[],
@@ -867,8 +908,8 @@ function renderGlossarySection(
867
908
  .map(renderGlossaryGroup)
868
909
  .join('')
869
910
 
870
- return `<h2>Glossary <span class="count">${entries.length}</span></h2>
871
- <input class="filter" type="search" id="gfilter" aria-label="Filter glossary terms" aria-controls="gloss" placeholder="term">
911
+ return `<h2>Glossary</h2>
912
+ <div class="filter-row"><input class="filter" type="search" id="gfilter" aria-label="Filter glossary terms" aria-controls="gloss" placeholder="Filter terms"><span class="filter-count" id="gloss-count" aria-live="polite">${termCount(entries.length, entries.length)}</span></div>
872
913
  <div class="gloss" id="gloss"><p class="empty">No term matches that. <button type="button" class="clear">Clear the filter</button></p>${rendered}</div>
873
914
  `
874
915
  }
@@ -910,7 +951,7 @@ function renderRootPage(workspaces: readonly WorkspaceSummary[]): string {
910
951
  ? 'Live'
911
952
  : 'Open'
912
953
  const blurb = `${workspace.lessons} lesson(s) &middot; ${workspace.reference} reference page(s) &middot; ${workspace.terms} term(s)`
913
- const inner = `<span class="num">${ordinalOf(workspace)}</span><b>${escapeHtml(titleCase(workspace.topic))}</b><span class="state${state === 'Live' ? ' done' : ''}">${state}</span><span class="blurb">${blurb}</span>`
954
+ const inner = `<span class="num">${ordinalOf(workspace)}</span><b>${escapeHtml(titleCase(workspace.topic))}</b><span class="state">${state}</span><span class="blurb">${blurb}</span>`
914
955
 
915
956
  return href === undefined
916
957
  ? `<li class="soon"><a href="#" aria-disabled="true" tabindex="-1">${inner}</a></li>`
@@ -1009,7 +1050,7 @@ async function renderContentsPage(
1009
1050
  const lessonRows = metas
1010
1051
  .map(
1011
1052
  (meta, index) =>
1012
- `<li><a href="${TEACH_LESSONS}/${meta.file}"><span class="num">${String(index + 1).padStart(2, '0')}</span><b>${escapeHtml(meta.title)}</b><span class="state done">Written</span><span class="blurb">${escapeHtml(meta.lede)}</span></a></li>`,
1053
+ `<li><a href="${TEACH_LESSONS}/${meta.file}"><span class="num">${String(index + 1).padStart(2, '0')}</span><b>${escapeHtml(meta.title)}</b><span class="blurb">${escapeHtml(meta.lede)}</span></a></li>`,
1013
1054
  )
1014
1055
  .join('')
1015
1056
 
@@ -1123,7 +1164,7 @@ async function rewriteLesson(
1123
1164
  ): Promise<LessonRewritten | LessonRefused> {
1124
1165
  const file = metas[index].file
1125
1166
  const path = join(root, detail.path, TEACH_LESSONS, file)
1126
- let html = await readFile(path, 'utf8')
1167
+ let html = dropHandWrittenIcon(await readFile(path, 'utf8'))
1127
1168
 
1128
1169
  const teachPrefix = '../../'
1129
1170
  const workspacePrefix = '../'
@@ -1172,11 +1213,12 @@ async function rewriteLesson(
1172
1213
  const regions: ReadonlyArray<readonly [Region, string]> = [
1173
1214
  // The stepper follows the workspace stylesheet so it wins the cascade at
1174
1215
  // equal specificity, which is what reaches a workspace seeded before it.
1175
- // The head script settles the panel before first paint, so it rides in the
1176
- // one region that sits inside `<head>`.
1216
+ // The icon and the head script both belong in `<head>`, so they ride in the
1217
+ // one region that sits there rather than a fifth marker every seeded
1218
+ // lesson would lack.
1177
1219
  [
1178
1220
  'style',
1179
- `<style>\n${legacy ? css : `${css}\n${QUIZ_CSS}`}\n</style>\n${headScript('lesson')}`,
1221
+ `${teachFavicon()}\n<style>\n${legacy ? css : `${css}\n${QUIZ_CSS}`}\n</style>\n${headScript('lesson')}`,
1180
1222
  ],
1181
1223
  ['header', header],
1182
1224
  ['footnav', `${renderFootNav(metas, index)}\n${PANE_CLOSE}`],
@@ -1,40 +0,0 @@
1
- ---
2
- name: migration-claude-md
3
- description: Why a bloated CLAUDE.md is rebalanced by proposal rather than by edit, and what the seed baseline settles
4
- ---
5
-
6
- # Migration claude md requirement
7
-
8
- ## Gap
9
-
10
- Without this skill, a session asked to slim `CLAUDE.md` deletes sections it judges stale and writes the rest into files it creates in the same pass, so a classification the user disagrees with is no longer one command to undo. The file is always-load context, so a wrong move costs every later session rather than one.
11
-
12
- Three more failures share a cause. The session has no baseline for what belongs in `CLAUDE.md`, so it reads toolkit-seeded always-load behavior as bloat and proposes moving it, and the next seed sync puts it back. A section carrying a rule and a narrative together gets sorted whole into one bucket, which loses half of it either way. And a target that already exists gets written over, since a proposal blind to the folder cannot tell a create from a collision.
13
-
14
- The skill also writes into `canon/context/`, a folder `migration-context` moves files into. A proposal drafted before those moves land cannot see the entries they create, so a move that should have resolved to an append reads as a create.
15
-
16
- ## Must
17
-
18
- - Classify every `##` section against the three tiers in a fixed order, so the same section lands the same way twice
19
- - Treat a section matching the seed baseline as always-load by definition and propose no move for it
20
- - Read the existing rules and context folders before resolving a target, so an existing entry resolves to an append and a taken rule slot resolves to a conflict
21
- - Derive a rule's glob from the scope the section names rather than from the folder it describes
22
- - Propose and stop, leaving every file write and the `CLAUDE.md` edit to the user
23
- - Report the already-lean case as a pass rather than manufacturing moves to justify the run
24
-
25
- ## Must not
26
-
27
- - Sort a section holding both a rule and a narrative into either bucket. Flag it for a manual split.
28
- - Create a rule file, create a context entry, or edit `CLAUDE.md`
29
- - Propose a move onto an existing rule path. Report it as a conflict and skip it.
30
-
31
- ## Guards
32
-
33
- - No `CLAUDE.md` at the project root stops before any classification runs
34
- - Fewer than three `##` sections reports the file already lean, since a rebalance needs something to rebalance
35
-
36
- ## Out of scope
37
-
38
- - Relocating `docs/` files, which `migration-context` proposes into this same `canon/context/` folder. Run that skill first when both apply, so Step 3 reads a folder its moves have already populated.
39
- - Scaffolding the rule files it proposes, which `create-rule` does with the numbering and frontmatter
40
- - Regenerating `canon/context/index.md`, which `canon indexes regen` does once the user has applied the moves
@@ -1,79 +0,0 @@
1
- ---
2
- name: migration-claude-md
3
- description: Classifies each section of a target project's bloated `CLAUDE.md` into the three-tier context model and proposes moves to path-scoped rules or context entries, keeping only always-load behavior in `CLAUDE.md`. Use when asked to "slim CLAUDE.md", "migrate CLAUDE.md to rules", "rebalance CLAUDE.md", or when a project's `CLAUDE.md` has grown large. Do NOT move `docs/` files (that is `migration-context`) and do NOT auto-execute. Proposal only.
4
- ---
5
-
6
- # Claude md migrate
7
-
8
- Rebalance a large `CLAUDE.md` so only always-load behavior stays in it, path-scoped behavior becomes a rule, and domain narrative becomes a context entry. Every move is a proposal the user applies by hand.
9
-
10
- Run `migration-context` first when both apply. Its moves populate `canon/context/`, and Step 3 has to read the folder those moves left so an existing entry resolves to an append rather than blocking the move that should have created it.
11
-
12
- A project the surface move has not reached keeps its context folder under `.claude/` rather than `canon/`. Read and propose entries into whichever root already carries the folder, and propose `canon/` only when neither does, since a new `canon/` folder would hide every entry the old one holds.
13
-
14
- ## Guards
15
-
16
- - If no `CLAUDE.md` exists at `pwd`, stop: `❌ No CLAUDE.md found at the project root.`
17
- - If `CLAUDE.md` has fewer than three `##` sections, stop: `✅ CLAUDE.md is already lean. Nothing to rebalance.`
18
-
19
- ## Step 1: discover
20
-
21
- Run these in parallel from `pwd`:
22
-
23
- - Read `CLAUDE.md`
24
- - `ls .claude/rules/ 2>/dev/null`: existing rule subdirs and numbers for placement
25
- - `ls canon/context/ 2>/dev/null`: existing context entries for conflict checks
26
- - `canon claude seeds list --json 2>/dev/null`: the base seed set. Read the `CLAUDE.md` entry's `content` as the always-load baseline. Skip this input when `canon` is not installed.
27
-
28
- ## Step 2: classify each section
29
-
30
- Split `CLAUDE.md` by `##` heading. Score each section against this order:
31
-
32
- 1. **Always-load.** Stays in `CLAUDE.md`. Signals: cross-cutting behavior that applies every session, output and formatting rules, project-wide invariants, conventions with no file scope. A section that matches the seed baseline is always-load by definition. Keep it and propose no move.
33
- 2. **Path-scoped.** Propose a rule. Signals: behavior that only applies when working in a folder (`frontend/`, `backend/`) or on a file type, framework-specific conventions, "when editing X do Y" phrasing. Derive the glob from the named scope.
34
- 3. **Domain narrative.** Propose a context entry. Signals: prose describing how a system or domain is built, decisions, gotchas, structure. Not do-or-do-not rules.
35
- 4. **Mixed.** Both rule and narrative in one section. Flag as "needs manual split". Do not propose a single target.
36
-
37
- ## Step 3: resolve targets
38
-
39
- For each section proposed for a move:
40
-
41
- - Path-scoped: propose `.claude/rules/<subdir>/<n>-<slug>.md` with a `paths:` glob. Pick the subdir and a free number the way `create-rule` does. If `.claude/rules/<subdir>/` already holds a rule on the topic, mark as "conflict" and skip.
42
- - Domain narrative: propose `canon/context/<domain>.md`. If the entry already exists, flat or as a same-named `canon/context/<domain>/` folder, propose appending to the file that owns it rather than creating a duplicate flat entry beside a domain already split.
43
-
44
- ## Step 4: output
45
-
46
- Print one grouped proposal block. Omit empty groups.
47
-
48
- ```markdown
49
- ## Keep in CLAUDE.md
50
-
51
- - `## <heading>` (<one-line reason: always-load behavior>)
52
-
53
- ## Extract to a path-scoped rule
54
-
55
- - `## <heading>` → `.claude/rules/<subdir>/<n>-<slug>.md`, paths `<glob>` (<reason>)
56
-
57
- ## Extract to a context entry
58
-
59
- - `## <heading>` → `canon/context/<domain>.md` (<reason>)
60
- - `## <heading>` → `canon/context/<domain>/<sub-area>.md`, appending to the domain's existing sibling file (<reason>)
61
-
62
- ## Needs manual split
63
-
64
- - `## <heading>` (rule and narrative mixed)
65
-
66
- ## Conflicts
67
-
68
- - `## <heading>` → `.claude/rules/<subdir>/<n>-<slug>.md` already exists. Skipping.
69
-
70
- ## Reminder
71
-
72
- Scaffold each rule with create-rule, which carries the rule standard.
73
- Follow the context standard for each new context entry.
74
- Run canon indexes regen after adding context entries.
75
- ```
76
-
77
- If every section classified as always-load, output: `✅ CLAUDE.md holds only always-load behavior. No moves proposed.`
78
-
79
- Do not edit `CLAUDE.md`, and do not create the rule or context files. The user reviews the proposal, then applies each move by hand.
@@ -1,36 +0,0 @@
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.
13
-
14
- A seed-derived `development.md` moved out of `docs/` comes back on the next seed sync. A move onto an existing `canon/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.
15
-
16
- ## Must
17
-
18
- - Classify by audience signal in the file rather than by its name or its folder position
19
- - Propose and stop. The `git mv` commands and the link fixes stay the user's to run.
20
- - Report every inbound reference to a moved path as a TODO line, leaving the reference itself untouched
21
- - Defer a seed-derived file to the seed sync path rather than proposing a move for it
22
- - Skip a move whose destination already exists and report it as a conflict
23
-
24
- ## Must not
25
-
26
- - 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.
27
- - Descend into `docs/` subfolders. List them for manual review and leave them alone.
28
-
29
- ## Guards
30
-
31
- - A missing `docs/` directory, or one holding no top-level markdown, stops before any classification runs
32
-
33
- ## Out of scope
34
-
35
- - Classifying `CLAUDE.md` sections, which `migration-claude-md` proposes into this same `canon/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.
36
- - Regenerating `canon/context/index.md`, which `canon indexes regen` does once the user has applied the moves
@@ -1,97 +0,0 @@
1
- ---
2
- name: migration-context
3
- description: Classifies markdown files in a target project's `docs/` folder and proposes `git mv` commands to relocate agent-flavored content to `canon/context/`. Use when asked to "migrate docs to context", "move docs to context", "split docs", or to align an existing project with the three-tier context model. Do NOT auto-execute moves or rewrite inbound links. Proposal only.
4
- ---
5
-
6
- # Claude context migrate
7
-
8
- Run this skill before `migration-claude-md` when both apply. That skill proposes entries into the same `canon/context/` folder, and an entry landing there first makes the corresponding move here read as a conflict and get skipped, which loses the richer `docs/` file.
9
-
10
- A project the surface move has not reached keeps its context folder under `.claude/` rather than `canon/`. Read and propose moves into whichever root already carries the folder, and propose `canon/` only when neither does, since a new `canon/` folder would hide every entry the old one holds.
11
-
12
- ## Guards
13
-
14
- - If no `docs/` directory exists at `pwd`, stop: `❌ No docs/ directory found.`
15
- - If `docs/` contains no `.md` files at the top level, stop: `❌ No markdown files in docs/.`
16
- - This skill operates on flat `docs/*.md` only. Subfolders under `docs/` are listed in the output as "manual review needed" and skipped.
17
-
18
- ## Step 1: discover
19
-
20
- Run these in parallel from `pwd`:
21
-
22
- - `ls docs/*.md 2>/dev/null`: list flat markdown files
23
- - `ls docs/*/ 2>/dev/null`: detect subfolders for the manual-review note
24
- - `ls canon/context/ 2>/dev/null`: detect already-migrated content for conflict checks
25
- - `canon claude seeds list --json 2>/dev/null`: get the canonical seed list to identify seed-derived files
26
-
27
- Read each discovered `docs/*.md` file in parallel.
28
-
29
- ## Step 2: classify
30
-
31
- For each file, score against this rule order:
32
-
33
- 1. **Seed-derived.** Filename matches `development.md` or `ci.md` and content shape resembles the toolkit's base seed (frontmatter plus a Setup or Triggers section plus tables for scripts or checks). These belong to the seed-migration path. Do not propose a move here.
34
- 2. **Agent-flavored.** Score the following signals. Classify as agent-flavored when two or more match:
35
- - Dense file paths in code fences
36
- - Sections titled "Anti-patterns", "Layer responsibilities", "Conventions", or "Commands"
37
- - Tables of CLI commands
38
- - Explicit "do not" rules
39
- 3. **Human-only.** Default fallback when no agent signals match. Typical signals:
40
- - Tutorial prose
41
- - Sections titled "Welcome", "Getting started", "Contributing", or "Installation"
42
- - Second-person narrative aimed at new contributors
43
- 4. **Mixed.** Both agent and human signals present. Flag as "needs manual split". Do not propose a move.
44
-
45
- ## Step 3: check conflicts and inbound links
46
-
47
- For every file proposed for a move:
48
-
49
- - If `canon/context/<filename>` already exists, or a same-named folder exists at `canon/context/` with `<filename>`'s `.md` extension dropped, mark as "conflict" and skip the move. A folder of the same name is still a collision, since the domain already lives there split into siblings.
50
- - Run `git grep -n "docs/<filename>"` (or grep equivalent) to list inbound references in `CLAUDE.md`, `README.md`, and other markdown. Report each as a TODO line. Do not rewrite.
51
-
52
- ## Step 4: output
53
-
54
- Print one grouped proposal block. Omit empty groups.
55
-
56
- ```markdown
57
- ## Move to canon/context/
58
-
59
- - docs/<file>.md → canon/context/<file>.md (<one-line reason>)
60
-
61
- ## Keep in docs/
62
-
63
- - docs/<file>.md (<one-line reason>)
64
-
65
- ## Defer to seed-sync
66
-
67
- - docs/<file>.md (matches toolkit base seed)
68
-
69
- ## Needs manual split
70
-
71
- - docs/<file>.md (mixed agent and human signals)
72
-
73
- ## Conflicts
74
-
75
- - docs/<file>.md → canon/context/<file>.md already exists. Skipping.
76
-
77
- ## Subfolders (manual review)
78
-
79
- - docs/<subfolder>/
80
-
81
- ## Suggested git mv commands
82
-
83
- git mv docs/<file>.md canon/context/<file>.md
84
-
85
- ## Inbound links to fix after moves
86
-
87
- - CLAUDE.md:42 references docs/<file>.md
88
- - README.md:17 references docs/<file>.md
89
-
90
- ## Reminder
91
-
92
- Run canon indexes regen to update canon/context/index.md.
93
- ```
94
-
95
- If every file classified as "Keep in docs/", output: `✅ All docs/ content is human-facing. No moves proposed.`
96
-
97
- Do not execute the `git mv` commands. Do not edit `CLAUDE.md` or `README.md`. The user runs the commands and fixes the links manually after reviewing.
@@ -1,55 +0,0 @@
1
- ---
2
- name: migration-standards-drop
3
- description: Why the standards-tree drop ships as an ordered proposal, what the two pre-delete reads catch, and why one citation form serves every surface inside a target
4
- ---
5
-
6
- # Migration standards drop requirement
7
-
8
- ## Gap
9
-
10
- Without this skill, nothing names the tree. The sync engine registers governance alone and the unmigrated scan carries an empty root-layout set, both by decision rather than by omission, so a target holding 21 installed standards files reports clean and the drop runs off whoever remembers to look. The census that found the population walked one folder on one machine and moved the count from four to seven.
11
-
12
- The order is the second failure and it has two halves. A session that deletes before syncing lands the installed rules citing a path it removed, and one that sweeps before syncing repoints citations the sync then rewrites.
13
-
14
- The delete otherwise runs blind in two directions. Two of the five targets measured on 2026-08-28 carried a runtime reader, one a hook hand-parsing a standard for its banned-word set, and a hook whose input is gone audits nothing while reporting a clean run. In the other direction, nothing separates a file the project wrote from a toolkit copy by name, and the corpus had moved on from all 20 files at one target, so a content diff sorts every file as changed and a session reading that diff either keeps everything or loses the one file no corpus carries.
15
-
16
- The repoint form is the fourth. A session applying the citation rule it knows from a shipped skill writes `${CLAUDE_SKILL_DIR}/../../standards/<name>.md`, which from a project-local skill at `.claude/skills/<name>/` resolves back into the tree the same pass deleted, and reports the sweep finished.
17
-
18
- The last is the name map. `prose` split into `markdown.md` plus the `write-human` skill and resolves as no standard, so a mechanical one-to-one repoint produces a citation to nothing and every check on it passes.
19
-
20
- ## Must
21
-
22
- - Detect from a read of `.claude/standards/` itself, since no report names the tree and a session waiting for one waits forever
23
- - Report the state a binary behind the published version puts the sync in, before naming a sync a stale binary would run
24
- - Split the tree against `canon standards list --json` three ways, separating an unchanged toolkit copy from a drifted one and from a name that resolves to no standard
25
- - Name every runtime reader ahead of any delete, splitting a reader from a citation by whether the file is markdown, and say when the search could not run
26
- - Give every citation inside a target one target form, `canon standards <name>`, and say why the plugin-root form fails there
27
- - Order the proposal sync, drop, sweep, and state that the sweep re-reads after the sync rather than before it
28
- - Report an unmatched name as the user's to place, naming the `prose` split as the recorded instance
29
- - Report the measured empty case as a pass, since the guard is what proves the read ran
30
-
31
- ## Must not
32
-
33
- - Run the sync, delete or move a file under the tree, or rewrite a citation
34
- - Sort a drifted file as project-authored or as toolkit-owned on a content diff alone
35
- - Propose the plugin-root citation form for a surface inside a target
36
- - Relocate the install stamp. One visit per target is cheaper than two and a skill is not a visit, so the stamp rides with whatever else reaches the target.
37
- - Stay in the catalog on the author typing its name alone. Nothing can answer that before the skill has run, so read the census back once it has and retire it if the answer is nobody.
38
-
39
- ## Guards
40
-
41
- - A directory with no `.claude/` stops, since nothing there installed a tree
42
- - A `.claude/standards/` holding no markdown is the pass rather than a refusal
43
- - No `canon` on `PATH` stops, since every citation this move writes names a verb the target has to run
44
-
45
- ## Out of scope
46
-
47
- - Classifying `CLAUDE.md` sections into the three-tier model: `migration-claude-md`
48
- - Relocating `docs/` files by audience: `migration-context`
49
- - Splitting a retired `.claude/` file into the folder that replaced it, which pairs one file against one folder rather than dropping a corpus: `migration-superseded`
50
- - Reconciling a seed file against its source section by section: `seed-sync`
51
- - Applying the drop and the sweep, which the user does after reviewing
52
-
53
- ## Why it ships while its consumer row is parked
54
-
55
- The one task performing this move is parked on the operator declining outside-repository work, so the skill arrives with three known consumers and no live instance. It ships anyway, because the parked row is about who is authorized to visit a target rather than about the move being wrong, and the skill is what makes the move repeatable by whoever is authorized. The evidence behind it comes from the census rather than from a run, which is the weaker of the two and is recorded here as such.