@erclx/canon 5.14.1 → 5.16.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 (77) hide show
  1. package/README.md +3 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/draft-screencast/REQUIREMENT.md +2 -1
  4. package/claude/skills/draft-screencast/SKILL.md +14 -9
  5. package/claude/skills/draft-wiki/SKILL.md +1 -1
  6. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  7. package/claude/skills/read-frames/SKILL.md +3 -2
  8. package/claude/skills/record-screencast/REQUIREMENT.md +3 -0
  9. package/claude/skills/record-screencast/SKILL.md +28 -5
  10. package/claude/skills/search-craft/REQUIREMENT.md +57 -0
  11. package/claude/skills/search-craft/SKILL.md +83 -0
  12. package/docs/agents/canvas.md +36 -0
  13. package/docs/agents/commands.md +3 -0
  14. package/docs/agents/demo.md +34 -7
  15. package/docs/agents/index.md +1 -0
  16. package/docs/agents/migrate.md +1 -1
  17. package/docs/agents/sandbox.md +25 -0
  18. package/docs/target/migrations.md +1 -1
  19. package/docs/workflow/ai-workflow.md +2 -3
  20. package/docs/workflow/skill-map.md +2 -1
  21. package/governance/renames.toml +7 -5
  22. package/governance/rules/canon/630-requirements.md +1 -1
  23. package/package.json +5 -2
  24. package/scripts/lib/sandbox-git.sh +2 -2
  25. package/src/canvas/address.ts +184 -0
  26. package/src/canvas/capture.ts +172 -0
  27. package/src/canvas/client/app.tsx +87 -0
  28. package/src/canvas/client/inspector.tsx +121 -0
  29. package/src/canvas/client/layers.tsx +118 -0
  30. package/src/canvas/client/main.tsx +62 -0
  31. package/src/canvas/client/pages-panel.tsx +157 -0
  32. package/src/canvas/client/shell.css +449 -0
  33. package/src/canvas/client/state.ts +372 -0
  34. package/src/canvas/client/surface.tsx +465 -0
  35. package/src/canvas/content.ts +617 -0
  36. package/src/canvas/index.html +14 -0
  37. package/src/canvas/server.ts +471 -0
  38. package/src/canvas/tokens.ts +71 -0
  39. package/src/claude/cases/misc.ts +5 -0
  40. package/src/claude/seeds.ts +0 -1
  41. package/src/cli.ts +2 -0
  42. package/src/commands/canvas.ts +522 -0
  43. package/src/commands/claude.ts +9 -14
  44. package/src/commands/demo.ts +6 -6
  45. package/src/commands/migrate.ts +1 -1
  46. package/src/commands/sandbox.ts +108 -0
  47. package/src/context/classify/prompts.ts +1 -1
  48. package/src/context/folders.ts +0 -1
  49. package/src/demo/compile.ts +8 -4
  50. package/src/demo/container.ts +1 -1
  51. package/src/gov/renames.ts +3 -2
  52. package/src/help.ts +1 -0
  53. package/src/migrate/rename.ts +5 -4
  54. package/src/record-root.ts +1 -0
  55. package/src/sandbox/equivalence.ts +847 -0
  56. package/src/serve/static.ts +25 -11
  57. package/src/surface-root.ts +0 -1
  58. package/src/teach/browser/compile.ts +44 -0
  59. package/src/teach/browser/dismiss.ts +76 -0
  60. package/src/teach/browser/focus-line.ts +19 -0
  61. package/src/teach/browser/glossary-filter.ts +44 -0
  62. package/src/teach/browser/head.ts +30 -0
  63. package/src/teach/browser/quiz.ts +19 -0
  64. package/src/teach/browser/sidebar.ts +233 -0
  65. package/src/teach/browser/theme.ts +18 -0
  66. package/src/teach/nav.ts +19 -374
  67. package/standards/architecture.md +1 -1
  68. package/standards/context.md +1 -1
  69. package/standards/design.md +1 -1
  70. package/standards/index.md +0 -1
  71. package/standards/requirements.md +4 -6
  72. package/standards/wireframes.md +2 -2
  73. package/tooling/claude/reference.md +0 -1
  74. package/tooling/claude/seeds/canon/REQUIREMENTS.md +0 -2
  75. package/governance/rules/canon/645-decisions.md +0 -12
  76. package/standards/decisions.md +0 -105
  77. package/tooling/claude/seeds/canon/decisions/index.md +0 -8
@@ -0,0 +1,522 @@
1
+ import { statSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import type { Command } from 'commander'
4
+ import {
5
+ addFrame,
6
+ addPage,
7
+ type ContentRefused,
8
+ canvasDir,
9
+ DEFAULT_FRAME,
10
+ listPages,
11
+ moveFrame,
12
+ readPage,
13
+ readSelection,
14
+ renamePage,
15
+ } from '@/canvas/content'
16
+ import { captureCanvas } from '@/canvas/capture'
17
+ import { CANVAS_PORT, startCanvas } from '@/canvas/server'
18
+ import { resolveFrameTokens } from '@/canvas/tokens'
19
+ import { displayPath, parsePort, waitForInterrupt } from '@/serve/report'
20
+ import {
21
+ intro,
22
+ logAdd,
23
+ logError,
24
+ logInfo,
25
+ logStep,
26
+ logWarn,
27
+ outro,
28
+ plural,
29
+ } from '@/ui'
30
+ import { mainWorktreeRoot } from '@/worktree'
31
+
32
+ const BANNER = 'canon canvas'
33
+
34
+ interface RootOptions {
35
+ readonly root?: string
36
+ readonly json?: boolean
37
+ }
38
+
39
+ interface Refused {
40
+ readonly ok: false
41
+ readonly reason:
42
+ | ContentRefused['reason']
43
+ | 'no-root'
44
+ | 'no-server'
45
+ | 'capture-failed'
46
+ readonly detail: string
47
+ }
48
+
49
+ type RootOutcome = { readonly ok: true; readonly root: string } | Refused
50
+
51
+ /**
52
+ * Refuses a root that is not a directory rather than letting the first write
53
+ * scaffold `.canon/canvas/` under a mistyped path, the way `canon serve`
54
+ * refuses a directory it cannot find.
55
+ */
56
+ async function resolveRoot(opts: RootOptions): Promise<RootOutcome> {
57
+ const root = opts.root
58
+ ? resolve(process.cwd(), opts.root)
59
+ : await mainWorktreeRoot()
60
+ if (!statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
61
+ return {
62
+ ok: false,
63
+ reason: 'no-root',
64
+ detail: `${root} is not a directory`,
65
+ }
66
+ }
67
+ return { ok: true, root }
68
+ }
69
+
70
+ function writeJson(record: unknown): void {
71
+ process.stdout.write(`${JSON.stringify(record)}\n`)
72
+ }
73
+
74
+ /** Every verb's refusal, framed on stderr and mirrored on stdout under --json. */
75
+ function refuse(refusal: Refused, emitJson: boolean): number {
76
+ if (emitJson) writeJson(refusal)
77
+ intro(BANNER)
78
+ logError(refusal.detail)
79
+ outro()
80
+ return 1
81
+ }
82
+
83
+ /**
84
+ * Parses a size flag. NaN for a value that is not a positive integer, which
85
+ * the content module then refuses as an invalid size.
86
+ */
87
+ function parseSize(raw: string | undefined, fallback: number): number {
88
+ if (raw === undefined) return fallback
89
+ const value = Number(raw)
90
+ return Number.isInteger(value) && value > 0 ? value : Number.NaN
91
+ }
92
+
93
+ function withRoot(command: Command): Command {
94
+ return command
95
+ .helpOption('-h, --help', 'Show this help message')
96
+ .option('--root <path>', 'Project root, defaulting to the main worktree')
97
+ .option('--json', 'Emit a machine-readable record on stdout')
98
+ }
99
+
100
+ export function register(program: Command): void {
101
+ const canvas = program
102
+ .command('canvas')
103
+ .helpOption('-h, --help', 'Show this help message')
104
+ .description(
105
+ 'Local canvas of pages and HTML frames (serve, list, page, frame, selection, capture)',
106
+ )
107
+ .addHelpText(
108
+ 'after',
109
+ [
110
+ '',
111
+ 'A page is a folder under .canon/canvas/, a frame is one HTML file in',
112
+ 'it, and layout.json beside them holds each frame box. Edit a frame by',
113
+ 'editing its file, and an open canvas reloads that frame.',
114
+ '',
115
+ 'Examples:',
116
+ ' canon canvas serve',
117
+ ' canon canvas page add drafts',
118
+ ' canon canvas frame add drafts hero --width 1440 --height 900',
119
+ ' canon canvas frame move drafts hero --x 200 --y 120',
120
+ ' canon canvas selection --json',
121
+ ' canon canvas capture drafts/hero',
122
+ ' canon canvas list --json',
123
+ '',
124
+ ].join('\n'),
125
+ )
126
+
127
+ withRoot(
128
+ canvas
129
+ .command('serve')
130
+ .description('Serve the canvas on localhost and print the link')
131
+ .option('--port <number>', `Port to try first, default ${CANVAS_PORT}`),
132
+ )
133
+ .addHelpText(
134
+ 'after',
135
+ [
136
+ '',
137
+ 'Exit codes:',
138
+ ' 0 the server stopped after running',
139
+ ' 1 refused, with the reason on stderr or in the JSON record',
140
+ '',
141
+ 'The server binds 127.0.0.1 and nothing else. It runs until',
142
+ 'interrupted, so a session wanting the link starts it in the',
143
+ 'background and reads the record off stdout.',
144
+ '',
145
+ ].join('\n'),
146
+ )
147
+ .action(async (opts: RootOptions & { port?: string }) => {
148
+ process.exitCode = await runServe(opts)
149
+ })
150
+
151
+ withRoot(
152
+ canvas.command('list').description('List pages and the frames on each'),
153
+ ).action(async (opts: RootOptions) => {
154
+ const resolved = await resolveRoot(opts)
155
+ if (!resolved.ok) {
156
+ process.exitCode = refuse(resolved, opts.json ?? false)
157
+ return
158
+ }
159
+ const { root } = resolved
160
+ const pages = listPages(root)
161
+ if (opts.json) {
162
+ writeJson({ ok: true, root, content: canvasDir(root), pages })
163
+ }
164
+ intro(BANNER)
165
+ if (pages.length === 0) {
166
+ logStep('No pages')
167
+ logInfo('canon canvas page add <name> adds one')
168
+ }
169
+ for (const page of pages) {
170
+ logStep(`${page.name} (${plural(page.frames.length, 'frame')})`)
171
+ for (const frame of page.frames) {
172
+ logInfo(`${frame.name} ${frame.width} × ${frame.height}`)
173
+ }
174
+ if (page.layoutIssue === 'malformed') {
175
+ logWarn('layout.json does not parse, so frames sit in a default row')
176
+ }
177
+ }
178
+ outro()
179
+ })
180
+
181
+ const page = canvas
182
+ .command('page')
183
+ .helpOption('-h, --help', 'Show this help message')
184
+ .description('Add or rename a page')
185
+
186
+ withRoot(
187
+ page
188
+ .command('add')
189
+ .description('Add a page')
190
+ .argument('<name>', 'Page name, one path segment'),
191
+ ).action(async (name: string, opts: RootOptions) => {
192
+ const resolved = await resolveRoot(opts)
193
+ const outcome = resolved.ok ? addPage(resolved.root, name) : resolved
194
+ if (!outcome.ok) {
195
+ process.exitCode = refuse(outcome, opts.json ?? false)
196
+ return
197
+ }
198
+ if (opts.json) writeJson(outcome)
199
+ intro(BANNER)
200
+ logAdd(`page ${outcome.page}`)
201
+ outro()
202
+ })
203
+
204
+ withRoot(
205
+ page
206
+ .command('rename')
207
+ .description('Rename a page, keeping its frames and layout')
208
+ .argument('<from>', 'Current page name')
209
+ .argument('<to>', 'New page name'),
210
+ ).action(async (from: string, to: string, opts: RootOptions) => {
211
+ const resolved = await resolveRoot(opts)
212
+ const outcome = resolved.ok ? renamePage(resolved.root, from, to) : resolved
213
+ if (!outcome.ok) {
214
+ process.exitCode = refuse(outcome, opts.json ?? false)
215
+ return
216
+ }
217
+ if (opts.json) writeJson({ ...outcome, from })
218
+ intro(BANNER)
219
+ logAdd(`page ${from} → ${outcome.page}`)
220
+ outro()
221
+ })
222
+
223
+ const frame = canvas
224
+ .command('frame')
225
+ .helpOption('-h, --help', 'Show this help message')
226
+ .description('Add a frame to a page, or move one')
227
+
228
+ withRoot(
229
+ frame
230
+ .command('add')
231
+ .description('Add a frame as a new HTML file, placed after the others')
232
+ .argument('<page>', 'Page to add the frame to')
233
+ .argument('<name>', 'Frame name, which becomes <name>.html')
234
+ .option('--width <px>', `Frame width, default ${DEFAULT_FRAME.width}`)
235
+ .option('--height <px>', `Frame height, default ${DEFAULT_FRAME.height}`),
236
+ ).action(
237
+ async (
238
+ pageName: string,
239
+ name: string,
240
+ opts: RootOptions & { width?: string; height?: string },
241
+ ) => {
242
+ const resolved = await resolveRoot(opts)
243
+ if (!resolved.ok) {
244
+ process.exitCode = refuse(resolved, opts.json ?? false)
245
+ return
246
+ }
247
+ const { root } = resolved
248
+ const outcome = addFrame(root, pageName, name, {
249
+ width: parseSize(opts.width, DEFAULT_FRAME.width),
250
+ height: parseSize(opts.height, DEFAULT_FRAME.height),
251
+ })
252
+ if (!outcome.ok) {
253
+ process.exitCode = refuse(outcome, opts.json ?? false)
254
+ return
255
+ }
256
+ if (opts.json) {
257
+ writeJson({
258
+ ...outcome,
259
+ path: resolve(canvasDir(root), outcome.page, outcome.file),
260
+ })
261
+ }
262
+ intro(BANNER)
263
+ logAdd(`${outcome.page}/${outcome.file}`)
264
+ logInfo(
265
+ `${outcome.box.width} × ${outcome.box.height} at ${outcome.box.x}, ${outcome.box.y}`,
266
+ )
267
+ outro()
268
+ },
269
+ )
270
+
271
+ withRoot(
272
+ frame
273
+ .command('move')
274
+ .description('Move a frame, keeping its size')
275
+ .argument('<page>', 'Page the frame is on')
276
+ .argument('<name>', 'Frame name')
277
+ .option('--x <px>', 'New x, defaulting to where the frame is')
278
+ .option('--y <px>', 'New y, defaulting to where the frame is'),
279
+ ).action(
280
+ async (
281
+ pageName: string,
282
+ name: string,
283
+ opts: RootOptions & { x?: string; y?: string },
284
+ ) => {
285
+ const resolved = await resolveRoot(opts)
286
+ if (!resolved.ok) {
287
+ process.exitCode = refuse(resolved, opts.json ?? false)
288
+ return
289
+ }
290
+ const { root } = resolved
291
+ const current = readPage(root, pageName)?.frames.find(
292
+ (candidate) => candidate.name === name,
293
+ )
294
+ const outcome = moveFrame(root, pageName, name, {
295
+ x: parsePosition(opts.x, current?.x ?? 0),
296
+ y: parsePosition(opts.y, current?.y ?? 0),
297
+ })
298
+ if (!outcome.ok) {
299
+ process.exitCode = refuse(outcome, opts.json ?? false)
300
+ return
301
+ }
302
+ if (opts.json) writeJson(outcome)
303
+ intro(BANNER)
304
+ logAdd(`${outcome.page}/${outcome.file}`)
305
+ logInfo(`at ${outcome.box.x}, ${outcome.box.y}`)
306
+ outro()
307
+ },
308
+ )
309
+
310
+ withRoot(
311
+ canvas
312
+ .command('selection')
313
+ .description(
314
+ 'Report the frame, or the element in it, the operator selected',
315
+ ),
316
+ )
317
+ .addHelpText(
318
+ 'after',
319
+ [
320
+ '',
321
+ 'Reports none when nothing is selected or when the selected frame has',
322
+ 'since been removed. Read this when the operator says "this one".',
323
+ '',
324
+ 'An element is named by its index in document order, with the tag,',
325
+ 'classes, and text it had when picked. stale is true once the frame',
326
+ 'file changed since, since the index may then name another element.',
327
+ '',
328
+ ].join('\n'),
329
+ )
330
+ .action(async (opts: RootOptions) => {
331
+ const resolved = await resolveRoot(opts)
332
+ if (!resolved.ok) {
333
+ process.exitCode = refuse(resolved, opts.json ?? false)
334
+ return
335
+ }
336
+ const { root } = resolved
337
+ const selected = readSelection(root)
338
+ if (opts.json) {
339
+ writeJson({
340
+ ok: true,
341
+ selection: selected
342
+ ? {
343
+ ...selected,
344
+ path: resolve(canvasDir(root), selected.page, selected.file),
345
+ }
346
+ : null,
347
+ })
348
+ }
349
+ intro(BANNER)
350
+ if (!selected) {
351
+ logStep('Nothing selected')
352
+ } else {
353
+ logStep(`${selected.page}/${selected.frame}`)
354
+ const { box, element } = selected
355
+ logInfo(`${box.width} × ${box.height} at ${box.x}, ${box.y}`)
356
+ if (element) {
357
+ const classes = element.classes.map((name) => `.${name}`).join('')
358
+ logInfo(
359
+ `<${element.tag}${classes}> at index ${element.index}: ${element.text}`,
360
+ )
361
+ if (element.stale) {
362
+ logWarn('The frame changed since, so the index may have moved')
363
+ }
364
+ }
365
+ }
366
+ outro()
367
+ })
368
+
369
+ withRoot(
370
+ canvas
371
+ .command('capture')
372
+ .description('Capture a frame, or every frame of a page, as a PNG')
373
+ .argument('<target>', '<page>/<frame> for one frame, <page> for all')
374
+ .option(
375
+ '-o, --out <path>',
376
+ 'The PNG for a frame, or the folder for a page; defaults to session scratch',
377
+ ),
378
+ )
379
+ .addHelpText(
380
+ 'after',
381
+ [
382
+ '',
383
+ 'Serves the canvas for the call and captures through that address, so',
384
+ 'the injected tokens are in the PNG and each frame renders at its own',
385
+ 'width. The raw file on disk is never captured.',
386
+ '',
387
+ 'Exit codes:',
388
+ ' 0 every frame rendered',
389
+ ' 1 refused or a frame failed, with the reason on stderr or in the JSON record',
390
+ '',
391
+ ].join('\n'),
392
+ )
393
+ .action(async (target: string, opts: RootOptions & { out?: string }) => {
394
+ const resolved = await resolveRoot(opts)
395
+ if (!resolved.ok) {
396
+ process.exitCode = refuse(resolved, opts.json ?? false)
397
+ return
398
+ }
399
+ let outcome: Awaited<ReturnType<typeof captureCanvas>>
400
+ try {
401
+ outcome = await captureCanvas(
402
+ resolved.root,
403
+ target,
404
+ opts.out ? resolve(process.cwd(), opts.out) : undefined,
405
+ )
406
+ } catch (error) {
407
+ const detail = error instanceof Error ? error.message : String(error)
408
+ process.exitCode = refuse(
409
+ { ok: false, reason: 'capture-failed', detail },
410
+ opts.json ?? false,
411
+ )
412
+ return
413
+ }
414
+ if (!outcome.ok) {
415
+ process.exitCode = refuse(outcome, opts.json ?? false)
416
+ return
417
+ }
418
+
419
+ const rows = outcome.captures.map(({ target: item, result }) =>
420
+ result.status === 'rendered'
421
+ ? {
422
+ page: item.page,
423
+ frame: item.frame,
424
+ status: result.status,
425
+ path: result.pngPath,
426
+ width: result.width,
427
+ height: result.height,
428
+ }
429
+ : {
430
+ page: item.page,
431
+ frame: item.frame,
432
+ status: result.status,
433
+ reason: result.reason,
434
+ },
435
+ )
436
+ const failed = rows.some((row) => row.status === 'failed')
437
+ if (opts.json) writeJson({ ok: !failed, captures: rows })
438
+ intro(BANNER)
439
+ for (const row of rows) {
440
+ if (row.status === 'rendered') {
441
+ logAdd(`${row.page}/${row.frame}`)
442
+ logInfo(`${displayPath(row.path)} ${row.width}x${row.height}`)
443
+ } else {
444
+ logError(`${row.page}/${row.frame}: ${row.reason}`)
445
+ }
446
+ }
447
+ outro()
448
+ if (failed) process.exitCode = 1
449
+ })
450
+ }
451
+
452
+ /** NaN for a value that is not a number, which the content module refuses. */
453
+ function parsePosition(raw: string | undefined, fallback: number): number {
454
+ if (raw === undefined) return fallback
455
+ return raw.trim() === '' ? Number.NaN : Number(raw)
456
+ }
457
+
458
+ async function runServe(
459
+ opts: RootOptions & { port?: string },
460
+ ): Promise<number> {
461
+ const emitJson = opts.json ?? false
462
+ const port = opts.port === undefined ? CANVAS_PORT : parsePort(opts.port)
463
+ if (port === undefined) {
464
+ return refuseServe('no-port', `${opts.port} is not a port`, emitJson)
465
+ }
466
+
467
+ const resolved = await resolveRoot(opts)
468
+ if (!resolved.ok) {
469
+ return refuseServe(resolved.reason, resolved.detail, emitJson)
470
+ }
471
+ const { root } = resolved
472
+ /*
473
+ * Imported here rather than at the top, so a command module the test runner
474
+ * loads never asks it for an HTML module it cannot parse.
475
+ */
476
+ const { default: shell } = await import('@/canvas/index.html')
477
+ const outcome = startCanvas(root, { port, shell })
478
+ if (!outcome.ok) {
479
+ return refuseServe(outcome.reason, outcome.detail, emitJson)
480
+ }
481
+
482
+ const tokens = resolveFrameTokens(root)
483
+ const { css: _css, ...tokenSource } = tokens
484
+ if (emitJson) {
485
+ writeJson({
486
+ ok: true,
487
+ root: outcome.root,
488
+ content: outcome.content,
489
+ host: outcome.host,
490
+ port: outcome.port,
491
+ url: outcome.url,
492
+ tokens: tokenSource,
493
+ })
494
+ }
495
+ intro(BANNER)
496
+ logStep('Serving')
497
+ logInfo(displayPath(outcome.content))
498
+ logStep('Open')
499
+ logInfo(outcome.url)
500
+ logStep('Tokens')
501
+ if (tokens.source === 'none') logWarn(tokens.notice)
502
+ else if (tokens.source === 'installed') {
503
+ for (const file of tokens.files) logInfo(file)
504
+ } else logInfo('this toolkit’s own token module')
505
+ logStep('Stop')
506
+ logInfo('Ctrl-C')
507
+
508
+ await waitForInterrupt(outcome.stop)
509
+ return 0
510
+ }
511
+
512
+ function refuseServe(
513
+ reason: string,
514
+ detail: string,
515
+ emitJson: boolean,
516
+ ): number {
517
+ if (emitJson) writeJson({ ok: false, reason, detail })
518
+ intro(BANNER)
519
+ logError(detail)
520
+ outro()
521
+ return 1
522
+ }
@@ -153,21 +153,16 @@ const SEEDED_FILES: readonly string[] = [
153
153
  'REQUIREMENTS.md',
154
154
  'DESIGN.md',
155
155
  ]
156
- const SEEDED_DIRS: readonly string[] = [
157
- 'decisions',
158
- 'memory',
159
- 'tasks',
160
- 'wireframes',
161
- ]
156
+ const SEEDED_DIRS: readonly string[] = ['memory', 'tasks', 'wireframes']
162
157
 
163
158
  /**
164
159
  * Where a `SEEDED_DIRS` entry resolves for the sync presence check.
165
160
  *
166
- * `wireframes` and `decisions` are tracked surface entries, resolved under
167
- * `canon/` ahead of `.claude/`, while `memory` and `tasks` are session records
168
- * resolved under `.canon/` ahead of `.claude/`. Routing every name through
169
- * `recordDir` reported a migrated target's `canon/wireframes/` or
170
- * `canon/decisions/` as missing, since that resolver never checks `canon/`.
161
+ * `wireframes` is a tracked surface entry, resolved under `canon/` ahead of
162
+ * `.claude/`, while `memory` and `tasks` are session records resolved under
163
+ * `.canon/` ahead of `.claude/`. Routing every name through `recordDir`
164
+ * reported a migrated target's `canon/wireframes/` as missing, since that
165
+ * resolver never checks `canon/`.
171
166
  */
172
167
  export function seededDirPath(resolved: string, name: string): string {
173
168
  return SURFACE_ENTRIES.includes(name)
@@ -605,9 +600,9 @@ async function runSync(target: string): Promise<number> {
605
600
 
606
601
  // A record folder resolves at either root, so a migrated target is reported as
607
602
  // seeded rather than sent to `canon claude init` to re-create records it
608
- // already holds. `wireframes` and `decisions` are tracked surface entries
609
- // instead, resolved through `surfaceDir`, since a migrated target holds them
610
- // at `canon/` rather than under either record root.
603
+ // already holds. `wireframes` is a tracked surface entry instead, resolved
604
+ // through `surfaceDir`, since a migrated target holds it at `canon/` rather
605
+ // than under either record root.
611
606
  logStep('Seeded')
612
607
  for (const name of SEEDED_FILES) {
613
608
  if (existsSync(join(resolved, '.claude', name))) logInfo(name)
@@ -73,7 +73,7 @@ export function register(program: Command): void {
73
73
  ' 1 refused, with the reason on stderr',
74
74
  '',
75
75
  'Examples:',
76
- ' canon demo compile .canon/tmp/screencast/inline-edit.md',
76
+ ' canon demo compile demos/inline-edit/beats.md --slug inline-edit',
77
77
  ' canon demo compile draft.md --out demos --force',
78
78
  '',
79
79
  ].join('\n'),
@@ -114,8 +114,8 @@ export function register(program: Command): void {
114
114
  ' 1 refused, with the reason on stderr',
115
115
  '',
116
116
  'Examples:',
117
- ' canon demo run demos/inline-edit.json',
118
- ' canon demo run demos/inline-edit.json --cursor ~/cursors/theme',
117
+ ' canon demo run demos/inline-edit/plan.json',
118
+ ' canon demo run demos/inline-edit/plan.json --cursor ~/cursors/theme',
119
119
  '',
120
120
  ].join('\n'),
121
121
  )
@@ -146,8 +146,8 @@ export function register(program: Command): void {
146
146
  ' 1 refused, with the reason on stderr',
147
147
  '',
148
148
  'Examples:',
149
- ' canon demo frames demos/inline-edit.webm',
150
- ' canon demo frames demos/inline-edit.webm --fps 2 --out frames',
149
+ ' canon demo frames demos/inline-edit/take/inline-edit.webm',
150
+ ' canon demo frames demos/inline-edit/take/inline-edit.webm --fps 2 --out frames',
151
151
  '',
152
152
  ].join('\n'),
153
153
  )
@@ -182,7 +182,7 @@ function runCompile(draftPath: string, opts: CompileOptions): number {
182
182
  }
183
183
 
184
184
  const slug = opts.slug ?? basename(source, extname(source))
185
- const target = resolve(process.cwd(), opts.out, `${slug}.json`)
185
+ const target = resolve(process.cwd(), opts.out, slug, 'plan.json')
186
186
 
187
187
  if (existsSync(target) && !opts.force) {
188
188
  logStep('Plan')
@@ -1222,7 +1222,7 @@ export function register(program: Command): void {
1222
1222
  '',
1223
1223
  'The changelog is never rewritten. Its entries record what shipped',
1224
1224
  'under the old name, and GitHub redirects the links they carry.',
1225
- 'aitk-sandbox is a separate repository and is left alone.',
1225
+ 'aitk-sandbox, the old name of erclx/canon-sandbox, is left alone.',
1226
1226
  '',
1227
1227
  'Examples:',
1228
1228
  ' canon migrate rename',