@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/docs/agents/commands.md +4 -1
- package/docs/target-projects.md +19 -0
- package/package.json +1 -1
- package/src/commands/migrate.ts +90 -13
- package/src/design/components.ts +1157 -4
- package/src/design/css.ts +44 -17
- package/src/migrate/plan.ts +13 -5
- package/src/migrate/rename.ts +209 -94
- package/src/migrate/skill-names.ts +89 -0
- package/src/teach/fonts.ts +28 -0
- package/src/teach/nav.ts +11 -1
- package/src/teach/workspace.ts +4 -1
package/docs/agents/commands.md
CHANGED
|
@@ -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.
|
package/docs/target-projects.md
CHANGED
|
@@ -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
package/src/commands/migrate.ts
CHANGED
|
@@ -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,
|
|
38
|
+
import { logError, logInfo, logStep, logWarn, plural } from '@/ui'
|
|
37
39
|
|
|
38
|
-
interface
|
|
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
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
-
|
|
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(
|
|
119
|
-
|
|
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/')
|