@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
@@ -0,0 +1,10 @@
1
+ <!-- A bound ledger: two pages fanned open around a spine, drawn as three
2
+ solid masses rather than thin strokes, so the shape survives being
3
+ rendered at favicon size instead of averaging into grey. The 10-unit
4
+ gap between each page and the spine, against an 80-unit frame, is what
5
+ keeps the split a visible channel at 16px; a first draft at half that
6
+ gap closed into an unreadable blob at the same size. -->
7
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80">
8
+ <path d="M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z" fill="currentColor" />
9
+ <rect x="44" y="15" width="12" height="70" rx="2" fill="currentColor" />
10
+ </svg>
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.110.0",
4
+ "version": "4.112.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -21,6 +21,7 @@ Review is the step that varies most. It gets skipped on a diff that needed one,
21
21
  - Own the review receipt's lifetime, since this chain writes it, cites it in its own closing block, and is the only body that can read whether the step keeping it is still using it
22
22
  - Delegate the ship sequence to `git-ship` rather than restating it, and name only what this chain adds to it
23
23
  - Open the pull request as a draft before the continuous integration watch begins, since a pull request marked after it is mergeable for the length of the run
24
+ - Prove the draft target's head is the current branch and its state is open before marking it, and stop with nothing marked on a mismatch, since the number is retyped between steps and a draft mark on a release pull request holds that release
24
25
  - Name the recovery for the stop it took, since the value of stopping is that the user knows where to resume
25
26
 
26
27
  ## Must not
@@ -190,14 +190,23 @@ This chain owns the receipt's lifetime, which is what makes the Output block's c
190
190
 
191
191
  Invoke `canon:git-ship`. That body owns the sequence, being the verify gate, memory capture, both doc syncs, staging, the commit grouping, the branch rename, the pull request, and the CI watch, along with the reason each step sits where it does. This step used to restate that list and the two drifted apart with nothing comparing them, so read the order there and never here.
192
192
 
193
- One thing this chain adds. Mark the pull request as a draft as soon as `git-ship`'s pull request step returns, ahead of its CI watch, then read the flag back:
193
+ One thing this chain adds. Mark the pull request as a draft as soon as `git-ship`'s pull request step returns, ahead of its CI watch. Run it as one command, where the shell proves the target before the mark and reads the flag back after it:
194
194
 
195
195
  ```bash
196
+ target=$(gh pr view <number> --json headRefName,state --jq '"\(.headRefName) \(.state)"') || exit 1
197
+ if [ "$target" != "$(git branch --show-current) OPEN" ]; then
198
+ printf '❌ #%s reads "%s", not "%s OPEN". The draft mark was not written.\n' "<number>" "$target" "$(git branch --show-current)" >&2
199
+ exit 1
200
+ fi
196
201
  gh pr ready --undo <number>
197
202
  gh pr view <number> --json isDraft
198
203
  ```
199
204
 
200
- Name the number `git-ship`'s pull request step returned on both calls rather than leaving either to resolve by branch. `${CLAUDE_SKILL_DIR}/../git-pr/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
205
+ Name the number `git-ship`'s pull request step returned in every position rather than leaving any to resolve by branch. `${CLAUDE_SKILL_DIR}/../git-pr/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
206
+
207
+ A refusal is a stop. Report the line the shell printed, then resolve the number from the `number=` and `head=` lines of `git-pr`'s output and re-run the command by hand. The comparison sits in the shell rather than in the session's reading of a printed value, which is the same split `git-pr`'s `assert_own_pr` makes, because the judgment that picked a wrong number is the one that would read the check.
208
+
209
+ The number crosses from that step's output into a command this session types, which is the one place a number gets authored rather than derived. A number misread there, or inferred from the newest pull request in view, would draft a stranger's pull request. A release pull request is the worst such target, since the mark holds the release and this role may not lift it again.
201
210
 
202
211
  Report what the read returned rather than what the command printed, since the exit says the call ran and says nothing about the state. A `true` reports a draft. A `false` reports the pull request as opened ready and unsupervised, and the chain stops there. Never re-issue the undo on a disagreeing read, which fights whoever readied it instead of guarding anything.
203
212
 
@@ -243,3 +252,4 @@ Every stop point leaves recoverable state. The user resumes manually from the ap
243
252
  | Inherited review findings | Fix findings, run `/git-ship` |
244
253
  | Self-introduced finding survived | Read the receipt for what the one repair pass left open, fix it, run `/git-ship` |
245
254
  | git-ship fails | Inspect hook or remote error, run again |
255
+ | Draft target is not this branch's | Nothing was marked. Read the number and `head=` from `git-pr`'s output, confirm they match, then run the mark by hand |
@@ -26,8 +26,8 @@ The last failure is a section no route reaches. `## Route` maps an intent or a d
26
26
  - Resolve every stack, rule, snippet, and standard name from a catalog at runtime
27
27
  - Run the CLI non-interactively and report the command run, what changed, and the full path of anything written
28
28
  - Read the overwrite contract from the body before any install or sync, and name each surface it lists as overwritten before running one
29
- - Read the reverse walk beside the forward sections, so a folder the toolkit stopped shipping and a migration with a live case both reach the user
30
- - Hand off a named migration to the skill the report names, and name an unmigrated domain's root and install path for the user to move themselves when no skill covers it
29
+ - Read the reverse walk beside the forward sections, so a folder the toolkit stopped shipping reaches the user
30
+ - Name an unmigrated domain's root and install path for the user to move themselves, since no skill covers the move
31
31
  - Route a measurement intent to the audit offers from the route table itself, rather than leaving the section reachable only by reading past the table
32
32
  - Rank a lifecycle row against the audit offers where the route table itself states it, so a session acting on the lifecycle row reads the ranking without opening the section being ranked
33
33
  - Offer every audit whose surface the target carries, and offer none whose surface it lacks
@@ -25,10 +25,10 @@ Run `canon sync --check . --json` before routing. It reports what a target is be
25
25
  Read seven fields off the report and carry each to `## Route`:
26
26
 
27
27
  - `unmigrated`: a domain sitting at the root layout with nothing under `.claude/`. The most urgent finding, because that domain reports no drift of its own while being entirely behind
28
- - `superseded`: a file a newer seed folder replaced. No command moves it, since the content is the project's own, so this routes to the skill that proposes the split rather than ending at the report
28
+ - `superseded`: a file a newer seed folder replaced. No command moves it, since the content is the project's own, so this routes to `canon:target-check` for the full per-domain reading rather than ending at the report
29
29
  - `seeds`: entries are `matching`, `stale`, `drifted`, or `missing`. Anything but `matching` needs the seed handoff
30
30
  - `domains[].entries`: per-file `stale`, `customized`, `stranded`, and `orphaned` as before
31
- - `reverse`: what the target holds that no live catalog claims. `unclaimed` lists folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`. Act on the first two and leave `project` alone, which history proved the project owns. `migrations` names a proposal-only skill with a live case here and the `reason` it was measured from
31
+ - `reverse`: what the target holds that no live catalog claims. `unclaimed` lists folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`. Act on the first two and leave `project` alone, which history proved the project owns
32
32
  - `historyUnavailable` on a domain, on `seeds`, or on `reverse`: nothing could be dated, so treat every difference as unverified and say so rather than reporting a file as untouched. It is set on `reverse` when the toolkit itself ships without history, which is the registry-install case, so the walk found nothing rather than finding a clean target
33
33
  - `tooling`: read `measured` first. Every count under it is zero when it is false, which is an absence of measurement rather than a measured zero. Past that, `chain` names the stacks the install resolved, nearest first, and `counts.gitignore` counts the managed ignore entries the target is missing.
34
34
 
@@ -43,8 +43,7 @@ Map the stated intent, or what `## Diagnose` found, to one lifecycle phase, then
43
43
  The two can name different rows, and a reply answers both rather than picking one. Run or hand off what the finding names, then answer the intent in the same reply. A measurement intent therefore reaches the audit offers below even on a target whose diagnostic found scaffold work, which is the case a session leaving from the scaffold row would otherwise never read.
44
44
 
45
45
  - A domain in `unmigrated`: name it, the root path, and the install path. No command moves it, so the user runs the relocation themselves
46
- - An entry in `migrations`: hand off to the skill its `skill` field names, spelled as the report spells it. Both of them propose without writing, so the handoff is where this stops
47
- - Anything in `superseded`: name which files and what replaced them, then hand off to `migration-superseded`. Both skills propose, so neither moves nor deletes the file
46
+ - Anything in `superseded`: name which files and what replaced them, then hand off to `canon:target-check`, which reports and never moves or deletes the file
48
47
  - A folder in `unclaimed`: name it and the attribution it carries. No command moves it and the content may be the project's own, so the decision is the user's
49
48
 
50
49
  - First-time scaffold of a fresh project: hand off to `target-setup`
@@ -23,6 +23,8 @@ A lookup that resolves by head branch alone carries its own failure. A branch na
23
23
  - Detect an open pull request and edit it in place, so a follow-up push keeps the body in sync instead of failing
24
24
  - Scope that detection to an open pull request on the current head and the default base, so neither a reused branch name nor a second base resolves the wrong one
25
25
  - Resolve the pull request once and reuse what that resolution returned, so the number recorded never depends on how a lookup ranks two pull requests sharing a head
26
+ - Prove before every write to an existing pull request that its head is the current branch and its state is open, and refuse with nothing written on a mismatch, so a wrong number reaching a write never lands on another branch's pull request
27
+ - Print the head branch beside the number, so a caller writing to the pull request later has a value to check the number against
26
28
  - Label from the paths the branch changed, against a map the project declares, so the label set belongs to the project rather than to the skill
27
29
  - Apply labels after the pull request exists, so a label the remote does not carry costs a warning rather than the pull request
28
30
  - Report a refused label, since a warning nothing surfaces leaves the run indistinguishable from one that labelled
@@ -135,23 +135,41 @@ This command reuses `.canon/tmp/pr/body.md`, which the pre-publish scan above al
135
135
 
136
136
  ```bash
137
137
  pr_labels="<comma-separated labels, empty when the map resolves to nothing>"
138
+ head_branch=$(git branch --show-current)
138
139
  git push -u origin HEAD || exit 1
139
140
  base_branch=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) || exit 1
140
- pr_number=$(gh pr list --head "$(git branch --show-current)" --base "$base_branch" --state open --json number --jq '.[0].number // empty')
141
+ assert_own_pr() {
142
+ target=$(gh pr view "$1" --json headRefName,state --jq '"\(.headRefName) \(.state)"') || exit 1
143
+ if [ "$target" != "$head_branch OPEN" ]; then
144
+ printf 'Refused: #%s reads "%s", not "%s OPEN". Nothing was written to it.\n' "$1" "$target" "$head_branch" >&2
145
+ exit 1
146
+ fi
147
+ }
148
+ pr_number=$(gh pr list --head "$head_branch" --base "$base_branch" --state open --json number --jq '.[0].number // empty')
141
149
  if [ -n "$pr_number" ]; then
150
+ assert_own_pr "$pr_number"
142
151
  pr_url=$(gh pr edit "$pr_number" --title "<title>" --body-file .canon/tmp/pr/body.md) || exit 1
143
152
  else
144
153
  pr_url=$(gh pr create --title "<title>" --body-file .canon/tmp/pr/body.md) || exit 1
145
154
  pr_number=${pr_url##*/}
155
+ assert_own_pr "$pr_number"
146
156
  fi
147
157
  if [ -n "$pr_labels" ]; then
148
158
  gh pr edit "$pr_number" --add-label "$pr_labels" >/dev/null ||
149
159
  printf 'Label apply failed. Create a missing label with: gh label create <name>\n' >&2
150
160
  fi
151
161
  rm -rf .canon/tmp/pr/body
152
- printf 'number=%s\nurl=%s\n' "$pr_number" "$pr_url"
162
+ printf 'number=%s\nurl=%s\nhead=%s\n' "$pr_number" "$pr_url" "$head_branch"
153
163
  ```
154
164
 
165
+ ### Binding every write to this branch's pull request
166
+
167
+ `assert_own_pr` runs ahead of both writes this command makes to a pull request that already exists, the title and body edit and the label edit. It reads the target's head branch and state and refuses unless they are this branch and `OPEN`. A refusal exits before anything is written, so a wrong number costs the run rather than a stranger's pull request. The function lives only inside this command, so the steps below that post comments or record the task are covered by taking `<number>` from this output rather than by the check itself.
168
+
169
+ The check compares a number against a branch and never derives a number from one, so it adds no lookup of the kind `### Resolving the pull request` retired. It holds whichever way a wrong number arrives. A number a session retyped by hand, or inferred from the newest pull request in view, reads as a foreign head here and stops.
170
+
171
+ The last output line carries `head=` so a caller relaying the number holds a branch to compare it against rather than a bare integer. A caller that writes to the pull request itself, such as a draft mark, runs the same comparison in the shell first, reading `gh pr view <number> --json headRefName,state` and refusing unless it matches `head` and `OPEN`.
172
+
155
173
  ### Find the UI checklist
156
174
 
157
175
  `ui-checklist` writes a visual checklist to `.canon/tmp/handoff/ui-checklist/<slug>.md` at the main worktree root when a change needs visual verification, with `<slug>` derived per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. This skill is the file's sole consumer. Resolve the main root the way `session-worktree` does (`git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`, falling back to `pwd`) and check for the file there. A missing file means no checklist was produced, and the two steps below each skip their checklist half.
@@ -238,7 +256,7 @@ canon pr evidence <number> --preview <url> --json
238
256
 
239
257
  ### Record the number on the task
240
258
 
241
- Write the `number` the final command printed onto the task the branch is closing. Do not resolve it again. `${CLAUDE_SKILL_DIR}/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
259
+ Write the `number` the final command printed onto the task the branch is closing. Do not resolve it again. A final command that refused on a head mismatch printed no number, so this step writes nothing on that run. `${CLAUDE_SKILL_DIR}/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
242
260
 
243
261
  The task is the one whose `Plan:` line names the plan this branch implemented. Name that plan by its file, which is `.canon/plans/feature-<slug>.md` at the main worktree root with `<slug>` derived per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. `plan-feature` writes the plan under the branch slug, so the two correspond on any branch that came through the plan-to-execute path. When the session already knows which plan it implemented, because a caller read it earlier in the chain, use that filename instead of re-deriving.
244
262
 
@@ -25,7 +25,7 @@ Read three fields off the same report, then two verbs:
25
25
 
26
26
  - `unmigrated`: a domain still at the root layout with nothing under `.claude/`. The most urgent finding, because such a domain reports no drift of its own while being entirely behind.
27
27
  - `superseded`: a file a newer seed folder replaced. No command moves it, since the content is the project's own.
28
- - `reverse`: `unclaimed` names folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`. `migrations` names a proposal-only skill with a live case here and the `reason` behind it.
28
+ - `reverse`: `unclaimed` names folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`.
29
29
  - `canon context audit --json`: required sections, entry length, citations, and index drift across the context folder.
30
30
  - `canon records validate <kind> --json`, once per record folder the target carries.
31
31
 
@@ -33,7 +33,7 @@ Read three fields off the same report, then two verbs:
33
33
  - Behind: any of those carrying an entry. Name the entry and its attribution.
34
34
  - Unread: `historyUnavailable` set on `reverse`, a `reverse` key the report does not carry at all, or either verb refusing. A missing `reverse` key is a binary predating the field rather than a target with nothing unclaimed.
35
35
 
36
- Routes by finding. `unmigrated` routes to a relocation the user runs, since no command moves it. `superseded` routes to `migration-superseded`. A `migrations` entry routes to the skill its `skill` field names, spelled as the report spells it. An `unclaimed` folder carrying `project` attribution routes nowhere, since history proved the project owns it.
36
+ Routes by finding. `unmigrated` routes to a relocation the user runs, since no command moves it. `superseded` routes to a split the user makes, since no command moves the content and no skill proposes it. An `unclaimed` folder carrying `project` attribution routes nowhere, since history proved the project owns it.
37
37
 
38
38
  ## 3. Governance rules
39
39
 
@@ -18,7 +18,7 @@ The shape of the workspace is fixed by `${CLAUDE_SKILL_DIR}/../../standards/teac
18
18
  - Write nothing outside the workspace folder, apart from the one handoff file Step 6 names. A durable page stays in `reference/` and is copied out by the skill that owns the destination, never by this one.
19
19
  - Do not open a second workspace on a subject one already covers. Resume that one.
20
20
  - Never promote a lesson. It is generated markup carrying a quiz and a learner, and no request makes it promotable.
21
- - Carry the toolkit's favicon on every page this skill writes, index and lesson alike, in the `<head>` verbatim: `<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" />`
21
+ - Never write a favicon link by hand. `canon teach nav` puts the toolkit's icon on every page it generates and inside a lesson's `style` region, so a hand-written one in a lesson's `<head>` gives the page two.
22
22
 
23
23
  ## Step 0: let the CLI resolve the workspace root
24
24
 
@@ -256,11 +256,9 @@ seed tree rather than from a fixed set of filenames, so a folder added later is
256
256
  covered without a code change. Only an exact stem matches, which leaves a
257
257
  suffixed variant such as `TASKS-ARCHIVE.md` unreported.
258
258
 
259
- Route it to `migration-superseded`. That skill resolves the standard governing
260
- `replacedBy` from the `appliesTo` the standards catalog declares, reads the
261
- destination shape with `canon standards <name>`, and proposes the split without
262
- writing. Where a folder has no governing standard, the entry earns a named
263
- refusal rather than a shape nobody stated.
259
+ No skill splits the file. The standard governing `replacedBy` is the one the
260
+ `appliesTo` the standards catalog declares, and `canon standards <name>` reads
261
+ its destination shape, so the split is the project's own to make.
264
262
 
265
263
  `unmigrated` names a domain sitting at the root layout an older toolkit installed
266
264
  to, with nothing at the path the current one reads. It carries `rootPath`,
@@ -350,7 +348,7 @@ check.
350
348
  `reverse` is the one section built by walking the target rather than the
351
349
  catalog. Every other surface enumerates toolkit-owned keys and asks whether the
352
350
  target matches, so a folder the toolkit deleted appears in none of them. It
353
- carries `unclaimed`, `migrations`, and `historyUnavailable`.
351
+ carries `unclaimed` and `historyUnavailable`.
354
352
 
355
353
  `unclaimed` names a folder the target holds at a top-level path the toolkit once
356
354
  shipped and has since deleted. The candidate roots come from the toolkit's own
@@ -381,13 +379,6 @@ colliding on a retired name costs the walk no reads. The cost is that a file the
381
379
  toolkit shipped and the target renamed goes unmatched, the same limit the
382
380
  `unmigrated` count carries.
383
381
 
384
- `migrations` names a proposal-only skill with a live case in this target. It
385
- fires on a `CLAUDE.md` past 250 lines for `migration-claude-md`, and on a `docs/` folder
386
- holding markdown with no populated `canon/context/` for `migration-context`.
387
- Each entry carries the skill name and the measurement behind it, so a consumer
388
- can check the proposal before running it. Without the field both skills are
389
- documented and unreachable from any report.
390
-
391
382
  #### What counts toward the gate
392
383
 
393
384
  `unmigrated` counts toward `--exit-code`, since running the relocation closes it.
@@ -172,7 +172,9 @@ canon teach nav regular-expressions --json
172
172
 
173
173
  With no topic it rewrites every workspace. The teach-root listing is always rewritten regardless, since it reports on every workspace and a scoped run leaving it stale would be a second kind of drift this verb exists to end.
174
174
 
175
- A lesson carries its chrome as four marker pairs the authoring skill writes empty: `canon:teach:style`, `canon:teach:header`, `canon:teach:footnav`, and `canon:teach:scripts`. This verb splices each one from the current workspace state, embedding the shared stylesheet and the script that settles the course panel before the page paints, rebuilding the course sidebar, the breadcrumb and jump menus, the prev/next footer nav, and the behavior scripts, and leaves the authored `<h1>`, lede, body, and quiz between the header and the footnav untouched.
175
+ A lesson carries its chrome as four marker pairs the authoring skill writes empty: `canon:teach:style`, `canon:teach:header`, `canon:teach:footnav`, and `canon:teach:scripts`. This verb splices each one from the current workspace state, embedding the toolkit's favicon, the shared stylesheet, and the script that settles the course panel before the page paints, rebuilding the course sidebar, the breadcrumb and jump menus, the prev/next footer nav, and the behavior scripts, and leaves the authored `<h1>`, lede, body, and quiz between the header and the footnav untouched.
176
+
177
+ It also deletes the one icon link the authoring skill used to have a session write by hand into a lesson's `<head>`, matched as that exact string, so the page keeps only the icon the `style` region carries. Any other link an author wrote stays.
176
178
 
177
179
  A workspace carrying no `course.css` gets its stylesheet pair seeded first, so the contents page it links is never unstyled, and an existing `course.css` is never overwritten. The embedded copy drops the seed's `@import` line, which resolved against the lesson's own folder and never loaded anything.
178
180
 
@@ -261,8 +261,6 @@ Each entry carries a verdict, since a dropped folder and one the project wrote a
261
261
 
262
262
  Nothing acts on any of them, and the verdict is what makes the list safe to read.
263
263
 
264
- The same field names a proposal-only skill with a live case here under `migrations`, which is how `canon:migration-claude-md` and `canon:migration-context` become reachable. Each entry carries the measurement behind the proposal rather than the proposal alone.
265
-
266
264
  Add `--json` for the machine-readable report, and `--exit-code` to fail a CI job when a target falls behind. Files the project authored itself never count toward that exit code, and neither do superseded artifacts, seed drift, tooling, or anything the reverse walk reports, since each names content the project is expected to edit or place itself. An unmigrated domain does count, because running the relocation closes it.
267
265
 
268
266
  Tooling reports under a section of its own, and `measured` there says whether the target ever recorded a chain. One that never ran a tooling sync reports unmeasured rather than clean, which is what separates tooling nobody has looked at from tooling that is current. A workspace root records nothing either way, since each package resolves its own chain.
@@ -285,17 +285,13 @@ This section is the corpus the coverage claim is measured against: every name `c
285
285
 
286
286
  ### Keep the project current with the toolkit
287
287
 
288
- | Skill | When to use |
289
- | -------------------------------- | -------------------------------------------------------------------------------------- |
290
- | `canon:target-check` | In a target project, to report per domain what it holds against what the toolkit ships |
291
- | `canon:seed-sync` | After a toolkit update, to reconcile installed seeds without losing customizations |
292
- | `canon:migration-claude-md` | When `CLAUDE.md` grew past what always-load context should carry |
293
- | `canon:migration-context` | When `docs/` holds agent-flavored files belonging in `canon/context/` |
294
- | `canon:migration-superseded` | When a drift report names a `.claude/` file a folder has replaced |
295
- | `canon:migration-standards-drop` | When the project still holds an installed `.claude/standards/` tree |
296
- | `canon:canon-feedback` | When something in the toolkit is broken, missing, or off, to open an issue on it |
297
- | `canon:canon-feedback-triage` | In the toolkit repo, to work through the open feedback issues |
298
- | `canon:canon-rollout` | In the toolkit repo, to take one change out to every consuming project at once |
288
+ | Skill | When to use |
289
+ | ----------------------------- | -------------------------------------------------------------------------------------- |
290
+ | `canon:target-check` | In a target project, to report per domain what it holds against what the toolkit ships |
291
+ | `canon:seed-sync` | After a toolkit update, to reconcile installed seeds without losing customizations |
292
+ | `canon:canon-feedback` | When something in the toolkit is broken, missing, or off, to open an issue on it |
293
+ | `canon:canon-feedback-triage` | In the toolkit repo, to work through the open feedback issues |
294
+ | `canon:canon-rollout` | In the toolkit repo, to take one change out to every consuming project at once |
299
295
 
300
296
  ### Generate an artifact on demand
301
297
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.110.0",
4
+ "version": "4.112.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -17,6 +17,7 @@
17
17
  "claude",
18
18
  "docs",
19
19
  "tsconfig.json",
20
+ "assets/brand/mark.svg",
20
21
  "!scripts/sandbox",
21
22
  "!scripts/eval",
22
23
  "!**/*.test.ts",
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env bash
2
+ # Builds the landing page, captures it, and stages the frames over
3
+ # assets/evidence/home, the baseline `canon pr evidence` compares against.
4
+ #
5
+ # Two workflows call it. pr-visual-checks.yml stages the frames into the runner
6
+ # tree to diff them against the baseline, and refresh-capture-frames.yml stages
7
+ # them to commit as the new baseline. Both need the same frames, so one script
8
+ # keeps the two from drifting apart.
9
+ #
10
+ # `tooling/web/configs/e2e/screenshot.ts` is reused as is. This checkout has no
11
+ # web/package.json, so the preview server and the cases run against the
12
+ # root-level scripts rather than through screenshot.sh, which assumes a
13
+ # per-project `bun run build`/`bun run preview` pair.
14
+ set -e
15
+ set -o pipefail
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
19
+
20
+ PORT=4321
21
+ BASE_URL="http://localhost:$PORT"
22
+ BASELINE="$PROJECT_ROOT/assets/evidence/home"
23
+
24
+ cd "$PROJECT_ROOT"
25
+
26
+ bun run web:build
27
+
28
+ bun run web:preview &
29
+ PREVIEW_PID=$!
30
+ trap 'kill "$PREVIEW_PID" 2>/dev/null || true' EXIT
31
+
32
+ for _ in $(seq 1 40); do
33
+ curl -sSf "$BASE_URL/" >/dev/null 2>&1 && break
34
+ sleep 0.25
35
+ done
36
+ curl -sSf "$BASE_URL/" >/dev/null 2>&1 || {
37
+ echo "Preview server not responding on port $PORT after 10s." >&2
38
+ exit 1
39
+ }
40
+
41
+ (cd web && SCREENSHOT_BASE_URL="$BASE_URL" bun ../tooling/web/configs/e2e/screenshot.ts)
42
+
43
+ # The committed tree is cleared first rather than copied over. A copy only adds
44
+ # and overwrites, so a section dropped from the case list would leave its
45
+ # frames in place and a diff would see no change. Clearing first shows that
46
+ # section as a deletion.
47
+ rm -rf "$BASELINE"
48
+ mkdir -p "$BASELINE"
49
+ cp -R web/screenshots/localhost/home/. "$BASELINE/"
50
+
51
+ # The sweep holds all four widths and this repository commits two, 320 and
52
+ # 1280. The case list is the place to change if that set moves.
53
+ find "$BASELINE" \( -name '768--*.png' -o -name '1536--*.png' \) -delete
54
+
55
+ # The shared golden config names the case that emulates nothing `default`, and
56
+ # this repository's baseline names that frame `light`.
57
+ find "$BASELINE" -name '*--default.png' -execdir sh -c 'mv "$1" "${1%--default.png}--light.png"' _ {} \;
58
+
59
+ echo "capture-home: staged frames in assets/evidence/home"
@@ -1,9 +1,8 @@
1
1
  import type { SkillCase } from '@/claude/skills-rank'
2
2
 
3
3
  /**
4
- * `target-setup`, `migration-*`, `canon-*`, and `create-rule`: scaffolding,
5
- * proposal-only migrations, and the toolkit's own reference and feedback
6
- * surfaces.
4
+ * `target-setup`, `canon-*`, and `create-rule`: scaffolding and the
5
+ * toolkit's own reference and feedback surfaces.
7
6
  *
8
7
  * The five `target-setup` cases are one skill read through five entry phrasings
9
8
  * rather than five rows that collapsed to a duplicate. Each names a different
@@ -36,26 +35,6 @@ export const SETUP_CASES: readonly SkillCase[] = [
36
35
  'Check that the dev server actually starts and the end-to-end suite passes against the scaffold.',
37
36
  expect: 'target-setup',
38
37
  },
39
- {
40
- prompt:
41
- 'This CLAUDE.md file has grown huge, break it apart into the tiered context model.',
42
- expect: 'migration-claude-md',
43
- },
44
- {
45
- prompt:
46
- 'Move the agent-flavored docs out of the docs folder and into context.',
47
- expect: 'migration-context',
48
- },
49
- {
50
- prompt:
51
- 'This file was replaced by a folder, help me split its content into it.',
52
- expect: 'migration-superseded',
53
- },
54
- {
55
- prompt:
56
- 'This project still carries a copy of the standards folder, get rid of it and point everything at the CLI.',
57
- expect: 'migration-standards-drop',
58
- },
59
38
  {
60
39
  prompt:
61
40
  "Before I run this sync, tell me exactly what it's going to overwrite.",
@@ -189,7 +189,6 @@ function renderCheck(report: CheckReport): void {
189
189
  }
190
190
 
191
191
  renderUnclaimed(report)
192
- renderMigrations(report)
193
192
 
194
193
  outro()
195
194
  // Scanned domains only. Tooling renders a section on every managed target, so
@@ -334,21 +333,6 @@ function describeUnclaimed(entry: UnclaimedFolder): string {
334
333
  return `${counted}, content published at ${entry.since.slice(0, 7)}`
335
334
  }
336
335
 
337
- /**
338
- * The proposal-only skills no other field reaches.
339
- */
340
- function renderMigrations(report: CheckReport): void {
341
- const { migrations } = report.reverse
342
- if (migrations.length === 0) return
343
-
344
- logStep('Migrations with a case here')
345
-
346
- for (const candidate of migrations) {
347
- logWarn(candidate.reason)
348
- logInfo(`Run /canon:${candidate.skill} for a proposal.`)
349
- }
350
- }
351
-
352
336
  async function runSync(target: string): Promise<number> {
353
337
  intro('canon sync')
354
338