@erclx/canon 4.6.1 → 4.7.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 (132) 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 +1 -1
  49. package/docs/agents/commands.md +5 -2
  50. package/docs/agents/demo.md +1 -1
  51. package/docs/agents/indexes.md +1 -1
  52. package/docs/agents/install-and-sync.md +1 -1
  53. package/docs/agents/intake.md +1 -1
  54. package/docs/agents/key-changes.md +1 -1
  55. package/docs/agents/records.md +12 -9
  56. package/docs/agents/restated.md +2 -2
  57. package/docs/agents/rule-citations.md +1 -1
  58. package/docs/agents/sandbox.md +1 -1
  59. package/docs/agents/tasks.md +6 -6
  60. package/docs/agents/teach.md +4 -4
  61. package/docs/ai-workflow.md +26 -19
  62. package/docs/operating-model.md +3 -3
  63. package/docs/target-projects.md +22 -2
  64. package/docs/visual-design-workflow.md +1 -1
  65. package/governance/rules/claude/555-tasks.md +3 -3
  66. package/governance/rules/claude/556-groundwork.md +2 -2
  67. package/governance/rules/claude/557-intake.md +2 -2
  68. package/governance/rules/claude/558-plan.md +3 -3
  69. package/governance/rules/claude/559-memory.md +2 -2
  70. package/governance/rules/claude/560-diagrams.md +3 -3
  71. package/governance/rules/claude/561-teach.md +2 -2
  72. package/governance/rules/claude/562-session.md +2 -2
  73. package/governance/rules/core/035-tasks.md +2 -2
  74. package/governance/rules/core/045-memory.md +3 -3
  75. package/governance/rules/core/055-scratch.md +2 -1
  76. package/governance/rules/core/085-worktrees.md +1 -1
  77. package/package.json +1 -1
  78. package/scripts/core/check-ignore-parity.sh +24 -51
  79. package/scripts/core/install-check.sh +4 -4
  80. package/scripts/core/snapshot.sh +1 -1
  81. package/scripts/manage-sandbox.sh +1 -1
  82. package/scripts/tooling/verify.sh +2 -2
  83. package/snippets/claude/decision-memo.md +1 -1
  84. package/snippets/claude/feature-recap.md +1 -1
  85. package/src/audits/catalog.ts +1 -1
  86. package/src/claude/cases/all.ts +1 -1
  87. package/src/claude/seeds.ts +13 -2
  88. package/src/claude/skills-rank.ts +1 -1
  89. package/src/cli.ts +4 -4
  90. package/src/commands/demo.ts +1 -1
  91. package/src/commands/design.ts +1 -1
  92. package/src/commands/feedback.ts +5 -5
  93. package/src/commands/gov.ts +17 -6
  94. package/src/commands/intake.ts +1 -1
  95. package/src/commands/migrate.ts +173 -2
  96. package/src/commands/pr.ts +1 -1
  97. package/src/commands/records.ts +1 -1
  98. package/src/commands/serve.ts +2 -2
  99. package/src/commands/slides.ts +1 -1
  100. package/src/commands/tasks.ts +5 -5
  101. package/src/commands/teach.ts +1 -1
  102. package/src/context/audit.ts +1 -1
  103. package/src/demo/compile.ts +1 -1
  104. package/src/gov/citations.ts +1 -1
  105. package/src/gov/restated.ts +3 -3
  106. package/src/migrate/apply.ts +67 -1
  107. package/src/migrate/records.ts +276 -0
  108. package/src/paths.ts +1 -1
  109. package/src/pr/paths.ts +1 -1
  110. package/src/record-root.ts +77 -27
  111. package/src/records/backup.ts +71 -17
  112. package/src/records/validate.ts +1 -1
  113. package/src/sync/layout.ts +11 -2
  114. package/src/tasks/answers.ts +1 -1
  115. package/src/tasks/archive.ts +20 -3
  116. package/standards/diagrams.md +4 -4
  117. package/standards/glossary.md +1 -1
  118. package/standards/groundwork.md +3 -3
  119. package/standards/index.md +2 -2
  120. package/standards/intake.md +3 -3
  121. package/standards/memory.md +2 -2
  122. package/standards/plan.md +3 -3
  123. package/standards/session.md +2 -2
  124. package/standards/standard.md +1 -1
  125. package/standards/tasks.md +9 -9
  126. package/standards/teach.md +2 -2
  127. package/standards/versioning.md +1 -1
  128. package/tooling/base/configs/.husky/post-merge +1 -1
  129. package/tooling/base/reference.md +1 -1
  130. package/tooling/claude/manifest.toml +13 -3
  131. package/tooling/claude/reference.md +28 -15
  132. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +8 -8
@@ -16,7 +16,7 @@ show_help() {
16
16
  echo -e "${GREY}│${NC} --keep ${GREY}# Keep tmp dir after run for inspection${NC}"
17
17
  echo -e "${GREY}│${NC} -h, --help ${GREY}# Show this help message${NC}"
18
18
  echo -e "${GREY}│${NC}"
19
- echo -e "${GREY}│${NC} Scaffolds <stack> into .claude/.tmp/verify-<stack>/, syncs the full"
19
+ echo -e "${GREY}│${NC} Scaffolds <stack> into .canon/tmp/verify-<stack>/, syncs the full"
20
20
  echo -e "${GREY}│${NC} layer chain, and runs check + test:e2e + screenshot."
21
21
  echo -e "${GREY}└${NC}"
22
22
  exit 0
@@ -100,7 +100,7 @@ main() {
100
100
  open_timeline "canon tooling verify $stack"
101
101
  trap close_timeline EXIT
102
102
 
103
- local tmp_root="$PROJECT_ROOT/.claude/.tmp"
103
+ local tmp_root="$PROJECT_ROOT/.canon/tmp"
104
104
  local tmp_dir="$tmp_root/verify-$stack"
105
105
 
106
106
  mkdir -p "$tmp_root"
@@ -1,4 +1,4 @@
1
- Produce a decision memo for a research or should-we question. This is the decision-heavy counterpart to `claude-feature`, which plans a build. Write one file per independent concern under `.claude/plans/feature-<slug>.md`, or answer inline when the plan is small. Use the same plans folder as `claude-feature`.
1
+ Produce a decision memo for a research or should-we question. This is the decision-heavy counterpart to `claude-feature`, which plans a build. Write one file per independent concern under `.canon/plans/feature-<slug>.md`, or answer inline when the plan is small. Use the same plans folder as `claude-feature`.
2
2
 
3
3
  Read the current state from the code and docs before recommending. Do not assume it.
4
4
 
@@ -1,4 +1,4 @@
1
- Recap the implementation this session completed. Read the plan file referenced in the session (usually `.claude/plans/feature-<slug>.md`) and produce a verification block.
1
+ Recap the implementation this session completed. Read the plan file referenced in the session (usually `.canon/plans/feature-<slug>.md`) and produce a verification block.
2
2
 
3
3
  Output this shape, nothing else:
4
4
 
@@ -488,7 +488,7 @@ export const AUDITS: readonly AuditSpec[] = [
488
488
  corpus: 'tracked',
489
489
  // The one reason this verb refuses for that is an absence rather than a
490
490
  // break, on the same test the reach check and the skill audit take: no
491
- // target adopts `.claude/context/`, `.claude/diagrams/`, and
491
+ // target adopts `.claude/context/`, `.canon/diagrams/`, and
492
492
  // `.claude/wireframes/` all at once, so without the allowance every such
493
493
  // project reports the verb unmeasured on every run and never changes.
494
494
  absentReasons: ['no-folders'] satisfies ContextAuditRefusal[],
@@ -12,7 +12,7 @@ import { SETUP_CASES } from '@/claude/cases/setup'
12
12
  * Bootstrapped from each skill's own quoted trigger phrases and then
13
13
  * rephrased and supplemented rather than shipped verbatim, since a positive
14
14
  * lifted unchanged from a trigger passes by construction and never tests a
15
- * person's own words. See `.claude/groundwork/42-ai-blueprint/08-spikes.md`
15
+ * person's own words. See `.canon/groundwork/42-ai-blueprint/08-spikes.md`
16
16
  * for the extraction arm this corpus replaces as the shipped measure.
17
17
  */
18
18
  export const SKILL_CASES: readonly SkillCase[] = [
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs'
2
2
  import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
3
  import { dirname, join } from 'node:path'
4
4
  import { copyPreservingMode } from '@/copy'
5
+ import { creationRel, isRecordEntry } from '@/record-root'
5
6
  import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
6
7
 
7
8
  const SEEDS_DIR = join('tooling', 'claude', 'seeds')
@@ -81,13 +82,23 @@ export function planSeeds(root: string, target: string): SeedEntry[] {
81
82
  }
82
83
 
83
84
  for (const subdir of SUBDIRS) {
85
+ // The seed tree authors every subdirectory under `.claude/`, and three of
86
+ // them are record folders that install under the record root instead. A
87
+ // target that has not migrated resolves back to `.claude/`, so the same seed
88
+ // lands beside the records already there rather than opening a second root.
89
+ // Scaffolding one under `.claude/` now would also land it outside the single
90
+ // `.canon/` ignore entry a target receives, which tracks the memory pen.
91
+ const installRel = isRecordEntry(subdir)
92
+ ? creationRel(target, subdir)
93
+ : join(CLAUDE_DIR, subdir)
94
+
84
95
  for (const name of listLevel(join(source, subdir))) {
85
96
  const rel = `${subdir}/${name}`
86
97
  seeds.push({
87
98
  src: join(source, subdir, name),
88
- dest: join(destDir, subdir, name),
99
+ dest: join(target, installRel, name),
89
100
  scanLabel: rel,
90
- applyLabel: join(CLAUDE_DIR, subdir, name),
101
+ applyLabel: join(installRel, name),
91
102
  scope: 'claude',
92
103
  executable: subdir === HOOKS,
93
104
  })
@@ -8,7 +8,7 @@ import { listSkillsAt, resolveSkillsCorpus } from '@/claude/skills-list'
8
8
  * asks whether the descriptions are separable by the words they use, and
9
9
  * Claude Code does not route this way.
10
10
  *
11
- * Ported from `.claude/groundwork/42-ai-blueprint/evidence/rank.ts`, which
11
+ * Ported from `.canon/groundwork/42-ai-blueprint/evidence/rank.ts`, which
12
12
  * ran once against this catalog and named the collisions this measure now
13
13
  * tracks on a cadence.
14
14
  */
package/src/cli.ts CHANGED
@@ -72,11 +72,11 @@ function showHelp(): void {
72
72
  `${GREY}│${NC} demo [cmd] ${GREY}# Record a running app (compile, run)${NC}`,
73
73
  `${GREY}│${NC} inventory [subj] ${GREY}# Report one computed property across every route${NC}`,
74
74
  `${GREY}│${NC} drive <url> <run> ${GREY}# Walk a page through named interactions and measure each state${NC}`,
75
- `${GREY}│${NC} feedback ${GREY}# Write toolkit feedback from stdin to .claude/review/feedback/${NC}`,
75
+ `${GREY}│${NC} feedback ${GREY}# Write toolkit feedback from stdin to .canon/review/feedback/${NC}`,
76
76
  `${GREY}│${NC} transcripts <url> ${GREY}# Fetch a YouTube transcript with metadata frontmatter${NC}`,
77
77
  `${GREY}│${NC} tasks [cmd] ${GREY}# Task board commands (archive)${NC}`,
78
- `${GREY}│${NC} intake [cmd] ${GREY}# Intake folders under .claude/intake/ (list, answer)${NC}`,
79
- `${GREY}│${NC} teach [cmd] ${GREY}# Learning workspaces under .claude/teach/ (list, open, resource, glossary)${NC}`,
78
+ `${GREY}│${NC} intake [cmd] ${GREY}# Intake folders under .canon/intake/ (list, answer)${NC}`,
79
+ `${GREY}│${NC} teach [cmd] ${GREY}# Learning workspaces under .canon/teach/ (list, open, resource, glossary)${NC}`,
80
80
  `${GREY}│${NC} comments [cmd] ${GREY}# Measure comment density and trend (scan)${NC}`,
81
81
  `${GREY}│${NC} context [cmd] ${GREY}# Report context folder health (audit)${NC}`,
82
82
  `${GREY}│${NC} markdown [cmd] ${GREY}# Report markdown against the attribute standards (audit)${NC}`,
@@ -121,7 +121,7 @@ function showHelp(): void {
121
121
  `${GREY}│${NC} canon slides render`,
122
122
  `${GREY}│${NC} canon slides list --json`,
123
123
  `${GREY}│${NC} canon capture assets/install.html`,
124
- `${GREY}│${NC} canon serve .claude/teach`,
124
+ `${GREY}│${NC} canon serve .canon/teach`,
125
125
  `${GREY}│${NC} canon inventory focus --json`,
126
126
  `${GREY}│${NC} canon drive http://localhost:4173 run.json --json`,
127
127
  `${GREY}│${NC} pbpaste | canon feedback`,
@@ -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
@@ -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.
@@ -43,7 +43,7 @@ const MIRRORS: readonly (readonly [string, string])[] = [
43
43
  *
44
44
  * Under one percent of the 2750 statements this repository offers. Tuned
45
45
  * against that corpus rather than reasoned to, which is what the plan asked of
46
- * the first run: `.claude/plans/` sits at 14 and is the anchor the motivating
46
+ * the first run: `.canon/plans/` sits at 14 and is the anchor the motivating
47
47
  * case turns on, while `file` sits at 371 and matches most of the tree.
48
48
  */
49
49
  export const COMMON_CEILING = 20
@@ -55,7 +55,7 @@ export const ANCHOR_FLOOR = 3
55
55
  * What a backticked token is worth against a plain word.
56
56
  *
57
57
  * An author marking a span as code named an identifier rather than describing
58
- * one, so `.claude/plans/archive/` says more about what a statement governs
58
+ * one, so `.canon/plans/archive/` says more about what a statement governs
59
59
  * than any two prose words do. Weighting it is what lets the floor rise high
60
60
  * enough to drop a coincidental word pair without losing a rule two surfaces
61
61
  * spelled entirely differently around one shared path.
@@ -267,7 +267,7 @@ interface Indexed<T extends Statement> {
267
267
  /**
268
268
  * Splits text into the tokens an anchor can be drawn from.
269
269
  *
270
- * A code span keeps its inner text whole, since `.claude/plans/archive/` is the
270
+ * A code span keeps its inner text whole, since `.canon/plans/archive/` is the
271
271
  * strongest anchor this corpus offers and splitting it on the punctuation would
272
272
  * leave three words every second bullet also carries.
273
273
  */