@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.
Files changed (41) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/canon-cli/REQUIREMENT.md +7 -3
  3. package/claude/skills/canon-cli/SKILL.md +8 -2
  4. package/claude/skills/canon-record/REQUIREMENT.md +34 -0
  5. package/claude/skills/canon-record/SKILL.md +54 -0
  6. package/claude/skills/canon-screencast/REQUIREMENT.md +1 -1
  7. package/claude/skills/canon-screencast/SKILL.md +3 -3
  8. package/claude/skills/claude-autoship/REQUIREMENT.md +3 -1
  9. package/claude/skills/claude-autoship/SKILL.md +26 -12
  10. package/claude/skills/{context-draft → draft-context}/REQUIREMENT.md +3 -3
  11. package/claude/skills/{context-draft → draft-context}/SKILL.md +1 -1
  12. package/claude/skills/{docs-draft → draft-docs}/REQUIREMENT.md +1 -1
  13. package/claude/skills/{docs-draft → draft-docs}/SKILL.md +1 -1
  14. package/claude/skills/draft-readme/REQUIREMENT.md +36 -0
  15. package/claude/skills/draft-readme/SKILL.md +65 -0
  16. package/claude/skills/{wireframe-draft → draft-wireframes}/REQUIREMENT.md +3 -3
  17. package/claude/skills/{wireframe-draft → draft-wireframes}/SKILL.md +1 -1
  18. package/claude/skills/identity/REQUIREMENT.md +40 -0
  19. package/claude/skills/identity/SKILL.md +86 -0
  20. package/claude/skills/repo-metadata/SKILL.md +6 -5
  21. package/claude/skills/test-first/REQUIREMENT.md +34 -0
  22. package/claude/skills/test-first/SKILL.md +24 -0
  23. package/docs/agents/demo.md +2 -0
  24. package/docs/agents/review-classification.md +2 -2
  25. package/docs/workflow/ai-workflow.md +23 -19
  26. package/docs/workflow/visual-design-workflow.md +1 -0
  27. package/governance/rules/core/070-planning.md +1 -0
  28. package/package.json +3 -1
  29. package/scripts/core/regen-agent-fixture.sh +1 -1
  30. package/src/claude/cases/authoring.ts +7 -3
  31. package/src/claude/cases/claude-workflow.ts +5 -0
  32. package/src/claude/cases/misc.ts +10 -0
  33. package/src/commands/tooling.ts +8 -1
  34. package/src/project-root.ts +34 -0
  35. package/src/shipped/references.ts +45 -15
  36. package/standards/readme.md +2 -0
  37. package/tooling/astro/configs/eslint.config.js +7 -1
  38. package/tooling/astro/reference.md +1 -1
  39. package/tooling/nextjs/configs/eslint.config.js +3 -2
  40. package/tooling/nextjs/reference.md +1 -1
  41. 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. For each field `diff` carries, put the change to the operator through the structured question surface: which of the differing fields to write. 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.
23
- 4. Report the fields the proposal left absent as unchanged, naming that neither the README nor `package.json` carried a source for them.
24
- 5. Stop. Do not run apply here even when the operator answers immediately, since answering is not yet an apply invocation.
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>" → "<proposed>"
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.
@@ -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` Step 5, 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.
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 5 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.
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 7 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/`.
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:systematic-debugging` | When a test fails or a bug surfaces, to force root cause first |
228
- | `canon:claude-ui-test` | After a UI change, to generate e2e tests and a visual checklist |
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-draft` | For a brand-new `docs/*.md` page, drafted against `standards/docs.md` |
295
- | `canon:context-draft` | For a brand-new `.claude/context/<domain>.md` entry, drafted against `standards/context.md` |
296
- | `canon:wireframe-draft` | For a brand-new `.claude/wireframes/<surface>.md` file, drafted against `standards/wireframes.md` |
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.62.1",
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/AgentView.astro. Every field below is either a
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-draft',
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-draft',
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: 'wireframe-draft',
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.',
@@ -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',
@@ -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()
@@ -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 only where `isStandardsPathResolvable` confirms the match
172
- * resolves against this checkout, the same gate `DOCS_PATH` runs. A body
173
- * illustrating the shape a project's own `standards/<name>.md` takes writes
174
- * the identical placeholder token this pattern matches, such as
175
- * `standards/<slug>.md` in `create-standard/SKILL.md` or `standards/<name>.md`
176
- * in `migration-standards-drop/SKILL.md`, and neither resolves to a real
177
- * file. Four such placeholders surfaced across three files the first time
178
- * this pattern ran unresolved, measured 2026-09-06.
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 `DOCS_PATH` match names a real file in this checkout.
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 isDocsPathResolvable(
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
- /** Whether a `STANDARDS_PATH` match names a real file in this checkout. */
254
- function isStandardsPathResolvable(path: string, root: string): boolean {
255
- return existsSync(join(root, path))
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 (!isDocsPathResolvable(file, match[0], root)) continue
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 (!isStandardsPathResolvable(match[0], root)) continue
362
+ if (!isStandardsPathReportable(match[0])) continue
333
363
  references.push({
334
364
  file,
335
365
  line: index + 1,
@@ -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__|pages)': 'KEBAB_CASE' },
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`'s `src/**/!(__tests__|pages)` pattern reaches no folder under a real ESLint run, `.astro`-only or otherwise. See `.claude/context/tooling.md` for the measurement.
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