@erclx/canon 5.69.0 → 5.70.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 (53) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +33 -159
  3. package/assets/brand/mark.svg +17 -9
  4. package/claude/.claude-plugin/plugin.json +1 -1
  5. package/claude/skills/app-drive/SKILL.md +3 -2
  6. package/claude/skills/app-drive/references/drive-record.md +52 -10
  7. package/claude/skills/app-drive/references/isolate.md +1 -1
  8. package/claude/skills/design-pick/SKILL.md +1 -0
  9. package/claude/skills/ui-craft/references/copy.md +4 -1
  10. package/claude/skills/ui-film/REQUIREMENT.md +4 -2
  11. package/claude/skills/ui-film/SKILL.md +24 -11
  12. package/claude/skills/ui-film/references/compose-craft.md +81 -0
  13. package/claude/skills/ui-film/references/config.md +6 -1
  14. package/claude/skills/ui-film/references/handoff.md +13 -3
  15. package/claude/skills/ui-film/references/take-craft.md +27 -1
  16. package/docs/index.md +1 -0
  17. package/docs/migrations/by-hand.md +107 -0
  18. package/docs/migrations/by-verb.md +118 -0
  19. package/docs/migrations/index.md +14 -0
  20. package/docs/target/index.md +1 -2
  21. package/docs/target/projects.md +2 -2
  22. package/docs/target/sync.md +22 -2
  23. package/docs/workflow/ai-workflow.md +1 -1
  24. package/governance/ledger/skills/ui-craft/copy.md +8 -4
  25. package/package.json +3 -1
  26. package/scripts/core/design-sync.ts +204 -0
  27. package/scripts/core/regen-web-favicon.ts +27 -12
  28. package/scripts/core/regen-web-previews.ts +8 -1
  29. package/scripts/core/render-film.ts +179 -0
  30. package/src/capture/frame-values.ts +12 -12
  31. package/src/commands/records.ts +3 -2
  32. package/src/commands/sandbox.ts +9 -2
  33. package/src/commands/take.ts +159 -11
  34. package/src/design/favicon.ts +85 -13
  35. package/src/design/reference.ts +98 -0
  36. package/src/design/render.ts +16 -14
  37. package/src/design/tokens.ts +4 -3
  38. package/src/gate/stages.ts +6 -0
  39. package/src/init/steps.ts +2 -2
  40. package/src/migrate/status.ts +3 -3
  41. package/src/records/validate.ts +14 -2
  42. package/src/sandbox/expect.ts +32 -0
  43. package/src/sandbox/headless/run.ts +6 -3
  44. package/src/take/camera.ts +99 -10
  45. package/src/take/film.ts +35 -2
  46. package/src/take/fixtures/input.html +138 -0
  47. package/src/take/keys.ts +193 -0
  48. package/src/take/log.ts +94 -1
  49. package/src/take/perform.ts +165 -10
  50. package/src/take/plan.ts +101 -6
  51. package/src/take/screencast.ts +28 -3
  52. package/src/take/stepper.ts +122 -18
  53. package/docs/target/migrations.md +0 -205
@@ -1,6 +1,13 @@
1
1
  import { openFrameEncoder } from '@/take/encode'
2
+ import { describeKey, modifierBits } from '@/take/keys'
2
3
  import { FPS, type LogEvent, START, VIEWPORT } from '@/take/log'
3
- import { type PointerInput, type Stage, performSteps } from '@/take/perform'
4
+ import {
5
+ type KeyInput,
6
+ type PointerInput,
7
+ type Stage,
8
+ TargetMissing,
9
+ performSteps,
10
+ } from '@/take/perform'
4
11
  import type { Beat } from '@/take/plan'
5
12
  import { type ShellPage, openShell } from '@/take/shell'
6
13
 
@@ -36,6 +43,9 @@ const LOAD_CAP_MS = 15_000
36
43
  const SETTLE_MS = 2500
37
44
  const LOADING_FRAME_CAP_MS = 250
38
45
 
46
+ /** How long a refusal waits on the frame it shows, since the page may be wedged. */
47
+ const STILL_CAP_MS = 2000
48
+
39
49
  /** A frame that took longer than `STALL_MS`, so the beat is filmed another way. */
40
50
  export class StepperStalled extends Error {
41
51
  constructor(
@@ -68,6 +78,7 @@ export async function filmStepped(
68
78
  const page = await openShell(request.executable, VIEWPORT)
69
79
  const encoder = openFrameEncoder(request.clip, FPS)
70
80
  const stallMs = request.stallMs ?? STALL_MS
81
+ let still: (() => Promise<Buffer | undefined>) | undefined
71
82
  try {
72
83
  let ticks = await load(page, request.url)
73
84
  const beginFrame = (screenshot: boolean) =>
@@ -81,6 +92,7 @@ export async function filmStepped(
81
92
  : {}),
82
93
  },
83
94
  )
95
+ still = () => frameStill(beginFrame)
84
96
  await pump(beginFrame, SETTLE_MS)
85
97
 
86
98
  const live = liveStage(page, beginFrame, stallMs)
@@ -128,16 +140,14 @@ export async function filmStepped(
128
140
  hold: async (ms) => {
129
141
  for (let i = 0; i < Math.round(ms / FRAME_MS); i += 1) await frame()
130
142
  },
131
- pointer: async (input) => {
132
- const sent = page.send('Input.dispatchMouseEvent', mouseEvent(input))
133
- await frame()
134
- await sent
135
- },
136
- key: async (text) => {
137
- const sent = page.send('Input.insertText', { text })
138
- await frame()
139
- await sent
140
- },
143
+ pointer: (input) =>
144
+ stepThen(
145
+ page.send('Input.dispatchMouseEvent', mouseEvent(input)),
146
+ frame,
147
+ ),
148
+ key: (text) => stepThen(page.send('Input.insertText', { text }), frame),
149
+ press: (input) =>
150
+ stepThen(page.send('Input.dispatchKeyEvent', keyEvent(input)), frame),
141
151
  evaluate: (expression) => page.evaluate(expression),
142
152
  }
143
153
 
@@ -153,12 +163,31 @@ export async function filmStepped(
153
163
  return { frames, events, still: last }
154
164
  } catch (error) {
155
165
  encoder.abort()
166
+ if (error instanceof TargetMissing && still) error.still = await still()
156
167
  throw error
157
168
  } finally {
158
169
  page.close()
159
170
  }
160
171
  }
161
172
 
173
+ /**
174
+ * Begins one frame with a screenshot for a refusal to show, through the same
175
+ * begin-frame call every kept frame uses, since a separate capture call can
176
+ * wait on a frame that never begins. Best effort, so a page that answers with
177
+ * nothing leaves the refusal standing without a still.
178
+ */
179
+ async function frameStill(
180
+ beginFrame: (screenshot: boolean) => Promise<{ screenshotData?: string }>,
181
+ ): Promise<Buffer | undefined> {
182
+ const reply = await Promise.race([
183
+ beginFrame(true).catch(() => undefined),
184
+ sleep(STILL_CAP_MS),
185
+ ])
186
+ return reply?.screenshotData
187
+ ? Buffer.from(reply.screenshotData, 'base64')
188
+ : undefined
189
+ }
190
+
162
191
  /**
163
192
  * Navigates and resolves with Chromium's monotonic clock in milliseconds, read
164
193
  * off the load event's own timestamp. The runtime's clock cannot stand in for
@@ -217,7 +246,7 @@ function liveStage(
217
246
  pointer: (input) => {
218
247
  const sent = page.send('Input.dispatchMouseEvent', mouseEvent(input))
219
248
  return withinStall(
220
- beginFrame(false).then(() => sent),
249
+ Promise.all([beginFrame(false), sent]),
221
250
  0,
222
251
  stallMs,
223
252
  ).then(() => {})
@@ -225,7 +254,15 @@ function liveStage(
225
254
  key: (text) => {
226
255
  const sent = page.send('Input.insertText', { text })
227
256
  return withinStall(
228
- beginFrame(false).then(() => sent),
257
+ Promise.all([beginFrame(false), sent]),
258
+ 0,
259
+ stallMs,
260
+ ).then(() => {})
261
+ },
262
+ press: (input) => {
263
+ const sent = page.send('Input.dispatchKeyEvent', keyEvent(input))
264
+ return withinStall(
265
+ Promise.all([beginFrame(false), sent]),
229
266
  0,
230
267
  stallMs,
231
268
  ).then(() => {})
@@ -234,18 +271,83 @@ function liveStage(
234
271
  }
235
272
  }
236
273
 
274
+ /**
275
+ * A pointer input as CDP takes it. `buttons` carries the held button through a
276
+ * drag's moves, or the page reads them as a hover, and `modifiers` carries the
277
+ * held keys as a bit field beside the key events that pressed them.
278
+ */
237
279
  function mouseEvent(input: PointerInput): Record<string, unknown> {
238
280
  const at = { x: input.x, y: input.y }
281
+ if (input.type === 'wheel') {
282
+ return { type: 'mouseWheel', ...at, deltaX: 0, deltaY: input.dy }
283
+ }
284
+ const modifiers = modifierBits(input.modifiers ?? [])
239
285
  switch (input.type) {
240
286
  case 'move':
241
- return { type: 'mouseMoved', ...at, button: 'none' }
287
+ return input.held
288
+ ? { type: 'mouseMoved', ...at, button: 'left', buttons: 1, modifiers }
289
+ : { type: 'mouseMoved', ...at, button: 'none', buttons: 0, modifiers }
242
290
  case 'down':
243
- return { type: 'mousePressed', ...at, button: 'left', clickCount: 1 }
244
291
  case 'up':
245
- return { type: 'mouseReleased', ...at, button: 'left', clickCount: 1 }
246
- case 'wheel':
247
- return { type: 'mouseWheel', ...at, deltaX: 0, deltaY: input.dy }
292
+ return {
293
+ type: input.type === 'down' ? 'mousePressed' : 'mouseReleased',
294
+ ...at,
295
+ button: 'left',
296
+ buttons: input.type === 'down' ? 1 : 0,
297
+ clickCount: input.clickCount,
298
+ modifiers,
299
+ }
300
+ }
301
+ }
302
+
303
+ /**
304
+ * A key input as CDP takes it. A key that types sends `keyDown` with its text
305
+ * only when no modifier but Shift is held, as Chromium's own keyboard does, so
306
+ * `Control+K` in a focused field inserts no K.
307
+ */
308
+ function keyEvent(input: KeyInput): Record<string, unknown> {
309
+ const descriptor = describeKey(input.key, {
310
+ shift: input.modifiers.includes('Shift'),
311
+ })
312
+ if (!descriptor) throw new Error(`no key descriptor for ${input.key}`)
313
+ const event = {
314
+ key: descriptor.key,
315
+ code: descriptor.code,
316
+ windowsVirtualKeyCode: descriptor.keyCode,
317
+ nativeVirtualKeyCode: descriptor.keyCode,
318
+ location: descriptor.location,
319
+ modifiers: modifierBits(input.modifiers),
320
+ }
321
+ if (input.type === 'up') return { type: 'keyUp', ...event }
322
+ const types =
323
+ descriptor.text !== undefined &&
324
+ input.modifiers.every((modifier) => modifier === 'Shift')
325
+ return types
326
+ ? {
327
+ type: 'keyDown',
328
+ ...event,
329
+ text: descriptor.text,
330
+ unmodifiedText: descriptor.text,
331
+ }
332
+ : { type: 'rawKeyDown', ...event }
333
+ }
334
+
335
+ /**
336
+ * Steps a frame, then awaits the input sent ahead of it. When the step fails,
337
+ * the input is left settling against a shell about to close, so its rejection
338
+ * is handled here rather than surfacing as an unhandled one.
339
+ */
340
+ async function stepThen(
341
+ sent: Promise<unknown>,
342
+ step: () => Promise<void>,
343
+ ): Promise<void> {
344
+ try {
345
+ await step()
346
+ } catch (error) {
347
+ sent.catch(() => {})
348
+ throw error
248
349
  }
350
+ await sent
249
351
  }
250
352
 
251
353
  async function withinStall<T>(
@@ -257,6 +359,8 @@ async function withinStall<T>(
257
359
  const stalled = new Promise<never>((_, fail) => {
258
360
  timer = setTimeout(() => fail(new StepperStalled(frame, stallMs)), stallMs)
259
361
  })
362
+ // A stall leaves the work pending until the shell closes and rejects it.
363
+ work.catch(() => {})
260
364
  try {
261
365
  return await Promise.race([work, stalled])
262
366
  } finally {
@@ -1,205 +0,0 @@
1
- ---
2
- title: Target migrations
3
- description: One-time moves a project scaffolded under an older toolkit runs before its next sync
4
- category: Agent surface
5
- ---
6
-
7
- # Target migrations
8
-
9
- Each section is a move a project runs once, when it was scaffolded before the change it names. Run `canon upgrade` first, then `canon migrate status --json` to list the moves a project still needs in the order to run them. The recurring sync that follows is in [target sync](sync.md).
10
-
11
- `canon:target-update` is the guided path through both. It runs the moves in that order once you approve its preview.
12
-
13
- ## Move the records first, once
14
-
15
- Gitignored session records, being the task board, plans, review reports, and scratch, moved from `.claude/` to `.canon/`. Run these from inside the project, and read the `ok` field of the `--json` record rather than the exit code.
16
-
17
- ```bash
18
- canon tooling sync claude . --write
19
- canon migrate records --json
20
- canon migrate records --write --json
21
- ```
22
-
23
- The first line takes the `.canon/` ignore entry. The second reports and the third applies, repointing files that cite a moved folder and deleting a `.gitignore` line `.canon/` covers, as `dropped`. A line naming an old path keeps it via `canon-keep-record-root` on that line or the nearest non-blank one above.
24
-
25
- Then sweep the citations inside the records, which the verb above never reaches.
26
-
27
- ```bash
28
- canon migrate record-tree --json
29
- canon migrate record-tree --write --json
30
- ```
31
-
32
- It covers `diagrams`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`. Read the report before `--write`. The record tree is untracked, and a wrong rewrite has no undo.
33
-
34
- ## Back records up off this machine, once
35
-
36
- Run `canon records push` once a private repository exists. It prints the one-time setup command when it finds none, and `canon init` prints the same reminder.
37
-
38
- ## Move rules off the flat layout, once
39
-
40
- Installed rules moved from `.claude/rules/<subdir>/` to `.claude/rules/canon/<subdir>/`. On the flat layout `canon gov sync` exits 0 reporting `No governance surfaces found in target`, and `canon gov install` lands a second copy.
41
-
42
- ```bash
43
- canon migrate rule-layout --json
44
- canon migrate rule-layout --write --json
45
- ```
46
-
47
- Run it once, ahead of `canon gov sync` or `canon gov install`.
48
-
49
- ## Rename the skill citations, once
50
-
51
- Plugin skills dropped their `claude-` prefix, so `canon:claude-docs` is now `canon:context-fold`. Five drafters merged into `write-doc`, seven `draft-` skills took verb-first names, and four joined their family. No old name keeps a pointer, and `canon migrate skill-names --write` moves a project. <!-- canon-keep-retired -->
52
-
53
- | Old name | New name |
54
- | ------------------ | ----------------------------------------------- |
55
- | `draft-doc` | `write-doc` <!-- canon-keep-retired --> |
56
- | `draft-figure` | `draw-figure` <!-- canon-keep-retired --> |
57
- | `draft-identity` | `design-identity` <!-- canon-keep-retired --> |
58
- | `draft-slides` | `make-deck` <!-- canon-keep-retired --> |
59
- | `draft-and-pick` | `design-pick` <!-- canon-keep-retired --> |
60
- | `draft-ready` | `plan-ready` <!-- canon-keep-retired --> |
61
- | `draft-screencast` | `script-screencast` <!-- canon-keep-retired --> |
62
- | `pick-by-eye` | `design-pick` <!-- canon-keep-retired --> |
63
- | `ui-record` | `ui-film` <!-- canon-keep-retired --> |
64
- | `ux-audit` | `ui-audit` <!-- canon-keep-retired --> |
65
- | `ux-measure` | `ui-measure` <!-- canon-keep-retired --> |
66
-
67
- Replace a citation of a skill with no successor by hand, or delete the sentence.
68
-
69
- - `canvas` is now `canvas-board`. The sweep rewrites only `canon:canvas` and `skills/canvas`, so replace a bare mention by hand. <!-- canon-keep-retired -->
70
- - `draft-ready` is rewritten only as `canon:draft-ready` and `skills/draft-ready`, since bare it also describes a pull request, so replace a bare mention by hand. <!-- canon-keep-retired -->
71
- - `design-extract` and `sketch-design` are replaced by `canon:canvas-board`. <!-- canon-keep-retired -->
72
- - `review-branch`, `seed-sync`, `restate-plainly`, `bash-cli-script`, `canon-operator`, `canon-rollout`, and `canon-feedback-triage` have no successor <!-- canon-keep-retired -->
73
- - `create-standard`, `markdown-propose`, `memory-review`, `project-commands`, `repo-metadata`, `ux-walkthrough`, and `plan-ready` have no successor <!-- canon-keep-retired -->
74
-
75
- The `memory` record kind left `canon records`, so `canon records validate memory` refuses as an unknown kind and `canon records migrate` is gone.
76
-
77
- - `canon repo metadata`, `canon autoship`, and the `120-bash` rule are gone, and `canon gov sync` removes the rule <!-- canon-keep-retired -->
78
- - `memory-capture`, the `603-memory` rule, and the `memory` standard are gone, so send a lesson to the context entry that owns the fact <!-- canon-keep-retired -->
79
-
80
- Two held files run, `.husky/post-merge` and `.claude/hooks/pr-create-log.sh`, and a stale copy calls a missing binary or names a removed skill. Resync what the toolkit owns before sweeping what the project wrote, and run the second line once per stack, naming a held hook with `--replace`.
81
-
82
- ```bash
83
- canon gov sync
84
- canon tooling sync <stack> . --write
85
- canon migrate skill-names --json
86
- canon migrate skill-names --write --json
87
- ```
88
-
89
- ## Rename the pull request template headings, once
90
-
91
- The template headings are now `## Key changes` and `## Technical context`, and `canon labels scan` compares case exactly. Rename them in `.github/pull_request_template.md`, or delete it and run `canon tooling sync base . --write`.
92
-
93
- ## Move the surfaces off `.claude/`, once
94
-
95
- `context/`, `wireframes/`, `ARCHITECTURE.md`, `REQUIREMENTS.md`, `DESIGN.md`, and the install stamp folder, now `canon/config/`, moved under `canon/`. `rules/`, `skills/`, `hooks/`, and `settings.json` stay where Claude Code reads them.
96
-
97
- ```bash
98
- canon migrate surface-roots --json
99
- canon migrate surface-roots --write --json
100
- ```
101
-
102
- It moves each surface with `git mv` and repoints every tracked citation. No other command notices a surface left at `.claude/`.
103
-
104
- ## Delete the retired snippets, once
105
-
106
- A project scaffolded while snippets installed may hold `.claude/snippets/` or a root `snippets/`.
107
-
108
- ```bash
109
- canon sync --check . --json | jq '.reverse'
110
- ```
111
-
112
- - `dropped` is the toolkit's copy, unchanged, so delete it
113
- - `unattributed` is often an edited copy, so read what changed first
114
- - `project` is the project's own content, so leave it
115
-
116
- From the registry the walk reports `historyUnavailable`, so check both paths by hand.
117
-
118
- ## Stop calling the standards verb, once
119
-
120
- `canon standards <name>` is gone, and a call now fails with a too-many-arguments error. Replace each call with the skill that owns the standard, at `references/<name>.md`. <!-- canon-allow-reference: verb retired -->
121
-
122
- | Skill | Standards |
123
- | ----------------- | ------------------------------------------------------------- |
124
- | `task-board` | `board`, `tasks`, `tasks-decline` |
125
- | `context-fold` | `context` |
126
- | `canvas-board` | `canvas`, `design` <!-- canon-keep-retired --> |
127
- | `docs-sync` | `docs`, `readme` |
128
- | `draw-figure` | `figures`, `mermaid` |
129
- | `create-rule` | `rule` |
130
- | `create-skill` | `skill`, `skill-paths`, `skill-practice`, `skill-requirement` |
131
- | `git-branch` | `branch`, `slug` |
132
- | `git-commit` | `commit`, `versioning` |
133
- | `git-pr` | `pr`, `publish` |
134
- | `markdown-craft` | `markdown` |
135
- | `teach-workspace` | `glossary`, `teach` <!-- canon-keep-retired --> |
136
- | `plan-groundwork` | `groundwork`, `groundwork-spikes` |
137
- | `plan-intake` | `intake` |
138
- | `plan-feature` | `plan`, `plan-lifecycle` |
139
- | `plan-ready` | `ready` <!-- canon-keep-retired --> |
140
- | `write-doc` | `requirements`, `wiki` |
141
- | `session-map` | `session` |
142
-
143
- The `standard` name became `skill-reference.md` in `create-skill`.
144
-
145
- ## Keep or delete the wireframes folder, your call
146
-
147
- The wireframes record is retired, and nothing deletes what a project holds.
148
-
149
- - `canon/wireframes/` stays yours and unaudited. Keep it as notes or delete it.
150
- - Delete the `@canon/wireframes/index.md` line in your `CLAUDE.md` with the folder. `canon claude sync` does not rewrite it.
151
- - `canon gov sync` removes `.claude/rules/canon/standards/wireframes.md`
152
-
153
- ## Keep or delete the retired product files, your call
154
-
155
- The learning workspace, deck renderer, app recorder, canvas, and design install are retired with their verbs, and nothing deletes what a project holds.
156
-
157
- - `.canon/teach/` holds HTML that opens as it is <!-- canon-keep-retired -->
158
- - `.canon/slides/` and a `demos/` folder hold sources nothing renders any more, so delete them once nothing cites them <!-- canon-keep-retired -->
159
- - Move the `.claude/design/project/` values, `fonts/`, and `fonts.css` to the stylesheet your `canon/DESIGN.md` lead names, then delete `.claude/design/`
160
-
161
- A reference to a retired skill takes a hand edit.
162
-
163
- - Replace a `canon:teach-workspace` reference with `canon:teach-craft` by hand, since `canon migrate skill-names` does not rewrite it <!-- canon-keep-retired -->
164
- - Delete a `canon:make-deck`, `canon:script-screencast`, `canon:record-screencast`, `canon:read-frames`, or `canon:video-craft` reference, since none has a successor <!-- canon-keep-retired -->
165
- - Delete a `canon:canvas-board` reference too. Frameboard, which `canon:design-pick` drives, reads each `.canon/canvas/` folder as a project, so move a kept page under one <!-- canon-keep-retired -->
166
-
167
- ## Move decisions out of a held architecture record, once
168
-
169
- The toolkit no longer seeds `canon/ARCHITECTURE.md`, and a held file and its `CLAUDE.md` import stay. Move a trap a session would hit into that domain's `## Gotchas`, then delete the file and its `@canon/ARCHITECTURE.md` line.
170
-
171
- ## Deny subagents and merges, and turn auto memory off, once
172
-
173
- The seeded `.claude/settings.json` now denies the `Agent` and `Task` tools and both merge commands, and sets `autoMemoryEnabled` to `false`. `canon claude sync` never rewrites a settings file you hold, so it warns while the file holds no default or lacks a merge entry. Removing `Agent` and `Task` later draws no warning.
174
-
175
- ```bash
176
- canon migrate session-defaults --json
177
- canon migrate session-defaults --write --json
178
- ```
179
-
180
- The verb only adds a key that is absent. A person who wants auto memory back sets it in their own `.claude/settings.local.json`.
181
-
182
- The verb adds `Bash(canon pr merge:*)` and `Bash(gh pr merge:*)` so a session cannot merge. A project migrated before them runs `--write` once more, which also restores `Agent` and `Task` for a person who removed them to allow subagents, so remove them again after.
183
-
184
- ## Move the capture cases out of the harness, once
185
-
186
- A web project's `e2e/screenshot.ts` is a config. It now loads `CASES` from a seeded `e2e/capture/cases.ts` that sync copies once and never overwrites. A harness you edited is held on `--write`, so the new one lands only once you pass `--replace e2e/screenshot.ts`.
187
-
188
- - Copy your cases from the old `e2e/screenshot.ts` into `e2e/capture/cases.ts`, then sync with `--replace e2e/screenshot.ts`.
189
- - Evidence now defaults to `e2e/evidence/`. Set `export const EVIDENCE_ROOT = 'evidence'` in `e2e/capture/cases.ts` to keep a committed baseline at the old path.
190
- - A run from a folder with no `e2e/capture/cases.ts` exits 1 and names the file.
191
-
192
- ## Fold the ui rules onto one pointer, once
193
-
194
- Eleven ui rules moved unchanged into the `ui-craft` skill, being `a11y`, `keyboard`, `pointer`, `forms`, `motion`, `ux-controls`, `ux-completeness`, `surface-capture`, `copy`, `interface-casing`, and `link-behavior`. Update the plugin, then sync, which folds each one you hold and `design-taste.md` onto one `craft.md` pointer without local edits.
195
-
196
- ```bash
197
- canon gov sync .
198
- ```
199
-
200
- ## Group a long paths section in `CLAUDE.md`, once
201
-
202
- The `## Key paths` section of `CLAUDE.md` now keeps to 5 bullets in a row, and every other section stays exempt. `canon claude sync` never rewrites a `CLAUDE.md` you hold, so it warns with the line of each longer run.
203
-
204
- - Regroup the section by hand under H3s, each opening on a one-sentence lead.
205
- - List `CLAUDE.md` in `canon/config/markdown.toml` to gate it from then on.