@erclx/canon 4.62.1 → 4.64.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/REQUIREMENT.md +7 -3
- package/claude/skills/canon-cli/SKILL.md +8 -2
- package/claude/skills/canon-record/REQUIREMENT.md +34 -0
- package/claude/skills/canon-record/SKILL.md +54 -0
- package/claude/skills/canon-screencast/REQUIREMENT.md +1 -1
- package/claude/skills/canon-screencast/SKILL.md +3 -3
- package/claude/skills/claude-autoship/REQUIREMENT.md +3 -1
- package/claude/skills/claude-autoship/SKILL.md +26 -12
- package/claude/skills/{context-draft → draft-context}/REQUIREMENT.md +3 -3
- package/claude/skills/{context-draft → draft-context}/SKILL.md +1 -1
- package/claude/skills/{docs-draft → draft-docs}/REQUIREMENT.md +1 -1
- package/claude/skills/{docs-draft → draft-docs}/SKILL.md +1 -1
- package/claude/skills/draft-readme/REQUIREMENT.md +36 -0
- package/claude/skills/draft-readme/SKILL.md +65 -0
- package/claude/skills/{wireframe-draft → draft-wireframes}/REQUIREMENT.md +3 -3
- package/claude/skills/{wireframe-draft → draft-wireframes}/SKILL.md +1 -1
- package/claude/skills/identity/REQUIREMENT.md +40 -0
- package/claude/skills/identity/SKILL.md +86 -0
- package/claude/skills/repo-metadata/SKILL.md +6 -5
- package/claude/skills/test-first/REQUIREMENT.md +34 -0
- package/claude/skills/test-first/SKILL.md +24 -0
- package/docs/agents/demo.md +2 -0
- package/docs/agents/review-classification.md +2 -2
- package/docs/workflow/ai-workflow.md +23 -19
- package/docs/workflow/visual-design-workflow.md +1 -0
- package/governance/rules/core/070-planning.md +1 -0
- package/package.json +3 -1
- package/scripts/core/regen-agent-fixture.sh +1 -1
- package/src/claude/cases/authoring.ts +7 -3
- package/src/claude/cases/claude-workflow.ts +5 -0
- package/src/claude/cases/misc.ts +10 -0
- package/src/commands/tooling.ts +8 -1
- package/src/project-root.ts +34 -0
- package/src/shipped/references.ts +45 -15
- package/standards/readme.md +2 -0
- package/tooling/astro/configs/eslint.config.js +7 -1
- package/tooling/astro/reference.md +1 -1
- package/tooling/nextjs/configs/eslint.config.js +3 -2
- package/tooling/nextjs/reference.md +1 -1
- package/tooling/web/configs/eslint.config.js +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: identity
|
|
3
|
+
description: Drafts a project's logo mark through draft-and-pick's render-and-pick loop, then composes the picked mark into an icon sequence and a 1200x630 social card. Use when asked to "make a logo", "design a logo mark", "create a favicon", "build the icon set", "generate a social card", "make an og:image", or "draft the logo and social card together". Do NOT use to mutate an existing logo file directly, which is a plain edit, or to record the mark's construction rules in DESIGN.md, which is out of scope.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Identity
|
|
7
|
+
|
|
8
|
+
One identity rendered twice: the same mark sized down to an icon sequence and composed with type into the social card sized up. Drafting both from one pick keeps the mark and its card composition from settling on shapes that do not match.
|
|
9
|
+
|
|
10
|
+
## Guards
|
|
11
|
+
|
|
12
|
+
- If `canon` is not on PATH, stop: `❌ canon CLI not found.`
|
|
13
|
+
- Draft no candidate for a mark the operator has not asked to make. This skill fires on a direct request, never on the model's own read that a project could use one.
|
|
14
|
+
|
|
15
|
+
## Step 1: read signal
|
|
16
|
+
|
|
17
|
+
Read these in parallel, skipping any that do not exist:
|
|
18
|
+
|
|
19
|
+
- `.claude/DESIGN.md`: the `## Personality`, `## Color`, and `## Typography` sections, the same three cells `claude-design-extract` Step 2 sources from
|
|
20
|
+
- `.claude/REQUIREMENTS.md`: the `## Personality` paragraph, when `.claude/DESIGN.md` carries none
|
|
21
|
+
- `CLAUDE.md`: the project's stated voice, when neither file above carries a personality signal
|
|
22
|
+
|
|
23
|
+
No signal from any of the three is not a stop. Draft against a neutral default and tag the color and type choices `? verify` per `${CLAUDE_SKILL_DIR}/../../standards/design.md`'s uncertainty tag.
|
|
24
|
+
|
|
25
|
+
## Step 2: detect the icon size sequence
|
|
26
|
+
|
|
27
|
+
Check the project's HTML entry points (`index.html`, `public/index.html`, `src/index.html`) for a `<link rel="icon">` or `<link rel="apple-touch-icon">` tag carrying a `sizes` attribute, and any `manifest.json` or `site.webmanifest` for an `icons` array. Take the union of every size found.
|
|
28
|
+
|
|
29
|
+
Fall back to the stated default when nothing is detected, since no `tooling/` stack scaffolds a `public/` folder, a favicon reference, or an `og:image` meta tag:
|
|
30
|
+
|
|
31
|
+
- `16x16`, `32x32`, `48x48`
|
|
32
|
+
- `180x180`
|
|
33
|
+
- `192x192`, `512x512`
|
|
34
|
+
|
|
35
|
+
This default set is raster-only. Step 6 already writes the vector source at `favicon.svg` regardless of which branch decided the sequence, so a project willing to reference an SVG favicon directly is covered either way.
|
|
36
|
+
|
|
37
|
+
Announce which of the two decided the sequence.
|
|
38
|
+
|
|
39
|
+
## Step 3: detect the write folder
|
|
40
|
+
|
|
41
|
+
Check for `public/`, `static/`, or a stack-declared asset folder, taking the first that exists. Fall back to the project root when none is detected, mirroring `claude-design-extract`'s own source-versus-greenfield split. Announce the folder so the operator can move the files if the project's own convention differs.
|
|
42
|
+
|
|
43
|
+
## Step 4: name the decision and the arms
|
|
44
|
+
|
|
45
|
+
Follow `${CLAUDE_SKILL_DIR}/../draft-and-pick/SKILL.md` Step 1, with the decision fixed rather than derived: "the project's logo mark and its composition into the social card." Vary the mark's shape or style across arms, keeping the card's type and layout fixed, per that skill's one-property rule. Draft each arm already inside the full 1200x630 card frame, mark and type together, so the pick settles the shape and the composition in one choice. Arm 0 is the current mark when the folder from Step 3 already holds a logo file (`favicon.svg`, `favicon.ico`, `logo.svg`, or similar). Arms start at 1 otherwise.
|
|
46
|
+
|
|
47
|
+
Structure every arm as a `.mark` element, the inline SVG alone, nested inside a `.card` element, the full composition, so Step 6 can address either without re-deriving them.
|
|
48
|
+
|
|
49
|
+
## Step 5: draft, render, pick, and loop
|
|
50
|
+
|
|
51
|
+
Follow `${CLAUDE_SKILL_DIR}/../draft-and-pick/SKILL.md` Steps 2 through 5 against the arms from Step 4: author the page, render and hand off, take the pick, and loop on it.
|
|
52
|
+
|
|
53
|
+
## Step 6: finalize
|
|
54
|
+
|
|
55
|
+
This step replaces `draft-and-pick`'s own Step 6, since the pick here produces several final files rather than one applied surface.
|
|
56
|
+
|
|
57
|
+
1. Extract the picked arm's `.mark` markup as the final vector source. Write it to `<write-folder>/favicon.svg`.
|
|
58
|
+
2. Under `<dest>/render/`, write one page per Step 2 size plus one for the card. Give every page's captured element the shared class `.render`, so one capture call renders the whole batch regardless of the size spread. `canon capture` opens every page at a fixed 2x device scale factor and screenshots the element at that scale, so declare each `.render` element at half its target dimension, `<w>/2` by `<h>/2`, to land the captured PNG on the literal target size rather than double it. Declare a machine-resolved font stack (`system-ui` behind a generic fallback) on each, since `canon capture` refuses a page naming no font at all.
|
|
59
|
+
- `icon-<w>x<h>.html`: the picked `.mark` markup, its `.render` wrapper sized `<w>/2` by `<h>/2`
|
|
60
|
+
- `og-image.html`: the picked `.card` markup, its `.render` wrapper sized 600x315 to capture at the native 1200x630
|
|
61
|
+
3. Render the batch:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
canon capture <dest>/render --selector .render --out <write-folder>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
4. Report each written file's path and the dimensions `canon capture` printed for it, confirming each equals its Step 2 target rather than half of it.
|
|
68
|
+
5. Delete `<dest>`, per `draft-and-pick`'s own scratch-folder rule.
|
|
69
|
+
|
|
70
|
+
## Response format
|
|
71
|
+
|
|
72
|
+
```plaintext
|
|
73
|
+
📝 Wrote <write-folder>/favicon.svg
|
|
74
|
+
📝 Wrote <write-folder>/icon-16x16.png (16x16)
|
|
75
|
+
📝 Wrote <write-folder>/og-image.png (1200x630)
|
|
76
|
+
|
|
77
|
+
Write folder: <detected <path>|defaulted to project root>. Move the files if this project's own convention differs.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## What this delegates
|
|
81
|
+
|
|
82
|
+
Cite these rather than restating them.
|
|
83
|
+
|
|
84
|
+
- `draft-and-pick` owns Steps 1 through 5 of the render-and-pick loop, cited above
|
|
85
|
+
- `claude-design-extract` owns building `.claude/DESIGN.md`. This skill only reads it.
|
|
86
|
+
- `canon capture` owns the render mechanics, its font refusal, and its reported dimensions
|
|
@@ -19,13 +19,14 @@ Reads what `canon repo metadata propose` computes locally and reports it against
|
|
|
19
19
|
|
|
20
20
|
1. Run `canon repo metadata propose --json`.
|
|
21
21
|
2. Read `diff` and `repo` from the record. `repo` is the `--repo` value the later apply step must carry. An empty `diff` means the remote already matches what this run computed: report that and stop, since there is nothing to answer.
|
|
22
|
-
3.
|
|
23
|
-
4.
|
|
24
|
-
5.
|
|
22
|
+
3. When `diff` carries a `description` field, compress its proposed value, the raw opening line `canon repo metadata propose` computed, into a single short phrase. Leave the shape and length of the phrase to judgment on each run. No numeric target applies. Keep the raw proposed value beside the compressed phrase in what gets reported, so the operator can check the phrase against the README line it came from.
|
|
23
|
+
4. For each field `diff` carries, put the change to the operator through the structured question surface: which of the differing fields to write. For `description`, the value offered to accept is the compressed phrase from step 3, not the raw line beside it. Rank accepting the proposed value first for a field whose current value is stale or wrong, and give the reject option the cost of leaving the remote as it stands. Never pre-select an answer for the operator.
|
|
24
|
+
5. Report the fields the proposal left absent as unchanged, naming that neither the README nor `package.json` carried a source for them.
|
|
25
|
+
6. Stop. Do not run apply here even when the operator answers immediately, since answering is not yet an apply invocation.
|
|
25
26
|
|
|
26
27
|
## Apply
|
|
27
28
|
|
|
28
|
-
1. Confirm every field about to be written was answered by the operator in this conversation. Carry no field forward unanswered.
|
|
29
|
+
1. Confirm every field about to be written was answered by the operator in this conversation. Carry no field forward unanswered. For `description`, the answered value is the compressed phrase Propose offered, never the raw README line reported beside it.
|
|
29
30
|
2. Run `canon repo metadata apply`, always passing `--repo <owner/name>` from the propose record's `repo` field, plus the flags for the answered fields: `--description <text>`, `--homepage <url>`, `--topics <comma-separated list>`. `--topics` is the full desired set, and the command reads the current set itself to compute what to add and remove. The command refuses rather than writing when `--repo` does not match what `--root` resolves to, so never omit it and never guess it from anything but the propose record's `repo` field.
|
|
30
31
|
3. Report the written state from the JSON record.
|
|
31
32
|
|
|
@@ -35,7 +36,7 @@ Propose:
|
|
|
35
36
|
|
|
36
37
|
```plaintext
|
|
37
38
|
📋 Repo metadata proposal
|
|
38
|
-
description: "<current>" → "<
|
|
39
|
+
description: "<current>" → "<compressed>" (derived from the README's opening line: "<raw>")
|
|
39
40
|
homepage: unchanged, no local source
|
|
40
41
|
topics: +<added>, -<removed>
|
|
41
42
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-first
|
|
3
|
+
description: Why the loop exists and where it stops short of a debugging session or a visual check
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Test first requirement
|
|
7
|
+
|
|
8
|
+
## Gap
|
|
9
|
+
|
|
10
|
+
Without this skill, a session implementing a planned change writes the test after the code, or writes both in one motion and never runs the test against the code as it stood before. Either way, nothing showed that the test would have caught the defect it exists to catch, so a test added after the fact protects nothing beyond what the author was already confident about.
|
|
11
|
+
|
|
12
|
+
## Must
|
|
13
|
+
|
|
14
|
+
- Write or extend the test before the implementation it covers, for any behavior whose shape is already known
|
|
15
|
+
- Run the test before implementing and confirm it fails for the missing behavior, not for an unrelated mistake in the test itself
|
|
16
|
+
- Implement the minimum the current test demands, and start a new test before extending past it
|
|
17
|
+
- Re-run the test after implementing and confirm the pass is the one the test was written to prove
|
|
18
|
+
|
|
19
|
+
## Must not
|
|
20
|
+
|
|
21
|
+
- Write the implementation and the test together with the test never run red first
|
|
22
|
+
- Treat a test that already passes before any code change as evidence for the new behavior
|
|
23
|
+
- Restate the reproducing-test step `canon:systematic-debugging` already owns for a failure with no known cause
|
|
24
|
+
- Decide whether a change looks right visually, which runs after implementation as a separate check
|
|
25
|
+
|
|
26
|
+
## Guards
|
|
27
|
+
|
|
28
|
+
- A test failing for a reason other than the missing behavior blocks moving to implementation. Fix the test first.
|
|
29
|
+
|
|
30
|
+
## Out of scope
|
|
31
|
+
|
|
32
|
+
- Finding the cause of an unexplained failure: `canon:systematic-debugging`
|
|
33
|
+
- Confirming visual output after a change lands: `070-planning.md` states the order and `claude-ui-test` covers it
|
|
34
|
+
- The mechanical audit of whether an implementation reached history ahead of its test: `canon gov test-order`, invoked from `claude-autoship`
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-first
|
|
3
|
+
description: Write the failing test for a behavior before writing the code that satisfies it, confirm it fails for the right reason, then implement the smallest change that turns it green. Auto-triggers before implementing a planned feature, adding a function or an endpoint, or extending existing behavior whose new shape is already decided. Do NOT use for a failure with no known cause, which is canon:systematic-debugging, or when the test for the behavior already exists and already passes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Test first
|
|
7
|
+
|
|
8
|
+
## The loop
|
|
9
|
+
|
|
10
|
+
1. Find or write the test for the behavior before touching the code it covers. Follow the pairing convention the project already uses, which is commonly a test sitting beside its subject under one name, so that `foo.ts` takes `foo.test.ts` beside it.
|
|
11
|
+
2. Write the test against the behavior as it should exist once the change lands, naming the case for what it proves rather than for the function it calls.
|
|
12
|
+
3. Run only that test and read the failure. Confirm it fails because the behavior is missing, not because of a typo, a missing import, or a signature the test itself got wrong. A test failing for the wrong reason still reads green the moment any code exists to satisfy that wrong reason, so treat any other failure as a defect in the test and fix the test before moving on.
|
|
13
|
+
4. Implement the smallest change that turns the failure into a pass. Do not add behavior the test does not ask for. A second behavior wants its own test written first, not a shortcut folded into the change already open.
|
|
14
|
+
5. Run the test again and confirm it passes for the reason it was written for, not by accident. Then run the surrounding suite so a change made to satisfy this test has not broken another.
|
|
15
|
+
|
|
16
|
+
## Extending existing behavior
|
|
17
|
+
|
|
18
|
+
- Changing what a function already does starts with changing what its test asserts. Run that changed test once before touching the implementation, and confirm it fails against the code as it stands today.
|
|
19
|
+
- A test that already passes before any code changes proves nothing about the new behavior. Widen the assertion or add a case until the suite fails against the current implementation.
|
|
20
|
+
|
|
21
|
+
## What this skill does not cover
|
|
22
|
+
|
|
23
|
+
- A failure with no known cause. Route through `canon:systematic-debugging` first, whose own fix phase already writes the reproducing test as part of finding the cause. This skill starts once the shape of the fix or the feature is already decided.
|
|
24
|
+
- Confirming a change looks right once it has landed, which is a separate concern from whether the test passed and runs after implementation rather than before it.
|
package/docs/agents/demo.md
CHANGED
|
@@ -23,6 +23,8 @@ So `canon demo compile` writes a second artifact rather than adding fields to a
|
|
|
23
23
|
|
|
24
24
|
The plan is committed, not scratch. Its timing is a starting point tuned by watching a recording, and the draft cannot reproduce a tuned value, so a recompile over an existing plan refuses and names `--force`.
|
|
25
25
|
|
|
26
|
+
`canon-record` is the routed way to run both verbs from a draft path: it compiles only when no plan exists yet, then runs, stopping to report any field still unresolved rather than guessing one.
|
|
27
|
+
|
|
26
28
|
| Option | Behavior |
|
|
27
29
|
| --------------- | ----------------------------------------------------------------- |
|
|
28
30
|
| `--out <dir>` | Directory the plan and its output paths point at, default `demos` |
|
|
@@ -64,13 +64,13 @@ An empty set refuses with `no-changes` rather than skipping. Both tests are univ
|
|
|
64
64
|
|
|
65
65
|
## Why it is a verb
|
|
66
66
|
|
|
67
|
-
The decision was three sentences in `claude-autoship`
|
|
67
|
+
The decision was three sentences in `claude-autoship`'s review step, applied by a session reading a bulleted list of paths. It failed three times. A driven arm on 2026-08-30 staged `.claude/skills/deploy-check/SKILL.md`, which that list names, and the chain skipped review and opened a draft pull request anyway. The rule was correct and the session did not apply it, and the two fixes before that one were each a rewrite of the same prose.
|
|
68
68
|
|
|
69
69
|
A rule a session can talk itself out of moves into a verb. That is the same argument the quiz-order draw in `canon teach lesson` was decided on: an instruction is a hope where a verb is a check.
|
|
70
70
|
|
|
71
71
|
## The written fallback
|
|
72
72
|
|
|
73
|
-
The verb ships with the CLI and Step
|
|
73
|
+
The verb ships with the CLI and Step 6 ships with the plugin, so a target holding an older binary meets a missing subcommand. The skill body keeps the written list and applies it by hand there, naming which of the two decided the run.
|
|
74
74
|
|
|
75
75
|
The fallback is never a skip. Failing open is the exact defect the verb closes, so a body that skips on anything other than a `skip` record would ship every branch unreviewed the moment the subcommand went absent.
|
|
76
76
|
|
|
@@ -157,7 +157,7 @@ The list stays written in the skill body as the fallback for a target whose inst
|
|
|
157
157
|
|
|
158
158
|
#### Memory in the chain
|
|
159
159
|
|
|
160
|
-
`git-ship` runs its verify gate and then opens on `claude-memory-capture`, which sends what the session learned to the surface that owns it. `autoship` reaches the same step by invoking that skill at its Step
|
|
160
|
+
`git-ship` runs its verify gate and then opens on `claude-memory-capture`, which sends what the session learned to the surface that owns it. `autoship` reaches the same step by invoking that skill at its Step 8 rather than restating the order. A fact about a domain carrying an entry in `.claude/context/index.md` is routed to that entry, and `claude-docs` folds it in on the next step, so it ships in the same pull request. Anything no entry owns stays a file in `.canon/memory/`.
|
|
161
161
|
|
|
162
162
|
Capture leads rather than trails because a routed fact edits a tracked file, which has to reach the branch before the commit steps run.
|
|
163
163
|
|
|
@@ -219,13 +219,14 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
219
219
|
|
|
220
220
|
### Build the feature
|
|
221
221
|
|
|
222
|
-
| Skill | When to use
|
|
223
|
-
| ---------------------------- |
|
|
224
|
-
| `canon:claude-worktree` | At the plan-to-execute boundary, to get an isolated tree and branch
|
|
225
|
-
| `canon:claude-autoship` | After plan approval, to chain implement, verify, review, draft PR
|
|
226
|
-
| `canon:project-commands` | When the project's own command needs running
|
|
227
|
-
| `canon:
|
|
228
|
-
| `canon:
|
|
222
|
+
| Skill | When to use |
|
|
223
|
+
| ---------------------------- | ------------------------------------------------------------------------- |
|
|
224
|
+
| `canon:claude-worktree` | At the plan-to-execute boundary, to get an isolated tree and branch |
|
|
225
|
+
| `canon:claude-autoship` | After plan approval, to chain implement, verify, review, draft PR |
|
|
226
|
+
| `canon:project-commands` | When the project's own command needs running |
|
|
227
|
+
| `canon:test-first` | Before implementing a planned change, to write and run its test red first |
|
|
228
|
+
| `canon:systematic-debugging` | When a test fails or a bug surfaces, to force root cause first |
|
|
229
|
+
| `canon:claude-ui-test` | After a UI change, to generate e2e tests and a visual checklist |
|
|
229
230
|
|
|
230
231
|
### Check the work before it leaves the branch
|
|
231
232
|
|
|
@@ -291,25 +292,28 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
291
292
|
| `canon:create-skill` | For a new `SKILL.md` |
|
|
292
293
|
| `canon:create-snippet` | For a reusable prompt |
|
|
293
294
|
| `canon:create-standard` | For a new authoring convention |
|
|
294
|
-
| `canon:docs
|
|
295
|
-
| `canon:context
|
|
296
|
-
| `canon:
|
|
295
|
+
| `canon:draft-docs` | For a brand-new `docs/*.md` page, drafted against `standards/docs.md` |
|
|
296
|
+
| `canon:draft-context` | For a brand-new `.claude/context/<domain>.md` entry, drafted against `standards/context.md` |
|
|
297
|
+
| `canon:draft-wireframes` | For a brand-new `.claude/wireframes/<surface>.md` file, drafted against `standards/wireframes.md` |
|
|
298
|
+
| `canon:draft-readme` | For a project's `README.md`, drafted against `standards/readme.md` |
|
|
297
299
|
| `canon:bash-script` | For an interactive, human-facing shell tool |
|
|
298
300
|
| `canon:bash-cli-script` | For a non-interactive automation, CI, or pipeline script |
|
|
299
301
|
| `canon:ci-workflow` | For a GitHub Actions workflow file |
|
|
300
302
|
| `canon:canon-slides-draft` | For a deck, drafted as `.claude/SLIDES.md` and rendered to PowerPoint |
|
|
301
303
|
| `canon:canon-screencast` | For a recording script with beats and defaults already seeded |
|
|
304
|
+
| `canon:canon-record` | For compiling and running a screencast draft into a recording and a still |
|
|
305
|
+
| `canon:identity` | For a project's logo mark and its social card, drafted through `draft-and-pick`'s pick loop |
|
|
302
306
|
|
|
303
307
|
### Answer a question at any point
|
|
304
308
|
|
|
305
|
-
| Skill | When to use
|
|
306
|
-
| --------------------------- |
|
|
307
|
-
| `canon:canon-cli` | Before a sync or install, to learn what it overwrites, merges, or leaves alone |
|
|
308
|
-
| `canon:index-lookup` | To find where a topic is documented across the tracked `index.md` catalogs
|
|
309
|
-
| `canon:youtube-transcripts` | When a video transcript is wanted in the repo as context
|
|
310
|
-
| `canon:claude-teach` | To learn a subject across sessions, in a workspace that holds the progress
|
|
311
|
-
| `canon:write-human` | Before drafting or revising prose, for voice, rhythm, and density
|
|
312
|
-
| `canon:restate-plainly` | When an answer or a document has to be read again in plain words
|
|
309
|
+
| Skill | When to use |
|
|
310
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
311
|
+
| `canon:canon-cli` | Before running an unfamiliar verb, a sync, or an install, to learn which command to run, which reference doc covers it, or what it overwrites, merges, or leaves alone |
|
|
312
|
+
| `canon:index-lookup` | To find where a topic is documented across the tracked `index.md` catalogs |
|
|
313
|
+
| `canon:youtube-transcripts` | When a video transcript is wanted in the repo as context |
|
|
314
|
+
| `canon:claude-teach` | To learn a subject across sessions, in a workspace that holds the progress |
|
|
315
|
+
| `canon:write-human` | Before drafting or revising prose, for voice, rhythm, and density |
|
|
316
|
+
| `canon:restate-plainly` | When an answer or a document has to be read again in plain words |
|
|
313
317
|
|
|
314
318
|
Every row answers a question rather than marking a point in a project's life, so a phase above would send a reader to the wrong group.
|
|
315
319
|
|
|
@@ -40,6 +40,7 @@ A cell no source anchors ends in `? verify`, and the preview shows that marker b
|
|
|
40
40
|
- `canon:claude-ux-audit` for UX gap detection on existing surfaces
|
|
41
41
|
- `canon:claude-ux-measure` for what a running surface costs to paint, read against published thresholds
|
|
42
42
|
- `canon:draft-and-pick` for a call settled by looking, drafting several candidates onto one page and taking your pick
|
|
43
|
+
- `canon:identity` to draft a project's logo mark and compose it into an icon sequence and a social card, through `draft-and-pick`'s own render-and-pick loop
|
|
43
44
|
- Anthropic's `frontend-design` plugin optional for light visual steering
|
|
44
45
|
|
|
45
46
|
### When to pick
|
|
@@ -13,6 +13,7 @@ description: Enforce planning standards before implementation
|
|
|
13
13
|
- Propose the simplest solution that satisfies the requirement before implementing complex patterns.
|
|
14
14
|
- Write or update tests as part of every implementation plan.
|
|
15
15
|
- Write the test for a behavior before the code that implements it. Confirm visual output after implementing it, not before.
|
|
16
|
+
- Load the `canon:test-first` skill before writing the implementation for a behavior whose test does not exist yet, and report it rather than proceeding silently when the skill does not resolve.
|
|
16
17
|
- Run `canon gov test-order` before shipping a branch. Fix what it names as reaching history ahead of its test.
|
|
17
18
|
- Load the `canon:systematic-debugging` skill before proposing a fix for a failing test, a surfaced bug, or behavior nobody has explained yet, and report it rather than proceeding silently when the skill does not resolve.
|
|
18
19
|
- Do not modify code without a confirmed plan.
|
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.64.0",
|
|
5
5
|
"description": "Infrastructure and quality tooling for developer workflows",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -62,9 +62,11 @@
|
|
|
62
62
|
"@tailwindcss/vite": "^4.2.4",
|
|
63
63
|
"@testing-library/jest-dom": "^6.9.1",
|
|
64
64
|
"@types/bun": "^1.2.10",
|
|
65
|
+
"@types/micromatch": "^4.0.10",
|
|
65
66
|
"astro": "^6.1.9",
|
|
66
67
|
"cspell": "^8.17.1",
|
|
67
68
|
"husky": "^9.1.7",
|
|
69
|
+
"micromatch": "^4.0.8",
|
|
68
70
|
"prettier": "^3.8.1",
|
|
69
71
|
"prettier-plugin-astro": "^0.14.1",
|
|
70
72
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
@@ -202,7 +202,7 @@ const summary = {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
const fixture = {
|
|
205
|
-
// Read by web/src/components/
|
|
205
|
+
// Read by web/src/components/agent-view.astro. Every field below is either a
|
|
206
206
|
// live read or a hand transcription, and this header is what says which.
|
|
207
207
|
generatedBy: "scripts/core/regen-agent-fixture.sh",
|
|
208
208
|
readAt: READ_AT,
|
|
@@ -40,17 +40,21 @@ export const AUTHORING_CASES: readonly SkillCase[] = [
|
|
|
40
40
|
{
|
|
41
41
|
prompt:
|
|
42
42
|
'Write a brand-new docs page for the capture command, nothing under docs/ covers it yet.',
|
|
43
|
-
expect: 'docs
|
|
43
|
+
expect: 'draft-docs',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
prompt: 'This project has no README.md at all, write one from scratch.',
|
|
47
|
+
expect: 'draft-readme',
|
|
44
48
|
},
|
|
45
49
|
{
|
|
46
50
|
prompt:
|
|
47
51
|
'Write a context entry for the payments domain, there is no .claude/context page for it yet.',
|
|
48
|
-
expect: 'context
|
|
52
|
+
expect: 'draft-context',
|
|
49
53
|
},
|
|
50
54
|
{
|
|
51
55
|
prompt:
|
|
52
56
|
'Draft a wireframe for the settings panel, nothing under .claude/wireframes covers that surface yet.',
|
|
53
|
-
expect: '
|
|
57
|
+
expect: 'draft-wireframes',
|
|
54
58
|
},
|
|
55
59
|
{
|
|
56
60
|
prompt: 'Say what that dense answer actually means in plain terms.',
|
|
@@ -79,6 +79,11 @@ export const CLAUDE_WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
79
79
|
prompt: 'Post a formal review with findings on that open pull request.',
|
|
80
80
|
expect: 'claude-pr-review',
|
|
81
81
|
},
|
|
82
|
+
{
|
|
83
|
+
prompt:
|
|
84
|
+
'The screencast draft is finished. Turn it into an actual video now.',
|
|
85
|
+
expect: 'canon-record',
|
|
86
|
+
},
|
|
82
87
|
{
|
|
83
88
|
prompt:
|
|
84
89
|
'Look over everything that changed on this branch for bugs and edge cases.',
|
package/src/claude/cases/misc.ts
CHANGED
|
@@ -12,6 +12,11 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
12
12
|
'Show me a few different treatments for this callout so I can pick one by looking.',
|
|
13
13
|
expect: 'draft-and-pick',
|
|
14
14
|
},
|
|
15
|
+
{
|
|
16
|
+
prompt:
|
|
17
|
+
'This project has no logo yet. Draft one and give me a social card to go with it.',
|
|
18
|
+
expect: 'identity',
|
|
19
|
+
},
|
|
15
20
|
{
|
|
16
21
|
prompt: 'Fire up the dev server the way this project documents it.',
|
|
17
22
|
expect: 'project-commands',
|
|
@@ -29,6 +34,11 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
29
34
|
prompt: "This test just started failing and I don't know why yet.",
|
|
30
35
|
expect: 'systematic-debugging',
|
|
31
36
|
},
|
|
37
|
+
{
|
|
38
|
+
prompt:
|
|
39
|
+
"I'm about to write the retry helper we settled on and nothing covers it yet, so get the failing case in first.",
|
|
40
|
+
expect: 'test-first',
|
|
41
|
+
},
|
|
32
42
|
{
|
|
33
43
|
prompt: 'Does our github about text still match what the readme says?',
|
|
34
44
|
expect: 'repo-metadata',
|
package/src/commands/tooling.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import type { Command } from 'commander'
|
|
3
3
|
import { execScript } from '@/exec'
|
|
4
|
-
import { PROJECT_ROOT } from '@/project-root'
|
|
4
|
+
import { findCheckoutMismatch, PROJECT_ROOT } from '@/project-root'
|
|
5
5
|
import {
|
|
6
6
|
injectConfigs,
|
|
7
7
|
injectGitignore,
|
|
@@ -268,6 +268,13 @@ async function runSync(
|
|
|
268
268
|
): Promise<number> {
|
|
269
269
|
intro('canon tooling sync')
|
|
270
270
|
|
|
271
|
+
const mismatch = findCheckoutMismatch(process.cwd())
|
|
272
|
+
if (mismatch !== undefined) {
|
|
273
|
+
logWarn(
|
|
274
|
+
`Resolved via ${PROJECT_ROOT}, not the checkout at ${mismatch}. Run \`bun ${mismatch}/src/cli.ts tooling sync ...\` to sync against that checkout instead.`,
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
271
278
|
if (opts.check === true && opts.write === true) {
|
|
272
279
|
logWarn('Pass --check or --write, not both.')
|
|
273
280
|
outro()
|
package/src/project-root.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
1
2
|
import { dirname, resolve } from 'node:path'
|
|
2
3
|
import { fileURLToPath } from 'node:url'
|
|
3
4
|
|
|
@@ -17,3 +18,36 @@ export const PROJECT_ROOT = resolve(
|
|
|
17
18
|
dirname(fileURLToPath(import.meta.url)),
|
|
18
19
|
'..',
|
|
19
20
|
)
|
|
21
|
+
|
|
22
|
+
const readPackageName = (root: string): string | undefined => {
|
|
23
|
+
try {
|
|
24
|
+
const raw = readFileSync(resolve(root, 'package.json'), 'utf8')
|
|
25
|
+
return (JSON.parse(raw) as { name?: string }).name
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Walks upward from `startDir` for the nearest ancestor `package.json`
|
|
33
|
+
* sharing this package's own `name`, and reports its path when that ancestor
|
|
34
|
+
* is not `PROJECT_ROOT`. A bare `canon` on PATH resolves `PROJECT_ROOT` to the
|
|
35
|
+
* installed package rather than to a checkout the caller may be standing in,
|
|
36
|
+
* which is the case this reports: a caller's cwd sitting inside a second
|
|
37
|
+
* checkout the running binary never resolved against.
|
|
38
|
+
*/
|
|
39
|
+
export function findCheckoutMismatch(startDir: string): string | undefined {
|
|
40
|
+
const ownName = readPackageName(PROJECT_ROOT)
|
|
41
|
+
if (ownName === undefined) return undefined
|
|
42
|
+
|
|
43
|
+
let dir = resolve(startDir)
|
|
44
|
+
while (true) {
|
|
45
|
+
if (readPackageName(dir) === ownName) {
|
|
46
|
+
return dir === PROJECT_ROOT ? undefined : dir
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parent = dirname(dir)
|
|
50
|
+
if (parent === dir) return undefined
|
|
51
|
+
dir = parent
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -168,14 +168,17 @@ const DOCS_PATH = /(?<![\w./-])docs\/[^\s`)\]]*\.md\b/g
|
|
|
168
168
|
* lookbehind rejects, so a citation already rewritten to the resolving form
|
|
169
169
|
* does not fail this check a second time.
|
|
170
170
|
*
|
|
171
|
-
* Reported
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* `
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
171
|
+
* Reported unless `isStandardsPathReportable` reads the match as a
|
|
172
|
+
* deliberate placeholder rather than a real citation. A body illustrating
|
|
173
|
+
* the shape a project's own `standards/<name>.md` takes writes a bracketed
|
|
174
|
+
* token this pattern also matches, such as `standards/<slug>.md` in
|
|
175
|
+
* `create-standard/SKILL.md` or `standards/<name>.md` in
|
|
176
|
+
* `migration-standards-drop/SKILL.md`, and neither names a real file. A bare,
|
|
177
|
+
* non-bracketed match now reports whether or not the file exists, unlike
|
|
178
|
+
* `DOCS_PATH`, which still gates on existence: a same-repository citation
|
|
179
|
+
* under this corpus names no target project's own tree the way a `docs/`
|
|
180
|
+
* path can, so shape alone separates a placeholder from a broken citation
|
|
181
|
+
* here with no false-positive class existence was catching.
|
|
179
182
|
*/
|
|
180
183
|
const STANDARDS_PATH = /(?<![\w./-])standards\/[^\s`)\]]*\.md\b/g
|
|
181
184
|
|
|
@@ -221,20 +224,37 @@ export interface ShippedReference {
|
|
|
221
224
|
}
|
|
222
225
|
|
|
223
226
|
/**
|
|
224
|
-
* Whether a
|
|
227
|
+
* Whether a matched path is a deliberate placeholder rather than a real
|
|
228
|
+
* citation, true when the text carries both `<` and `>`. A real path never
|
|
229
|
+
* carries either character, so the two classes cannot collide.
|
|
230
|
+
*/
|
|
231
|
+
function isPlaceholderPath(path: string): boolean {
|
|
232
|
+
return path.includes('<') && path.includes('>')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Whether a `DOCS_PATH` match should be reported.
|
|
225
237
|
*
|
|
226
238
|
* `file` gates the `docs/` corpus out by the caller's own location rather
|
|
227
239
|
* than by a pattern exemption: a citation from inside `docs/` is read
|
|
228
240
|
* together with the rest of that corpus through the same `canon docs`
|
|
229
241
|
* resolution, which is a weaker claim than one from a skill body with no
|
|
230
242
|
* `docs/` sibling at all.
|
|
243
|
+
*
|
|
244
|
+
* The placeholder check runs ahead of `existsSync` rather than replacing it.
|
|
245
|
+
* Shape alone cannot separate a genuinely broken same-repository citation
|
|
246
|
+
* from a legitimate illustration of a target project's own tree, such as
|
|
247
|
+
* `docs/retry.md`, and no signal in the text draws that line, so existence
|
|
248
|
+
* still does the work of muting those. The placeholder check only catches
|
|
249
|
+
* the bracketed illustrations existence alone would report as broken.
|
|
231
250
|
*/
|
|
232
|
-
function
|
|
251
|
+
function isDocsPathReportable(
|
|
233
252
|
file: string,
|
|
234
253
|
path: string,
|
|
235
254
|
root: string,
|
|
236
255
|
): boolean {
|
|
237
256
|
if (file.startsWith('docs/')) return false
|
|
257
|
+
if (isPlaceholderPath(path)) return false
|
|
238
258
|
return existsSync(join(root, path))
|
|
239
259
|
}
|
|
240
260
|
|
|
@@ -250,9 +270,19 @@ function isStandardsPathScope(file: string): boolean {
|
|
|
250
270
|
return file.startsWith('claude/skills/') && !file.endsWith('/REQUIREMENT.md')
|
|
251
271
|
}
|
|
252
272
|
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Whether a `STANDARDS_PATH` match should be reported: every match that is
|
|
275
|
+
* not a placeholder, whether or not the named file exists.
|
|
276
|
+
*
|
|
277
|
+
* Existence used to gate this the same way `isDocsPathReportable` gates its
|
|
278
|
+
* corpus, which conflated a deliberate placeholder with a genuinely broken,
|
|
279
|
+
* real-looking citation: both fail existence and both passed muted. A bare
|
|
280
|
+
* `standards/<name>.md` names no target-project tree the way a `docs/` path
|
|
281
|
+
* can, so shape alone separates the two classes here with no false-positive
|
|
282
|
+
* class existence was catching.
|
|
283
|
+
*/
|
|
284
|
+
function isStandardsPathReportable(path: string): boolean {
|
|
285
|
+
return !isPlaceholderPath(path)
|
|
256
286
|
}
|
|
257
287
|
|
|
258
288
|
/**
|
|
@@ -318,7 +348,7 @@ export function referencesIn(
|
|
|
318
348
|
}
|
|
319
349
|
|
|
320
350
|
for (const match of line.matchAll(DOCS_PATH)) {
|
|
321
|
-
if (!
|
|
351
|
+
if (!isDocsPathReportable(file, match[0], root)) continue
|
|
322
352
|
references.push({
|
|
323
353
|
file,
|
|
324
354
|
line: index + 1,
|
|
@@ -329,7 +359,7 @@ export function referencesIn(
|
|
|
329
359
|
|
|
330
360
|
if (isStandardsPathScope(file)) {
|
|
331
361
|
for (const match of line.matchAll(STANDARDS_PATH)) {
|
|
332
|
-
if (!
|
|
362
|
+
if (!isStandardsPathReportable(match[0])) continue
|
|
333
363
|
references.push({
|
|
334
364
|
file,
|
|
335
365
|
line: index + 1,
|
package/standards/readme.md
CHANGED
|
@@ -46,6 +46,8 @@ Scoped to the README at a repository root. A nested README documenting a folder,
|
|
|
46
46
|
- Do not include license text. Reference the `LICENSE` file.
|
|
47
47
|
- Do not include detailed contribution guidelines. Reference `CONTRIBUTING.md`.
|
|
48
48
|
- Do not include extensive troubleshooting guides. Use a wiki or separate documentation.
|
|
49
|
+
- A README that points to the project's own context entries instead of restating them satisfies the required list by routing rather than carrying. A repository documented in `docs/` or `.claude/context/` names the entry point and the listing command, then stops. The reader is one hop away, the same way `## Badges` treats zero as complete.
|
|
50
|
+
- A README missing an H1, or whose headings restate the tool that scaffolded it rather than the project, is unedited generator output rather than a page anyone wrote. Draft over it. Nothing here asks a session to preserve a section a scaffold wrote and nobody replaced.
|
|
49
51
|
|
|
50
52
|
### Badges
|
|
51
53
|
|
|
@@ -50,10 +50,16 @@ export default defineConfig([
|
|
|
50
50
|
],
|
|
51
51
|
'check-file/folder-naming-convention': [
|
|
52
52
|
'error',
|
|
53
|
-
{ 'src/**/!(__tests__
|
|
53
|
+
{ 'src/**/!(__tests__)/': 'KEBAB_CASE' },
|
|
54
54
|
],
|
|
55
55
|
},
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
files: ['src/pages/**/*.{ts,tsx,astro}'],
|
|
59
|
+
rules: {
|
|
60
|
+
'check-file/folder-naming-convention': 'off',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
57
63
|
{
|
|
58
64
|
files: ['**/*.{jsx,tsx}'],
|
|
59
65
|
plugins: {
|
|
@@ -21,7 +21,7 @@ The astro stack covers Astro + TypeScript projects: content sites, marketing sit
|
|
|
21
21
|
- `vitest.config.ts`: uses `getViteConfig` from `astro/config` (not `mergeConfig`). jsdom, globals, setup file, `passWithNoTests: true`, v8 coverage, `**/*.astro` in coverage excludes.
|
|
22
22
|
- `playwright.config.ts`: all browsers, `webServer` runs `bun run build && bun run preview` on port `4321` plus `WORKTREE_PORT_OFFSET`, `reuseExistingServer: false`. Astro's dev/prod gap is wide (MDX, island hydration, asset optimization), so E2E always tests the built `dist/`. `DIST_PREBUILT` set in the environment drops the `build` half, running `bun run preview` alone against a `dist/` a prior CI job already produced.
|
|
23
23
|
- `tsconfig.json`: extends `astro/tsconfigs/strict`, adds `skipLibCheck`, `vitest/globals` and `@testing-library/jest-dom` in types, `@/` paths.
|
|
24
|
-
- `eslint.config.js`: overrides the web layer. Adds `eslint-plugin-astro` (`.astro` parser via `astro-eslint-parser`). React-hooks scoped to `.jsx`/`.tsx` only (`.astro` is not React). The shared block's `files` selector includes `.astro`, so `check-file/filename-naming-convention` reaches `.ts`, `.tsx`, and `.astro` under `KEBAB_CASE`, overriding Astro's own PascalCase component convention deliberately, on the ground that a component's name in markup comes from the import binding rather than the filename. `.js` and `.jsx` stay out of the rule's own pattern, matching `web` and `nextjs`. `check-file/folder-naming-convention`
|
|
24
|
+
- `eslint.config.js`: overrides the web layer. Adds `eslint-plugin-astro` (`.astro` parser via `astro-eslint-parser`). React-hooks scoped to `.jsx`/`.tsx` only (`.astro` is not React). The shared block's `files` selector includes `.astro`, so `check-file/filename-naming-convention` reaches `.ts`, `.tsx`, and `.astro` under `KEBAB_CASE`, overriding Astro's own PascalCase component convention deliberately, on the ground that a component's name in markup comes from the import binding rather than the filename. `.js` and `.jsx` stay out of the rule's own pattern, matching `web` and `nextjs`. `check-file/folder-naming-convention` reaches every `src/**` folder except `__tests__` and the whole `pages/` subtree, which carries its own off-block for the bracket-named dynamic routes and nested slug folders Astro's file-based routing produces. See `.claude/context/tooling.md` for the measurement.
|
|
25
25
|
|
|
26
26
|
## Typecheck
|
|
27
27
|
|