@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.
- package/README.md +5 -7
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/references/verb-absent.md +0 -1
- package/claude/skills/canon-feedback/REQUIREMENT.md +1 -1
- package/claude/skills/canon-feedback/SKILL.md +2 -2
- package/claude/skills/canon-operator/REQUIREMENT.md +2 -2
- package/claude/skills/canon-operator/SKILL.md +3 -3
- package/claude/skills/codebase-layout/REQUIREMENT.md +1 -0
- package/claude/skills/codebase-layout/SKILL.md +6 -0
- package/claude/skills/create-rule/REQUIREMENT.md +1 -1
- package/claude/skills/create-standard/REQUIREMENT.md +0 -2
- package/claude/skills/decision-escalate/REQUIREMENT.md +1 -2
- package/claude/skills/draft-context/REQUIREMENT.md +1 -1
- package/claude/skills/draft-docs/REQUIREMENT.md +1 -1
- package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
- package/claude/skills/plan-intake/SKILL.md +1 -1
- package/claude/skills/target-setup/REQUIREMENT.md +2 -2
- package/claude/skills/target-setup/SKILL.md +1 -1
- package/claude/skills/target-setup/references/detect.md +1 -1
- package/claude/skills/target-setup/references/indexes.md +1 -5
- package/docs/agents/commands.md +1 -3
- package/docs/agents/counts.md +1 -2
- package/docs/agents/docs.md +1 -1
- package/docs/agents/drift-surfaces.md +6 -4
- package/docs/agents/index.md +1 -1
- package/docs/agents/indexes.md +1 -1
- package/docs/agents/install-and-sync.md +1 -8
- package/docs/agents/markdown-audit.md +2 -2
- package/docs/agents/restated.md +7 -5
- package/docs/agents/review-classification.md +1 -2
- package/docs/agents/scripting.md +2 -3
- package/docs/agents/skills-reach.md +3 -3
- package/docs/agents/state-scoped-risk.md +1 -1
- package/docs/target/index.md +4 -4
- package/docs/target/{target-migrations.md → migrations.md} +17 -1
- package/docs/target/{target-projects.md → projects.md} +5 -7
- package/docs/target/{target-scenarios.md → scenarios.md} +1 -1
- package/docs/target/{target-sync.md → sync.md} +4 -2
- package/docs/workflow/ai-workflow.md +3 -7
- package/docs/workflow/operating-model.md +4 -4
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map.md +1 -2
- package/docs/workflow/visual-design-workflow.md +5 -5
- package/docs/workflow/zshrc-aliases.md +1 -1
- package/package.json +1 -2
- package/scripts/core/check-plugin-boundary.sh +1 -1
- package/scripts/core/install-check.sh +3 -4
- package/scripts/core/regen-hero.sh +2 -9
- package/src/autoship/paths.ts +3 -5
- package/src/claude/cases/authoring.ts +1 -5
- package/src/claude/skills-reach.ts +1 -2
- package/src/cli.ts +0 -2
- package/src/commands/feedback-format.ts +0 -1
- package/src/commands/gov.ts +8 -7
- package/src/commands/markdown.ts +2 -2
- package/src/counts/catalogs.ts +0 -16
- package/src/counts/scan.ts +1 -1
- package/src/docs/read.ts +25 -25
- package/src/gate/measures.ts +52 -0
- package/src/gate/stages.ts +9 -0
- package/src/gov/restated.ts +60 -22
- package/src/help.ts +0 -2
- package/src/init/steps.ts +1 -1
- package/src/migrate/skill-names.ts +4 -5
- package/src/secrets/shipped.ts +2 -2
- package/src/serve/static.ts +1 -2
- package/src/shipped/references.ts +7 -8
- package/src/sync/engine.ts +2 -2
- package/standards/context.md +2 -0
- package/standards/docs.md +1 -0
- package/standards/index.md +0 -1
- package/standards/skill.md +1 -2
- package/standards/wiki.md +2 -1
- package/tooling/base/reference.md +2 -2
- package/tooling/claude/reference.md +1 -1
- package/claude/skills/create-snippet/REQUIREMENT.md +0 -39
- package/claude/skills/create-snippet/SKILL.md +0 -30
- package/scripts/snippets/create.sh +0 -156
- package/snippets/align.md +0 -12
- package/snippets/claude/decision-memo.md +0 -39
- package/snippets/claude/feature-recap.md +0 -19
- package/snippets/claude/figma-steps.md +0 -24
- package/snippets/compact-summary.md +0 -5
- package/snippets/decision-help.md +0 -6
- package/snippets/meta-prompt.md +0 -14
- package/snippets/research-prompt.md +0 -7
- package/snippets/session-notes.md +0 -11
- package/snippets/snippets.toml +0 -5
- package/snippets/step-by-step.md +0 -10
- package/snippets/web-research.md +0 -21
- package/src/commands/snippets.ts +0 -90
- package/src/snippets/categories.ts +0 -66
- package/src/snippets/list.ts +0 -32
- package/src/snippets/presets.ts +0 -50
- package/standards/snippets.md +0 -76
package/src/gate/measures.ts
CHANGED
|
@@ -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.
|
package/src/gate/stages.ts
CHANGED
|
@@ -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
|
package/src/gov/restated.ts
CHANGED
|
@@ -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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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,
|
|
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
|
-
`${
|
|
510
|
+
`${rulesRel.replaceAll('\\', '/')}/${file.replaceAll('\\', '/')}`,
|
|
477
511
|
),
|
|
478
512
|
)
|
|
479
513
|
}
|
|
480
514
|
|
|
481
515
|
/**
|
|
482
|
-
* Every prose line and bullet in a
|
|
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,
|
|
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 = `${
|
|
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
|
|
643
|
-
*
|
|
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
|
|
664
|
-
|
|
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
|
|
668
|
-
|
|
669
|
-
|
|
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/
|
|
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
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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
|
package/src/secrets/shipped.ts
CHANGED
|
@@ -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`
|
|
17
|
-
*
|
|
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. */
|
package/src/serve/static.ts
CHANGED
|
@@ -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`
|
|
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
|
|
14
|
-
* through an
|
|
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`
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
*/
|
package/src/sync/engine.ts
CHANGED
|
@@ -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
|
|
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
|
|
136
|
+
* all three unset reproduces the behavior gov already had.
|
|
137
137
|
*/
|
|
138
138
|
export interface SyncAdapter {
|
|
139
139
|
readonly banner: string
|
package/standards/context.md
CHANGED
|
@@ -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
|
|
package/standards/index.md
CHANGED
|
@@ -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
|
package/standards/skill.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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`, `
|
|
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.
|