@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.
- package/assets/brand/mark.svg +10 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
- package/claude/skills/auto-ship/SKILL.md +12 -2
- package/claude/skills/canon-operator/REQUIREMENT.md +2 -2
- package/claude/skills/canon-operator/SKILL.md +3 -4
- package/claude/skills/git-pr/REQUIREMENT.md +2 -0
- package/claude/skills/git-pr/SKILL.md +21 -3
- package/claude/skills/target-check/references/domains.md +2 -2
- package/claude/skills/teach-workspace/SKILL.md +1 -1
- package/docs/agents/install-and-sync.md +4 -13
- package/docs/agents/teach.md +3 -1
- package/docs/target-projects.md +0 -2
- package/docs/workflow/ai-workflow.md +7 -11
- package/package.json +2 -1
- package/scripts/core/capture-home.sh +59 -0
- package/src/claude/cases/setup.ts +2 -23
- package/src/commands/sync.ts +0 -16
- package/src/design/components.ts +153 -152
- package/src/design/favicon.ts +8 -0
- package/src/design/tokens.ts +1 -1
- package/src/shipped/references.ts +1 -2
- package/src/sync/reverse.ts +3 -74
- package/src/teach/nav.ts +55 -13
- package/claude/skills/migration-claude-md/REQUIREMENT.md +0 -40
- package/claude/skills/migration-claude-md/SKILL.md +0 -79
- package/claude/skills/migration-context/REQUIREMENT.md +0 -36
- package/claude/skills/migration-context/SKILL.md +0 -97
- package/claude/skills/migration-standards-drop/REQUIREMENT.md +0 -55
- package/claude/skills/migration-standards-drop/SKILL.md +0 -113
- package/claude/skills/migration-superseded/REQUIREMENT.md +0 -44
- package/claude/skills/migration-superseded/SKILL.md +0 -115
package/src/design/favicon.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/design/tokens.ts
CHANGED
|
@@ -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
|
|
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
|
|
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/`
|
package/src/sync/reverse.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
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: [],
|
|
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,
|
|
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
|
|
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
|
-
${
|
|
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
|
|
871
|
-
<input class="filter" type="search" id="gfilter" aria-label="Filter glossary terms" aria-controls="gloss" placeholder="
|
|
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) · ${workspace.reference} reference page(s) · ${workspace.terms} term(s)`
|
|
913
|
-
const inner = `<span class="num">${ordinalOf(workspace)}</span><b>${escapeHtml(titleCase(workspace.topic))}</b><span class="state
|
|
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="
|
|
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
|
|
1176
|
-
// one region that sits
|
|
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
|
-
|
|
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.
|