@erclx/canon 4.6.1 → 4.8.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 (145) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/canon-feedback-file/SKILL.md +3 -3
  3. package/claude/skills/canon-feedback-triage/SKILL.md +1 -1
  4. package/claude/skills/canon-rollout/SKILL.md +1 -1
  5. package/claude/skills/canon-screencast/SKILL.md +5 -5
  6. package/claude/skills/canon-slides-draft/SKILL.md +1 -1
  7. package/claude/skills/claude-address-review/SKILL.md +4 -4
  8. package/claude/skills/claude-autoship/SKILL.md +17 -17
  9. package/claude/skills/claude-design-extract/SKILL.md +2 -2
  10. package/claude/skills/claude-diagram/SKILL.md +13 -13
  11. package/claude/skills/claude-docs/SKILL.md +23 -23
  12. package/claude/skills/claude-feature/SKILL.md +6 -6
  13. package/claude/skills/claude-groundwork/SKILL.md +16 -16
  14. package/claude/skills/claude-intake/SKILL.md +8 -8
  15. package/claude/skills/claude-intake-answer/SKILL.md +1 -1
  16. package/claude/skills/claude-markdown-propose/REQUIREMENT.md +2 -2
  17. package/claude/skills/claude-markdown-propose/SKILL.md +8 -8
  18. package/claude/skills/claude-markdown-propose/references/format.md +3 -3
  19. package/claude/skills/claude-memory-capture/SKILL.md +13 -13
  20. package/claude/skills/claude-memory-review/SKILL.md +28 -28
  21. package/claude/skills/claude-memory-review/references/receipt-format.md +1 -1
  22. package/claude/skills/claude-orchestrate/SKILL.md +9 -9
  23. package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +1 -1
  24. package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +1 -1
  25. package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +1 -1
  26. package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +1 -1
  27. package/claude/skills/claude-orchestrate/scripts/poll.sh +1 -1
  28. package/claude/skills/claude-planner/REQUIREMENT.md +1 -1
  29. package/claude/skills/claude-planner/SKILL.md +6 -6
  30. package/claude/skills/claude-pr-review/SKILL.md +5 -5
  31. package/claude/skills/claude-review/SKILL.md +4 -4
  32. package/claude/skills/claude-seed-sync/SKILL.md +2 -2
  33. package/claude/skills/claude-tasks/SKILL.md +16 -16
  34. package/claude/skills/claude-teach/SKILL.md +9 -9
  35. package/claude/skills/claude-ui-test/SKILL.md +3 -3
  36. package/claude/skills/claude-ux-audit/SKILL.md +5 -5
  37. package/claude/skills/claude-ux-measure/SKILL.md +3 -3
  38. package/claude/skills/claude-worker/SKILL.md +2 -2
  39. package/claude/skills/claude-worktree/SKILL.md +3 -3
  40. package/claude/skills/git-issue/SKILL.md +4 -4
  41. package/claude/skills/git-pr/SKILL.md +6 -6
  42. package/claude/skills/git-ship/SKILL.md +3 -3
  43. package/claude/skills/git-split/REQUIREMENT.md +1 -1
  44. package/claude/skills/git-split/SKILL.md +20 -20
  45. package/claude/skills/migration-superseded/SKILL.md +4 -4
  46. package/claude/skills/session-map/SKILL.md +2 -2
  47. package/claude/skills/session-resume/SKILL.md +8 -8
  48. package/docs/agents/capture.md +17 -11
  49. package/docs/agents/commands.md +8 -5
  50. package/docs/agents/demo.md +2 -2
  51. package/docs/agents/driver.md +2 -2
  52. package/docs/agents/index.md +1 -1
  53. package/docs/agents/indexes.md +1 -1
  54. package/docs/agents/install-and-sync.md +1 -1
  55. package/docs/agents/intake.md +1 -1
  56. package/docs/agents/key-changes.md +1 -1
  57. package/docs/agents/records.md +12 -9
  58. package/docs/agents/restated.md +2 -2
  59. package/docs/agents/rule-citations.md +1 -1
  60. package/docs/agents/sandbox.md +1 -1
  61. package/docs/agents/state-scoped-risk.md +1 -1
  62. package/docs/agents/tasks.md +6 -6
  63. package/docs/agents/teach.md +4 -4
  64. package/docs/ai-workflow.md +26 -19
  65. package/docs/operating-model.md +3 -3
  66. package/docs/target-projects.md +22 -2
  67. package/docs/visual-design-workflow.md +1 -1
  68. package/governance/rules/claude/555-tasks.md +3 -3
  69. package/governance/rules/claude/556-groundwork.md +2 -2
  70. package/governance/rules/claude/557-intake.md +2 -2
  71. package/governance/rules/claude/558-plan.md +3 -3
  72. package/governance/rules/claude/559-memory.md +2 -2
  73. package/governance/rules/claude/560-diagrams.md +3 -3
  74. package/governance/rules/claude/561-teach.md +2 -2
  75. package/governance/rules/claude/562-session.md +2 -2
  76. package/governance/rules/core/035-tasks.md +2 -2
  77. package/governance/rules/core/045-memory.md +3 -3
  78. package/governance/rules/core/055-scratch.md +2 -1
  79. package/governance/rules/core/085-worktrees.md +1 -1
  80. package/package.json +1 -2
  81. package/scripts/core/check-ignore-parity.sh +24 -51
  82. package/scripts/core/install-check.sh +4 -4
  83. package/scripts/core/regen-hero.sh +6 -3
  84. package/scripts/core/snapshot.sh +1 -1
  85. package/scripts/manage-sandbox.sh +1 -1
  86. package/scripts/tooling/verify.sh +2 -2
  87. package/snippets/claude/decision-memo.md +1 -1
  88. package/snippets/claude/feature-recap.md +1 -1
  89. package/src/audits/catalog.ts +1 -1
  90. package/src/capture/render.ts +166 -0
  91. package/src/capture/sources.ts +42 -0
  92. package/src/capture/stamp.ts +52 -0
  93. package/src/claude/cases/all.ts +1 -1
  94. package/src/claude/seeds.ts +13 -2
  95. package/src/claude/skills-rank.ts +1 -1
  96. package/src/cli.ts +5 -5
  97. package/src/commands/capture.ts +52 -41
  98. package/src/commands/demo.ts +1 -1
  99. package/src/commands/design.ts +1 -1
  100. package/src/commands/feedback.ts +5 -5
  101. package/src/commands/gov.ts +17 -6
  102. package/src/commands/intake.ts +1 -1
  103. package/src/commands/migrate.ts +173 -2
  104. package/src/commands/pr.ts +1 -1
  105. package/src/commands/records.ts +1 -1
  106. package/src/commands/serve.ts +2 -2
  107. package/src/commands/slides.ts +1 -1
  108. package/src/commands/tasks.ts +5 -5
  109. package/src/commands/teach.ts +1 -1
  110. package/src/context/audit.ts +1 -1
  111. package/src/demo/compile.ts +1 -1
  112. package/src/demo/drive.ts +5 -8
  113. package/src/driver/drive.ts +7 -8
  114. package/src/gate/measures.ts +1 -1
  115. package/src/gate/stages.ts +1 -1
  116. package/src/gov/citations.ts +1 -1
  117. package/src/gov/restated.ts +3 -3
  118. package/src/inventory/walk.ts +3 -3
  119. package/src/migrate/apply.ts +67 -1
  120. package/src/migrate/records.ts +276 -0
  121. package/src/paths.ts +1 -1
  122. package/src/pr/paths.ts +1 -1
  123. package/src/record-root.ts +77 -27
  124. package/src/records/backup.ts +71 -17
  125. package/src/records/validate.ts +1 -1
  126. package/src/sync/layout.ts +11 -2
  127. package/src/tasks/answers.ts +1 -1
  128. package/src/tasks/archive.ts +20 -3
  129. package/standards/diagrams.md +4 -4
  130. package/standards/glossary.md +1 -1
  131. package/standards/groundwork.md +3 -3
  132. package/standards/index.md +2 -2
  133. package/standards/intake.md +3 -3
  134. package/standards/memory.md +2 -2
  135. package/standards/plan.md +3 -3
  136. package/standards/session.md +2 -2
  137. package/standards/standard.md +1 -1
  138. package/standards/tasks.md +9 -9
  139. package/standards/teach.md +2 -2
  140. package/standards/versioning.md +1 -1
  141. package/tooling/base/configs/.husky/post-merge +1 -1
  142. package/tooling/base/reference.md +1 -1
  143. package/tooling/claude/manifest.toml +13 -3
  144. package/tooling/claude/reference.md +28 -15
  145. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +8 -8
@@ -1,22 +1,37 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { relative, resolve } from 'node:path'
3
3
  import type { Command } from 'commander'
4
- import { frameError, intro, logError, logInfo, outro, pipeOutput } from '@/ui'
4
+ import {
5
+ INSTALL_BROWSER,
6
+ isBrowserMissing,
7
+ isEngineMissing,
8
+ } from '@/browser/engine'
9
+ import {
10
+ frameError,
11
+ intro,
12
+ logError,
13
+ logInfo,
14
+ logWarn,
15
+ outro,
16
+ pipeOutput,
17
+ } from '@/ui'
5
18
 
6
19
  const DEFAULT_SOURCE = 'assets'
7
- const DEFAULT_SELECTOR = '.window'
8
20
 
9
21
  /**
10
- * Both aliases are type queries rather than imports, so `@/capture/render`
11
- * never appears in this module's runtime import list.
22
+ * Both aliases are type queries rather than imports, so naming the render
23
+ * module's types adds no static import of it. The only reference that survives
24
+ * to runtime is the `import()` inside the action.
12
25
  */
13
26
  type Renderer = typeof import('@/capture/render')
14
27
  type CaptureResult = Awaited<ReturnType<Renderer['captureSources']>>[number]
15
28
 
16
29
  /**
17
- * Holds wiring only. Every browser reference sits behind `loadRenderer`,
18
- * because `src/cli.ts` imports this module at startup and the render module is
19
- * excluded from the published package.
30
+ * Holds wiring only. Every browser reference sits behind a dynamic import,
31
+ * because `src/cli.ts` imports this module at startup and resolving the engine
32
+ * there would put a browser launch in front of every other command. That is the
33
+ * same reason `src/commands/demo.ts`, `src/commands/inventory.ts`, and
34
+ * `src/commands/driver.ts` state for themselves.
20
35
  */
21
36
  export function register(program: Command): void {
22
37
  program
@@ -24,20 +39,27 @@ export function register(program: Command): void {
24
39
  .description('Render HTML capture sources to PNG')
25
40
  .argument('[source]', 'HTML file or a directory of them', DEFAULT_SOURCE)
26
41
  .option('-o, --out <dir>', 'Output directory, defaults beside the source')
27
- .option('-s, --selector <selector>', 'Element to capture', DEFAULT_SELECTOR)
42
+ .option('-s, --selector <selector>', 'Element to capture')
28
43
  .action(
29
44
  async (
30
45
  source: string,
31
- opts: { out?: string; selector: string },
46
+ opts: { out?: string; selector?: string },
32
47
  ): Promise<void> => {
33
- const renderer = await loadRenderer()
34
- if (!renderer) {
48
+ /**
49
+ * Refused rather than defaulted, and refused ahead of every other
50
+ * check, so the message names the invocation rather than whatever the
51
+ * working directory happens to hold. The element a capture crops to is
52
+ * a property of the page's own markup, and the class this command used
53
+ * to assume is declared by two committed sources in one repository.
54
+ */
55
+ if (!opts.selector) {
35
56
  frameError(
36
- 'capture is toolkit-only and is absent from an installed canon',
57
+ '--selector names the element to capture and has no default. See canon capture --help.',
37
58
  )
38
59
  process.exitCode = 1
39
60
  return
40
61
  }
62
+ const selector = opts.selector
41
63
 
42
64
  const sourcePath = resolve(process.cwd(), source)
43
65
  if (!existsSync(sourcePath)) {
@@ -49,8 +71,9 @@ export function register(program: Command): void {
49
71
  intro('Capture')
50
72
  let results: CaptureResult[]
51
73
  try {
74
+ const renderer = await import('@/capture/render')
52
75
  results = await renderer.captureSources(sourcePath, {
53
- selector: opts.selector,
76
+ selector,
54
77
  outDir: opts.out ? resolve(process.cwd(), opts.out) : undefined,
55
78
  })
56
79
  } catch (error) {
@@ -85,16 +108,28 @@ export function register(program: Command): void {
85
108
  }
86
109
 
87
110
  /**
88
- * Closes an open frame around a failure the render module raised for the whole
89
- * run rather than for one source, such as a browser binary that is not
90
- * installed. The engine's own message is readable, so it is passed through
91
- * intact instead of being summarized.
111
+ * Closes an open frame around a failure that stopped the whole run rather than
112
+ * one source. Two of them are setup states rather than defects and each names
113
+ * the step that clears it: an engine package that never resolved, and a browser
114
+ * binary that was never downloaded. Every other failure passes through intact,
115
+ * since the engine's own message is readable and summarizing it loses what it
116
+ * said.
92
117
  */
93
118
  function reportInFrame(error: unknown): void {
119
+ if (isEngineMissing(error)) {
120
+ logError('the browser engine is not installed in this project')
121
+ logWarn(`Install it with: ${INSTALL_BROWSER}`)
122
+ outro()
123
+ return
124
+ }
125
+
94
126
  const message = error instanceof Error ? error.message : String(error)
95
127
  const [first, ...rest] = message.split('\n')
96
128
  logError(first ?? 'capture failed')
97
129
  if (rest.length) pipeOutput(rest.join('\n'))
130
+ if (isBrowserMissing(error)) {
131
+ logWarn(`Install the browser binary with: ${INSTALL_BROWSER}`)
132
+ }
98
133
  outro()
99
134
  }
100
135
 
@@ -107,27 +142,3 @@ function displayPath(path: string): string {
107
142
  const fromCwd = relative(process.cwd(), path)
108
143
  return fromCwd.startsWith('..') ? path : fromCwd
109
144
  }
110
-
111
- /**
112
- * Reports absence only when the module or its engine cannot be resolved, which
113
- * is the published-package case. Any other import failure is a defect inside
114
- * the render module and propagates, rather than being reported as a feature
115
- * the package left out.
116
- */
117
- async function loadRenderer(): Promise<Renderer | undefined> {
118
- try {
119
- return await import('@/capture/render')
120
- } catch (error) {
121
- if (isModuleNotFound(error)) return undefined
122
- throw error
123
- }
124
- }
125
-
126
- function isModuleNotFound(error: unknown): boolean {
127
- return (
128
- typeof error === 'object' &&
129
- error !== null &&
130
- 'code' in error &&
131
- error.code === 'ERR_MODULE_NOT_FOUND'
132
- )
133
- }
@@ -61,7 +61,7 @@ export function register(program: Command): void {
61
61
  ' 1 refused, with the reason on stderr',
62
62
  '',
63
63
  'Examples:',
64
- ' canon demo compile .claude/.tmp/screencast/inline-edit.md',
64
+ ' canon demo compile .canon/tmp/screencast/inline-edit.md',
65
65
  ' canon demo compile draft.md --out demos --force',
66
66
  '',
67
67
  ].join('\n'),
@@ -17,7 +17,7 @@ export function register(program: Command): void {
17
17
  .option(
18
18
  '-o, --out <path>',
19
19
  'Output directory',
20
- creationRel('review', 'design'),
20
+ creationRel(process.cwd(), 'review', 'design'),
21
21
  )
22
22
  .action((opts: { source: string; out: string }) => {
23
23
  const sourcePath = resolve(process.cwd(), opts.source)
@@ -40,10 +40,10 @@ function isToolkitSource(): boolean {
40
40
  * single ignore entry and the single backed-folder entry it already had.
41
41
  */
42
42
  function writeLocal(body: string): string {
43
- // Creation, so the destination is the creation default rather than the
44
- // resolved read root. A toolkit checkout that has migrated its records already
45
- // carries the folder and resolves the same path either way.
46
- const relativeDir = creationRel('review', 'feedback')
43
+ // Resolved against the same root the write joins onto, so a checkout that has
44
+ // not migrated its records writes this beside the ones already there rather
45
+ // than opening a second root nothing reads.
46
+ const relativeDir = creationRel(PROJECT_ROOT, 'review', 'feedback')
47
47
  const reviewDir = join(PROJECT_ROOT, relativeDir)
48
48
  mkdirSync(reviewDir, { recursive: true })
49
49
  const filename = `feedback-${deriveSlug(body)}-${timestamp()}.md`
@@ -57,7 +57,7 @@ export function register(program: Command): void {
57
57
  program
58
58
  .command('feedback')
59
59
  .description(
60
- 'Write toolkit feedback from stdin to .claude/review/feedback/, or open a GitHub issue with --github',
60
+ 'Write toolkit feedback from stdin to .canon/review/feedback/, or open a GitHub issue with --github',
61
61
  )
62
62
  .option(
63
63
  '--github',
@@ -66,7 +66,17 @@ import {
66
66
  select,
67
67
  } from '@/ui'
68
68
 
69
- const PAYLOAD_REL = creationRel(SCRATCH, 'gov', 'rules.md')
69
+ /**
70
+ * Where the rules payload lands, resolved per target rather than once at import.
71
+ *
72
+ * The record root a project uses is a fact about that project, and this command
73
+ * writes into a target the caller named, so a value computed here against this
74
+ * process's own tree would point every target at whichever root the toolkit
75
+ * happens to hold.
76
+ */
77
+ function payloadRel(root: string): string {
78
+ return creationRel(root, SCRATCH, 'gov', 'rules.md')
79
+ }
70
80
  const RULES_REL = join('.claude', 'rules')
71
81
 
72
82
  interface InstallOptions {
@@ -170,7 +180,7 @@ export function register(program: Command): void {
170
180
 
171
181
  gov
172
182
  .command('build')
173
- .description('Concatenate installed rules into .claude/.tmp/gov/rules.md')
183
+ .description('Concatenate installed rules into .canon/tmp/gov/rules.md')
174
184
  .argument('[target]', 'Target directory', '.')
175
185
  .helpOption('-h, --help', 'Show this help message')
176
186
  .action(async (target: string) => {
@@ -1222,6 +1232,7 @@ async function runBuild(target: string): Promise<number> {
1222
1232
  intro('canon gov build')
1223
1233
 
1224
1234
  const resolved = resolve(target)
1235
+ const payload = payloadRel(resolved)
1225
1236
  const rulesDir = join(resolved, RULES_REL)
1226
1237
 
1227
1238
  if (!existsSync(rulesDir)) {
@@ -1237,7 +1248,7 @@ async function runBuild(target: string): Promise<number> {
1237
1248
  }
1238
1249
 
1239
1250
  const shouldBuild = await select({
1240
- message: `Build ${files.length} rules to ${PAYLOAD_REL}?`,
1251
+ message: `Build ${files.length} rules to ${payload}?`,
1241
1252
  options: [
1242
1253
  { value: true, label: 'Yes' },
1243
1254
  { value: false, label: 'No' },
@@ -1252,15 +1263,15 @@ async function runBuild(target: string): Promise<number> {
1252
1263
  }
1253
1264
 
1254
1265
  logStep('Building rules payload')
1255
- const output = join(resolved, PAYLOAD_REL)
1266
+ const output = join(resolved, payload)
1256
1267
  await mkdir(dirname(output), { recursive: true })
1257
1268
  await writeFile(output, buildRulesPayload(files))
1258
- logAdd(PAYLOAD_REL)
1269
+ logAdd(payload)
1259
1270
 
1260
1271
  const { GREEN, NC } = palette(process.stderr)
1261
1272
  outro()
1262
1273
  process.stderr.write(
1263
- `${GREEN}✓ Rules built (${files.length} rules → ${PAYLOAD_REL})${NC}\n`,
1274
+ `${GREEN}✓ Rules built (${files.length} rules → ${payload})${NC}\n`,
1264
1275
  )
1265
1276
  return 0
1266
1277
  }
@@ -47,7 +47,7 @@ interface AnswerCommandOptions {
47
47
  export function register(program: Command): void {
48
48
  const intake = program
49
49
  .command('intake')
50
- .description('Read and answer intake folders in .claude/intake/')
50
+ .description('Read and answer intake folders in .canon/intake/')
51
51
  .helpOption('-h, --help', 'Show this help message')
52
52
 
53
53
  intake
@@ -1,7 +1,14 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
1
3
  import type { Command } from 'commander'
2
4
  import { listRepositoryFiles } from '@/git-files'
3
- import { applyRename, readSources } from '@/migrate/apply'
5
+ import { applyRecordsMove, applyRename, readSources } from '@/migrate/apply'
4
6
  import { isToolkitOwned, planRename, type RenamePlan } from '@/migrate/plan'
7
+ import {
8
+ ignoresDestination,
9
+ planRecordsMove,
10
+ type RecordsPlan,
11
+ } from '@/migrate/records'
5
12
  import { logError, logInfo, logStep, logWarn, pipeOutput, plural } from '@/ui'
6
13
 
7
14
  interface RenameOptions {
@@ -128,12 +135,176 @@ function toRecord(
128
135
  }
129
136
  }
130
137
 
138
+ interface RecordsOptions {
139
+ readonly json?: boolean
140
+ readonly write?: boolean
141
+ readonly root?: string
142
+ }
143
+
144
+ /**
145
+ * Moves a project's session records to `.canon/` and repoints what cites them.
146
+ *
147
+ * The ignore gate runs before anything is planned. Every folder being moved is
148
+ * ignored where it stands, so landing them under a root the project does not
149
+ * ignore publishes the memory pen into the next commit, and reporting a plan
150
+ * the caller cannot safely apply is worse than refusing to draw one.
151
+ */
152
+ async function runRecords(opts: RecordsOptions): Promise<number> {
153
+ const root = opts.root ?? process.cwd()
154
+
155
+ const gitignore = readGitignore(root)
156
+ if (gitignore === undefined) {
157
+ logError(
158
+ `No .gitignore at ${root}, so the records cannot be moved somewhere they stay ignored.`,
159
+ )
160
+ return 1
161
+ }
162
+
163
+ if (!ignoresDestination(gitignore)) {
164
+ logError(
165
+ 'This project does not ignore .canon/, and every record folder being moved is ignored where it stands.',
166
+ )
167
+ logError(
168
+ 'Run canon tooling sync --write to take the ignore entry, then run this again.',
169
+ )
170
+ return 1
171
+ }
172
+
173
+ const files = await listRepositoryFiles(root)
174
+ if (files === undefined) {
175
+ logError(`Could not list files under ${root}. Is it a git repository?`)
176
+ return 1
177
+ }
178
+
179
+ const plan = planRecordsMove(root, await readSources(root, files))
180
+
181
+ // stdout, so the record pipes clean. `pipeOutput` frames to stderr, which is
182
+ // where this command's report belongs and where a JSON record does not.
183
+ if (opts.json) {
184
+ process.stdout.write(
185
+ `${JSON.stringify(toRecordsRecord(plan, opts.write))}\n`,
186
+ )
187
+ }
188
+
189
+ reportRecords(plan)
190
+
191
+ if (plan.collisions.length > 0) {
192
+ logError(
193
+ `${plural(plan.collisions.length, 'destination')} already exist under .canon/. Merging two record folders is not a call this verb takes.`,
194
+ )
195
+ for (const path of plan.collisions) logError(` ${path}`)
196
+ return 1
197
+ }
198
+
199
+ if (plan.moves.length === 0 && plan.entries.length === 0) return 0
200
+
201
+ if (!opts.write) {
202
+ logWarn('Nothing was written. Pass --write to apply this plan.')
203
+ return 2
204
+ }
205
+
206
+ const applied = await applyRecordsMove(root, plan)
207
+ logStep(
208
+ `Rewrote ${plural(applied.written, 'file')} and moved ${plural(applied.moved, 'folder')}.`,
209
+ )
210
+
211
+ // `failed` carries a citation the write loop could not land as well as a
212
+ // folder the rename loop stopped on, so the line names a path rather than a
213
+ // folder. Reading a rejected write back as a folder that would not move sends
214
+ // the reader looking at the wrong half of the verb.
215
+ if (applied.failed.length > 0) {
216
+ logError(`Could not write ${plural(applied.failed.length, 'path')}.`)
217
+ for (const path of applied.failed) logError(` ${path}`)
218
+ return 1
219
+ }
220
+
221
+ return 0
222
+ }
223
+
224
+ function readGitignore(root: string): string | undefined {
225
+ try {
226
+ return readFileSync(join(root, '.gitignore'), 'utf8')
227
+ } catch {
228
+ return undefined
229
+ }
230
+ }
231
+
232
+ function reportRecords(plan: RecordsPlan): void {
233
+ logInfo(`${plural(plan.moves.length, 'folder')} to move.`)
234
+ for (const move of plan.moves) logInfo(` ${move.from} -> ${move.to}`)
235
+ logInfo(
236
+ `${plural(plan.entries.length, 'file')} to change, ${plural(plan.rewritten, 'citation')} to rewrite.`,
237
+ )
238
+ logInfo(`${plural(plan.kept, 'citation')} marked to keep the old root.`)
239
+
240
+ if (plan.excluded.length > 0) {
241
+ logInfo(`${plural(plan.excluded.length, 'file')} excluded from the sweep.`)
242
+ }
243
+ }
244
+
245
+ function toRecordsRecord(
246
+ plan: RecordsPlan,
247
+ wrote: boolean | undefined,
248
+ ): unknown {
249
+ return {
250
+ ok: plan.collisions.length === 0,
251
+ wrote: wrote === true,
252
+ moves: plan.moves,
253
+ collisions: plan.collisions,
254
+ files: plan.entries.length,
255
+ rewritten: plan.rewritten,
256
+ kept: plan.kept,
257
+ excluded: plan.excluded.length,
258
+ paths: plan.entries.map((entry) => ({
259
+ path: entry.path,
260
+ rewritten: entry.rewritten,
261
+ })),
262
+ }
263
+ }
264
+
131
265
  export function register(program: Command): void {
132
266
  const migrate = program
133
267
  .command('migrate')
134
- .description('Move a project off the retired aitk name')
268
+ .description('Move a project off a retired name or layout')
135
269
  .helpOption('-h, --help', 'Show this help message')
136
270
 
271
+ migrate
272
+ .command('records')
273
+ .description('Move the gitignored session records to .canon/')
274
+ .helpOption('-h, --help', 'Show this help message')
275
+ .option('--json', 'Add a machine-readable record on stdout')
276
+ .option('--write', 'Apply the plan rather than reporting it')
277
+ .option(
278
+ '--root <path>',
279
+ 'Project root, defaulting to the working directory',
280
+ )
281
+ .addHelpText(
282
+ 'after',
283
+ [
284
+ '',
285
+ 'Exit codes:',
286
+ ' 0 nothing to move, or --write applied the whole plan',
287
+ ' 1 refused, or a move failed',
288
+ ' 2 a plan exists and --write was not passed',
289
+ '',
290
+ 'The project has to ignore .canon/ before anything moves. Every folder',
291
+ 'this relocates is ignored where it stands, and landing one under a root',
292
+ 'the project tracks publishes the memory pen into the next commit.',
293
+ '',
294
+ 'A line carrying canon-keep-record-root, or the line below it, keeps the',
295
+ 'old root. Prose that dates a decision needs it; a live path does not.',
296
+ '',
297
+ 'Examples:',
298
+ ' canon migrate records',
299
+ ' canon migrate records --write',
300
+ ' canon migrate records --json',
301
+ '',
302
+ ].join('\n'),
303
+ )
304
+ .action(async (opts: RecordsOptions) => {
305
+ process.exitCode = await runRecords(opts)
306
+ })
307
+
137
308
  migrate
138
309
  .command('rename')
139
310
  .description('Rewrite every unprotected aitk token to canon')
@@ -130,7 +130,7 @@ export function register(program: Command): void {
130
130
  'Examples:',
131
131
  ' canon pr key-changes',
132
132
  ' canon pr key-changes 1265 --json',
133
- ' canon pr key-changes --body .claude/.tmp/body.md --base origin/main',
133
+ ' canon pr key-changes --body .canon/tmp/body.md --base origin/main',
134
134
  '',
135
135
  ].join('\n'),
136
136
  )
@@ -226,7 +226,7 @@ function backupHelp(verb: 'push' | 'pull'): string {
226
226
  '',
227
227
  ...direction,
228
228
  '',
229
- 'The history lives in a second git directory at .claude/.records.git, pointed at a',
229
+ 'The history lives in a second git directory at .canon/.records.git, pointed at a',
230
230
  'private repository a person creates once. The verbs refuse with the setup command',
231
231
  'when it is absent, and refuse when its origin is also a remote of this project.',
232
232
  '',
@@ -36,8 +36,8 @@ export function register(program: Command): void {
36
36
  'starts it in the background and reads the record off stdout.',
37
37
  '',
38
38
  'Examples:',
39
- ' canon serve .claude/teach',
40
- ' canon serve .claude/teach --entry 03-fde-system-design/index.html',
39
+ ' canon serve .canon/teach',
40
+ ' canon serve .canon/teach --entry 03-fde-system-design/index.html',
41
41
  ' canon serve dist --port 4000 --json',
42
42
  '',
43
43
  ].join('\n'),
@@ -20,7 +20,7 @@ export function register(program: Command): void {
20
20
  .option(
21
21
  '-o, --out <path>',
22
22
  'Output directory',
23
- creationRel('review', 'slides'),
23
+ creationRel(process.cwd(), 'review', 'slides'),
24
24
  )
25
25
  .option('-v, --variant <variant>', 'Override variant (light or dark)')
26
26
  .option(
@@ -76,7 +76,7 @@ interface OutcomeCommandOptions {
76
76
  export function register(program: Command): void {
77
77
  const tasks = program
78
78
  .command('tasks')
79
- .description('Manage the task board in .claude/tasks/')
79
+ .description('Manage the task board in .canon/tasks/')
80
80
  .helpOption('-h, --help', 'Show this help message')
81
81
 
82
82
  tasks
@@ -157,8 +157,8 @@ export function register(program: Command): void {
157
157
  '',
158
158
  'Locations:',
159
159
  ' unstated the task carries no Plan: line',
160
- ' live the target resolves inside .claude/plans/',
161
- ' archived the target resolves inside .claude/plans/archive/',
160
+ ' live the target resolves inside .canon/plans/',
161
+ ' archived the target resolves inside .canon/plans/archive/',
162
162
  ' outside the target resolves somewhere else',
163
163
  '',
164
164
  'Exit codes:',
@@ -201,11 +201,11 @@ export function register(program: Command): void {
201
201
  'exit code, which a shell function wrapping canon can flatten to zero.',
202
202
  '',
203
203
  'A relative path resolves against the project root first and against',
204
- '.claude/tasks/ second, so the ../plans/ link a board row writes works.',
204
+ '.canon/tasks/ second, so the ../plans/ link a board row writes works.',
205
205
  '',
206
206
  'Examples:',
207
207
  ' canon tasks plan-answers dispatch-answer-gate',
208
- ' canon tasks plan-answers .claude/plans/feature-dispatch-answer-gate.md',
208
+ ' canon tasks plan-answers .canon/plans/feature-dispatch-answer-gate.md',
209
209
  ' canon tasks plan-answers ../plans/feature-dispatch-answer-gate.md',
210
210
  ' canon tasks plan-answers dispatch-answer-gate --json',
211
211
  '',
@@ -76,7 +76,7 @@ interface GlossaryCommandOptions {
76
76
  export function register(program: Command): void {
77
77
  const teach = program
78
78
  .command('teach')
79
- .description('Manage learning workspaces in .claude/teach/')
79
+ .description('Manage learning workspaces in .canon/teach/')
80
80
  .helpOption('-h, --help', 'Show this help message')
81
81
 
82
82
  teach
@@ -19,7 +19,7 @@ import { isStubSeed } from '@/seed-marker'
19
19
  * Every reason `canon context audit` refuses for.
20
20
  *
21
21
  * `no-folders` is the one ordinary absence: a project that never adopted
22
- * `.claude/context/`, `.claude/diagrams/`, or `.claude/wireframes/` names no
22
+ * `.claude/context/`, `.canon/diagrams/`, or `.claude/wireframes/` names no
23
23
  * corpus this audit can measure, the same state `no-skills` reads for the
24
24
  * skill corpora. The other four are a malformed invocation or a checkout git
25
25
  * cannot read, which stay a break rather than an absence.
@@ -5,7 +5,7 @@ import type { Beat, Draft } from '@/demo/beats'
5
5
  * separate artifacts on purpose: a beat carries no target, no wait condition,
6
6
  * and no timing, and putting those four fields on every beat would destroy the
7
7
  * property the draft was designed around. See
8
- * `.claude/groundwork/38-demo-recorder/06-decision.md`.
8
+ * `.canon/groundwork/38-demo-recorder/06-decision.md`.
9
9
  *
10
10
  * A compiled plan is committed rather than scratch, because the timing below is
11
11
  * a starting point the operator tunes and the draft cannot reproduce a tuned
package/src/demo/drive.ts CHANGED
@@ -20,15 +20,12 @@ declare global {
20
20
  * the demo feature adds lives here, and `src/commands/demo.ts` reaches it
21
21
  * through a dynamic import so no other command resolves the engine at startup.
22
22
  *
23
- * Unlike `@/capture/render`, this module ships. The capture command is excluded
24
- * from the published package because it regenerates images committed to this
25
- * repository, and that reason does not transfer to a command whose whole
26
- * purpose is running in someone else's project.
27
- *
28
23
  * It imports `playwright-core` rather than `@playwright/test`, which stays a
29
- * development dependency for the capture module. Shipping puts the import in
30
- * every target's dependency tree, and a target needs the driver rather than a
31
- * test runner and an assertion library. Both are pinned to one version rather
24
+ * development dependency the published tarball never carries. Shipping puts the
25
+ * import in every target's dependency tree, and a target needs the driver
26
+ * rather than a test runner and an assertion library. Every browser module here
27
+ * takes the same import for that reason, `@/capture/render` included since it
28
+ * started shipping too. Both are pinned to one version rather
32
29
  * than a range, because `bunx playwright install chromium` fetches the browser
33
30
  * revision the installed engine expects and a float would leave a target
34
31
  * resolving a binary its engine cannot launch.
@@ -23,19 +23,18 @@ import type {
23
23
  * `src/commands/driver.ts` reaches it through a dynamic import so no other
24
24
  * command resolves the engine at startup.
25
25
  *
26
- * Like `@/demo/drive` and `@/inventory/walk`, and unlike `@/capture/render`,
27
- * this module ships. A command whose whole purpose is measuring someone else's
28
- * page cannot stay toolkit-only, and nothing here reaches into `@/capture/`,
29
- * which `files` in `package.json` excludes from the published package, so the
30
- * two surfaces move independently.
26
+ * Like `@/demo/drive`, `@/inventory/walk`, and `@/capture/render`, this module
27
+ * ships. A command whose whole purpose is measuring someone else's page cannot
28
+ * stay toolkit-only, and nothing here reaches into `@/capture/`, so the two
29
+ * surfaces move independently.
31
30
  *
32
31
  * What separates it from `canon capture` is the axis it adds. A render answers
33
32
  * about a page as it loads, and every defect that exists only after a menu
34
33
  * opens, an answer is chosen, or the page scrolls is invisible to one. Probes
35
34
  * therefore run after a step rather than on arrival, and a run reaches the load
36
- * state by opening with a `wait` step of its own. That matters most where
37
- * capture does not run, since it is toolkit-only and this command is then the
38
- * only thing measuring the page at all.
35
+ * state by opening with a `wait` step of its own. That matters most where no
36
+ * capture runs against the page, since this command is then the only thing
37
+ * measuring it at all.
39
38
  */
40
39
 
41
40
  /** Each probe keyed by the name a caller writes in the run. */
@@ -100,7 +100,7 @@ export const SANDBOX_UNDECLARED_CEILING = 47
100
100
  export const AUDITS_BASELINE = '.claude/canon/baseline.json'
101
101
 
102
102
  export const HERO_STAMP_FAILURE =
103
- 'The hero set disagrees with the stamp written when the image was captured. Run canon capture assets/hero.html and commit all three files together.'
103
+ 'The hero set disagrees with the stamp written when the image was captured. Run canon capture assets/hero.html --selector .window and commit all three files together.'
104
104
 
105
105
  function parseJson(payload: string): unknown {
106
106
  try {
@@ -189,7 +189,7 @@ export const STAGES: readonly Stage[] = [
189
189
  kind: 'drift',
190
190
  pathspec: 'assets/hero.html',
191
191
  failure:
192
- 'Hero counts drifted. Run bun run check, then canon capture assets/hero.html, and commit assets/hero.html with assets/hero.png and assets/hero.stamp.',
192
+ 'Hero counts drifted. Run bun run check, then canon capture assets/hero.html --selector .window, and commit assets/hero.html with assets/hero.png and assets/hero.stamp.',
193
193
  },
194
194
  { kind: 'measure', measure: heroStamp },
195
195
  ],
@@ -357,7 +357,7 @@ function globMatches(root: string, glob: string): boolean {
357
357
  /**
358
358
  * Which of `paths` git ignores, or nothing when git could not answer.
359
359
  *
360
- * Session scratch is the class this reaches. `.claude/tasks/index.md` is real
360
+ * Session scratch is the class this reaches. `.canon/tasks/index.md` is real
361
361
  * in a live project, absent from a fresh clone and from every linked worktree,
362
362
  * and a rule naming it is right either way. Resolving against the filesystem
363
363
  * alone would make the verdict depend on which tree the check ran in.