@erclx/canon 4.110.0 → 4.112.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/assets/brand/mark.svg +10 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
- package/claude/skills/auto-ship/SKILL.md +12 -2
- package/claude/skills/canon-operator/REQUIREMENT.md +2 -2
- package/claude/skills/canon-operator/SKILL.md +3 -4
- package/claude/skills/git-pr/REQUIREMENT.md +2 -0
- package/claude/skills/git-pr/SKILL.md +21 -3
- package/claude/skills/target-check/references/domains.md +2 -2
- package/claude/skills/teach-workspace/SKILL.md +1 -1
- package/docs/agents/install-and-sync.md +4 -13
- package/docs/agents/teach.md +3 -1
- package/docs/target-projects.md +0 -2
- package/docs/workflow/ai-workflow.md +7 -11
- package/package.json +2 -1
- package/scripts/core/capture-home.sh +59 -0
- package/src/claude/cases/setup.ts +2 -23
- package/src/commands/sync.ts +0 -16
- package/src/design/components.ts +153 -152
- package/src/design/favicon.ts +8 -0
- package/src/design/tokens.ts +1 -1
- package/src/shipped/references.ts +1 -2
- package/src/sync/reverse.ts +3 -74
- package/src/teach/nav.ts +55 -13
- package/claude/skills/migration-claude-md/REQUIREMENT.md +0 -40
- package/claude/skills/migration-claude-md/SKILL.md +0 -79
- package/claude/skills/migration-context/REQUIREMENT.md +0 -36
- package/claude/skills/migration-context/SKILL.md +0 -97
- package/claude/skills/migration-standards-drop/REQUIREMENT.md +0 -55
- package/claude/skills/migration-standards-drop/SKILL.md +0 -113
- package/claude/skills/migration-superseded/REQUIREMENT.md +0 -44
- package/claude/skills/migration-superseded/SKILL.md +0 -115
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<!-- A bound ledger: two pages fanned open around a spine, drawn as three
|
|
2
|
+
solid masses rather than thin strokes, so the shape survives being
|
|
3
|
+
rendered at favicon size instead of averaging into grey. The 10-unit
|
|
4
|
+
gap between each page and the spine, against an 80-unit frame, is what
|
|
5
|
+
keeps the split a visible channel at 16px; a first draft at half that
|
|
6
|
+
gap closed into an unreadable blob at the same size. -->
|
|
7
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80">
|
|
8
|
+
<path d="M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z" fill="currentColor" />
|
|
9
|
+
<rect x="44" y="15" width="12" height="70" rx="2" fill="currentColor" />
|
|
10
|
+
</svg>
|
|
@@ -21,6 +21,7 @@ Review is the step that varies most. It gets skipped on a diff that needed one,
|
|
|
21
21
|
- Own the review receipt's lifetime, since this chain writes it, cites it in its own closing block, and is the only body that can read whether the step keeping it is still using it
|
|
22
22
|
- Delegate the ship sequence to `git-ship` rather than restating it, and name only what this chain adds to it
|
|
23
23
|
- Open the pull request as a draft before the continuous integration watch begins, since a pull request marked after it is mergeable for the length of the run
|
|
24
|
+
- Prove the draft target's head is the current branch and its state is open before marking it, and stop with nothing marked on a mismatch, since the number is retyped between steps and a draft mark on a release pull request holds that release
|
|
24
25
|
- Name the recovery for the stop it took, since the value of stopping is that the user knows where to resume
|
|
25
26
|
|
|
26
27
|
## Must not
|
|
@@ -190,14 +190,23 @@ This chain owns the receipt's lifetime, which is what makes the Output block's c
|
|
|
190
190
|
|
|
191
191
|
Invoke `canon:git-ship`. That body owns the sequence, being the verify gate, memory capture, both doc syncs, staging, the commit grouping, the branch rename, the pull request, and the CI watch, along with the reason each step sits where it does. This step used to restate that list and the two drifted apart with nothing comparing them, so read the order there and never here.
|
|
192
192
|
|
|
193
|
-
One thing this chain adds. Mark the pull request as a draft as soon as `git-ship`'s pull request step returns, ahead of its CI watch,
|
|
193
|
+
One thing this chain adds. Mark the pull request as a draft as soon as `git-ship`'s pull request step returns, ahead of its CI watch. Run it as one command, where the shell proves the target before the mark and reads the flag back after it:
|
|
194
194
|
|
|
195
195
|
```bash
|
|
196
|
+
target=$(gh pr view <number> --json headRefName,state --jq '"\(.headRefName) \(.state)"') || exit 1
|
|
197
|
+
if [ "$target" != "$(git branch --show-current) OPEN" ]; then
|
|
198
|
+
printf '❌ #%s reads "%s", not "%s OPEN". The draft mark was not written.\n' "<number>" "$target" "$(git branch --show-current)" >&2
|
|
199
|
+
exit 1
|
|
200
|
+
fi
|
|
196
201
|
gh pr ready --undo <number>
|
|
197
202
|
gh pr view <number> --json isDraft
|
|
198
203
|
```
|
|
199
204
|
|
|
200
|
-
Name the number `git-ship`'s pull request step returned
|
|
205
|
+
Name the number `git-ship`'s pull request step returned in every position rather than leaving any to resolve by branch. `${CLAUDE_SKILL_DIR}/../git-pr/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
|
|
206
|
+
|
|
207
|
+
A refusal is a stop. Report the line the shell printed, then resolve the number from the `number=` and `head=` lines of `git-pr`'s output and re-run the command by hand. The comparison sits in the shell rather than in the session's reading of a printed value, which is the same split `git-pr`'s `assert_own_pr` makes, because the judgment that picked a wrong number is the one that would read the check.
|
|
208
|
+
|
|
209
|
+
The number crosses from that step's output into a command this session types, which is the one place a number gets authored rather than derived. A number misread there, or inferred from the newest pull request in view, would draft a stranger's pull request. A release pull request is the worst such target, since the mark holds the release and this role may not lift it again.
|
|
201
210
|
|
|
202
211
|
Report what the read returned rather than what the command printed, since the exit says the call ran and says nothing about the state. A `true` reports a draft. A `false` reports the pull request as opened ready and unsupervised, and the chain stops there. Never re-issue the undo on a disagreeing read, which fights whoever readied it instead of guarding anything.
|
|
203
212
|
|
|
@@ -243,3 +252,4 @@ Every stop point leaves recoverable state. The user resumes manually from the ap
|
|
|
243
252
|
| Inherited review findings | Fix findings, run `/git-ship` |
|
|
244
253
|
| Self-introduced finding survived | Read the receipt for what the one repair pass left open, fix it, run `/git-ship` |
|
|
245
254
|
| git-ship fails | Inspect hook or remote error, run again |
|
|
255
|
+
| Draft target is not this branch's | Nothing was marked. Read the number and `head=` from `git-pr`'s output, confirm they match, then run the mark by hand |
|
|
@@ -26,8 +26,8 @@ The last failure is a section no route reaches. `## Route` maps an intent or a d
|
|
|
26
26
|
- Resolve every stack, rule, snippet, and standard name from a catalog at runtime
|
|
27
27
|
- Run the CLI non-interactively and report the command run, what changed, and the full path of anything written
|
|
28
28
|
- Read the overwrite contract from the body before any install or sync, and name each surface it lists as overwritten before running one
|
|
29
|
-
- Read the reverse walk beside the forward sections, so a folder the toolkit stopped shipping
|
|
30
|
-
-
|
|
29
|
+
- Read the reverse walk beside the forward sections, so a folder the toolkit stopped shipping reaches the user
|
|
30
|
+
- Name an unmigrated domain's root and install path for the user to move themselves, since no skill covers the move
|
|
31
31
|
- Route a measurement intent to the audit offers from the route table itself, rather than leaving the section reachable only by reading past the table
|
|
32
32
|
- Rank a lifecycle row against the audit offers where the route table itself states it, so a session acting on the lifecycle row reads the ranking without opening the section being ranked
|
|
33
33
|
- Offer every audit whose surface the target carries, and offer none whose surface it lacks
|
|
@@ -25,10 +25,10 @@ Run `canon sync --check . --json` before routing. It reports what a target is be
|
|
|
25
25
|
Read seven fields off the report and carry each to `## Route`:
|
|
26
26
|
|
|
27
27
|
- `unmigrated`: a domain sitting at the root layout with nothing under `.claude/`. The most urgent finding, because that domain reports no drift of its own while being entirely behind
|
|
28
|
-
- `superseded`: a file a newer seed folder replaced. No command moves it, since the content is the project's own, so this routes to
|
|
28
|
+
- `superseded`: a file a newer seed folder replaced. No command moves it, since the content is the project's own, so this routes to `canon:target-check` for the full per-domain reading rather than ending at the report
|
|
29
29
|
- `seeds`: entries are `matching`, `stale`, `drifted`, or `missing`. Anything but `matching` needs the seed handoff
|
|
30
30
|
- `domains[].entries`: per-file `stale`, `customized`, `stranded`, and `orphaned` as before
|
|
31
|
-
- `reverse`: what the target holds that no live catalog claims. `unclaimed` lists folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`. Act on the first two and leave `project` alone, which history proved the project owns
|
|
31
|
+
- `reverse`: what the target holds that no live catalog claims. `unclaimed` lists folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`. Act on the first two and leave `project` alone, which history proved the project owns
|
|
32
32
|
- `historyUnavailable` on a domain, on `seeds`, or on `reverse`: nothing could be dated, so treat every difference as unverified and say so rather than reporting a file as untouched. It is set on `reverse` when the toolkit itself ships without history, which is the registry-install case, so the walk found nothing rather than finding a clean target
|
|
33
33
|
- `tooling`: read `measured` first. Every count under it is zero when it is false, which is an absence of measurement rather than a measured zero. Past that, `chain` names the stacks the install resolved, nearest first, and `counts.gitignore` counts the managed ignore entries the target is missing.
|
|
34
34
|
|
|
@@ -43,8 +43,7 @@ Map the stated intent, or what `## Diagnose` found, to one lifecycle phase, then
|
|
|
43
43
|
The two can name different rows, and a reply answers both rather than picking one. Run or hand off what the finding names, then answer the intent in the same reply. A measurement intent therefore reaches the audit offers below even on a target whose diagnostic found scaffold work, which is the case a session leaving from the scaffold row would otherwise never read.
|
|
44
44
|
|
|
45
45
|
- A domain in `unmigrated`: name it, the root path, and the install path. No command moves it, so the user runs the relocation themselves
|
|
46
|
-
-
|
|
47
|
-
- Anything in `superseded`: name which files and what replaced them, then hand off to `migration-superseded`. Both skills propose, so neither moves nor deletes the file
|
|
46
|
+
- Anything in `superseded`: name which files and what replaced them, then hand off to `canon:target-check`, which reports and never moves or deletes the file
|
|
48
47
|
- A folder in `unclaimed`: name it and the attribution it carries. No command moves it and the content may be the project's own, so the decision is the user's
|
|
49
48
|
|
|
50
49
|
- First-time scaffold of a fresh project: hand off to `target-setup`
|
|
@@ -23,6 +23,8 @@ A lookup that resolves by head branch alone carries its own failure. A branch na
|
|
|
23
23
|
- Detect an open pull request and edit it in place, so a follow-up push keeps the body in sync instead of failing
|
|
24
24
|
- Scope that detection to an open pull request on the current head and the default base, so neither a reused branch name nor a second base resolves the wrong one
|
|
25
25
|
- Resolve the pull request once and reuse what that resolution returned, so the number recorded never depends on how a lookup ranks two pull requests sharing a head
|
|
26
|
+
- Prove before every write to an existing pull request that its head is the current branch and its state is open, and refuse with nothing written on a mismatch, so a wrong number reaching a write never lands on another branch's pull request
|
|
27
|
+
- Print the head branch beside the number, so a caller writing to the pull request later has a value to check the number against
|
|
26
28
|
- Label from the paths the branch changed, against a map the project declares, so the label set belongs to the project rather than to the skill
|
|
27
29
|
- Apply labels after the pull request exists, so a label the remote does not carry costs a warning rather than the pull request
|
|
28
30
|
- Report a refused label, since a warning nothing surfaces leaves the run indistinguishable from one that labelled
|
|
@@ -135,23 +135,41 @@ This command reuses `.canon/tmp/pr/body.md`, which the pre-publish scan above al
|
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
137
|
pr_labels="<comma-separated labels, empty when the map resolves to nothing>"
|
|
138
|
+
head_branch=$(git branch --show-current)
|
|
138
139
|
git push -u origin HEAD || exit 1
|
|
139
140
|
base_branch=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) || exit 1
|
|
140
|
-
|
|
141
|
+
assert_own_pr() {
|
|
142
|
+
target=$(gh pr view "$1" --json headRefName,state --jq '"\(.headRefName) \(.state)"') || exit 1
|
|
143
|
+
if [ "$target" != "$head_branch OPEN" ]; then
|
|
144
|
+
printf 'Refused: #%s reads "%s", not "%s OPEN". Nothing was written to it.\n' "$1" "$target" "$head_branch" >&2
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
}
|
|
148
|
+
pr_number=$(gh pr list --head "$head_branch" --base "$base_branch" --state open --json number --jq '.[0].number // empty')
|
|
141
149
|
if [ -n "$pr_number" ]; then
|
|
150
|
+
assert_own_pr "$pr_number"
|
|
142
151
|
pr_url=$(gh pr edit "$pr_number" --title "<title>" --body-file .canon/tmp/pr/body.md) || exit 1
|
|
143
152
|
else
|
|
144
153
|
pr_url=$(gh pr create --title "<title>" --body-file .canon/tmp/pr/body.md) || exit 1
|
|
145
154
|
pr_number=${pr_url##*/}
|
|
155
|
+
assert_own_pr "$pr_number"
|
|
146
156
|
fi
|
|
147
157
|
if [ -n "$pr_labels" ]; then
|
|
148
158
|
gh pr edit "$pr_number" --add-label "$pr_labels" >/dev/null ||
|
|
149
159
|
printf 'Label apply failed. Create a missing label with: gh label create <name>\n' >&2
|
|
150
160
|
fi
|
|
151
161
|
rm -rf .canon/tmp/pr/body
|
|
152
|
-
printf 'number=%s\nurl=%s\n' "$pr_number" "$pr_url"
|
|
162
|
+
printf 'number=%s\nurl=%s\nhead=%s\n' "$pr_number" "$pr_url" "$head_branch"
|
|
153
163
|
```
|
|
154
164
|
|
|
165
|
+
### Binding every write to this branch's pull request
|
|
166
|
+
|
|
167
|
+
`assert_own_pr` runs ahead of both writes this command makes to a pull request that already exists, the title and body edit and the label edit. It reads the target's head branch and state and refuses unless they are this branch and `OPEN`. A refusal exits before anything is written, so a wrong number costs the run rather than a stranger's pull request. The function lives only inside this command, so the steps below that post comments or record the task are covered by taking `<number>` from this output rather than by the check itself.
|
|
168
|
+
|
|
169
|
+
The check compares a number against a branch and never derives a number from one, so it adds no lookup of the kind `### Resolving the pull request` retired. It holds whichever way a wrong number arrives. A number a session retyped by hand, or inferred from the newest pull request in view, reads as a foreign head here and stops.
|
|
170
|
+
|
|
171
|
+
The last output line carries `head=` so a caller relaying the number holds a branch to compare it against rather than a bare integer. A caller that writes to the pull request itself, such as a draft mark, runs the same comparison in the shell first, reading `gh pr view <number> --json headRefName,state` and refusing unless it matches `head` and `OPEN`.
|
|
172
|
+
|
|
155
173
|
### Find the UI checklist
|
|
156
174
|
|
|
157
175
|
`ui-checklist` writes a visual checklist to `.canon/tmp/handoff/ui-checklist/<slug>.md` at the main worktree root when a change needs visual verification, with `<slug>` derived per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. This skill is the file's sole consumer. Resolve the main root the way `session-worktree` does (`git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`, falling back to `pwd`) and check for the file there. A missing file means no checklist was produced, and the two steps below each skip their checklist half.
|
|
@@ -238,7 +256,7 @@ canon pr evidence <number> --preview <url> --json
|
|
|
238
256
|
|
|
239
257
|
### Record the number on the task
|
|
240
258
|
|
|
241
|
-
Write the `number` the final command printed onto the task the branch is closing. Do not resolve it again. `${CLAUDE_SKILL_DIR}/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
|
|
259
|
+
Write the `number` the final command printed onto the task the branch is closing. Do not resolve it again. A final command that refused on a head mismatch printed no number, so this step writes nothing on that run. `${CLAUDE_SKILL_DIR}/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
|
|
242
260
|
|
|
243
261
|
The task is the one whose `Plan:` line names the plan this branch implemented. Name that plan by its file, which is `.canon/plans/feature-<slug>.md` at the main worktree root with `<slug>` derived per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. `plan-feature` writes the plan under the branch slug, so the two correspond on any branch that came through the plan-to-execute path. When the session already knows which plan it implemented, because a caller read it earlier in the chain, use that filename instead of re-deriving.
|
|
244
262
|
|
|
@@ -25,7 +25,7 @@ Read three fields off the same report, then two verbs:
|
|
|
25
25
|
|
|
26
26
|
- `unmigrated`: a domain still at the root layout with nothing under `.claude/`. The most urgent finding, because such a domain reports no drift of its own while being entirely behind.
|
|
27
27
|
- `superseded`: a file a newer seed folder replaced. No command moves it, since the content is the project's own.
|
|
28
|
-
- `reverse`: `unclaimed` names folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`.
|
|
28
|
+
- `reverse`: `unclaimed` names folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`.
|
|
29
29
|
- `canon context audit --json`: required sections, entry length, citations, and index drift across the context folder.
|
|
30
30
|
- `canon records validate <kind> --json`, once per record folder the target carries.
|
|
31
31
|
|
|
@@ -33,7 +33,7 @@ Read three fields off the same report, then two verbs:
|
|
|
33
33
|
- Behind: any of those carrying an entry. Name the entry and its attribution.
|
|
34
34
|
- Unread: `historyUnavailable` set on `reverse`, a `reverse` key the report does not carry at all, or either verb refusing. A missing `reverse` key is a binary predating the field rather than a target with nothing unclaimed.
|
|
35
35
|
|
|
36
|
-
Routes by finding. `unmigrated` routes to a relocation the user runs, since no command moves it. `superseded` routes to
|
|
36
|
+
Routes by finding. `unmigrated` routes to a relocation the user runs, since no command moves it. `superseded` routes to a split the user makes, since no command moves the content and no skill proposes it. An `unclaimed` folder carrying `project` attribution routes nowhere, since history proved the project owns it.
|
|
37
37
|
|
|
38
38
|
## 3. Governance rules
|
|
39
39
|
|
|
@@ -18,7 +18,7 @@ The shape of the workspace is fixed by `${CLAUDE_SKILL_DIR}/../../standards/teac
|
|
|
18
18
|
- Write nothing outside the workspace folder, apart from the one handoff file Step 6 names. A durable page stays in `reference/` and is copied out by the skill that owns the destination, never by this one.
|
|
19
19
|
- Do not open a second workspace on a subject one already covers. Resume that one.
|
|
20
20
|
- Never promote a lesson. It is generated markup carrying a quiz and a learner, and no request makes it promotable.
|
|
21
|
-
-
|
|
21
|
+
- Never write a favicon link by hand. `canon teach nav` puts the toolkit's icon on every page it generates and inside a lesson's `style` region, so a hand-written one in a lesson's `<head>` gives the page two.
|
|
22
22
|
|
|
23
23
|
## Step 0: let the CLI resolve the workspace root
|
|
24
24
|
|
|
@@ -256,11 +256,9 @@ seed tree rather than from a fixed set of filenames, so a folder added later is
|
|
|
256
256
|
covered without a code change. Only an exact stem matches, which leaves a
|
|
257
257
|
suffixed variant such as `TASKS-ARCHIVE.md` unreported.
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
`
|
|
261
|
-
destination shape
|
|
262
|
-
writing. Where a folder has no governing standard, the entry earns a named
|
|
263
|
-
refusal rather than a shape nobody stated.
|
|
259
|
+
No skill splits the file. The standard governing `replacedBy` is the one the
|
|
260
|
+
`appliesTo` the standards catalog declares, and `canon standards <name>` reads
|
|
261
|
+
its destination shape, so the split is the project's own to make.
|
|
264
262
|
|
|
265
263
|
`unmigrated` names a domain sitting at the root layout an older toolkit installed
|
|
266
264
|
to, with nothing at the path the current one reads. It carries `rootPath`,
|
|
@@ -350,7 +348,7 @@ check.
|
|
|
350
348
|
`reverse` is the one section built by walking the target rather than the
|
|
351
349
|
catalog. Every other surface enumerates toolkit-owned keys and asks whether the
|
|
352
350
|
target matches, so a folder the toolkit deleted appears in none of them. It
|
|
353
|
-
carries `unclaimed
|
|
351
|
+
carries `unclaimed` and `historyUnavailable`.
|
|
354
352
|
|
|
355
353
|
`unclaimed` names a folder the target holds at a top-level path the toolkit once
|
|
356
354
|
shipped and has since deleted. The candidate roots come from the toolkit's own
|
|
@@ -381,13 +379,6 @@ colliding on a retired name costs the walk no reads. The cost is that a file the
|
|
|
381
379
|
toolkit shipped and the target renamed goes unmatched, the same limit the
|
|
382
380
|
`unmigrated` count carries.
|
|
383
381
|
|
|
384
|
-
`migrations` names a proposal-only skill with a live case in this target. It
|
|
385
|
-
fires on a `CLAUDE.md` past 250 lines for `migration-claude-md`, and on a `docs/` folder
|
|
386
|
-
holding markdown with no populated `canon/context/` for `migration-context`.
|
|
387
|
-
Each entry carries the skill name and the measurement behind it, so a consumer
|
|
388
|
-
can check the proposal before running it. Without the field both skills are
|
|
389
|
-
documented and unreachable from any report.
|
|
390
|
-
|
|
391
382
|
#### What counts toward the gate
|
|
392
383
|
|
|
393
384
|
`unmigrated` counts toward `--exit-code`, since running the relocation closes it.
|
package/docs/agents/teach.md
CHANGED
|
@@ -172,7 +172,9 @@ canon teach nav regular-expressions --json
|
|
|
172
172
|
|
|
173
173
|
With no topic it rewrites every workspace. The teach-root listing is always rewritten regardless, since it reports on every workspace and a scoped run leaving it stale would be a second kind of drift this verb exists to end.
|
|
174
174
|
|
|
175
|
-
A lesson carries its chrome as four marker pairs the authoring skill writes empty: `canon:teach:style`, `canon:teach:header`, `canon:teach:footnav`, and `canon:teach:scripts`. This verb splices each one from the current workspace state, embedding the shared stylesheet and the script that settles the course panel before the page paints, rebuilding the course sidebar, the breadcrumb and jump menus, the prev/next footer nav, and the behavior scripts, and leaves the authored `<h1>`, lede, body, and quiz between the header and the footnav untouched.
|
|
175
|
+
A lesson carries its chrome as four marker pairs the authoring skill writes empty: `canon:teach:style`, `canon:teach:header`, `canon:teach:footnav`, and `canon:teach:scripts`. This verb splices each one from the current workspace state, embedding the toolkit's favicon, the shared stylesheet, and the script that settles the course panel before the page paints, rebuilding the course sidebar, the breadcrumb and jump menus, the prev/next footer nav, and the behavior scripts, and leaves the authored `<h1>`, lede, body, and quiz between the header and the footnav untouched.
|
|
176
|
+
|
|
177
|
+
It also deletes the one icon link the authoring skill used to have a session write by hand into a lesson's `<head>`, matched as that exact string, so the page keeps only the icon the `style` region carries. Any other link an author wrote stays.
|
|
176
178
|
|
|
177
179
|
A workspace carrying no `course.css` gets its stylesheet pair seeded first, so the contents page it links is never unstyled, and an existing `course.css` is never overwritten. The embedded copy drops the seed's `@import` line, which resolved against the lesson's own folder and never loaded anything.
|
|
178
180
|
|
package/docs/target-projects.md
CHANGED
|
@@ -261,8 +261,6 @@ Each entry carries a verdict, since a dropped folder and one the project wrote a
|
|
|
261
261
|
|
|
262
262
|
Nothing acts on any of them, and the verdict is what makes the list safe to read.
|
|
263
263
|
|
|
264
|
-
The same field names a proposal-only skill with a live case here under `migrations`, which is how `canon:migration-claude-md` and `canon:migration-context` become reachable. Each entry carries the measurement behind the proposal rather than the proposal alone.
|
|
265
|
-
|
|
266
264
|
Add `--json` for the machine-readable report, and `--exit-code` to fail a CI job when a target falls behind. Files the project authored itself never count toward that exit code, and neither do superseded artifacts, seed drift, tooling, or anything the reverse walk reports, since each names content the project is expected to edit or place itself. An unmigrated domain does count, because running the relocation closes it.
|
|
267
265
|
|
|
268
266
|
Tooling reports under a section of its own, and `measured` there says whether the target ever recorded a chain. One that never ran a tooling sync reports unmeasured rather than clean, which is what separates tooling nobody has looked at from tooling that is current. A workspace root records nothing either way, since each package resolves its own chain.
|
|
@@ -285,17 +285,13 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
285
285
|
|
|
286
286
|
### Keep the project current with the toolkit
|
|
287
287
|
|
|
288
|
-
| Skill
|
|
289
|
-
|
|
|
290
|
-
| `canon:target-check`
|
|
291
|
-
| `canon:seed-sync`
|
|
292
|
-
| `canon:
|
|
293
|
-
| `canon:
|
|
294
|
-
| `canon:
|
|
295
|
-
| `canon:migration-standards-drop` | When the project still holds an installed `.claude/standards/` tree |
|
|
296
|
-
| `canon:canon-feedback` | When something in the toolkit is broken, missing, or off, to open an issue on it |
|
|
297
|
-
| `canon:canon-feedback-triage` | In the toolkit repo, to work through the open feedback issues |
|
|
298
|
-
| `canon:canon-rollout` | In the toolkit repo, to take one change out to every consuming project at once |
|
|
288
|
+
| Skill | When to use |
|
|
289
|
+
| ----------------------------- | -------------------------------------------------------------------------------------- |
|
|
290
|
+
| `canon:target-check` | In a target project, to report per domain what it holds against what the toolkit ships |
|
|
291
|
+
| `canon:seed-sync` | After a toolkit update, to reconcile installed seeds without losing customizations |
|
|
292
|
+
| `canon:canon-feedback` | When something in the toolkit is broken, missing, or off, to open an issue on it |
|
|
293
|
+
| `canon:canon-feedback-triage` | In the toolkit repo, to work through the open feedback issues |
|
|
294
|
+
| `canon:canon-rollout` | In the toolkit repo, to take one change out to every consuming project at once |
|
|
299
295
|
|
|
300
296
|
### Generate an artifact on demand
|
|
301
297
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erclx/canon",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.112.0",
|
|
5
5
|
"description": "Infrastructure and quality tooling for developer workflows",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"claude",
|
|
18
18
|
"docs",
|
|
19
19
|
"tsconfig.json",
|
|
20
|
+
"assets/brand/mark.svg",
|
|
20
21
|
"!scripts/sandbox",
|
|
21
22
|
"!scripts/eval",
|
|
22
23
|
"!**/*.test.ts",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Builds the landing page, captures it, and stages the frames over
|
|
3
|
+
# assets/evidence/home, the baseline `canon pr evidence` compares against.
|
|
4
|
+
#
|
|
5
|
+
# Two workflows call it. pr-visual-checks.yml stages the frames into the runner
|
|
6
|
+
# tree to diff them against the baseline, and refresh-capture-frames.yml stages
|
|
7
|
+
# them to commit as the new baseline. Both need the same frames, so one script
|
|
8
|
+
# keeps the two from drifting apart.
|
|
9
|
+
#
|
|
10
|
+
# `tooling/web/configs/e2e/screenshot.ts` is reused as is. This checkout has no
|
|
11
|
+
# web/package.json, so the preview server and the cases run against the
|
|
12
|
+
# root-level scripts rather than through screenshot.sh, which assumes a
|
|
13
|
+
# per-project `bun run build`/`bun run preview` pair.
|
|
14
|
+
set -e
|
|
15
|
+
set -o pipefail
|
|
16
|
+
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
19
|
+
|
|
20
|
+
PORT=4321
|
|
21
|
+
BASE_URL="http://localhost:$PORT"
|
|
22
|
+
BASELINE="$PROJECT_ROOT/assets/evidence/home"
|
|
23
|
+
|
|
24
|
+
cd "$PROJECT_ROOT"
|
|
25
|
+
|
|
26
|
+
bun run web:build
|
|
27
|
+
|
|
28
|
+
bun run web:preview &
|
|
29
|
+
PREVIEW_PID=$!
|
|
30
|
+
trap 'kill "$PREVIEW_PID" 2>/dev/null || true' EXIT
|
|
31
|
+
|
|
32
|
+
for _ in $(seq 1 40); do
|
|
33
|
+
curl -sSf "$BASE_URL/" >/dev/null 2>&1 && break
|
|
34
|
+
sleep 0.25
|
|
35
|
+
done
|
|
36
|
+
curl -sSf "$BASE_URL/" >/dev/null 2>&1 || {
|
|
37
|
+
echo "Preview server not responding on port $PORT after 10s." >&2
|
|
38
|
+
exit 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
(cd web && SCREENSHOT_BASE_URL="$BASE_URL" bun ../tooling/web/configs/e2e/screenshot.ts)
|
|
42
|
+
|
|
43
|
+
# The committed tree is cleared first rather than copied over. A copy only adds
|
|
44
|
+
# and overwrites, so a section dropped from the case list would leave its
|
|
45
|
+
# frames in place and a diff would see no change. Clearing first shows that
|
|
46
|
+
# section as a deletion.
|
|
47
|
+
rm -rf "$BASELINE"
|
|
48
|
+
mkdir -p "$BASELINE"
|
|
49
|
+
cp -R web/screenshots/localhost/home/. "$BASELINE/"
|
|
50
|
+
|
|
51
|
+
# The sweep holds all four widths and this repository commits two, 320 and
|
|
52
|
+
# 1280. The case list is the place to change if that set moves.
|
|
53
|
+
find "$BASELINE" \( -name '768--*.png' -o -name '1536--*.png' \) -delete
|
|
54
|
+
|
|
55
|
+
# The shared golden config names the case that emulates nothing `default`, and
|
|
56
|
+
# this repository's baseline names that frame `light`.
|
|
57
|
+
find "$BASELINE" -name '*--default.png' -execdir sh -c 'mv "$1" "${1%--default.png}--light.png"' _ {} \;
|
|
58
|
+
|
|
59
|
+
echo "capture-home: staged frames in assets/evidence/home"
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { SkillCase } from '@/claude/skills-rank'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* `target-setup`, `
|
|
5
|
-
*
|
|
6
|
-
* surfaces.
|
|
4
|
+
* `target-setup`, `canon-*`, and `create-rule`: scaffolding and the
|
|
5
|
+
* toolkit's own reference and feedback surfaces.
|
|
7
6
|
*
|
|
8
7
|
* The five `target-setup` cases are one skill read through five entry phrasings
|
|
9
8
|
* rather than five rows that collapsed to a duplicate. Each names a different
|
|
@@ -36,26 +35,6 @@ export const SETUP_CASES: readonly SkillCase[] = [
|
|
|
36
35
|
'Check that the dev server actually starts and the end-to-end suite passes against the scaffold.',
|
|
37
36
|
expect: 'target-setup',
|
|
38
37
|
},
|
|
39
|
-
{
|
|
40
|
-
prompt:
|
|
41
|
-
'This CLAUDE.md file has grown huge, break it apart into the tiered context model.',
|
|
42
|
-
expect: 'migration-claude-md',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
prompt:
|
|
46
|
-
'Move the agent-flavored docs out of the docs folder and into context.',
|
|
47
|
-
expect: 'migration-context',
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
prompt:
|
|
51
|
-
'This file was replaced by a folder, help me split its content into it.',
|
|
52
|
-
expect: 'migration-superseded',
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
prompt:
|
|
56
|
-
'This project still carries a copy of the standards folder, get rid of it and point everything at the CLI.',
|
|
57
|
-
expect: 'migration-standards-drop',
|
|
58
|
-
},
|
|
59
38
|
{
|
|
60
39
|
prompt:
|
|
61
40
|
"Before I run this sync, tell me exactly what it's going to overwrite.",
|
package/src/commands/sync.ts
CHANGED
|
@@ -189,7 +189,6 @@ function renderCheck(report: CheckReport): void {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
renderUnclaimed(report)
|
|
192
|
-
renderMigrations(report)
|
|
193
192
|
|
|
194
193
|
outro()
|
|
195
194
|
// Scanned domains only. Tooling renders a section on every managed target, so
|
|
@@ -334,21 +333,6 @@ function describeUnclaimed(entry: UnclaimedFolder): string {
|
|
|
334
333
|
return `${counted}, content published at ${entry.since.slice(0, 7)}`
|
|
335
334
|
}
|
|
336
335
|
|
|
337
|
-
/**
|
|
338
|
-
* The proposal-only skills no other field reaches.
|
|
339
|
-
*/
|
|
340
|
-
function renderMigrations(report: CheckReport): void {
|
|
341
|
-
const { migrations } = report.reverse
|
|
342
|
-
if (migrations.length === 0) return
|
|
343
|
-
|
|
344
|
-
logStep('Migrations with a case here')
|
|
345
|
-
|
|
346
|
-
for (const candidate of migrations) {
|
|
347
|
-
logWarn(candidate.reason)
|
|
348
|
-
logInfo(`Run /canon:${candidate.skill} for a proposal.`)
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
336
|
async function runSync(target: string): Promise<number> {
|
|
353
337
|
intro('canon sync')
|
|
354
338
|
|