@erclx/canon 5.69.0 → 5.71.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 (67) hide show
  1. package/CHANGELOG.md +25 -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/git-pr/references/task-number.md +13 -0
  10. package/claude/skills/markdown-craft/references/markdown.md +1 -0
  11. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +1 -0
  12. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +6 -20
  13. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +10 -0
  14. package/claude/skills/role-orchestrator/references/orchestrator-talk-launch.md +30 -0
  15. package/claude/skills/task-board/references/tasks-record-lines.md +40 -0
  16. package/claude/skills/task-board/references/tasks.md +4 -8
  17. package/claude/skills/ui-craft/references/copy.md +4 -1
  18. package/claude/skills/ui-film/REQUIREMENT.md +4 -2
  19. package/claude/skills/ui-film/SKILL.md +24 -11
  20. package/claude/skills/ui-film/references/compose-craft.md +81 -0
  21. package/claude/skills/ui-film/references/config.md +6 -1
  22. package/claude/skills/ui-film/references/handoff.md +13 -3
  23. package/claude/skills/ui-film/references/take-craft.md +27 -1
  24. package/docs/index.md +1 -0
  25. package/docs/migrations/by-hand.md +107 -0
  26. package/docs/migrations/by-verb.md +118 -0
  27. package/docs/migrations/index.md +14 -0
  28. package/docs/target/index.md +1 -2
  29. package/docs/target/projects.md +2 -2
  30. package/docs/target/sync.md +22 -2
  31. package/docs/workflow/ai-workflow.md +1 -1
  32. package/governance/ledger/skills/ui-craft/copy.md +8 -4
  33. package/package.json +3 -1
  34. package/scripts/core/design-sync.ts +204 -0
  35. package/scripts/core/regen-web-favicon.ts +27 -12
  36. package/scripts/core/regen-web-previews.ts +8 -1
  37. package/scripts/core/render-film.ts +179 -0
  38. package/src/capture/frame-values.ts +12 -12
  39. package/src/commands/markdown.ts +6 -1
  40. package/src/commands/records.ts +3 -2
  41. package/src/commands/sandbox.ts +9 -2
  42. package/src/commands/take.ts +159 -11
  43. package/src/commands/tasks.ts +133 -0
  44. package/src/design/favicon.ts +85 -13
  45. package/src/design/reference.ts +98 -0
  46. package/src/design/render.ts +16 -14
  47. package/src/design/tokens.ts +4 -3
  48. package/src/gate/stages.ts +6 -0
  49. package/src/init/steps.ts +2 -2
  50. package/src/markdown/colons.ts +102 -16
  51. package/src/migrate/status.ts +3 -3
  52. package/src/records/validate.ts +14 -2
  53. package/src/sandbox/expect.ts +32 -0
  54. package/src/sandbox/headless/run.ts +6 -3
  55. package/src/take/camera.ts +99 -10
  56. package/src/take/film.ts +35 -2
  57. package/src/take/fixtures/input.html +138 -0
  58. package/src/take/keys.ts +193 -0
  59. package/src/take/log.ts +94 -1
  60. package/src/take/perform.ts +165 -10
  61. package/src/take/plan.ts +101 -6
  62. package/src/take/screencast.ts +28 -3
  63. package/src/take/stepper.ts +122 -18
  64. package/src/tasks/archive.ts +54 -1
  65. package/src/tasks/record.ts +118 -0
  66. package/src/tasks/validate.ts +37 -0
  67. package/docs/target/migrations.md +0 -205
@@ -15,6 +15,9 @@ import {
15
15
  readPendingBranches,
16
16
  readPlanTargets,
17
17
  readPullRequest,
18
+ readSessions,
19
+ SESSION_ID_PATTERN,
20
+ SESSIONS_PREFIX,
18
21
  tasksDir,
19
22
  } from '@/tasks/archive'
20
23
  import { checkWaiting, type LinkPromotion, promoteOnLink } from '@/tasks/board'
@@ -34,6 +37,9 @@ const ORIGIN_PREFIXES = ['Plan:', 'Groundwork:', 'Intake:', 'Issue:'] as const
34
37
  *
35
38
  * `several-plans` answers a `Plan:` line that already links more than one plan.
36
39
  * `plan-link` cannot know which link to drop, so it writes nothing.
40
+ *
41
+ * `no-name` answers a session id the roster no longer lists, called with no
42
+ * name, since an id written alone reads as nobody to the next reader.
37
43
  */
38
44
  export const RECORD_REFUSALS = [
39
45
  'no-board',
@@ -44,6 +50,7 @@ export const RECORD_REFUSALS = [
44
50
  'no-plan',
45
51
  'several-plans',
46
52
  'bad-input',
53
+ 'no-name',
47
54
  ] as const
48
55
 
49
56
  export type RecordRefusal = (typeof RECORD_REFUSALS)[number]
@@ -88,6 +95,18 @@ export interface PlanRecorded {
88
95
 
89
96
  export type PlanOutcome = PlanRecorded | RecordRefused
90
97
 
98
+ export interface SessionRecorded {
99
+ readonly ok: true
100
+ readonly stem: string
101
+ readonly path: string
102
+ readonly sessionId: string
103
+ /** The name the line carries for the id, which a repeat call never renames. */
104
+ readonly name: string
105
+ readonly action: Exclude<LineAction, 'corrected'>
106
+ }
107
+
108
+ export type SessionOutcome = SessionRecorded | RecordRefused
109
+
91
110
  export interface OutcomesClosed {
92
111
  readonly ok: true
93
112
  readonly stem: string
@@ -228,6 +247,47 @@ export function clearPendingBranch(
228
247
  return { text: lines.join('\n'), changed: true }
229
248
  }
230
249
 
250
+ /**
251
+ * Names a session on the `Sessions:` line, keyed on the id, so a dispatcher
252
+ * and `git-pr` recording the same worker write it once. The line sits below
253
+ * every other line a task carries above its outcomes, so a pull request
254
+ * recorded later lands above it.
255
+ */
256
+ export function writeSessionsLine(
257
+ text: string,
258
+ name: string,
259
+ sessionId: string,
260
+ ): { readonly text: string; readonly action: SessionRecorded['action'] } {
261
+ const entry = `${name} (${sessionId})`
262
+ const lines = text.split('\n')
263
+ const existing = lines.findIndex((line) => line.startsWith(SESSIONS_PREFIX))
264
+
265
+ if (existing !== -1) {
266
+ if (lines[existing].includes(`(${sessionId})`)) {
267
+ return { text, action: 'unchanged' }
268
+ }
269
+ lines[existing] = `${lines[existing].trimEnd()}, ${entry}`
270
+ return { text: lines.join('\n'), action: 'appended' }
271
+ }
272
+
273
+ const line = `${SESSIONS_PREFIX} ${entry}`
274
+ const anchor = lastOriginLine(lines, [
275
+ ...ORIGIN_PREFIXES,
276
+ 'Pull request:',
277
+ PENDING_PREFIX,
278
+ ])
279
+ if (anchor !== undefined) {
280
+ lines.splice(anchor + 1, 0, line)
281
+ return { text: lines.join('\n'), action: 'added' }
282
+ }
283
+
284
+ const heading = lines.findIndex((entry) => entry.startsWith('# '))
285
+ if (heading === -1) return { text: `${line}\n${text}`, action: 'added' }
286
+
287
+ lines.splice(heading + 1, 0, '', line)
288
+ return { text: lines.join('\n'), action: 'added' }
289
+ }
290
+
231
291
  export type PlanLineWrite =
232
292
  | {
233
293
  readonly ok: true
@@ -566,6 +626,64 @@ export async function recordPlan(
566
626
  }
567
627
  }
568
628
 
629
+ /**
630
+ * Records a session that worked a task on its `Sessions:` line. The caller
631
+ * resolves `name` from the roster or takes it from `--name`, and an undefined
632
+ * name refuses rather than writing a bare id. The id is checked first, since a
633
+ * malformed one is a caller defect and every later check would hide it.
634
+ */
635
+ export async function recordSession(
636
+ root: string,
637
+ selector: RecordSelector,
638
+ sessionId: string,
639
+ name: string | undefined,
640
+ ): Promise<SessionOutcome> {
641
+ if (!SESSION_ID_PATTERN.test(sessionId)) {
642
+ return refuse(
643
+ 'bad-input',
644
+ `Not a full sessionId: ${sessionId}. Pass the whole id canon sessions list --json carries.`,
645
+ [sessionId],
646
+ )
647
+ }
648
+
649
+ if (name === undefined) {
650
+ return refuse(
651
+ 'no-name',
652
+ `No live session carries ${sessionId}, so its name cannot be read. Pass --name.`,
653
+ [sessionId],
654
+ )
655
+ }
656
+
657
+ const trimmed = name.trim()
658
+ if (trimmed.length === 0 || /[\r\n]/.test(trimmed)) {
659
+ return refuse('bad-input', 'A session name is one non-empty line.', [name])
660
+ }
661
+
662
+ const opened = await openTask(root, selector)
663
+ if ('ok' in opened) return opened
664
+
665
+ const { stem, path } = opened
666
+ const written = writeSessionsLine(
667
+ await readFile(path, 'utf8'),
668
+ trimmed,
669
+ sessionId,
670
+ )
671
+ if (written.action !== 'unchanged') await writeFile(path, written.text)
672
+
673
+ const listed = readSessions(written.text).find(
674
+ (entry) => entry.sessionId === sessionId,
675
+ )
676
+
677
+ return {
678
+ ok: true,
679
+ stem,
680
+ path,
681
+ sessionId,
682
+ name: listed?.name ?? trimmed,
683
+ action: written.action,
684
+ }
685
+ }
686
+
569
687
  /**
570
688
  * Marks the named outcomes `[x]` in place. `task-complete` runs this against a
571
689
  * board it can read and cannot edit from a linked worktree, and the positions
@@ -10,7 +10,9 @@ import {
10
10
  readPendingBranches,
11
11
  readPlanTargets,
12
12
  readPullRequest,
13
+ readSessions,
13
14
  resolveLivePlan,
15
+ SESSIONS_PREFIX,
14
16
  tasksDir,
15
17
  } from '@/tasks/archive'
16
18
  import {
@@ -66,6 +68,7 @@ export const FINDING_KINDS = [
66
68
  'blocker-settled',
67
69
  'blocker-unresolved',
68
70
  'blocker-declined',
71
+ 'sessions-misshapen',
69
72
  ] as const
70
73
 
71
74
  export type FindingKind = (typeof FINDING_KINDS)[number]
@@ -841,6 +844,39 @@ async function planDisagreement(
841
844
  }
842
845
  }
843
846
 
847
+ /**
848
+ * Reports a `Sessions:` line that does not parse as name and id pairs, since
849
+ * the reader takes such a line as naming nobody and a retro would find the
850
+ * sessions gone. A row whose task file is gone is left to `task-unresolved`.
851
+ */
852
+ async function checkSessions(
853
+ rows: readonly BoardRow[],
854
+ dir: string,
855
+ ): Promise<Finding[]> {
856
+ const found = await Promise.all(
857
+ rows.map(async (row): Promise<Finding | undefined> => {
858
+ if (!row.stem) return undefined
859
+ const file = join(dir, `${row.stem}.md`)
860
+ if (!existsSync(file)) return undefined
861
+
862
+ const text = await readFile(file, 'utf8')
863
+ const line = text
864
+ .split('\n')
865
+ .find((entry) => entry.startsWith(SESSIONS_PREFIX))
866
+ if (line === undefined || readSessions(text).length > 0) return undefined
867
+
868
+ return {
869
+ kind: 'sessions-misshapen',
870
+ group: row.group,
871
+ subject: row.stem,
872
+ message: `carries "${line}", which does not read as name (sessionId) pairs, so no session it names can be recovered. Rewrite it through canon tasks session.`,
873
+ }
874
+ }),
875
+ )
876
+
877
+ return found.filter((finding): finding is Finding => finding !== undefined)
878
+ }
879
+
844
880
  /**
845
881
  * Tests the claim a group name makes about a plan. `## Needs a plan` says none
846
882
  * is live and `## Up next` says one is written, and neither group has a `Plan`
@@ -1638,6 +1674,7 @@ export async function validateBoard(
1638
1674
  ...checkMapping(rows, backlog, dir),
1639
1675
  ...(await groupRuleFindings(rows, root)),
1640
1676
  ...(await checkPlanAgreement(rows, dir, root)),
1677
+ ...(await checkSessions(rows, dir)),
1641
1678
  ...(await checkStaleTouches(rows, root)),
1642
1679
  ...checkCellWidth(rows),
1643
1680
  ...checkOrdering(rows),
@@ -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.