@erclx/canon 4.66.0 → 4.67.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.66.0",
4
+ "version": "4.67.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -51,6 +51,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
51
51
  | `canon records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
52
52
  | `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
53
53
  | `canon migrate rename` | Rewrite every unprotected `aitk` token to `canon` and move the paths that carry the name, reporting the plan without `--write` (`--scope`, `--json`) |
54
+ | `canon migrate skill-names` | Move the prefixed skill folders onto their two-word names and rewrite every citation, reporting the plan without `--write` (`--root`, `--json`) |
54
55
  | `canon migrate records` | Move the gitignored session records to `.canon/` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
55
56
  | `canon migrate record-tree` | Repoint the old-root citations inside the records themselves, scoped to the live folders and reporting every line without `--write` (`--root`, `--json`) |
56
57
  | `canon migrate rule-layout` | Move a target's installed rules from the flat `.claude/rules/<subdir>/` layout to `.claude/rules/canon/<subdir>/`, reporting the plan without `--write` (`--root`, `--json`) |
@@ -138,7 +139,7 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
138
139
  | `secrets` | `scan` |
139
140
  | `deps` | `audit` |
140
141
  | `labels` | `audit` |
141
- | `migrate` | `rename`, `records` |
142
+ | `migrate` | `rename`, `skill-names`, `records` |
142
143
  | `autoship` | `classify` |
143
144
  | `pr` | `key-changes`, `head`, `checks` |
144
145
  | `audits` | `run`, `list` |
@@ -153,6 +154,8 @@ Common patterns:
153
154
 
154
155
  `migrate rename` moves a project off the retired `aitk` name. It reports until `--write` is passed, and `--scope target` rewrites the toolkit-owned folders alone, reporting every other citation as one the project owns rather than editing prose somebody wrote. A project installing `canon` fresh never needs it.
155
156
 
157
+ `migrate skill-names` moves the plugin skills that carried a `claude-` prefix onto their two-word names and rewrites every citation of one. It shares its engine with `migrate rename` and differs in what it is scanning for: a skill name is a whole name rather than a word stem, so it matches only where the word ends, which is what keeps a longer word that merely opens with a skill name, such as `claude-worktrees`, out of it. It takes no `--scope`, since the skill folders it moves are authored in the toolkit and no target holds a copy of that catalog, which leaves a target's own citations of a renamed skill as the whole of what it rewrites there. The changelog and the eval transcripts are passed over on the argument the other sweep already carries, that each records what shipped or what a session ran under whatever name was current then.
158
+
156
159
  `migrate records` moves a project's session records from `.claude/` to `.canon/` and rewrites every tracked file that cites one. It reports until `--write` is passed, and refuses outright when the project does not already ignore `.canon/`, since every folder it relocates is ignored where it stands and landing one under a tracked root commits the memory pen. Take the ignore entry with `canon tooling sync --write` first.
157
160
 
158
161
  A record folder already present at the destination is a refusal rather than a merge, and a line carrying `canon-keep-record-root`, or the nearest non-blank line below it, keeps the old spelling for prose that dates a decision. The records themselves are never swept: everything under `.canon/` and every `.claude/` record folder is passed over and reported as a count on its own line, which is what keeps the run that follows the ignore collapse touching the same files as one before it. Running it twice rewrites nothing, which is the check that the exclusions, the markers, and that skip all fired.
@@ -173,6 +173,25 @@ canon migrate rule-layout --write --json
173
173
 
174
174
  The first line reports the plan and the second applies it, relocating each rule and carrying an edited one's recorded hash forward so it still reports as edited on the next sync rather than reading clean. Run it once, ahead of `canon gov sync` or `canon gov install`, on any project scaffolded before this move landed.
175
175
 
176
+ ### Rename the skill citations, once
177
+
178
+ Twenty-five plugin skills dropped their `claude-` prefix for two-word names, so `canon:claude-docs` answers as `canon:docs-fold` and `canon:claude-tasks` as `canon:task-board`. A project that installed governance or tooling before that release holds files naming the old ones, and the plugin answers to none of them.
179
+
180
+ Two of those files run rather than sit there. `.husky/post-merge` prints a command for a person to type, and `.claude/hooks/pr-create-log.sh` hands a session a message naming a skill, so a stale copy tells someone to invoke something that no longer exists. A rule under `.claude/rules/canon/core/` names skills too, though a rule is read rather than run.
181
+
182
+ Resync what the toolkit owns, then sweep what the project wrote:
183
+
184
+ ```bash
185
+ canon gov sync
186
+ canon tooling sync <stack> . --write
187
+ canon migrate skill-names --json
188
+ canon migrate skill-names --write --json
189
+ ```
190
+
191
+ Run the second line once per stack the project holds, since the two hooks above arrive from different ones. The third reports the plan and the fourth applies it.
192
+
193
+ The order carries the reason. A sync replaces each toolkit-owned copy with one already carrying the new names, and the sweep afterwards reaches the prose the project wrote itself. Sweeping first rewrites those installed files in place, which moves them off the hash the install recorded, so the next sync reads them as edited by the project and leaves them alone.
194
+
176
195
  ### Check first
177
196
 
178
197
  `canon sync --check <path>` reports what has drifted without writing anything. It splits each difference by cause, which is the question that decides what to do next.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.66.0",
4
+ "version": "4.67.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -5,6 +5,7 @@ import { listRepositoryFiles } from '@/git-files'
5
5
  import { indexSourceRules } from '@/gov/adapter'
6
6
  import { applyRecordsMove, applyRename, readSources } from '@/migrate/apply'
7
7
  import { isToolkitOwned, planRename, type RenamePlan } from '@/migrate/plan'
8
+ import { AITK_RULES, type RenameRules } from '@/migrate/rename'
8
9
  import {
9
10
  applyRecordTree,
10
11
  planRecordTree,
@@ -24,6 +25,7 @@ import {
24
25
  type RuleLayoutPlan,
25
26
  walkFlatRules,
26
27
  } from '@/migrate/rule-layout'
28
+ import { SKILL_NAME_MAP, SKILL_NAME_RULES } from '@/migrate/skill-names'
27
29
  import {
28
30
  applyScratchEvidence,
29
31
  planScratchEvidence,
@@ -33,12 +35,15 @@ import {
33
35
  } from '@/migrate/scratch-evidence'
34
36
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
35
37
  import { readStamp, stampedHashes } from '@/sync/stamp'
36
- import { logError, logInfo, logStep, logWarn, pipeOutput, plural } from '@/ui'
38
+ import { logError, logInfo, logStep, logWarn, plural } from '@/ui'
37
39
 
38
- interface RenameOptions {
40
+ interface SweepOptions {
39
41
  readonly json?: boolean
40
42
  readonly write?: boolean
41
43
  readonly root?: string
44
+ }
45
+
46
+ interface RenameOptions extends SweepOptions {
42
47
  readonly scope?: string
43
48
  }
44
49
 
@@ -51,12 +56,11 @@ function isScope(value: string): value is Scope {
51
56
  }
52
57
 
53
58
  /**
54
- * Reports rather than writes without `--write`, matching `canon records
55
- * migrate`. A rename touching this many files has no undo short of the branch
56
- * it ran on, so the safe outcome sits on the default path.
59
+ * The `aitk` sweep, which is the one rename with a second population to name.
60
+ * A target holds toolkit-owned folders whose content came from here beside
61
+ * prose that project wrote, and the scope is what separates them.
57
62
  */
58
63
  async function runRename(opts: RenameOptions): Promise<number> {
59
- const root = opts.root ?? process.cwd()
60
64
  const scope = opts.scope ?? 'self'
61
65
 
62
66
  if (!isScope(scope)) {
@@ -64,6 +68,33 @@ async function runRename(opts: RenameOptions): Promise<number> {
64
68
  return 1
65
69
  }
66
70
 
71
+ return runSweep(opts, AITK_RULES, scope)
72
+ }
73
+
74
+ /**
75
+ * The skill rename owns no scope. Every folder it moves is authored here, and
76
+ * no target holds a copy of this repository's skill catalog, so there is no
77
+ * second population for a caller to name.
78
+ */
79
+ async function runSkillNames(opts: SweepOptions): Promise<number> {
80
+ return runSweep(opts, SKILL_NAME_RULES, undefined)
81
+ }
82
+
83
+ /**
84
+ * One sweep over a repository's tracked files under whichever rules it was
85
+ * handed.
86
+ *
87
+ * Reports rather than writes without `--write`, matching `canon records
88
+ * migrate`. A rename touching this many files has no undo short of the branch
89
+ * it ran on, so the safe outcome sits on the default path.
90
+ */
91
+ async function runSweep(
92
+ opts: SweepOptions,
93
+ rules: RenameRules,
94
+ scope: Scope | undefined,
95
+ ): Promise<number> {
96
+ const root = opts.root ?? process.cwd()
97
+
67
98
  const files = await listRepositoryFiles(root)
68
99
  if (files === undefined) {
69
100
  logError(`Could not list files under ${root}. Is it a git repository?`)
@@ -72,7 +103,7 @@ async function runRename(opts: RenameOptions): Promise<number> {
72
103
 
73
104
  const scoped = scope === 'target' ? files.filter(isToolkitOwned) : files
74
105
  const sources = await readSources(root, scoped)
75
- const plan = planRename(sources)
106
+ const plan = planRename(sources, rules)
76
107
 
77
108
  // A target scope reports the citations it did not rewrite, since prose the
78
109
  // project wrote is theirs to change and a sweep editing it underneath them
@@ -84,12 +115,15 @@ async function runRename(opts: RenameOptions): Promise<number> {
84
115
  root,
85
116
  files.filter((f) => !isToolkitOwned(f)),
86
117
  ),
118
+ rules,
87
119
  ).entries.length
88
120
  : 0
89
121
 
122
+ // stdout, so the record pipes clean. `pipeOutput` frames to stderr, which is
123
+ // where this command's report belongs and where a JSON record does not.
90
124
  if (opts.json) {
91
- pipeOutput(
92
- JSON.stringify(toRecord(plan, scope, citations, opts.write), null, 2),
125
+ process.stdout.write(
126
+ `${JSON.stringify(toRecord(plan, scope, citations, opts.write), null, 2)}\n`,
93
127
  )
94
128
  }
95
129
 
@@ -115,8 +149,12 @@ async function runRename(opts: RenameOptions): Promise<number> {
115
149
  return 0
116
150
  }
117
151
 
118
- function report(plan: RenamePlan, scope: Scope, citations: number): void {
119
- logInfo(`Scope ${scope}.`)
152
+ function report(
153
+ plan: RenamePlan,
154
+ scope: Scope | undefined,
155
+ citations: number,
156
+ ): void {
157
+ if (scope !== undefined) logInfo(`Scope ${scope}.`)
120
158
  logInfo(
121
159
  `${plural(plan.entries.length, 'file')} to change, ${plural(plan.renamed, 'occurrence')} to rewrite.`,
122
160
  )
@@ -138,12 +176,12 @@ function report(plan: RenamePlan, scope: Scope, citations: number): void {
138
176
 
139
177
  function toRecord(
140
178
  plan: RenamePlan,
141
- scope: Scope,
179
+ scope: Scope | undefined,
142
180
  citations: number,
143
181
  wrote: boolean | undefined,
144
182
  ): unknown {
145
183
  return {
146
- scope,
184
+ ...(scope === undefined ? {} : { scope }),
147
185
  wrote: wrote === true,
148
186
  files: plan.entries.length,
149
187
  renamed: plan.renamed,
@@ -851,6 +889,45 @@ export function register(program: Command): void {
851
889
  process.exitCode = await runRename(opts)
852
890
  })
853
891
 
892
+ migrate
893
+ .command('skill-names')
894
+ .description('Move the prefixed skill folders onto their two-word names')
895
+ .helpOption('-h, --help', 'Show this help message')
896
+ .option('--json', 'Add a machine-readable record on stdout')
897
+ .option('--write', 'Apply the plan rather than reporting it')
898
+ .option(
899
+ '--root <path>',
900
+ 'Project root, defaulting to the working directory',
901
+ )
902
+ .addHelpText(
903
+ 'after',
904
+ [
905
+ '',
906
+ `Rewrites ${Object.keys(SKILL_NAME_MAP).length} skill names and moves the folders that carry them.`,
907
+ 'It takes no scope. The skill folders it moves are authored in the',
908
+ 'toolkit and no target holds a copy of that catalog, so in a target it',
909
+ 'rewrites citations of a renamed skill and moves nothing.',
910
+ '',
911
+ 'Exit codes:',
912
+ ' 0 nothing to rewrite, or --write applied the whole plan',
913
+ ' 1 refused, or a move failed',
914
+ ' 2 a plan exists and --write was not passed',
915
+ '',
916
+ 'The changelog is never rewritten, and neither is an eval transcript.',
917
+ 'Each records what shipped or what a session ran under the name that',
918
+ 'was current then.',
919
+ '',
920
+ 'Examples:',
921
+ ' canon migrate skill-names',
922
+ ' canon migrate skill-names --write',
923
+ ' canon migrate skill-names --json',
924
+ '',
925
+ ].join('\n'),
926
+ )
927
+ .action(async (opts: SweepOptions) => {
928
+ process.exitCode = await runSkillNames(opts)
929
+ })
930
+
854
931
  migrate
855
932
  .command('rule-layout')
856
933
  .description('Move installed rules from the flat layout onto canon/')