@erclx/canon 4.120.0 → 4.121.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 (72) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/SKILL.md +29 -115
  3. package/claude/skills/auto-ship/references/draft-mark.md +30 -0
  4. package/claude/skills/auto-ship/references/failure-recovery.md +27 -0
  5. package/claude/skills/auto-ship/references/tier-failures.md +26 -0
  6. package/claude/skills/auto-ship/references/verb-absent.md +37 -0
  7. package/claude/skills/auto-ship/references/worktree-name.md +22 -0
  8. package/claude/skills/bash-script/SKILL.md +1 -1
  9. package/claude/skills/bash-script/references/patterns.md +0 -99
  10. package/claude/skills/bash-script/references/prompts.md +100 -0
  11. package/claude/skills/create-skill/SKILL.md +1 -1
  12. package/claude/skills/create-snippet/SKILL.md +1 -1
  13. package/claude/skills/create-standard/SKILL.md +1 -1
  14. package/claude/skills/design-extract/SKILL.md +1 -1
  15. package/claude/skills/design-taste/REQUIREMENT.md +2 -2
  16. package/claude/skills/docs-sync/SKILL.md +1 -1
  17. package/claude/skills/draft-context/SKILL.md +1 -1
  18. package/claude/skills/draft-docs/SKILL.md +1 -1
  19. package/claude/skills/draft-figure/SKILL.md +1 -1
  20. package/claude/skills/draft-readme/SKILL.md +1 -1
  21. package/claude/skills/draft-wiki/SKILL.md +1 -1
  22. package/claude/skills/draft-wireframes/SKILL.md +1 -1
  23. package/claude/skills/git-issue/SKILL.md +1 -1
  24. package/claude/skills/git-issue/references/issue.md +1 -1
  25. package/claude/skills/git-pr/SKILL.md +1 -1
  26. package/claude/skills/git-stage/SKILL.md +1 -1
  27. package/claude/skills/memory-capture/SKILL.md +1 -1
  28. package/claude/skills/restate-plainly/REQUIREMENT.md +1 -1
  29. package/claude/skills/review-address/SKILL.md +4 -3
  30. package/claude/skills/review-branch/REQUIREMENT.md +1 -0
  31. package/claude/skills/review-branch/SKILL.md +5 -6
  32. package/claude/skills/review-craft/REQUIREMENT.md +43 -0
  33. package/claude/skills/review-craft/SKILL.md +50 -0
  34. package/claude/skills/review-craft/references/security.md +21 -0
  35. package/claude/skills/review-craft/references/ui.md +72 -0
  36. package/claude/skills/review-pr/REQUIREMENT.md +1 -0
  37. package/claude/skills/review-pr/SKILL.md +1 -1
  38. package/claude/skills/review-pr/references/post.md +1 -1
  39. package/claude/skills/role-planner/REQUIREMENT.md +1 -1
  40. package/claude/skills/standards-audit/SKILL.md +1 -1
  41. package/claude/skills/write-human/REQUIREMENT.md +7 -4
  42. package/claude/skills/write-human/SKILL.md +4 -2
  43. package/claude/skills/write-human/references/machine-tells.md +1 -1
  44. package/claude/skills/write-human/references/source-material.md +5 -2
  45. package/docs/agents/audits.md +1 -1
  46. package/docs/agents/commands.md +1 -1
  47. package/docs/agents/markdown-audit.md +13 -19
  48. package/docs/agents/pr-local.md +6 -3
  49. package/docs/target-projects.md +1 -1
  50. package/docs/workflow/ai-workflow.md +1 -0
  51. package/governance/rules/claude/500-prose.md +1 -1
  52. package/governance/rules/claude/501-markdown.md +3 -3
  53. package/package.json +1 -1
  54. package/src/autoship/paths.ts +3 -2
  55. package/src/claude/cases/workflow.ts +5 -0
  56. package/src/commands/audits.ts +2 -2
  57. package/src/commands/markdown.ts +7 -9
  58. package/src/commands/pr.ts +11 -4
  59. package/src/gate/measures.ts +4 -4
  60. package/src/gate/stages.ts +0 -1
  61. package/src/markdown/bans.ts +11 -68
  62. package/src/markdown/gate.ts +1 -1
  63. package/src/markdown/scan.ts +7 -88
  64. package/standards/docs.md +1 -1
  65. package/standards/index.md +1 -1
  66. package/standards/markdown.md +5 -7
  67. package/standards/pr.md +1 -1
  68. package/standards/publish.md +1 -1
  69. package/standards/readme.md +2 -2
  70. package/tooling/base/configs/scripts/verify.sh +2 -2
  71. package/tooling/base/seeds/canon/context/development.md +1 -1
  72. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +5 -6
@@ -7,7 +7,7 @@ description: Scope boundary between the writing rules a scan enforces and the on
7
7
 
8
8
  ## Gap
9
9
 
10
- Without this skill, a session is graded on the words it avoids and never on how the prose reads. The banned words, characters, and spellings ship as data the CLI and the installed hook both read, so that half is measured on every run. The voice half sat under a heading in a standard nothing opened, so a session wrote the whole passage without it and the audit reported a clean exit over prose that read machine-written.
10
+ Without this skill, a session is graded on the words it avoids and never on how the prose reads. The banned characters ship as data the CLI and the installed hook both read, so that half is measured on every run. The voice half sat under a heading in a standard nothing opened, so a session wrote the whole passage without it and the audit reported a clean exit over prose that read machine-written.
11
11
 
12
12
  The output of that arrangement is specific rather than general. Paragraphs come back with every sentence landing within a few words of the last, with consecutive sentences opening on the same word and the same grammatical shape, and with the compression rule applied until the verb is gone, so the passage trades uniform sentences for uniform fragments. A session told to be terse and told nothing about cadence produces exactly that, since cutting is the only instruction it holds.
13
13
 
@@ -21,13 +21,15 @@ Importing that catalog wholesale fails on a different measurement. External mate
21
21
  - State cadence as something to measure, giving the sentence-length spread and the repeated-opening count a session can check, since a rule to vary sentence length is one a session satisfies by assertion
22
22
  - Ban the fragment trade by name, since compression is the instruction a terse register already carries and the verb is what it drops first
23
23
  - Name the machine tells a scan cannot reach, and hold the catalog behind a stated trigger so a run that needs none pays no read for it
24
- - Point at the banned words rather than restating them, since they ship as package data a command reads and a second copy drifts with nothing comparing the two
24
+ - Carry the marketing words and vague qualifiers as word-choice guidance, since no command reads them and a word ban catches the token and misses the habit behind it
25
+ - Point at the banned characters rather than restating them, since they ship as package data a command reads and a second copy drifts with nothing comparing the two
25
26
  - Record which external patterns were adopted, which were declined, and the measurement behind each, so a later session extends the position instead of re-deriving it
26
27
  - Yield voice to a surface that states its own, and hold rhythm, density, and the tells on every surface
27
28
 
28
29
  ## Must not
29
30
 
30
- - Restate the banned words, characters, or spellings, at any length or as an example set
31
+ - Restate the banned characters, at any length or as an example set
32
+ - Present the word guidance as a gate, or restate it on a second surface, since this body is its one copy
31
33
  - Claim the reported failure is closed. Cadence is a property of a passage and nothing measures the output against these rules yet, so this skill states the rule and the measurement is owed separately.
32
34
  - Rewrite a term the corpus already uses because an external source bans it, without the count and a recorded position
33
35
  - Grow a second voice register inside this body. One baseline covers every surface, and a register a real surface needs arrives as an overlay that yields on voice alone.
@@ -39,7 +41,8 @@ Importing that catalog wholesale fails on a different measurement. External mate
39
41
 
40
42
  ## Out of scope
41
43
 
42
- - The banned words, characters, and spellings, which `markdown.md` states for a reader and `canon markdown audit` gates from package data
44
+ - The banned characters, which `markdown.md` states for a reader and `canon markdown audit` gates from package data
45
+ - Spelling, which a project's spell checker enforces
43
46
  - Markdown mechanics, being headings, lists, code spans, punctuation, and file references
44
47
  - Restating text that already exists in plainer words, which `restate-plainly` owns on a different trigger, since a restatement is asked for by name and this arrives on a glob match. The rules here still govern how that restatement reads, and that body cites this one rather than carrying a copy.
45
48
  - What voice a given surface declares, which belongs to the standard governing that surface
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: write-human
3
- description: Carries the voice, rhythm, sentence construction, and information density rules a banned-word list cannot express, plus the catalog of machine tells that survive a clean ban scan. Use when writing or revising prose a person will read, when a passage reads flat or uniform, or when asked to "make this read like a person", "fix the cadence", "this reads like AI wrote it", "vary the sentences", or "tighten this without gutting it". Do NOT use for banned words, characters, or spellings, which `markdown.md` states and `canon markdown audit` gates, and do NOT use to restate text that already exists in plainer words, which is `restate-plainly`, whose body loads these rules from here.
3
+ description: Carries the voice, word choice, rhythm, sentence construction, and information density rules a banned-character scan cannot express, plus the catalog of machine tells that survive a clean ban scan. Use when writing or revising prose a person will read, when a passage reads flat or uniform, or when asked to "make this read like a person", "fix the cadence", "this reads like AI wrote it", "vary the sentences", or "tighten this without gutting it". Do NOT use for banned characters, which `markdown.md` states and `canon markdown audit` gates, and do NOT use to restate text that already exists in plainer words, which is `restate-plainly`, whose body loads these rules from here.
4
4
  ---
5
5
 
6
6
  # Write human
@@ -9,7 +9,7 @@ Write prose that reads as though a person wrote it. A ban list subtracts words a
9
9
 
10
10
  Load this before drafting a passage rather than after. A revision pass recovers the words and never recovers the structure the draft already settled.
11
11
 
12
- The banned words, characters, and spellings sit in `${CLAUDE_SKILL_DIR}/../../standards/markdown.md`. They stay in force here. This skill adds to them and never restates them, since `canon markdown audit` reads them from the package and a second copy would drift.
12
+ The banned characters sit in `${CLAUDE_SKILL_DIR}/../../standards/markdown.md`. They stay in force here. This skill adds to them and never restates them, since `canon markdown audit` reads them from the package and a second copy would drift. The word guidance under `## Voice` is the one copy of that list, and no command reads it.
13
13
 
14
14
  ## What yields and what does not
15
15
 
@@ -23,6 +23,8 @@ The banned words, characters, and spellings sit in `${CLAUDE_SKILL_DIR}/../../st
23
23
  - Use active voice, and present tense unless past or future is factually correct.
24
24
  - Name the actor in a sentence that has one. Passive voice with an unnamed actor hides who acts and reads as evasion.
25
25
  - Prefer direct verbs and plain words. Write `use` over `utilize`, `help` over `facilitate`, `is` over `serves as`.
26
+ - Drop marketing words: `seamless`, `robust`, `powerful`, `revolutionary`, `enhanced`, `allows`, and `leverage`. Each claims a quality without showing it, so state what the thing does and let the reader judge it.
27
+ - Drop vague qualifiers: `simply`, `just`, `easily`, `quickly`, `very`, and `really`. Cut the word where the sentence stands without it, and give the measurement where the degree matters.
26
28
  - Be direct on an established fact. Hedge on a genuinely uncertain claim and say which it is.
27
29
  - Assume developer-level knowledge. Skip the explanation the reader already carries.
28
30
  - Front-load the key information in a paragraph, and give every sentence something the reader did not have.
@@ -5,7 +5,7 @@ description: Patterns that survive a clean ban scan, why each is invisible to a
5
5
 
6
6
  # Machine tells
7
7
 
8
- Each pattern below is a shape rather than a token, so no closed word set matches it and `canon markdown audit` reports a clean exit over a passage carrying every one. Read this when revising text a model drafted, or when a passage passes the scan and still reads wrong.
8
+ Each pattern below is a shape rather than a token, so no closed word set matches it and the character scan `canon markdown audit` runs reports a clean exit over a passage carrying every one. Read this when revising text a model drafted, or when a passage passes the scan and still reads wrong.
9
9
 
10
10
  Work top down. The first three account for most of what a reader calls machine-written.
11
11
 
@@ -21,10 +21,13 @@ The ambiguity rules travel with that layer for the same reason. Compression is w
21
21
 
22
22
  - **The ban on abstract metaphor nouns.** The nouns it names are load-bearing terms in this corpus rather than decoration, and the count below is what decides it.
23
23
  - **Document mode as a selector.** The source has the writer pick a mode before drafting. In this corpus a document's shape is already fixed by the standard governing its type, so a mode selector adds a second authority over one question and the two will disagree.
24
- - **Any restatement of a banned word set.** The bans ship as package data that `canon markdown audit` reads, and a copy here would be a second source for one list with a machine reading the other.
25
24
  - **A voice register per surface.** One baseline covers every surface. A surface needing a warmer register states its own voice and claims the yield, which lifts voice alone and leaves the bans, the rhythm rules, and the tells in force.
26
25
 
27
- The first of those four rests on a count. The source names `surface` among the nouns it bans, and tracked markdown in the authoring repository used `surface` 593 times, `harness` 95, `scaffolding` 17, and `primitive` 4. Adopting the item either rewrites established vocabulary across the corpus or is ignored in silence, and a reader arriving later cannot tell which happened.
26
+ The first of those three rests on a count. The source names `surface` among the nouns it bans, and tracked markdown in the authoring repository used `surface` 593 times, `harness` 95, `scaffolding` 17, and `primitive` 4. Adopting the item either rewrites established vocabulary across the corpus or is ignored in silence, and a reader arriving later cannot tell which happened.
27
+
28
+ ## What moved in from the markdown standard
29
+
30
+ The marketing words and vague qualifiers under `## Voice` were a gated word set in the markdown standard until the audit narrowed to characters. A word ban catches the token and misses the habit behind it, so a writer swaps `robust` for a synonym and the claim stays unproven. The list now lives here as guidance a reader applies, and this body is its one copy, since no command reads it and a second copy would drift with nothing comparing the two.
28
31
 
29
32
  ## Where this diverges
30
33
 
@@ -36,7 +36,7 @@ Each is invoked as the CLI the caller is running rather than as a global `canon`
36
36
 
37
37
  ## What gates and what reports
38
38
 
39
- Five findings fail the run: an unresolved context citation, a banned character, word, or spelling, a relative link resolving to nothing on disk, a skill folder carrying no `REQUIREMENT.md`, and a credential-shaped value in the tree the package ships. Each is a fact with no false-positive class behind it.
39
+ Five findings fail the run: an unresolved context citation, a banned character, a relative link resolving to nothing on disk, a skill folder carrying no `REQUIREMENT.md`, and a credential-shaped value in the tree the package ships. Each is a fact with no false-positive class behind it.
40
40
 
41
41
  Four of the five are the ones `canon gate run` already fails a push on. The secret scan is the one entry gating without a stage behind it, added on that same fact-or-judgment test rather than as a side effect of registering a measure, and the toolkit already ranks content leaving the repository above content that stays.
42
42
 
@@ -75,7 +75,7 @@ Full help: `canon <command> --help`. Bare `canon --help` lists every top-level c
75
75
  | `canon context classify sweep` | Classify every section of the five canonical doc types, split at H3, as keep, rewrite, or move (`--doc-types`, `--json`) |
76
76
  | `canon context classifier show` | Report the resolved classifier backend and model and which source decided them (`--json`) |
77
77
  | `canon context classifier set` | Write the project classifier setting to `canon/config/classifier.toml` (`--backend`, `--model`, `--json`) |
78
- | `canon markdown audit` | Fail any markdown path on a banned character, word, or spelling, or a dead relative link, and report the structural checkpoints |
78
+ | `canon markdown audit` | Fail any markdown path on a banned character or a dead relative link, and report the structural checkpoints |
79
79
  | `canon claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
80
80
  | `canon standards audit` | Report the corpus against the success-criterion gate `standards/standard.md` states, failing only on a standard new to the branch (`--json`, `--arrivals-only`) |
81
81
  | `canon claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`) |
@@ -27,7 +27,7 @@ A bare run measures every markdown file git lists, tracked plus untracked-and-no
27
27
 
28
28
  ## Where the rules come from
29
29
 
30
- The three ban sets and all ten checkpoints ship with the `canon` package as data, in `src/markdown/bans.ts` and `src/markdown/structure.ts`. Every project is measured against the same sets whether or not it installed any standards, and no file has to resolve for a run to mean something.
30
+ The ban set and all ten checkpoints ship with the `canon` package as data, in `src/markdown/bans.ts` and `src/markdown/structure.ts`. Every project is measured against the same set whether or not it installed any standards, and no file has to resolve for a run to mean something.
31
31
 
32
32
  Seven of the ten are stated in `markdown.md` and the three cadence numbers are stated in the `write-human` skill. That split is the content boundary rather than an accident: `markdown.md` carries the enforced rules a scan can decide, and the skill carries the rhythm rules a ban list cannot express. A cadence number moved in the skill and left in the code drifts the same way, so move both.
33
33
 
@@ -35,11 +35,9 @@ Reading them out of the standards per run was the original design. It put a pars
35
35
 
36
36
  `markdown.md` still states every ban and every checkpoint, and a reader follows it rather than the code. Nothing compares the two, so a number moved in one place and left in the other drifts silently. Move both in the same change.
37
37
 
38
- The sets are closed rather than extensible, so a project cannot add a term by editing a file. What decides that is the measurement behind them: 21 terms across 483 markdown files report a clean exit, and every occurrence of a banned word in the corpus sits inside the ban list itself or inside an example demonstrating the ban. The set is a prior an author already knows rather than a filter that has caught anything, and enumeration cannot close the gap it aims at, since `just`, `allows`, and `very` carry honest uses no literal match separates.
38
+ The set is closed rather than extensible, so a project cannot add a term by editing a file. It holds the two characters alone, since a character is the one class a literal match settles. The set once carried 13 words and 6 British spellings as well. The words moved to the `write-human` skill as guidance, because a word ban catches the token and misses the habit, and `just`, `allows`, and `very` carry honest uses no literal match separates. Spelling went to a project's spell checker, which already reads every word.
39
39
 
40
- Freezing the spellings gave up a property worth naming. They were derived by applying the standard's own suffix pairs to its own examples, so an example added there extended the check with no code edit. The set is now carried whole, and `analyse` stays out of it for the reason it was always out: the standard's example is `analyze`, which ends in `-yze` rather than the `-ize` its rule states.
41
-
42
- A set shipped empty is reported rather than passed. It finds nothing and would exit clean, which reports a corpus nobody checked as a corpus carrying no violation, so the run names the empty set and exits `1`. The sets ship with the package, so a defect in the build is the only cause left.
40
+ A set shipped empty is reported rather than passed. It finds nothing and would exit clean, which reports a corpus nobody checked as a corpus carrying no violation, so the run names the empty set and exits `3`. The set ships with the package, so a defect in the build is the only cause left.
43
41
 
44
42
  `canon standards <name>` still resolves a standard at the authoring root, then the package corpus, and prints it, so the human catalog reads without a project copy on disk.
45
43
 
@@ -47,17 +45,11 @@ A set shipped empty is reported rather than passed. It finds nothing and would e
47
45
 
48
46
  ### Bans
49
47
 
50
- Three closed sets report a hit: the characters `markdown.md` bans under `## Punctuation`, the single lowercase words it bans under `## Language`, and the British spellings of the American examples that section lists.
51
-
52
- Deriving the spellings rather than pattern-matching a suffix is what keeps `exercises`, `promises`, and `revised` out of the report. A suffix pattern over the same corpus produced 46 false positives from words of that shape, and a closed set of whole words reaches none of them.
48
+ One closed set reports a hit: the characters `markdown.md` bans under `## Punctuation`.
53
49
 
54
50
  Frontmatter, fenced blocks, inline code spans, and link destinations are excluded. Without the code-span exclusion each standard would report its own backticked examples, and without the link exclusion a semicolon in a query string would report as prose no rewrite can fix.
55
51
 
56
- A banned word is bounded on a word character or a hyphen either side. A plain word boundary sits after a hyphen, so a banned word ending a hyphenated compound reported from inside one, and a compound is a single word to the reader who wrote it.
57
-
58
- A banned spelling keeps the plain word boundary, hyphens included. The two bans target different things: a word ban targets the word, so reading a compound as one word is correct, while a spelling ban targets the orthography inside it, which sits in `behaviour-driven` as plainly as it sits alone.
59
-
60
- Two ban shapes stay unmeasured and the report says so on every run. A multi-word ban escapes the harvest by width, whether it carries a placeholder standing in for the rest of the sentence or spells the phrase out in full, and every rule under `## Voice` is a judgment. The bans `## Language` states over what a sentence may claim sit in the first group by construction, since a literal match over a pattern reports the compliant text and reaches none of the violations. A report listing hits without naming those would read as a verdict on the whole standard.
52
+ Word choice stays unmeasured and the report says so on every run. Single words travel in the `write-human` skill as guidance a reader applies. A multi-word ban escapes the harvest by width, whether it carries a placeholder standing in for the rest of the sentence or spells the phrase out in full, and every rule under `## Voice` is a judgment. The bans `## Language` states over what a sentence may claim are patterns of the multi-word kind by construction, since a literal match over a pattern reports the compliant text and reaches none of the violations. A report listing hits without naming those would read as a verdict on the whole standard.
61
53
 
62
54
  ### Links
63
55
 
@@ -108,7 +100,7 @@ A bullet, a heading, a table row, a blockquote, a blank line, and a fence each e
108
100
 
109
101
  ### Cadence
110
102
 
111
- Uniform cadence is the failure a ban list cannot express. A ban set states negatives, and fragments, verbless clauses, and sentences that all run one length are each the absence of something, so no addition to the thirteen banned words reaches any of them. The shape layer already measured a bullet, a paragraph, and a run, and stopped one level above where that failure lives.
103
+ Uniform cadence is the failure a ban list cannot express. A ban set states negatives, and fragments, verbless clauses, and sentences that all run one length are each the absence of something, so no word added to a ban list reaches any of them. The shape layer already measured a bullet, a paragraph, and a run, and stopped one level above where that failure lives.
112
104
 
113
105
  Cadence measures a paragraph on two numbers. The spread is the words between its longest and shortest sentence, and the opener count is the times one word opens a sentence in it. A spread of five words or under reads as one cadence, and a word opening more than two sentences is a pattern rather than a coincidence. Both come from `## Rhythm` in the `write-human` skill, which states them about prose a person reads, and this measures against that statement rather than setting a threshold of its own.
114
106
 
@@ -134,7 +126,7 @@ The Length step sums rendered lines over the whole source, frontmatter and fence
134
126
 
135
127
  ## Exit codes
136
128
 
137
- Exit codes are `0` for a completed run with no gating finding, `1` for a refusal, `2` for a ban hit or a dead relative link, and `3` for a shipped ban set that arrived empty. A banned character, word, or spelling and a relative link resolving to nothing on disk each fail the run, both facts a scan settles rather than a reader judging. Bullet, paragraph, and depth weight are judgments a reader settles, and cadence is a distribution whose healthy range moves with the surface, so all four report under every code. Length reports the same way, since the audit also runs on a plan kept out of version control, and the Document ceiling stage in `canon gate run` owns that verdict instead.
129
+ Exit codes are `0` for a completed run with no gating finding, `1` for a refusal, `2` for a ban hit or a dead relative link, and `3` for a shipped ban set that arrived empty. A banned character and a relative link resolving to nothing on disk each fail the run, both facts a scan settles rather than a reader judging. Bullet, paragraph, and depth weight are judgments a reader settles, and cadence is a distribution whose healthy range moves with the surface, so all four report under every code. Length reports the same way, since the audit also runs on a plan kept out of version control, and the Document ceiling stage in `canon gate run` owns that verdict instead.
138
130
 
139
131
  `3` is separate from `1` because the two want different responses from a caller. A refusal means no corpus was built, and the `Markdown bans` stage in `canon gate run` is right to report it as unmeasured rather than as a pass. An empty set means the corpus was walked and nothing was looked for, so that stage fails the push on `3` rather than skipping.
140
132
 
@@ -154,11 +146,11 @@ Rewrite the sentence rather than swapping the banned token for a near-synonym. T
154
146
 
155
147
  A code span clears the report too, since the ban scan walks around one, and it is the answer only where the token is genuinely an identifier under discussion. `## Code and identifiers` in `markdown.md` reserves the span for commands, API names, file paths, and identifiers, so backticking a quoted utterance spends one rule to satisfy another and leaves the corpus no cleaner.
156
148
 
157
- A hit the closed set cannot separate from correct prose is the case with no third option. `markdown.md` bans vague qualifiers and lists the tokens those qualifiers happen to spell, so the temporal `just` reports as the vague one. The rule as written reaches neither, and rewriting the sentence is what the toolkit settled on over building an exemption path, for the reasons below.
149
+ A hit the closed set could not separate from correct prose was the case with no third option while the set still carried words. The temporal `just` reported as the vague qualifier it happened to spell, and rewriting the sentence is what the toolkit settled on over building an exemption path, for the reasons below. That class is part of why the words left the gate.
158
150
 
159
151
  ### Where the rules are enforced
160
152
 
161
- Five surfaces apply the ban sets and four of them go through this verb. `.claude/hooks/standards-audit.sh` runs it against a single file after each markdown edit, the seed copy a project installs does the same, the `Markdown bans` stage in `canon gate run` runs it across the whole corpus before this repository's own push, and the same-named stage in `tooling/base/configs/scripts/verify.sh` runs it across a scaffolded project's tracked markdown before its own push. Each hook parsed its own copy of the word bans in awk before that, which left a British spelling passing at edit time and failing the push with nothing in between explaining the difference.
153
+ Five surfaces apply the ban set and four of them go through this verb. `.claude/hooks/standards-audit.sh` runs it against a single file after each markdown edit, the seed copy a project installs does the same, the `Markdown bans` stage in `canon gate run` runs it across the whole corpus before this repository's own push, and the same-named stage in `tooling/base/configs/scripts/verify.sh` runs it across a scaffolded project's tracked markdown before its own push. Each hook parsed its own copy of the word bans in awk before that, which left a British spelling passing at edit time and failing the push with nothing in between explaining the difference.
162
154
 
163
155
  The seed copy moved onto the verb when the sets became data, since its awk had nothing left to parse. It resolves one runner where the toolkit copy resolves two, looking for no checkout source, and a machine carrying no `canon` gets a report naming the binary to install rather than a silent pass. `scripts/core/check-seed-independence.sh` scopes its walk to markdown and leaves the seed hooks outside it, which its own comment records as deliberate.
164
156
 
@@ -170,7 +162,7 @@ The fifth surface reads the standards directly and is not a consolidation left h
170
162
 
171
163
  The hook prefers a checkout's own `src/cli.ts` over a globally installed binary, so it and the push stage read one build. A published binary lags a branch by whatever has not been released, which would put a ban kind added on the branch into the push and not into the edit. It reads its findings out of the `--json` record rather than off the exit code, so an older binary still reports where the fallback applies. It reads `bans.emptySets` out of the same record, so a set the verb shipped empty reaches the author as a check narrowed to what it could measure rather than as a clean pass.
172
164
 
173
- That field replaced `bans.missingStandards`, which answered a standard resolving under none of three roots. The sets ship with the package now, so the state it named cannot occur and the narrowed check has one cause left, a defect in the build. The hook keeps reading a field either way, since a reader cannot tell a narrowed check from a clean one without it.
165
+ That field replaced `bans.missingStandards`, which answered a standard resolving under none of three roots. The set ships with the package now, so the state it named cannot occur and the narrowed check has one cause left, a defect in the build. The hook keeps reading a field either way, since a reader cannot tell a narrowed check from a clean one without it.
174
166
 
175
167
  Both hooks answer an absent record as well. A completed run always writes the record and a refusal writes none, so an empty one means the verb declined to measure rather than measured and found nothing. The verb needs a git repository to build its corpus and refuses without one, which is a project the seeded hook can be installed into, and reading the findings alone reported that as a clean file.
176
168
 
@@ -188,9 +180,11 @@ Masking took 7 of the weight-only paragraphs the checkpoint reported at 400 and
188
180
 
189
181
  ### How the ban count reached zero
190
182
 
183
+ The set still carried words and spellings when the gate turned on, so this section and the next are history rather than a description of the current scan.
184
+
191
185
  Eight word hits stood between the baseline and a gate, and only three carried the sense the standard bans. `leverage` sat in the requirements worldview, `allows` in the claude stack reference, and one `just` was the vague qualifier in a skill body. Those three lost the qualifier rather than the word.
192
186
 
193
- The other five were correct prose the closed set cannot separate from a violation. Four were the temporal `just`, meaning a moment ago, in phrases like the implementation `just` completed and the field the user `just` edited. The fifth quoted an anti-pattern a skill exists to forbid. `markdown.md` bans vague qualifiers and lists the tokens those qualifiers happen to spell, so the rule as written reaches none of the five while the scan reaches all of them.
187
+ The other five were correct prose the closed set cannot separate from a violation. Four were the temporal `just`, meaning a moment ago, in phrases like the implementation `just` completed and the field the user `just` edited. The fifth quoted an anti-pattern a skill exists to forbid. `markdown.md` banned vague qualifiers and listed the tokens those qualifiers happen to spell, so the rule as written reached none of the five while the scan reached all of them.
194
188
 
195
189
  ### Why they were rewritten rather than exempted
196
190
 
@@ -29,9 +29,12 @@ The HTML test is there because a working tree often holds more than one
29
29
  listener. A test runner's own server, or a dev server's reload socket, answers
30
30
  HTTP too, and a reviewer opening either gets a bare 404 rather than the branch.
31
31
 
32
- Sockets are read through `lsof` where it is installed. A Linux machine without
33
- it falls back to `/proc/net/tcp` and `/proc/net/tcp6`, matched against each
34
- process's `fd` table. A machine with neither refuses as `no-listener-reader`.
32
+ Sockets are read from `/proc/net/tcp` and `/proc/net/tcp6` wherever they exist,
33
+ matched against each process's `fd` table, and through `lsof` on a machine
34
+ without them. Linux reads `/proc` even with `lsof` installed, since `lsof` 4.95
35
+ drops a process whose truncated name holds an unmatched `(`, which is what
36
+ Next's `next-server (vX.Y.Z)` title is cut down to. A machine with neither
37
+ refuses as `no-listener-reader`.
35
38
 
36
39
  The port is never derived from the worktree. A port helper needs a per-stack
37
40
  base port the toolkit cannot know, and a server started by hand or by an
@@ -102,7 +102,7 @@ Run `canon tooling list --json` and `canon gov list --json` to see the current c
102
102
 
103
103
  `canon init` installs base tooling, Claude workflow, and governance, and scaffolds `.claude/wiki/`. Governance defaults to the `base` stack, so a bare init carries the rules that route a project. Pass `--stack <name>` to install a framework stack instead.
104
104
 
105
- No standard is written into the project. Each governance rule's authority line names `canon standards <name>`, which answers from the corpus inside the CLI's own package, and every toolkit skill names the copy in its own plugin root. `canon markdown audit` needs no standard at all, its ban sets and checkpoints shipping with the package as data.
105
+ No standard is written into the project. Each governance rule's authority line names `canon standards <name>`, which answers from the corpus inside the CLI's own package, and every toolkit skill names the copy in its own plugin root. `canon markdown audit` needs no standard at all, its ban set and checkpoints shipping with the package as data.
106
106
 
107
107
  No snippet is written into the project either. `claude/snippets` in the plugin cache symlinks to the toolkit's own `snippets/`, so a session reaches one at its `@` reference with no install step, the same live resolution `claude/standards` gives a rule's authority line.
108
108
 
@@ -231,6 +231,7 @@ This section is the corpus the coverage claim is measured against: every name `c
231
231
  | `canon:project-commands` | When the project's own command needs running |
232
232
  | `canon:test-first` | Before implementing a planned change, to run its test red, green, then refactor |
233
233
  | `canon:test-craft` | When writing or changing any test, to pick its layer and filter what it asserts |
234
+ | `canon:review-craft` | When reviewing a change, for what to look for and how much evidence a finding needs |
234
235
  | `canon:systematic-debugging` | When a test fails or a bug surfaces, to force root cause first |
235
236
  | `canon:ui-checklist` | After a UI change, to write what to look at and name what ships untested |
236
237
 
@@ -12,4 +12,4 @@ paths:
12
12
  - Load it before drafting a passage, not after revising one.
13
13
  - Report it rather than proceeding silently when the skill does not resolve.
14
14
  - Do not work these rules from memory.
15
- - Banned words, spellings, punctuation, formatting, and file references are a separate topic. `501-markdown` routes them.
15
+ - Punctuation, formatting, and file references are a separate topic. `501-markdown` routes them.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Route markdown edits to the markdown standard for headings, lists, punctuation, banned words, and frontmatter wording
2
+ description: Route markdown edits to the markdown standard for headings, lists, punctuation, and frontmatter wording
3
3
  paths:
4
4
  - '**/*.md'
5
5
  ---
@@ -8,7 +8,7 @@ paths:
8
8
 
9
9
  ## Authority
10
10
 
11
- - Follow the markdown standard for headings, paragraph and list structure, code spans, punctuation, emphasis, file references, the banned words and spellings, and the wording of a `title` or `description`. It is the single source. Read it with `canon standards markdown`.
12
- - Read it before a substantial markdown edit. Do not work the banned words or characters from memory.
11
+ - Follow the markdown standard for headings, paragraph and list structure, code spans, punctuation, emphasis, file references, and the wording of a `title` or `description`. It is the single source. Read it with `canon standards markdown`.
12
+ - Read it before a substantial markdown edit. Do not work the banned characters from memory.
13
13
  - Run `canon markdown audit <path>` after the edit, and rewrite the sentence carrying a hit rather than swapping the token for a near-synonym.
14
14
  - Voice, rhythm, and sentence construction are a separate topic. `500-prose` routes them.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.120.0",
4
+ "version": "4.121.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -2,8 +2,9 @@
2
2
  * The surfaces where markdown states what an agent does, and the extensions
3
3
  * that read as prose, held as data one command parses.
4
4
  *
5
- * Lifted verbatim in content from the list `auto-ship/SKILL.md` Step 5
6
- * carried, which a session was asked to apply by hand. It failed that
5
+ * Lifted verbatim in content from the list `auto-ship` Step 6 carried, whose
6
+ * written fallback copy now sits in `auto-ship/references/verb-absent.md`,
7
+ * which a session was asked to apply by hand. It failed that
7
8
  * application three times, so the set moved here and the body now calls a verb
8
9
  * that reads it. Being machine-parsed makes it permanently exempt from any
9
10
  * later design that folds a standard back into the surface citing it, per the
@@ -99,6 +99,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
99
99
  'Look over everything that changed on this branch for bugs and edge cases.',
100
100
  expect: 'review-branch',
101
101
  },
102
+ {
103
+ prompt:
104
+ 'What should a review of a change look for beyond bugs, and how much evidence does a finding need?',
105
+ expect: 'review-craft',
106
+ },
102
107
  {
103
108
  prompt: 'Draft me a script with beats for a screencast recording.',
104
109
  expect: 'draft-screencast',
@@ -76,8 +76,8 @@ export function register(program: Command): void {
76
76
  ' 3 an audit did not report, so the run measured less than the set',
77
77
  '',
78
78
  'It gates on exactly what already gates a push and on nothing new:',
79
- 'an unresolved context citation, a banned character, word, or spelling,',
80
- 'and a skill folder carrying no REQUIREMENT.md. Every other measure is a',
79
+ 'an unresolved context citation, a banned character, and a skill folder',
80
+ 'carrying no REQUIREMENT.md. Every other measure is a',
81
81
  'judgment a reader settles, and failing a push on one teaches',
82
82
  'contributors to route around the stage.',
83
83
  '',
@@ -67,7 +67,7 @@ export function register(program: Command): void {
67
67
  markdown
68
68
  .command('audit')
69
69
  .description(
70
- 'Fail on a banned character, word, or spelling, and report bullet, paragraph, cadence, depth, and length',
70
+ 'Fail on a banned character, and report bullet, paragraph, cadence, depth, and length',
71
71
  )
72
72
  .argument(
73
73
  '[path...]',
@@ -82,8 +82,8 @@ export function register(program: Command): void {
82
82
  'Exit codes:',
83
83
  ' 0 the audit completed with no gating finding',
84
84
  ' 1 refused, with the reason on stderr',
85
- ' 2 a banned character, word, or spelling is present, or a relative',
86
- ' link resolves to nothing on disk',
85
+ ' 2 a banned character is present, or a relative link resolves',
86
+ ' to nothing on disk',
87
87
  ' 3 a shipped ban set is empty, so the run measured nothing',
88
88
  '',
89
89
  'A ban hit and a dead link are each a fact and gate unconditionally.',
@@ -192,8 +192,6 @@ async function runAudit(
192
192
  unmatchedPaths: scope.unmatched,
193
193
  bans: {
194
194
  characters: bans.characters,
195
- words: bans.words,
196
- spellings: bans.spellings,
197
195
  emptySets: empty,
198
196
  },
199
197
  checkpoints: {
@@ -284,19 +282,19 @@ function reportBans(
284
282
 
285
283
  if (empty.length > 0) {
286
284
  logWarn(
287
- `Not measured. The shipped set is empty for: ${empty.join(', ')}. The sets ship with the canon package, so an empty one is a defect in the build rather than a missing install.`,
285
+ `Not measured. The shipped set is empty for: ${empty.join(', ')}. The set ships with the canon package, so an empty set is a defect in the build rather than a missing install.`,
288
286
  )
289
287
  return
290
288
  }
291
289
 
292
290
  logInfo(
293
- `${plural(bans.characters.length, 'character')}, ${plural(bans.words.length, 'word')}, and ${plural(bans.spellings.length, 'spelling')} shipped with the canon package`,
291
+ `${plural(bans.characters.length, 'character')} shipped with the canon package`,
294
292
  )
295
293
  logInfo(
296
294
  'Frontmatter, fenced blocks, code spans, and link destinations are excluded.',
297
295
  )
298
296
  logInfo(
299
- 'Phrase bans and every voice rule are patterns rather than closed sets, and stay a judgment for the reader.',
297
+ 'Word bans, phrase bans, and every voice rule are guidance for the reader',
300
298
  )
301
299
 
302
300
  const carrying = reports
@@ -304,7 +302,7 @@ function reportBans(
304
302
  .sort((a, b) => b.bans.length - a.bans.length)
305
303
 
306
304
  if (carrying.length === 0) {
307
- logInfo('No banned character, word, or spelling.')
305
+ logInfo('No banned character.')
308
306
  return
309
307
  }
310
308
 
@@ -558,7 +558,8 @@ export function register(program: Command): void {
558
558
  'A process in a sibling worktree, in the main checkout seen from a',
559
559
  'linked worktree, or in a linked worktree seen from the main checkout',
560
560
  'is never reported, since its server shows another branch. Sockets are',
561
- 'read through lsof, or through /proc on a Linux machine without it.',
561
+ 'read through /proc where it exists, or through lsof on a machine',
562
+ 'without it.',
562
563
  '',
563
564
  '--remove reads the pull request comment carrying the pr-evidence',
564
565
  'marker and replaces its **Local preview:** line with --note, editing',
@@ -1759,13 +1760,20 @@ async function readProcListeners(): Promise<Listener[] | undefined> {
1759
1760
  }
1760
1761
 
1761
1762
  /**
1762
- * The machine half of a detection: lsof where it is installed, `/proc` on a
1763
- * Linux machine without it, and no reader anywhere else.
1763
+ * The machine half of a detection: `/proc` wherever it exists, lsof on a
1764
+ * machine without it, and no reader anywhere else.
1764
1765
  */
1765
1766
  function machineLocalRunner(): LocalRunner {
1766
1767
  const hasLsof = Bun.which('lsof') !== null
1767
1768
  return {
1768
1769
  async listListeners() {
1770
+ // lsof 4.95 drops every process whose kernel-truncated name holds an
1771
+ // unmatched `(`, which Next's `next-server (vX.Y.Z)` title cuts down to,
1772
+ // so `/proc` goes first despite costing several times what lsof does.
1773
+ if (existsSync('/proc/net/tcp')) {
1774
+ const listeners = await readProcListeners()
1775
+ if (listeners !== undefined) return listeners
1776
+ }
1769
1777
  if (hasLsof) {
1770
1778
  const result = await $`lsof -nP -iTCP -sTCP:LISTEN -Fpn`
1771
1779
  .quiet()
@@ -1774,7 +1782,6 @@ function machineLocalRunner(): LocalRunner {
1774
1782
  // read some process, and the rows it did print are real either way.
1775
1783
  if (result.exitCode <= 1) return parseLsofListeners(result.text())
1776
1784
  }
1777
- if (existsSync('/proc/net/tcp')) return readProcListeners()
1778
1785
  return undefined
1779
1786
  },
1780
1787
  async cwdOf(pid) {
@@ -438,8 +438,8 @@ export const recordIdempotence: Measure = async (ctx) => {
438
438
  }
439
439
 
440
440
  /**
441
- * A banned character, word, or spelling is a fact rather than a threshold, so
442
- * it fails the push while bullet, paragraph, and depth weight stay advisory.
441
+ * A banned character is a fact rather than a threshold, so it fails the push
442
+ * while bullet, paragraph, and depth weight stay advisory.
443
443
  *
444
444
  * The whole corpus is measured rather than the changed files, because a
445
445
  * `Do not use` bullet added to a standard bans a token retroactively and no
@@ -453,7 +453,7 @@ export const markdownBans: Measure = async (ctx) => {
453
453
  const { exitCode } = await ctx.cli(['markdown', 'audit', '--json'])
454
454
 
455
455
  if (exitCode === 0) {
456
- return { emissions: [info('No banned character, word, or spelling')] }
456
+ return { emissions: [info('No banned character')] }
457
457
  }
458
458
 
459
459
  if (exitCode === 1) {
@@ -476,7 +476,7 @@ export const markdownBans: Measure = async (ctx) => {
476
476
  return {
477
477
  emissions: [output(frame.all)],
478
478
  failure:
479
- 'Markdown prose carries a banned character, word, or spelling, or a relative link resolves to nothing on disk. Rewrite the sentence or fix the link, and reach for a code span only where the token is genuinely an identifier under discussion.',
479
+ 'Markdown prose carries a banned character, or a relative link resolves to nothing on disk. Rewrite the sentence or fix the link, and reach for a code span only where the token is genuinely an identifier under discussion.',
480
480
  }
481
481
  }
482
482
 
@@ -97,7 +97,6 @@ export const TEST_CORPORA_PATTERNS = [
97
97
  '^standards/markdown\\.md$',
98
98
  '^tooling/base/reference\\.md$',
99
99
  '^tooling/web/configs/scripts/worktree-port\\.sh$',
100
- '^\\.cspell/banned-spellings\\.txt$',
101
100
  '^scripts/lib/worktree\\.sh$',
102
101
  '^scripts/core/check-ignore-parity\\.sh$',
103
102
  ]
@@ -10,85 +10,28 @@ import type { BanSets } from '@/markdown/scan'
10
10
  const CHARACTERS = ['—', ';'] as const
11
11
 
12
12
  /**
13
- * Single lowercase words `markdown.md` bans under `## Language`.
13
+ * The one closed set the audit measures, owned here rather than harvested
14
+ * from the standard stating it.
14
15
  *
15
- * A multi-word ban is absent by the same test the character set applies. The
16
- * standard bans a pattern like `It's not X, it's Y` with a placeholder standing
17
- * in for the rest of the sentence, so no literal match reaches it and the
18
- * phrase bans stay a reader's judgment.
19
- */
20
- const WORDS = [
21
- 'seamless',
22
- 'robust',
23
- 'powerful',
24
- 'revolutionary',
25
- 'enhanced',
26
- 'allows',
27
- 'leverage',
28
- 'simply',
29
- 'just',
30
- 'easily',
31
- 'quickly',
32
- 'very',
33
- 'really',
34
- ] as const
35
-
36
- /**
37
- * British spellings of the American examples `markdown.md` lists.
38
- *
39
- * The set is carried whole rather than derived from a suffix rule, because a
40
- * suffix pattern run over prose produced 46 of the 58 false positives measured
41
- * during intake: `exercises`, `promises`, and `revised` all end in `-ise` and
42
- * none is a British spelling. Matching whole words reaches none of them.
43
- *
44
- * `analyse` is absent because the standard's example is `analyze`, which ends
45
- * in `-yze` rather than the `-ize` its rule states. The set records what the
46
- * standard reaches rather than what a reader would extend it to, so adding the
47
- * spelling here would widen the check past the prose it answers to.
48
- */
49
- const SPELLINGS = [
50
- 'organise',
51
- 'summarise',
52
- 'recognise',
53
- 'behaviour',
54
- 'colour',
55
- 'centre',
56
- ] as const
57
-
58
- /**
59
- * The three closed sets the audit measures, owned here rather than harvested
60
- * from the standards stating them.
61
- *
62
- * Parsing the prose was the alternative and it put a parser contract on two
63
- * documents authored for people, which the prose standard had to carry a
64
- * paragraph of its own to protect. The sets are a prior an author already
65
- * knows rather than a filter: measured at `60fc97bf` on 2026-08-19, these 21
66
- * terms ran across 483 markdown files for a clean exit, and every one of the
67
- * 70 occurrences of a banned word sat inside the ban list itself or inside an
68
- * example demonstrating the ban.
69
- *
70
- * The set is closed rather than extensible. Enumeration cannot close the gap
71
- * it aims at, and each addition costs a false-positive class, since `just`,
72
- * `allows`, and `very` have honest uses no literal match separates. A project
73
- * wanting its own vocabulary is asking for a different feature than this one.
16
+ * Parsing the prose was the alternative and it put a parser contract on a
17
+ * document authored for people. The set is closed rather than extensible,
18
+ * since a character is the one class a literal match settles. A word is left
19
+ * to `write-human` as guidance and a spelling to cspell, because a word ban
20
+ * catches the token and misses the habit behind it.
74
21
  */
75
22
  export const BAN_SETS: BanSets = {
76
23
  characters: CHARACTERS,
77
- words: WORDS,
78
- spellings: SPELLINGS,
79
24
  }
80
25
 
81
26
  /**
82
- * Names any set that arrived empty, so a run measuring nothing says so.
27
+ * Names the set when it arrived empty, so a run measuring nothing says so.
83
28
  *
84
29
  * Empty is not the same state as finding no hit. A scan with no terms reports
85
30
  * a clean file having looked for nothing, which is the silence the audit
86
- * refused to ship back when a standard could go missing. The sets ship with
87
- * the package now, so the only way one empties is an edit to this file, and
31
+ * refused to ship back when a standard could go missing. The set ships with
32
+ * the package now, so the only way it empties is an edit to this file, and
88
33
  * the guard is what keeps that edit loud rather than quiet.
89
34
  */
90
35
  export function emptyBanSets(sets: BanSets = BAN_SETS): string[] {
91
- return (['characters', 'words', 'spellings'] as const).filter(
92
- (name) => sets[name].length === 0,
93
- )
36
+ return (['characters'] as const).filter((name) => sets[name].length === 0)
94
37
  }
@@ -21,7 +21,7 @@ export interface GateInput {
21
21
  /**
22
22
  * Whether the audit found something that should fail the caller.
23
23
  *
24
- * A banned character, word, or spelling and a relative link resolving to
24
+ * A banned character and a relative link resolving to
25
25
  * nothing on disk are both facts rather than a threshold, so either gates
26
26
  * unconditionally and there is no widened mode to reach for. The standards
27
27
  * decide what counts as banned, which keeps this answering how many rather