@erclx/canon 4.127.0 → 5.1.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 (95) hide show
  1. package/README.md +5 -7
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/auto-ship/references/verb-absent.md +0 -1
  4. package/claude/skills/canon-feedback/REQUIREMENT.md +1 -1
  5. package/claude/skills/canon-feedback/SKILL.md +2 -2
  6. package/claude/skills/canon-operator/REQUIREMENT.md +2 -2
  7. package/claude/skills/canon-operator/SKILL.md +3 -3
  8. package/claude/skills/codebase-layout/REQUIREMENT.md +1 -0
  9. package/claude/skills/codebase-layout/SKILL.md +6 -0
  10. package/claude/skills/create-rule/REQUIREMENT.md +1 -1
  11. package/claude/skills/create-standard/REQUIREMENT.md +0 -2
  12. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -2
  13. package/claude/skills/draft-context/REQUIREMENT.md +1 -1
  14. package/claude/skills/draft-docs/REQUIREMENT.md +1 -1
  15. package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
  16. package/claude/skills/plan-intake/SKILL.md +1 -1
  17. package/claude/skills/target-setup/REQUIREMENT.md +2 -2
  18. package/claude/skills/target-setup/SKILL.md +1 -1
  19. package/claude/skills/target-setup/references/detect.md +1 -1
  20. package/claude/skills/target-setup/references/indexes.md +1 -5
  21. package/docs/agents/commands.md +1 -3
  22. package/docs/agents/counts.md +1 -2
  23. package/docs/agents/docs.md +1 -1
  24. package/docs/agents/drift-surfaces.md +6 -4
  25. package/docs/agents/index.md +1 -1
  26. package/docs/agents/indexes.md +1 -1
  27. package/docs/agents/install-and-sync.md +1 -8
  28. package/docs/agents/markdown-audit.md +2 -2
  29. package/docs/agents/restated.md +7 -5
  30. package/docs/agents/review-classification.md +1 -2
  31. package/docs/agents/scripting.md +2 -3
  32. package/docs/agents/skills-reach.md +3 -3
  33. package/docs/agents/state-scoped-risk.md +1 -1
  34. package/docs/target/index.md +4 -4
  35. package/docs/target/{target-migrations.md → migrations.md} +17 -1
  36. package/docs/target/{target-projects.md → projects.md} +5 -7
  37. package/docs/target/{target-scenarios.md → scenarios.md} +1 -1
  38. package/docs/target/{target-sync.md → sync.md} +4 -2
  39. package/docs/workflow/ai-workflow.md +3 -7
  40. package/docs/workflow/operating-model.md +4 -4
  41. package/docs/workflow/parallel-features.md +1 -1
  42. package/docs/workflow/skill-map.md +1 -2
  43. package/docs/workflow/visual-design-workflow.md +5 -5
  44. package/docs/workflow/zshrc-aliases.md +1 -1
  45. package/package.json +1 -2
  46. package/scripts/core/check-plugin-boundary.sh +1 -1
  47. package/scripts/core/install-check.sh +3 -4
  48. package/scripts/core/regen-hero.sh +2 -9
  49. package/src/autoship/paths.ts +3 -5
  50. package/src/claude/cases/authoring.ts +1 -5
  51. package/src/claude/skills-reach.ts +1 -2
  52. package/src/cli.ts +0 -2
  53. package/src/commands/feedback-format.ts +0 -1
  54. package/src/commands/gov.ts +8 -7
  55. package/src/commands/markdown.ts +2 -2
  56. package/src/counts/catalogs.ts +0 -16
  57. package/src/counts/scan.ts +1 -1
  58. package/src/docs/read.ts +25 -25
  59. package/src/gate/measures.ts +52 -0
  60. package/src/gate/stages.ts +9 -0
  61. package/src/gov/restated.ts +60 -22
  62. package/src/help.ts +0 -2
  63. package/src/init/steps.ts +1 -1
  64. package/src/migrate/skill-names.ts +4 -5
  65. package/src/secrets/shipped.ts +2 -2
  66. package/src/serve/static.ts +1 -2
  67. package/src/shipped/references.ts +7 -8
  68. package/src/sync/engine.ts +2 -2
  69. package/standards/context.md +2 -0
  70. package/standards/docs.md +1 -0
  71. package/standards/index.md +0 -1
  72. package/standards/skill.md +1 -2
  73. package/standards/wiki.md +2 -1
  74. package/tooling/base/reference.md +2 -2
  75. package/tooling/claude/reference.md +1 -1
  76. package/claude/skills/create-snippet/REQUIREMENT.md +0 -39
  77. package/claude/skills/create-snippet/SKILL.md +0 -30
  78. package/scripts/snippets/create.sh +0 -156
  79. package/snippets/align.md +0 -12
  80. package/snippets/claude/decision-memo.md +0 -39
  81. package/snippets/claude/feature-recap.md +0 -19
  82. package/snippets/claude/figma-steps.md +0 -24
  83. package/snippets/compact-summary.md +0 -5
  84. package/snippets/decision-help.md +0 -6
  85. package/snippets/meta-prompt.md +0 -14
  86. package/snippets/research-prompt.md +0 -7
  87. package/snippets/session-notes.md +0 -11
  88. package/snippets/snippets.toml +0 -5
  89. package/snippets/step-by-step.md +0 -10
  90. package/snippets/web-research.md +0 -21
  91. package/src/commands/snippets.ts +0 -90
  92. package/src/snippets/categories.ts +0 -66
  93. package/src/snippets/list.ts +0 -32
  94. package/src/snippets/presets.ts +0 -50
  95. package/standards/snippets.md +0 -76
@@ -361,6 +361,58 @@ export const documentCeiling: Measure = async (ctx) => {
361
361
  }
362
362
  }
363
363
 
364
+ /**
365
+ * Whether a path's stem, its basename up to the first `.`, repeats its
366
+ * immediate parent folder, either whole or as a `<folder>-` prefix.
367
+ *
368
+ * Exact rather than singular-aware, so `standards/standard.md` passes. A false
369
+ * positive on a gate stage teaches a contributor to route around it, and a
370
+ * widened match is cheap to add once a real echo slips through.
371
+ */
372
+ export function isFolderEchoedName(path: string): boolean {
373
+ const segments = path.split('/')
374
+ if (segments.length < 2) return false
375
+ const folder = segments[segments.length - 2] ?? ''
376
+ const stem = (segments[segments.length - 1] ?? '').split('.')[0] ?? ''
377
+ if (folder === '' || stem === '') return false
378
+ return stem === folder || stem.startsWith(`${folder}-`)
379
+ }
380
+
381
+ /**
382
+ * Every tracked file whose name repeats the folder it sits in, which says the
383
+ * folder's name twice in every path that cites it.
384
+ *
385
+ * Reads paths only, so it runs over the whole tree rather than the markdown
386
+ * corpus. It stays out of `canon markdown audit`, which runs inside every
387
+ * target, where a folder-named file may be a convention the target chose.
388
+ */
389
+ export const folderEchoedNames: Measure = async (ctx) => {
390
+ const files = await listRepositoryFiles(ctx.root)
391
+ if (files === undefined) {
392
+ return {
393
+ emissions: [],
394
+ unmeasured:
395
+ 'The tracked file list could not be read, so no filename was checked.',
396
+ }
397
+ }
398
+
399
+ const echoed = files.filter(isFolderEchoedName)
400
+ if (echoed.length === 0) {
401
+ return {
402
+ emissions: [
403
+ info(
404
+ `No filename repeats its folder across ${plural(files.length, 'tracked file')}`,
405
+ ),
406
+ ],
407
+ }
408
+ }
409
+
410
+ return {
411
+ emissions: echoed.map((path) => warn(`${path} repeats its folder's name`)),
412
+ failure: `${echoed.length === 1 ? 'One tracked file repeats its' : `${echoed.length} tracked files repeat their`} folder's name: ${echoed.join(', ')}. Rename each for what it covers inside the folder, dropping the prefix the folder already states.`,
413
+ }
414
+ }
415
+
364
416
  /**
365
417
  * A second run of the records move should rewrite nothing, and the count is
366
418
  * only knowable once the folders themselves have landed.
@@ -4,6 +4,7 @@ import {
4
4
  captureStamps,
5
5
  clientCommandCitations,
6
6
  documentCeiling,
7
+ folderEchoedNames,
7
8
  markdownBans,
8
9
  type Measure,
9
10
  pluginManifests,
@@ -366,6 +367,14 @@ export const STAGES: readonly Stage[] = [
366
367
  label: 'Document ceiling',
367
368
  checks: [{ kind: 'measure', measure: documentCeiling }],
368
369
  },
370
+ {
371
+ // Unscoped, since any branch can add a file anywhere and one git ls-files
372
+ // costs nothing. Reads paths rather than markdown, so it sits here rather
373
+ // than in the markdown audit that also runs inside every target.
374
+ id: 'folder-echoed-names',
375
+ label: 'Folder-echoed filenames',
376
+ checks: [{ kind: 'measure', measure: folderEchoedNames }],
377
+ },
369
378
  {
370
379
  // A rule citing a file that moved fails silently. The consumed-copy drift
371
380
  // stage passes an authored rule and its copy that are wrong together, and
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
+ import { RULE_DIRS } from '@/gov/citations'
3
4
 
4
5
  /** The always-loaded file whose bullets are the subjects this sweep matches. */
5
6
  export const INSTRUCTIONS_REL = 'CLAUDE.md'
@@ -11,15 +12,26 @@ export const SEED_REL = join('tooling', 'claude', 'seeds', 'CLAUDE.md')
11
12
  export const SHIPPED_SKILLS_REL = join('claude', 'skills')
12
13
 
13
14
  /**
14
- * Path-scoped rules, read from the authoring root rather than the consumed
15
- * copy under `.claude/rules/`. A stack takes a whole rule folder, so a bullet
16
- * landing anywhere under here ships to every target the stack reaches, the
17
- * same way a bullet in the always-loaded file or the seed does. That is what
18
- * makes a rule an instruction surface rather than a place a rule is merely
19
- * quoted, and it is why a bullet moved here from the seed still belongs to
20
- * the corpus this sweep reads rather than leaving it.
15
+ * The project's own skill bodies. Here that is the toolkit's internal skills,
16
+ * and in a target it is that project's own, which is equally a place a rule
17
+ * gets restated in prose.
18
+ */
19
+ export const PROJECT_SKILLS_REL = join('.claude', 'skills')
20
+
21
+ /** Every root a candidate body is read from, each reported under its own path. */
22
+ const SKILL_ROOTS: readonly string[] = [SHIPPED_SKILLS_REL, PROJECT_SKILLS_REL]
23
+
24
+ /*
25
+ * Path-scoped rules are read from every root in `RULE_DIRS`, the authoring
26
+ * roots rather than the consumed copy under `.claude/rules/`. A stack takes a
27
+ * whole rule folder, so a bullet landing anywhere under `governance/rules/`
28
+ * ships to every target the stack reaches, the same way a bullet in the
29
+ * always-loaded file or the seed does, and `internal/rules/` loads the same
30
+ * way for every session in this repository. That is what makes a rule an
31
+ * instruction surface rather than a place a rule is merely quoted, and it is
32
+ * why a bullet moved into either root from the seed still belongs to the
33
+ * corpus this sweep reads rather than leaving it.
21
34
  */
22
- export const RULES_REL = join('governance', 'rules')
23
35
 
24
36
  /**
25
37
  * Path pairs whose duplication is deliberate and already recorded.
@@ -174,6 +186,22 @@ const STOPWORDS = new Set([
174
186
  */
175
187
  const PROHIBITIONS = ['never', 'do not', "don't", 'avoid', 'refuse']
176
188
 
189
+ /**
190
+ * A `no` opening a clause or a comma-set phrase directly on a code span, as in
191
+ * `pinned to major tags, no `@latest``.
192
+ *
193
+ * `no` is admitted here and only here, since sitting on a backticked
194
+ * identifier it names the thing ruled out rather than qualifying a verb. The
195
+ * phrase boundary is what keeps description out: `returns no `jq` output` puts
196
+ * the marker after a verb and reports what happens.
197
+ *
198
+ * `not` and `never` stay out. Admitting them turned two agreeing pairs into
199
+ * contradictions, `, not `pwd`` and `, never `@latest``, because the subject
200
+ * each matched spelled its own negation on plain words or in a clause the
201
+ * anchors tied against, so the reading flipped on one side alone.
202
+ */
203
+ const NEGATED_SPAN = /(?:^|[,;:(])\s*no\s+`/
204
+
177
205
  export type RestatedRefusal = 'no-instructions' | 'no-surfaces'
178
206
 
179
207
  export type Restatement = 'mirror' | 'repetition' | 'contradiction'
@@ -315,7 +343,10 @@ function prohibits(text: string): boolean {
315
343
  .replace(/^[^a-z]*/, '')
316
344
  .replace(/^(and|but|so|then|also|however)[\s,]+/, '')
317
345
 
318
- return PROHIBITIONS.some((marker) => opening.startsWith(marker))
346
+ return (
347
+ PROHIBITIONS.some((marker) => opening.startsWith(marker)) ||
348
+ NEGATED_SPAN.test(opening)
349
+ )
319
350
  }
320
351
 
321
352
  /**
@@ -465,7 +496,10 @@ function readBullets(root: string, relative: string): Statement[] {
465
496
  * is reused per file rather than rebuilt for prose, and frontmatter is read
466
497
  * past for the same reason it is: no line there starts with `- `.
467
498
  */
468
- function readRuleFiles(root: string, rulesRoot: string): Statement[] {
499
+ function readRuleFiles(root: string, rulesRel: string): Statement[] {
500
+ const rulesRoot = join(root, rulesRel)
501
+ if (!existsSync(rulesRoot)) return []
502
+
469
503
  const files = [
470
504
  ...new Bun.Glob('**/*.md').scanSync({ cwd: rulesRoot, onlyFiles: true }),
471
505
  ].sort()
@@ -473,13 +507,13 @@ function readRuleFiles(root: string, rulesRoot: string): Statement[] {
473
507
  return files.flatMap((file) =>
474
508
  readBullets(
475
509
  root,
476
- `${RULES_REL.replaceAll('\\', '/')}/${file.replaceAll('\\', '/')}`,
510
+ `${rulesRel.replaceAll('\\', '/')}/${file.replaceAll('\\', '/')}`,
477
511
  ),
478
512
  )
479
513
  }
480
514
 
481
515
  /**
482
- * Every prose line and bullet in a shipped body.
516
+ * Every prose line and bullet in a skill body under one skills root.
483
517
  *
484
518
  * Wider than the bullet rule above because the motivating case was stated in a
485
519
  * body as a paragraph, so a bullet-only read would miss the one instance this
@@ -487,7 +521,10 @@ function readRuleFiles(root: string, rulesRoot: string): Statement[] {
487
521
  * heading names a section rather than stating a rule, and a fenced block is an
488
522
  * example whose words are the surrounding prose's by construction.
489
523
  */
490
- function readBodyLines(root: string, skillsRoot: string): Candidate[] {
524
+ function readBodyLines(root: string, skillsRel: string): Candidate[] {
525
+ const skillsRoot = join(root, skillsRel)
526
+ if (!existsSync(skillsRoot)) return []
527
+
491
528
  const candidates: Candidate[] = []
492
529
 
493
530
  const files = [
@@ -500,7 +537,7 @@ function readBodyLines(root: string, skillsRoot: string): Candidate[] {
500
537
  for (const file of files) {
501
538
  const posix = file.replaceAll('\\', '/')
502
539
  const skill = posix.split('/')[0]
503
- const relative = `${SHIPPED_SKILLS_REL.replaceAll('\\', '/')}/${posix}`
540
+ const relative = `${skillsRel.replaceAll('\\', '/')}/${posix}`
504
541
 
505
542
  const lines = readFileSync(join(root, relative), 'utf8').split('\n')
506
543
  let fenced = false
@@ -639,8 +676,9 @@ function classify(
639
676
  * defect the sweep exists for, and a recall-first reading can be narrowed from
640
677
  * real output where the reverse cannot.
641
678
  *
642
- * The always-loaded file and every rule each open the search as a subject, and
643
- * the seed, the shipped bodies, and every rule again close it as a candidate.
679
+ * The always-loaded file and every rule under either rule root each open the
680
+ * search as a subject, and the seed, the skill bodies under both skill roots,
681
+ * and every rule again close it as a candidate.
644
682
  * A rule sits on both sides because a stack ships a whole rule folder, so a
645
683
  * bullet duplicated between two rules reaches a target exactly as a bullet
646
684
  * duplicated between the always-loaded file and a rule does, and neither shape
@@ -660,14 +698,14 @@ export function readRestated(root: string): RestatedReport {
660
698
  kind: 'seed' as const,
661
699
  }))
662
700
 
663
- const skillsRoot = join(root, SHIPPED_SKILLS_REL)
664
- const bodies = existsSync(skillsRoot) ? readBodyLines(root, skillsRoot) : []
701
+ const bodies = SKILL_ROOTS.flatMap((skillsRel) =>
702
+ readBodyLines(root, skillsRel),
703
+ )
665
704
  const bodyFiles = new Set(bodies.map((candidate) => candidate.file)).size
666
705
 
667
- const rulesRoot = join(root, RULES_REL)
668
- const ruleStatements = existsSync(rulesRoot)
669
- ? readRuleFiles(root, rulesRoot)
670
- : []
706
+ const ruleStatements = RULE_DIRS.flatMap((rulesRel) =>
707
+ readRuleFiles(root, rulesRel),
708
+ )
671
709
  const rules: Candidate[] = ruleStatements.map((statement) => ({
672
710
  ...statement,
673
711
  kind: 'rule' as const,
package/src/help.ts CHANGED
@@ -26,7 +26,6 @@ export const COMMAND_GROUPS: { title: string; rows: CommandRow[] }[] = [
26
26
  rows: [
27
27
  ['gov [cmd]', 'Governance rules (install, sync, list)'],
28
28
  ['standards [cmd]', 'Standards (install, sync, list, <name>)'],
29
- ['snippets [cmd]', 'Prompt snippets (create, list)'],
30
29
  ['tooling [cmd]', 'Tooling stacks (sync, ref, create)'],
31
30
  ['claude [cmd]', 'Claude workflow (init, sync, setup)'],
32
31
  ['wiki [cmd]', 'Wiki pages (init)'],
@@ -93,7 +92,6 @@ const EXAMPLES = [
93
92
  'canon gov sync ../my-app',
94
93
  'canon gov restated --json',
95
94
  'canon standards markdown',
96
- 'canon snippets list',
97
95
  'canon init ../my-app',
98
96
  'canon tooling sync base',
99
97
  'canon tooling create',
package/src/init/steps.ts CHANGED
@@ -5,7 +5,7 @@ import type { DomainStep } from '@/init/run'
5
5
  export type RunFactory = (args: readonly string[]) => () => Promise<boolean>
6
6
 
7
7
  /**
8
- * States the same fact as `docs/target/target-migrations.md`'s backup section in its
8
+ * States the same fact as `docs/target/migrations.md`'s backup section in its
9
9
  * own words. Keep the two in step by hand; nothing compares them.
10
10
  */
11
11
  const RECORDS_SETUP_NOTICE =
@@ -93,11 +93,10 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
93
93
  * one destination, so the ordering is asserted directly.
94
94
  *
95
95
  * It matches whole tokens. A skill name is a complete name rather than a word
96
- * stem, and the corpus holds one word that opens with a name and means
97
- * something else: `wiki/claude/claude-worktrees.md` documents the harness
98
- * feature, not the skill, and every sibling in that folder is named
99
- * `claude-<topic>.md` for a Claude Code concept. Sixteen occurrences of it
100
- * would have moved to a name the folder's own convention contradicts.
96
+ * stem, and a word can open with a name and mean something else: a filename
97
+ * such as `claude-worktrees.md` names the harness feature, not the skill, and
98
+ * a stem matching by prefix would move it to a name that no longer describes
99
+ * what the file documents.
101
100
  *
102
101
  * No article fixup travels with it. `auto-ship` is the one destination opening
103
102
  * on a vowel sound, and the corpus spells an article before it once, which is
@@ -13,8 +13,8 @@ import { join } from 'node:path'
13
13
  *
14
14
  * What it does not cover is the plugin, which a marketplace install reads live
15
15
  * from `claude/` rather than from a tarball. That folder is a `files` entry
16
- * too, and its `standards` and `snippets` symlinks resolve into trees the field
17
- * lists in their own right, so both routes land inside the same corpus.
16
+ * too, and its `standards` symlink resolves into a tree the field lists in its
17
+ * own right, so both routes land inside the same corpus.
18
18
  */
19
19
  export type ShipEntries =
20
20
  /** The field declares a corpus, which is what this check reads. */
@@ -186,8 +186,7 @@ export function resolveWithin(
186
186
  * Re-tests containment after following symlinks. `resolveWithin` is lexical
187
187
  * and `resolve` does not follow a link, so a link inside the root clears that
188
188
  * test while the file it points at sits outside. This repository is a live
189
- * instance, since `claude/standards` and `claude/snippets` are links out of
190
- * `claude/`.
189
+ * instance, since `claude/standards` is a link out of `claude/`.
191
190
  *
192
191
  * Only a path that exists is checked, because a link can be followed only once
193
192
  * there is something on the other end, and a path that resolves to nothing is
@@ -10,8 +10,8 @@ import { isMarked } from '@/exempt-marker'
10
10
  * on purpose rather than by omission. What decides membership here is whether
11
11
  * anything serves the file to a reader who does not hold this repository: a
12
12
  * skill body loads into a session, a docs page is read through `canon docs`, a
13
- * standard through `canon standards`, a rule through a glob match, a snippet
14
- * through an `@` expansion, and a seed through an install. A `src/` doc comment
13
+ * standard through `canon standards`, a rule through a glob match, and a seed
14
+ * through an install. A `src/` doc comment
15
15
  * is read by someone working on the toolkit, in this checkout, where every
16
16
  * number resolves.
17
17
  *
@@ -20,17 +20,16 @@ import { isMarked } from '@/exempt-marker'
20
20
  * `#191512`, which no width or boundary rule separates from a pull request
21
21
  * number, so telling them apart would need the line's syntactic position.
22
22
  *
23
- * `claude/standards` and `claude/snippets` are symlinks into two of the roots
24
- * below, and a walk that followed them would read those two corpora twice and
25
- * report every finding in them under two paths. Nothing is lost by not
26
- * following, since both are corpora here in their own right.
23
+ * `claude/standards` is a symlink into one of the roots below, and a walk that
24
+ * followed it would read that corpus twice and report every finding in it
25
+ * under two paths. Nothing is lost by not following, since it is a corpus here
26
+ * in its own right.
27
27
  */
28
28
  export const SHIPPED_CORPORA = [
29
29
  'claude',
30
30
  'docs',
31
31
  'governance',
32
32
  'scripts',
33
- 'snippets',
34
33
  'standards',
35
34
  'tooling',
36
35
  ] as const
@@ -142,7 +141,7 @@ const SAME_REPOSITORY =
142
141
  * `canon/context/` carries no equivalent pattern. The same resolution test
143
142
  * cannot separate a path every scaffolded project holds, such as
144
143
  * `canon/context/index.md`, from this repository's own domain entry, such
145
- * as `canon/context/indexes.md`, since both resolve here and only the
144
+ * as `canon/context/context-model/indexes.md`, since both resolve here and only the
146
145
  * second is a defect. That is a semantic read no pattern makes, so it stays
147
146
  * with the rule and the review checklist rather than joining this reader.
148
147
  */
@@ -129,11 +129,11 @@ export type NonInteractivePolicy =
129
129
  /**
130
130
  * The two holes every domain sync leaves open: where a destination file's
131
131
  * source lives, and what counts as a change beyond a plain content diff.
132
- * Everything else in a sync is identical across gov, snippets, and standards,
132
+ * Everything else in a sync is identical across domains,
133
133
  * so it lives in the engine.
134
134
  *
135
135
  * The optional members exist because one adapter needed each of them. Leaving
136
- * all three unset reproduces the behavior gov and snippets already had.
136
+ * all three unset reproduces the behavior gov already had.
137
137
  */
138
138
  export interface SyncAdapter {
139
139
  readonly banner: string
@@ -111,6 +111,8 @@ Only the `development` entry carries this section. It is not a general-purpose h
111
111
  - Retire a decision or gotcha once its subject is gone, rewriting the bullet to state the current design rather than leaving the narration of what it replaced beside it. A rejected alternative is not a retired one, so what was tried and why it lost stays whatever its age. The rule above protects content whose subject is live, and this one releases content whose subject is not.
112
112
  - Rewrite a decision a later one replaced rather than appending the replacement beside it. The subject is still live, so the rule above does not reach it, and two bullets on one subject leave a reader to work out which of them is current. State the design that stands and keep the superseded reasoning only where it is the alternative that lost.
113
113
  - Split into a folder (`canon/context/<domain>/<sub-area>.md`) when a domain has three or more sub-areas that do not fit cleanly in one file. That split is the natural ceiling.
114
+ - Group whole single-file entries of two or more small domains into one folder when they share a kind, such as small feature domains that each own a verb. Each entry still covers one domain, and the folder's generated `index.md` carries a `subtitle` naming the kind. It names an entry to start with only when that entry frames the others. A grouping folder is not a split, so no entry inside it is a sub-area of another.
115
+ - Name an entry inside a split or grouping folder per the `codebase-layout` skill, which never repeats the folder's name as the entry's stem or prefix.
114
116
  - Keep a split domain's `index.md` generated. The catalog body is rewritten on every regen, so the domain's own overview and layout belong in a sibling file rather than in it, and the `subtitle` is what names the file to start with.
115
117
  - Author a catalog that grows a row per shipped thing as a bullet list, never a markdown table. A table pads its columns to a shared width, so one row outgrowing its column reflows every row and turns a one-line edit into a whole-file rewrite that conflicts with any sibling branch. This is what forced the split above.
116
118
 
package/standards/docs.md CHANGED
@@ -72,6 +72,7 @@ A page failing these is non-conforming even when it satisfies every shape rule b
72
72
  - Move every page carrying that value. A shelf split across two depths is harder to scan than either arrangement taken whole.
73
73
  - Give the subfolder a catalog page of its own, so the parent lists one entry where it listed several. The parent lists a subfolder as a folder rather than as a member of the shelf it replaced, so nothing on that page restores the grouping.
74
74
  - Retarget every link into a moved page in the same change. A link resolving nowhere is what the move costs, and nothing else reports it.
75
+ - Name a moved page per the `codebase-layout` skill, which drops the prefix the subfolder now states.
75
76
 
76
77
  ## Template
77
78
 
@@ -34,7 +34,6 @@ Reference docs for consistent authoring across the toolkit and target projects.
34
34
  - [Skill requirement reference](skill-requirement.md): Shape and content rules for the REQUIREMENT.md beside a skill
35
35
  - [Claude skill reference](skill.md): Claude skill structure and authoring rules
36
36
  - [Slug reference](slug.md): Transform from a git branch name to a slug, and the three responses to an empty result
37
- - [Snippet reference](snippets.md): Snippet reference and authoring conventions
38
37
  - [Standard reference](standard.md): Shape and content rules for authoring a standard
39
38
  - [Tasks reference](tasks.md): Folder layout, filename convention, file format, origin lines, and archiving for task files under .canon/tasks/
40
39
  - [Teach reference](teach.md): Workspace layout, ordinal naming, frontmatter, and the mission and learning-record formats for a learning workspace
@@ -17,7 +17,6 @@ Does not govern:
17
17
 
18
18
  - What a `REQUIREMENT.md` must answer, its sections, and its template: `skill-requirement.md`
19
19
  - Path-scoped coding rules, which load on a file match rather than on a request match: `rule.md`
20
- - Single-purpose chat prompts carrying no frontmatter, references, or scripts: `snippets.md`
21
20
  - Voice, rhythm, and sentence construction in a skill body: the `write-human` skill
22
21
  - Punctuation, formatting, and word choice in a skill body: `markdown.md`
23
22
  - The transform from a branch name to a slug a skill carries in a filename: `slug.md`
@@ -128,7 +127,7 @@ allowed-tools: <tools required>
128
127
 
129
128
  - Avoid flags that dispatch between alternate flows. The model misreads them and runs the vanilla path. Dry-run-style toggles are fine. For alternate flows, prefer a separate skill or manual invocation of two skills in sequence.
130
129
  - When a skill should fire from multiple callers, rely on description matching with strong trigger phrases. Do not hardcode `Skill` calls in sibling skills that could trigger it naturally.
131
- - Before collapsing a manual multi-step flow into a skill, ask what the manual pauses do. Where a pause carries external timing, error-surfacing, or judgment weight, prefer a snippet or explicit per-step confirmation.
130
+ - Before collapsing a manual multi-step flow into a skill, ask what the manual pauses do. Where a pause carries external timing, error-surfacing, or judgment weight, prefer explicit per-step confirmation.
132
131
 
133
132
  ### Output and tuning
134
133
 
package/standards/wiki.md CHANGED
@@ -37,7 +37,8 @@ A page failing either is non-conforming even when it satisfies every shape rule
37
37
 
38
38
  ## Naming
39
39
 
40
- - Name a page with a `claude-` prefix before the kebab subject.
40
+ - Name a page by its kebab subject alone. The vendor folder it sits in already names the vendor, so a prefix repeats the folder.
41
+ - The `codebase-layout` skill carries the same rule for every folder in a project, beyond the wiki.
41
42
 
42
43
  ## Sourcing
43
44
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  The base layer covers every project the toolkit scaffolds, whatever language sits on top. It ships formatting, spelling, shell linting, conventional commits, git hooks, CI, and one maintenance script. Every other stack extends it, so a decision made here is one every stack inherits.
6
6
 
7
- A repository with several language roots layers this stack once at the root and skips it per subtree, since git honors only one `core.hooksPath` and a re-dropped husky silently breaks. `canon docs target-scenarios` carries the recipe.
7
+ A repository with several language roots layers this stack once at the root and skips it per subtree, since git honors only one `core.hooksPath` and a re-dropped husky silently breaks. `canon docs scenarios` carries the recipe.
8
8
 
9
9
  ## What ships as golden configs
10
10
 
@@ -97,4 +97,4 @@ Sticky negative knowledge. Do not relearn.
97
97
 
98
98
  `bun run check` repairs a checkout rather than gating one. It runs `format` first to auto-fix drifted code, then asserts only what the formatters could not fix. The pull request workflow calls `check:format`, `check:spell`, and `check:shell` directly, and those three are the gate.
99
99
 
100
- `scripts/verify.sh`'s `Markdown bans` stage is the exception: it gates rather than repairs. It runs `canon markdown audit` over every tracked markdown file except `CHANGELOG.md`, since a generated changelog carries commit subjects nobody wrote against the ban set, and exits `2` on a finding or `3` on a shipped-empty ban set, failing `bun run check` and the `pre-push` hook that calls it. A refusal to measure exits `1` and is logged as a skip rather than a failure, as does a corpus with no file left to check once `CHANGELOG.md` is set aside. The stage needs `canon` on PATH. `canon docs target-projects` names the install command. Without it, the stage is skipped and logged, and coverage falls back to the three checks above.
100
+ `scripts/verify.sh`'s `Markdown bans` stage is the exception: it gates rather than repairs. It runs `canon markdown audit` over every tracked markdown file except `CHANGELOG.md`, since a generated changelog carries commit subjects nobody wrote against the ban set, and exits `2` on a finding or `3` on a shipped-empty ban set, failing `bun run check` and the `pre-push` hook that calls it. A refusal to measure exits `1` and is logged as a skip rather than a failure, as does a corpus with no file left to check once `CHANGELOG.md` is set aside. The stage needs `canon` on PATH. `canon docs projects` names the install command. Without it, the stage is skipped and logged, and coverage falls back to the three checks above.
@@ -82,7 +82,7 @@ The first entry covers the whole record root, so a record folder added later nee
82
82
  ## CLAUDE.md
83
83
 
84
84
  - Ships as a seed with placeholder project name, description, and key paths. Fill these in after init.
85
- - Section layout: `Context`, `Indexes`, `Markdown`, `Key paths`, `Spelling`, `Snippets`, `Tasks`, `Memory`. Each section groups rules that share a surface so skills can detect, extend, or skip by heading.
85
+ - Section layout: `Context`, `Indexes`, `Markdown`, `Key paths`, `Spelling`, `Tasks`, `Memory`. Each section groups rules that share a surface so skills can detect, extend, or skip by heading.
86
86
  - The memory rules favor signal over volume. Saving only after a second occurrence or an explicit user correction keeps entries focused on patterns worth preventing, not first-occurrence slips. The 3-line cap stops the built-in auto-memory from drifting into narrative writeups that bury the rule underneath the recovery story.
87
87
  - The `Context` section points at `.claude/` state docs one line. `plan-feature` does the parallel read when invoked. The seed intentionally does not re-list every file.
88
88
 
@@ -1,39 +0,0 @@
1
- ---
2
- name: create-snippet
3
- description: Why a snippet needs its shape read from the reference and its write surface resolved before anything is drafted
4
- ---
5
-
6
- # Create snippet requirement
7
-
8
- ## Gap
9
-
10
- Without this skill, a snippet is drafted the way any other markdown file is drafted, so it arrives carrying frontmatter, headings, and a fill-in placeholder. None of the invocation channels strip those. Typing `>slug` in a chat inserts the scaffolding verbatim, and the file that was meant to be one instruction reads as a document.
11
-
12
- The write surface is the second failure. The toolkit authors at `snippets/` and a target project may still hold a stale `.claude/snippets/` copy from before that domain's install channel retired, so a session picks whichever folder it noticed first. A toolkit snippet written into the consumed copy fails the drift assertion that regenerates that folder. A snippet written there in a target project is correct and looks identical, which is why the surface has to be resolved from what the project has rather than guessed.
13
-
14
- Inside the project surface, a third failure sits one level deeper. A project-authored snippet sharing a name with a toolkit one used to be indistinguishable from it, back when `canon snippets sync` walked that folder and could not tell which side owned the file. That verb retired with the domain's install channel, so nothing reads ownership by location there any more, but the `project/` subfolder still separates the two by eye, which is worth keeping even with no sync left to protect from.
15
-
16
- ## Must
17
-
18
- - Read the bundled snippet reference before drafting, since the shape rules are what the invocation channels depend on
19
- - Resolve the write surface from which snippet folder the project has, rather than from whichever one the session noticed first
20
- - On the project surface, write under its `project/` subfolder, keeping a project-authored file visually apart from a toolkit-installed one
21
- - Confirm the slug and the full body with the user before writing
22
- - Emit the written path in full, so the terminal can resolve it
23
- - State what the resolved surface implies after writing, since a root file needs the consumed copy regenerated and a project-local one needs copying to the toolkit to ship
24
-
25
- ## Must not
26
-
27
- - Restate the snippet authoring conventions in the body, which the bundled reference owns and which would drift from it
28
- - Write a standard, which answers to a different authoring contract
29
-
30
- ## Guards
31
-
32
- - Neither snippet folder exists: stop, since there is nowhere for the snippet to live
33
-
34
- ## Out of scope
35
-
36
- - Editing a snippet that already exists
37
- - A standard, which `create-standard` owns
38
- - Installing and syncing the snippets the toolkit ships, which the `canon snippets` commands own
39
- - What a snippet is and how it is invoked, which the bundled snippet reference states
@@ -1,30 +0,0 @@
1
- ---
2
- name: create-snippet
3
- description: Creates a new snippet file in `snippets/` or `.claude/snippets/`. Use when asked to "create a snippet", add a snippet, write a reusable prompt, or make a new snippet. Do NOT use to edit an existing snippet.
4
- ---
5
-
6
- # Create snippet
7
-
8
- Creates one snippet file. Read these files in parallel:
9
-
10
- - `${CLAUDE_SKILL_DIR}/../../standards/markdown.md`: punctuation and formatting for all generated text
11
- - The `write-human` skill: voice, rhythm, and sentence construction for all generated text
12
- - `${CLAUDE_SKILL_DIR}/../../standards/snippets.md`: authoring conventions, invocation channels, use patterns
13
-
14
- ## Guards
15
-
16
- - If neither `snippets/` nor `.claude/snippets/` exists, stop: `❌ No snippets/ or .claude/snippets/ directory found.`
17
-
18
- ## Steps
19
-
20
- 1. Resolve the write surface: `snippets/` at the root if present, which is the toolkit's own authoring source. Otherwise `.claude/snippets/`, a stale copy a target project holds from before its install channel retired.
21
- 2. Draft the content from the user's description. The snippet reference governs structure, invocation, and authoring conventions.
22
- 3. Confirm the slug and full content with the user before writing
23
- 4. Write the file. On the root surface: `<surface>/<category>/<slug>.md`, or `<surface>/<slug>.md` when the snippet takes no category. On the `.claude/` surface: the same shape nested one level deeper under `project/`, a convention kept for its own sake now that nothing syncs that folder.
24
-
25
- ## After writing
26
-
27
- Emit the full path on its own line.
28
-
29
- - Root surface: this is the toolkit's authoring source. Remind the user to run `bun run check` to regenerate the consumed copy under `.claude/`.
30
- - `.claude/` surface: the file is project-local, under `.claude/snippets/project/`. Remind the user to copy it to the toolkit repo, under `snippets/<category>/<name>.md`, if it should ship to every project.