@erclx/canon 4.41.0 → 4.43.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/claude/skills/canon-cli/SKILL.md +1 -0
- package/claude/skills/claude-autoship/SKILL.md +1 -1
- package/claude/skills/claude-docs/SKILL.md +1 -1
- package/claude/skills/claude-ui-test/REQUIREMENT.md +4 -1
- package/claude/skills/claude-ui-test/SKILL.md +3 -3
- package/claude/skills/git-pr/SKILL.md +28 -0
- package/docs/workflow/ai-workflow.md +1 -1
- package/package.json +1 -1
- package/tooling/astro/configs/src/components/dev/scenarios.astro +127 -0
- package/tooling/astro/reference.md +7 -0
|
@@ -91,7 +91,7 @@ Do not loop. Do not bypass hooks.
|
|
|
91
91
|
|
|
92
92
|
If the diff touches UI files (JSX, TSX, Vue, Svelte, HTML, or CSS under `src/`), invoke `canon:claude-ui-test`.
|
|
93
93
|
|
|
94
|
-
If `claude-ui-test` produces a manual checklist, stop: `❌ UI requires visual verification. Checklist at .canon/
|
|
94
|
+
If `claude-ui-test` produces a manual checklist, stop: `❌ UI requires visual verification. Checklist at .canon/tmp/ui-checklist/<slug>.md, which reaches the pull request once /git-ship runs. Verify manually, then run /git-ship.`
|
|
95
95
|
|
|
96
96
|
If all UI changes are covered by e2e tests, continue.
|
|
97
97
|
|
|
@@ -235,7 +235,7 @@ For each receipt, count the H2 items still marked 📝 pending:
|
|
|
235
235
|
|
|
236
236
|
That standard owns what a fold writes and which entry types take one. `claude-memory-review` collects a receipt on the same rule, so neither body restates it.
|
|
237
237
|
|
|
238
|
-
Do not sweep `
|
|
238
|
+
Do not sweep `ux-audit-*.md` or `ux-measure-*.md` (standalone deliverables). Those sit at `.canon/review/` itself rather than under a producer folder, so the two globs above never reach them.
|
|
239
239
|
|
|
240
240
|
Output one line per file swept:
|
|
241
241
|
|
|
@@ -11,13 +11,15 @@ Without this skill, UI work ships on the confidence of the session that wrote it
|
|
|
11
11
|
|
|
12
12
|
The visual half is where the record is lost. A checklist printed into chat scrolls away before anyone verifies it, so the ship step has nothing to gate on and the work reads as verified because a list was produced. Written from a linked worktree against `pwd`, the file lands where the caller does not look.
|
|
13
13
|
|
|
14
|
+
Writing the checklist correctly to disk does not close the gap either. The file sits under a gitignored folder on the machine that authored it, so a pull request reviewer, on that machine or another, never sees it. A record that only the author can open reads as verified for the same reason a record that never existed does.
|
|
15
|
+
|
|
14
16
|
## Must
|
|
15
17
|
|
|
16
18
|
- Split every change into automatable and visual-only before writing anything
|
|
17
19
|
- Read the project's existing test config and patterns before writing a test against them
|
|
18
20
|
- Assert a user action and its outcome per test, covering the happy path and the key edge cases
|
|
19
21
|
- Run the tests after writing them and fix what fails
|
|
20
|
-
- Write a produced checklist to the
|
|
22
|
+
- Write a produced checklist to the `.canon/tmp/ui-checklist/<slug>.md` handoff at the main worktree root, overwriting
|
|
21
23
|
- Report that everything is covered rather than manufacturing a checklist to show work
|
|
22
24
|
|
|
23
25
|
## Must not
|
|
@@ -26,6 +28,7 @@ The visual half is where the record is lost. A checklist printed into chat scrol
|
|
|
26
28
|
- Re-test what unit or component tests written during implementation already cover
|
|
27
29
|
- Repeat the full checklist in chat, which is what made it evaporate
|
|
28
30
|
- Stage or commit the checklist, which is gitignored scratch
|
|
31
|
+
- Talk to GitHub directly. Posting the checklist to a pull request belongs to `git-pr`, the sole consumer of the handoff file
|
|
29
32
|
|
|
30
33
|
## Guards
|
|
31
34
|
|
|
@@ -59,19 +59,19 @@ If all changes are automatable, skip the manual checklist:
|
|
|
59
59
|
|
|
60
60
|
Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. Fall back to `latest` on an empty result.
|
|
61
61
|
|
|
62
|
-
When a manual checklist is produced, write it directly to `.canon/
|
|
62
|
+
When a manual checklist is produced, write it directly to `.canon/tmp/ui-checklist/<slug>.md` at the main worktree root, not the current worktree. Resolve that root the way `claude-worktree` does. Create the directory if it does not exist. Always overwrite. This is a handoff file rather than a deliverable: `git-pr` posts it as a pull request comment once one opens, then removes it, and nothing here talks to `gh` directly.
|
|
63
63
|
|
|
64
64
|
From a linked worktree the file-editing tools refuse that path, so the checklist goes out through `Bash`. Send the `mkdir -p` and the heredoc as two plain commands rather than joining them with `&&`, which is refused as compound.
|
|
65
65
|
|
|
66
66
|
Skip the file write when all changes are covered by e2e tests and no checklist was produced.
|
|
67
67
|
|
|
68
|
-
The `.canon/
|
|
68
|
+
The `.canon/tmp/` directory is gitignored. Do not stage or commit the file.
|
|
69
69
|
|
|
70
70
|
## Output order
|
|
71
71
|
|
|
72
72
|
1. Write and run e2e tests (report pass/fail)
|
|
73
73
|
2. If a manual checklist was produced, write it to file, then output only the file path in chat:
|
|
74
|
-
`📝 Wrote .canon/
|
|
74
|
+
`📝 Wrote .canon/tmp/ui-checklist/<slug>.md`
|
|
75
75
|
3. If no checklist was needed: `✅ All changes covered by e2e tests. No manual verification needed.`
|
|
76
76
|
|
|
77
77
|
Do not repeat the full checklist in chat.
|
|
@@ -135,6 +135,30 @@ rm -rf .canon/tmp/pr
|
|
|
135
135
|
printf 'number=%s\nurl=%s\n' "$pr_number" "$pr_url"
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
### Post the UI checklist
|
|
139
|
+
|
|
140
|
+
`claude-ui-test` writes a manual checklist to `.canon/tmp/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 step is the file's sole consumer. Resolve the main root the way `claude-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 there is nothing to post.
|
|
141
|
+
|
|
142
|
+
When it exists, scan it against `${CLAUDE_SKILL_DIR}/../../standards/publish.md` before posting, the same as the pull request body above. Post it as its own comment on `<number>`, the number the final command above resolved, rather than folding it into the body, since a later push editing the body would overwrite checkboxes a reviewer already ticked:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
gh pr comment <number> --body-file <main-root>/.canon/tmp/ui-checklist/<slug>.md
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Run the cleanup below only once that call reports success. On a failure, stop and leave the file in place: a retry needs the checklist to still be there, and deleting it on a failed post loses the only copy with nothing landed on the pull request.
|
|
149
|
+
|
|
150
|
+
From a linked worktree the file-editing tools refuse a main-root path, so the cleanup goes out through `Bash` as two plain commands, the file and then the folder, rather than joined by `&&`, which is refused as compound:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
rm <main-root>/.canon/tmp/ui-checklist/<slug>.md
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
rmdir <main-root>/.canon/tmp/ui-checklist 2>/dev/null || true
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The `rmdir` is a no-op when another branch's pending checklist still sits in the folder, which keeps this step from deleting a handoff that is not its own.
|
|
161
|
+
|
|
138
162
|
### Record the number on the task
|
|
139
163
|
|
|
140
164
|
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.
|
|
@@ -167,4 +191,8 @@ Add a further line only when the labelling command printed its warning, quoting
|
|
|
167
191
|
|
|
168
192
|
`⚠️ Labels not applied: <what gh reported>`
|
|
169
193
|
|
|
194
|
+
Add a line when the UI checklist step posted a comment, naming the pull request it landed on:
|
|
195
|
+
|
|
196
|
+
`📋 Posted the UI checklist to <number>.`
|
|
197
|
+
|
|
170
198
|
Do not add any other text.
|
|
@@ -78,7 +78,7 @@ When features are independent, run them in parallel instead of sequentially. Use
|
|
|
78
78
|
|
|
79
79
|
- Create a worktree per feature, then start a Claude Code session in each
|
|
80
80
|
- Invoke `canon:claude-feature` in each session. Plans land at the main worktree root as `.canon/plans/feature-<slug>.md`, one per feature, no collisions. Small features stay in chat and skip the file.
|
|
81
|
-
- Implement, verify, and review each feature independently. `claude-review`
|
|
81
|
+
- Implement, verify, and review each feature independently. `claude-review` writes a per-branch report at the main worktree root (`review/branch/review-<slug>.md`), and `claude-ui-test` writes a per-branch checklist handoff there too (`tmp/ui-checklist/<slug>.md`) that `git-pr` posts to the pull request and removes, so parallel sessions do not overwrite each other. The slug is the branch name with any leading type segment dropped, so `feat/jwt-expiration` and the plan at `feature-jwt-expiration.md` meet on one name
|
|
82
82
|
- Ship each worktree separately with `canon:git-ship`
|
|
83
83
|
- For full autonomy per worktree, invoke `canon:claude-autoship` instead of the manual chain. Approve the plan, walk away, come back to a pull request the chain marked as a draft and then read the flag back on. The mark says the work has had no review yet, and it holds no window, since readying a pull request to merge lifts it and is the operator's act.
|
|
84
84
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Serves candidate treatments of one decision from the running page, selected
|
|
4
|
+
* by a query parameter, with a switcher for moving between them.
|
|
5
|
+
*
|
|
6
|
+
* It exists because a decision about how something feels to cause cannot be
|
|
7
|
+
* settled from a capture or a recording, both of which are passive, and the
|
|
8
|
+
* operator has to drive the candidates themselves. Five decisions were served
|
|
9
|
+
* that way in one run, each with a hand-written parameter, switcher, and
|
|
10
|
+
* removal, none of which resembled the last.
|
|
11
|
+
*
|
|
12
|
+
* Renders nothing in a production build, and nothing in development either
|
|
13
|
+
* until the page is asked for an arm by name, so a page loaded without the
|
|
14
|
+
* parameter is what ships.
|
|
15
|
+
*
|
|
16
|
+
* An arm carries CSS when the decision is a treatment. When it is a value the
|
|
17
|
+
* page reads at runtime, a pace or a shader uniform, the arm carries no CSS
|
|
18
|
+
* and the module holding that value reads the active id off
|
|
19
|
+
* `document.documentElement.dataset[param]`.
|
|
20
|
+
*/
|
|
21
|
+
interface Arm {
|
|
22
|
+
/** Appears in the URL. Use `0` for what ships, so the baseline is an arm. */
|
|
23
|
+
readonly id: string
|
|
24
|
+
/** Names the arm and what it costs, since an arm with no cost is not one. */
|
|
25
|
+
readonly label: string
|
|
26
|
+
readonly css?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Props {
|
|
30
|
+
/** The query parameter, which is also the dataset key an arm is read from. */
|
|
31
|
+
readonly param: string
|
|
32
|
+
readonly arms: readonly Arm[]
|
|
33
|
+
/**
|
|
34
|
+
* Where the switcher mounts, as a selector. A decision inside a modal needs
|
|
35
|
+
* its switcher inside that modal, since anything outside it sits under the
|
|
36
|
+
* backdrop. Defaults to the body.
|
|
37
|
+
*/
|
|
38
|
+
readonly mountInto?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { param, arms, mountInto } = Astro.props
|
|
42
|
+
|
|
43
|
+
// The component leaves the production tree rather than gating itself at
|
|
44
|
+
// runtime, so neither an arm nor the parameter can reach a reader.
|
|
45
|
+
const enabled = import.meta.env.DEV
|
|
46
|
+
|
|
47
|
+
// Held as a string and written with `set:html`. Inside an Astro expression a
|
|
48
|
+
// script's children are parsed the way JSX children are, so every brace in the
|
|
49
|
+
// source reads as an interpolation and the body ships as an unevaluated
|
|
50
|
+
// literal. It did, once, silently.
|
|
51
|
+
const SOURCE = `
|
|
52
|
+
const wanted = new URLSearchParams(location.search).get(param)
|
|
53
|
+
const active = arms.find((arm) => arm.id === wanted)
|
|
54
|
+
|
|
55
|
+
if (active) {
|
|
56
|
+
document.documentElement.dataset[param] = active.id
|
|
57
|
+
|
|
58
|
+
if (active.css) {
|
|
59
|
+
const style = document.createElement('style')
|
|
60
|
+
style.textContent = active.css
|
|
61
|
+
document.head.appendChild(style)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const render = () => {
|
|
65
|
+
const host = mountInto ? document.querySelector(mountInto) : document.body
|
|
66
|
+
if (!host) return
|
|
67
|
+
const bar = document.createElement('div')
|
|
68
|
+
bar.setAttribute('data-scenario-switcher', '')
|
|
69
|
+
for (const arm of arms) {
|
|
70
|
+
const link = document.createElement('a')
|
|
71
|
+
link.href = '?' + param + '=' + arm.id
|
|
72
|
+
link.textContent = arm.label
|
|
73
|
+
if (arm.id === active.id) link.setAttribute('data-current', 'true')
|
|
74
|
+
bar.appendChild(link)
|
|
75
|
+
}
|
|
76
|
+
host.appendChild(bar)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (document.readyState === 'loading') {
|
|
80
|
+
document.addEventListener('DOMContentLoaded', render)
|
|
81
|
+
} else {
|
|
82
|
+
render()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
`
|
|
86
|
+
|
|
87
|
+
const SWITCHER_CSS = `
|
|
88
|
+
/* Bottom left rather than bottom centre, which Astro's dev toolbar owns.
|
|
89
|
+
Centred, the toolbar sits over the switcher and swallows every click on it. */
|
|
90
|
+
[data-scenario-switcher] {
|
|
91
|
+
position: fixed;
|
|
92
|
+
left: 1.5rem;
|
|
93
|
+
bottom: 1.5rem;
|
|
94
|
+
z-index: 60;
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-wrap: wrap;
|
|
97
|
+
gap: 0.5rem;
|
|
98
|
+
max-width: calc(100vw - 3rem);
|
|
99
|
+
font: 500 13px system-ui, sans-serif;
|
|
100
|
+
}
|
|
101
|
+
[data-scenario-switcher] a {
|
|
102
|
+
padding: 0.4rem 0.8rem;
|
|
103
|
+
border: 1px solid rgb(128 128 128 / 0.4);
|
|
104
|
+
border-radius: 999px;
|
|
105
|
+
background: rgb(255 255 255 / 0.9);
|
|
106
|
+
color: #1a1815;
|
|
107
|
+
text-decoration: none;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
}
|
|
110
|
+
[data-scenario-switcher] a[data-current='true'] {
|
|
111
|
+
background: #1a1815;
|
|
112
|
+
color: #f8f4ed;
|
|
113
|
+
}
|
|
114
|
+
`
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
enabled && (
|
|
119
|
+
<script
|
|
120
|
+
is:inline
|
|
121
|
+
define:vars={{ param, arms, mountInto }}
|
|
122
|
+
set:html={SOURCE}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
{enabled && <style is:global set:html={SWITCHER_CSS} />}
|
|
@@ -58,3 +58,10 @@ Append to the `## Scripts` table:
|
|
|
58
58
|
## CI docs (extend)
|
|
59
59
|
|
|
60
60
|
In `.claude/context/ci.md`, the Typecheck row's assertion reads: `` `astro check` passes ``. The Build row's assertion reads: `` `astro build` succeeds ``.
|
|
61
|
+
|
|
62
|
+
## Scenario switcher
|
|
63
|
+
|
|
64
|
+
- `src/components/dev/scenarios.astro` ships as a golden config, always overwritten on sync, since it is toolkit-authored infrastructure rather than a file a project hand-edits.
|
|
65
|
+
- Import it into a page under test to drive candidate treatments of one decision by hand, selected by a query parameter, with a switcher for moving between them.
|
|
66
|
+
- Guarded by `import.meta.env.DEV`, so it renders nothing and ships nothing in a production build.
|
|
67
|
+
- Astro-only. It uses `is:inline`, `define:vars`, and `set:html`, which only the `.astro` file format parses. `vite-react` gets no equivalent until a real decision drives one.
|