@erclx/canon 5.5.0 → 5.7.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 (42) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/codebase-layout/REQUIREMENT.md +3 -0
  3. package/claude/skills/codebase-layout/SKILL.md +28 -0
  4. package/claude/skills/codebase-layout/references/adopted.md +6 -0
  5. package/claude/skills/plan-feature/REQUIREMENT.md +4 -0
  6. package/claude/skills/plan-feature/SKILL.md +4 -0
  7. package/claude/skills/review-craft/references/ui.md +2 -0
  8. package/claude/skills/review-pr/SKILL.md +2 -2
  9. package/claude/skills/review-ui/REQUIREMENT.md +54 -0
  10. package/claude/skills/review-ui/SKILL.md +108 -0
  11. package/claude/skills/review-ui/references/driving.md +50 -0
  12. package/claude/skills/review-ui/references/post.md +60 -0
  13. package/claude/skills/review-ui/scripts/pw.sh +60 -0
  14. package/claude/skills/role-orchestrator/SKILL.md +2 -2
  15. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +15 -1
  16. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +6 -3
  17. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +10 -4
  18. package/claude/skills/role-orchestrator/scripts/poll.sh +72 -10
  19. package/claude/skills/role-reviewer/REQUIREMENT.md +2 -2
  20. package/claude/skills/role-reviewer/SKILL.md +8 -5
  21. package/claude/skills/teach-workspace/REQUIREMENT.md +3 -0
  22. package/claude/skills/teach-workspace/SKILL.md +2 -1
  23. package/claude/skills/teach-workspace/references/lesson-craft.md +8 -1
  24. package/claude/skills/test-craft/REQUIREMENT.md +4 -1
  25. package/claude/skills/test-craft/SKILL.md +20 -2
  26. package/claude/skills/test-craft/references/adopted.md +3 -1
  27. package/claude/skills/ui-checklist/REQUIREMENT.md +1 -0
  28. package/claude/skills/ui-checklist/SKILL.md +11 -1
  29. package/docs/agents/index.md +1 -1
  30. package/docs/agents/skills-audit.md +6 -3
  31. package/docs/workflow/operating-model.md +1 -1
  32. package/docs/workflow/skill-map.md +9 -8
  33. package/package.json +1 -1
  34. package/src/claude/cases/workflow.ts +5 -0
  35. package/src/claude/skills-audit.ts +81 -1
  36. package/src/commands/claude.ts +33 -0
  37. package/src/gate/measures.ts +43 -1
  38. package/src/gate/stages.ts +9 -0
  39. package/standards/index.md +1 -0
  40. package/standards/skill-practice.md +51 -0
  41. package/standards/skill.md +3 -1
  42. package/tooling/web/seeds/.cspell/tech-stack.txt +1 -0
@@ -12,16 +12,40 @@ export const DESCRIPTION_LIMIT = 1024
12
12
  /** The two headings the requirement template declares, matched at any level. */
13
13
  export const REQUIREMENT_SECTIONS: readonly string[] = ['Gap', 'Must']
14
14
 
15
+ /** The one tree that ships to a target, and the only one the practice list names. */
16
+ export const SHIPPED_CORPUS = join('claude', 'skills')
17
+
15
18
  /**
16
19
  * Both trees the standard governs. `claude/skills/` ships to a target and
17
20
  * `.claude/skills/` stays here, and every rule measured below applies to each,
18
21
  * so a corpus reading one of them reports a pass over half the subject.
19
22
  */
20
23
  export const CORPORA: readonly string[] = [
21
- join('claude', 'skills'),
24
+ SHIPPED_CORPUS,
22
25
  join('.claude', 'skills'),
23
26
  ]
24
27
 
28
+ /**
29
+ * The practice skills `standards/skill-practice.md` describes, keyed by corpus-relative
30
+ * folder rather than by name, so a target's own `.claude/skills/` folder that
31
+ * shares a name is never swept in. The list lives here rather than in a skill's
32
+ * frontmatter because a skill declaring its own kind could exempt itself.
33
+ */
34
+ export const PRACTICE_SKILLS: readonly string[] = [
35
+ join(SHIPPED_CORPUS, 'codebase-layout'),
36
+ join(SHIPPED_CORPUS, 'test-craft'),
37
+ ]
38
+
39
+ /** The closing H2s a practice skill carries, matched exactly outside fences. */
40
+ export const PRACTICE_SECTIONS: readonly string[] = [
41
+ 'Excuses and rebuttals',
42
+ 'Red flags',
43
+ 'Before handing over',
44
+ ]
45
+
46
+ /** The source ledger every practice skill carries beside its body. */
47
+ export const PRACTICE_LEDGER = join('references', 'adopted.md')
48
+
25
49
  /**
26
50
  * The one reason this audit refuses. A project carrying neither corpus is the
27
51
  * ordinary state of a target that has not adopted either skills convention,
@@ -34,6 +58,8 @@ const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
34
58
 
35
59
  const HEADING = /^#{1,6}\s+(.+?)\s*$/
36
60
 
61
+ const SECTION = /^##\s+(.+?)\s*$/
62
+
37
63
  /**
38
64
  * An ISO date, read as provenance wherever a reader is told it. The context
39
65
  * audit's detector clears a date after "measured" or "verified", and a skill
@@ -69,6 +95,7 @@ export interface SkillsAudit {
69
95
  readonly longDescription: readonly SkillFinding[]
70
96
  readonly requirementSections: readonly SkillFinding[]
71
97
  readonly datedProvenance: readonly SkillFinding[]
98
+ readonly practiceShape: readonly SkillFinding[]
72
99
  }
73
100
 
74
101
  interface SkillSource {
@@ -79,6 +106,9 @@ interface SkillSource {
79
106
  readonly description: string | undefined
80
107
  /** Undefined when the folder carries no `REQUIREMENT.md` at all. */
81
108
  readonly requirementHeadings: readonly string[] | undefined
109
+ /** Every H2 in `SKILL.md` outside a fence, for the practice shape. */
110
+ readonly sections: readonly string[]
111
+ readonly hasLedger: boolean
82
112
  /**
83
113
  * `SKILL.md` and every reference below the folder, keyed by the path a
84
114
  * finding names. `REQUIREMENT.md` and `EVAL.md` are left out, since the
@@ -134,6 +164,13 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
134
164
  datedProvenance: sources.flatMap((source) =>
135
165
  source.bodies.flatMap(dateFindings),
136
166
  ),
167
+ practiceShape: [
168
+ ...missingPracticeSkills(
169
+ present.some((corpus) => corpus.rel === SHIPPED_CORPUS),
170
+ sources,
171
+ ),
172
+ ...sources.flatMap(practiceFindings),
173
+ ],
137
174
  }
138
175
  }
139
176
 
@@ -190,6 +227,11 @@ async function readSkill(
190
227
  description: declared(readField(fields, 'description')),
191
228
  requirementHeadings:
192
229
  requirement === undefined ? undefined : headings(requirement),
230
+ sections: bodyLines(body)
231
+ .filter((line) => !line.fenced)
232
+ .map((line) => SECTION.exec(line.text)?.[1])
233
+ .filter((text): text is string => text !== undefined),
234
+ hasLedger: existsSync(join(skillDir, PRACTICE_LEDGER)),
193
235
  bodies: [
194
236
  { rel: join(skillRel, 'SKILL.md'), text: body },
195
237
  ...references.map((path, index) => ({
@@ -252,6 +294,44 @@ function sectionFindings(source: SkillSource): SkillFinding[] {
252
294
  ]
253
295
  }
254
296
 
297
+ /**
298
+ * A listed folder the shipped corpus does not hold, such as a renamed skill or
299
+ * a misspelled append, would otherwise read as a pass. Silent where the
300
+ * shipped corpus is absent, since a target carries none of the listed skills.
301
+ */
302
+ function missingPracticeSkills(
303
+ hasShippedCorpus: boolean,
304
+ sources: readonly SkillSource[],
305
+ ): SkillFinding[] {
306
+ if (!hasShippedCorpus) return []
307
+ const found = new Set(sources.map((source) => source.rel))
308
+ return PRACTICE_SKILLS.filter((rel) => !found.has(rel)).map((rel) => ({
309
+ rel,
310
+ detail: 'missing skill: no folder under the shipped corpus',
311
+ }))
312
+ }
313
+
314
+ /**
315
+ * Matches each section as an exact H2, so a heading carrying trailing text or
316
+ * sitting at another level reads as missing rather than as a near match the
317
+ * standard never names.
318
+ */
319
+ function practiceFindings(source: SkillSource): SkillFinding[] {
320
+ if (!PRACTICE_SKILLS.includes(source.rel)) return []
321
+
322
+ const sections = PRACTICE_SECTIONS.filter(
323
+ (section) => !source.sections.includes(section),
324
+ ).map((section) => ({
325
+ rel: source.rel,
326
+ detail: `missing section: ${section}`,
327
+ }))
328
+ const ledger = source.hasLedger
329
+ ? []
330
+ : [{ rel: source.rel, detail: `missing ledger: ${PRACTICE_LEDGER}` }]
331
+
332
+ return [...sections, ...ledger]
333
+ }
334
+
255
335
  /**
256
336
  * A date inside a fence or a code span is example data a reader is shown, such
257
337
  * as a sample frontmatter block, so only prose outside both is read.
@@ -26,7 +26,11 @@ import {
26
26
  auditSkills,
27
27
  CORPORA,
28
28
  DESCRIPTION_LIMIT,
29
+ PRACTICE_LEDGER,
30
+ PRACTICE_SECTIONS,
31
+ PRACTICE_SKILLS,
29
32
  REQUIREMENT_SECTIONS,
33
+ SHIPPED_CORPUS,
30
34
  type SkillFinding,
31
35
  type SkillsAudit,
32
36
  type SkillsAuditRefusal,
@@ -1398,6 +1402,7 @@ async function runSkillsAudit(
1398
1402
  reportFolder(report)
1399
1403
  reportRequirementShape(report)
1400
1404
  reportProvenance(report)
1405
+ reportPracticeShape(report)
1401
1406
  reportUnmeasured()
1402
1407
  outro()
1403
1408
  }
@@ -1420,11 +1425,14 @@ async function runSkillsAudit(
1420
1425
  folderName: report.folderName,
1421
1426
  requirementSections: report.requirementSections,
1422
1427
  datedProvenance: report.datedProvenance,
1428
+ practiceShape: report.practiceShape,
1423
1429
  },
1424
1430
  checkpoints: {
1425
1431
  descriptionLimit: DESCRIPTION_LIMIT,
1426
1432
  requirementSections: REQUIREMENT_SECTIONS,
1427
1433
  corpora: CORPORA,
1434
+ practiceSkills: PRACTICE_SKILLS,
1435
+ practiceSections: PRACTICE_SECTIONS,
1428
1436
  },
1429
1437
  })}\n`,
1430
1438
  )
@@ -1615,6 +1623,31 @@ function reportProvenance(report: SkillsAudit): void {
1615
1623
  reportFindings(report.datedProvenance)
1616
1624
  }
1617
1625
 
1626
+ /**
1627
+ * Reports without failing the run for the reason provenance does. The list
1628
+ * names shipped folders alone, which the step says so a target reading a pass
1629
+ * knows its own skills were never in scope.
1630
+ */
1631
+ function reportPracticeShape(report: SkillsAudit): void {
1632
+ logStep('Practice shape')
1633
+ logInfo(
1634
+ `A listed practice skill closes with ${PRACTICE_SECTIONS.map((section) => `## ${section}`).join(', ')}, and carries ${PRACTICE_LEDGER}.`,
1635
+ )
1636
+ logInfo(
1637
+ `The list is kept here and names ${plural(PRACTICE_SKILLS.length, 'shipped skill')}, so a skill outside ${SHIPPED_CORPUS} is never read.`,
1638
+ )
1639
+
1640
+ if (report.practiceShape.length === 0) {
1641
+ logInfo('Every listed practice skill carries its shape.')
1642
+ return
1643
+ }
1644
+
1645
+ logWarn(
1646
+ `${plural(report.practiceShape.length, 'missing part')} across the listed practice skills`,
1647
+ )
1648
+ reportFindings(report.practiceShape)
1649
+ }
1650
+
1618
1651
  /**
1619
1652
  * Stated on every run, including the run where everything above passed. A
1620
1653
  * report that lists only what it measured reads as a verdict on the standard
@@ -13,7 +13,12 @@ import {
13
13
  measureArchitecture,
14
14
  missingRevisit,
15
15
  } from '@/context/architecture'
16
- import { auditSkills } from '@/claude/skills-audit'
16
+ import {
17
+ auditSkills,
18
+ PRACTICE_LEDGER,
19
+ PRACTICE_SKILLS,
20
+ SHIPPED_CORPUS,
21
+ } from '@/claude/skills-audit'
17
22
  import { listRepositoryFiles } from '@/git-files'
18
23
  import { ceilingFindings } from '@/markdown/ceiling'
19
24
  import { resolveMarkdown } from '@/markdown/files'
@@ -314,6 +319,43 @@ export const skillProvenance: Measure = async (ctx) => {
314
319
  }
315
320
  }
316
321
 
322
+ /**
323
+ * Every listed practice skill against the closing sections and the ledger
324
+ * `standards/skill-practice.md` requires of one.
325
+ *
326
+ * Read in-process for the reason `skillProvenance` is, so the verb keeps its
327
+ * missing-requirement-only exit. The list names shipped folders alone, so a
328
+ * tree without `claude/skills/` reports unmeasured rather than a vacuous pass.
329
+ */
330
+ export const skillPracticeShape: Measure = async (ctx) => {
331
+ const report = await auditSkills(ctx.root)
332
+ if (!report.corpora.some((corpus) => corpus.rel === SHIPPED_CORPUS)) {
333
+ return {
334
+ emissions: [],
335
+ unmeasured: `no skill corpus at ${SHIPPED_CORPUS} resolved, so no practice skill was read.`,
336
+ }
337
+ }
338
+
339
+ const found = report.practiceShape
340
+ if (found.length > 0) {
341
+ const where = found
342
+ .map((finding) => `${finding.rel} ${finding.detail}`)
343
+ .join(', ')
344
+ return {
345
+ emissions: [],
346
+ failure: `${plural(found.length, 'practice shape finding')}: ${where}. Add the closing H2 exactly as named, or the ledger at ${PRACTICE_LEDGER}.`,
347
+ }
348
+ }
349
+
350
+ return {
351
+ emissions: [
352
+ info(
353
+ `${plural(PRACTICE_SKILLS.length, 'practice skill')} carry every closing section and a ledger`,
354
+ ),
355
+ ],
356
+ }
357
+ }
358
+
317
359
  /**
318
360
  * Every tracked markdown document against the whole-document ceiling.
319
361
  *
@@ -14,6 +14,7 @@ import {
14
14
  seedStandards,
15
15
  rawFieldFileReference,
16
16
  shippedReferences,
17
+ skillPracticeShape,
17
18
  skillProvenance,
18
19
  standardCriteria,
19
20
  unreferencedRules,
@@ -486,6 +487,14 @@ export const STAGES: readonly Stage[] = [
486
487
  label: 'Skill provenance',
487
488
  checks: [{ kind: 'measure', measure: skillProvenance }],
488
489
  },
490
+ {
491
+ // The audit verb only reports a listed practice skill missing a closing
492
+ // section or its ledger, so a target is told without failing. The list
493
+ // names this repository's shipped skills, so the stage fails here alone.
494
+ id: 'skill-practice-shape',
495
+ label: 'Skill practice shape',
496
+ checks: [{ kind: 'measure', measure: skillPracticeShape }],
497
+ },
489
498
  {
490
499
  id: 'standard-criteria',
491
500
  label: 'Standard success criteria',
@@ -31,6 +31,7 @@ Reference docs for consistent authoring across the toolkit and target projects.
31
31
  - [Requirements reference](requirements.md): Shape and content rules for canon/REQUIREMENTS.md
32
32
  - [Governance rule reference](rule.md): Rule frontmatter, body shape, and voice for .claude/rules files
33
33
  - [Session map reference](session.md): Filename and location, the sections a handoff carries, the write and read procedures, and how a role extends it
34
+ - [Practice skill reference](skill-practice.md): Closing sections and source ledger for a skill carrying engineering practice
34
35
  - [Skill requirement reference](skill-requirement.md): Shape and content rules for the REQUIREMENT.md beside a skill
35
36
  - [Claude skill reference](skill.md): Claude skill structure and authoring rules
36
37
  - [Slug reference](slug.md): Transform from a git branch name to a slug, and the three responses to an empty result
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: Practice skill reference
3
+ description: Closing sections and source ledger for a skill carrying engineering practice
4
+ ---
5
+
6
+ # Practice skill reference
7
+
8
+ Applies to a practice skill, one carrying engineering judgment a session applies while it works, where the failure is skipping the practice or cutting it short rather than not knowing it. Its body takes the reference shape, and this file fixes how it closes and what it cites.
9
+
10
+ ## Scope
11
+
12
+ Governs the closing sections of a practice skill's `skills/<name>/SKILL.md` and the source ledger at `skills/<name>/references/adopted.md` beside it.
13
+
14
+ Does not govern:
15
+
16
+ - The skill folder, its frontmatter, the body rules, and the choice between skill types: `skill.md`
17
+ - The `REQUIREMENT.md` beside the body: `skill-requirement.md`
18
+ - Voice, rhythm, and sentence construction: the `write-human` skill
19
+ - Punctuation, formatting, and word choice: `markdown.md`
20
+
21
+ ## What a working practice skill looks like
22
+
23
+ A practice skill works when a session about to cut the practice short meets the answer before it does:
24
+
25
+ - Does every excuse row name a reason a session has actually given to skip the practice?
26
+ - Can a session spot each red flag in its own work while it is still working?
27
+ - Can a session run every line of the closing checklist against what it produced, and get a yes or a no?
28
+ - Can a reader open each source the ledger names and find the version the body states?
29
+
30
+ A practice skill failing any of these is non-conforming even when every heading below is present.
31
+
32
+ ## Closing sections
33
+
34
+ - Follow every concern group with three H2s in this order: `## Excuses and rebuttals`, `## Red flags`, and `## Before handing over`. A section naming what the skill delegates comes after the three, and nothing else does.
35
+ - Spell each heading exactly. An H3, or a heading carrying more words such as `## Red flags that mean start over`, reads as missing.
36
+ - Write `## Excuses and rebuttals` as a two-column table pairing a reason a session gives to skip or shortcut the practice with the answer to it. Never write a row rebutting an objection nobody raised, since that row is ceremony. Revisit when a practice skill's table is observed failing to stop an excuse it names.
37
+ - Write `## Red flags` as bullets naming signs a session can observe in its own work mid-task, never a restatement of a rule above.
38
+ - Write `## Before handing over` as the exit checklist, each line a check against what the session produced.
39
+
40
+ ## Source ledger
41
+
42
+ - Carry `references/adopted.md` in every practice skill, recording each external source the body draws on.
43
+ - Link each source, or pin it to a commit, so a reader can open the version the body adopted.
44
+ - State what was adopted, what was declined, and why for each, so a later session extends the position rather than re-deriving it.
45
+ - Draw first from the default shelf, [Software Engineering at Google](https://abseil.io/resources/swe-book/html/toc.html) and [Google's engineering practices](https://google.github.io/eng-practices/), and record a source from elsewhere the same way.
46
+
47
+ ## Which skills are practice skills
48
+
49
+ - `canon claude skills audit` keeps the list of practice skills and checks each listed one for the three closing sections and the ledger.
50
+ - Never mark a skill as a practice skill in its own frontmatter, since a self-declared kind lets a skill exempt itself.
51
+ - The list names this toolkit's shipped skills alone, so a project's own practice skill takes this shape with no check behind it.
@@ -16,6 +16,7 @@ Governs a skill folder under `skills/` as one artifact: `SKILL.md`, its siblings
16
16
  Does not govern:
17
17
 
18
18
  - What a `REQUIREMENT.md` must answer, its sections, and its template: `skill-requirement.md`
19
+ - A practice skill's closing sections and ledger: `skill-practice.md`
19
20
  - Path-scoped coding rules, which load on a file match rather than on a request match: `rule.md`
20
21
  - Voice, rhythm, and sentence construction in a skill body: the `write-human` skill
21
22
  - Punctuation, formatting, and word choice in a skill body: `markdown.md`
@@ -45,6 +46,7 @@ Pick the type before writing. It decides the body shape.
45
46
 
46
47
  - Reference: conventions, patterns, and domain knowledge Claude applies inline. The body is rule bullets grouped by concern.
47
48
  - Task: step-by-step workflows Claude executes as actions. The body is numbered steps plus the rules constraining them.
49
+ - Practice: judgment a session may skip, shaped by `skill-practice.md`.
48
50
 
49
51
  A task skill takes the template below. A reference skill takes the same frontmatter and replaces `## Steps` and `## Rules` with one H2 per concern group, each holding constraint bullets.
50
52
 
@@ -105,7 +107,7 @@ allowed-tools: <tools required>
105
107
  - Group bullets under H2 headings by domain concern. Keep dos and don'ts together under the topic they belong to rather than splitting them into flat rules and constraints sections.
106
108
  - One actionable constraint per bullet. Prefer the `X over Y` form for preferences.
107
109
  - Do not include code examples unless a one to three line inline snippet captures a pattern the model cannot infer.
108
- - Do not duplicate general knowledge the model already has. Focus on project-specific conventions and preferences. Revisit when removing a line of general knowledge from a skill is shown to change what the agent does.
110
+ - Keep a statement only when removing it would change what the agent does, naming a principle as a few-word handle rather than explaining it. Revisit when a run shows the model ignoring a principle named only as a handle.
109
111
 
110
112
  ### Progressive disclosure
111
113
 
@@ -16,5 +16,6 @@ serviceworker
16
16
  sidepanel
17
17
  tabindex
18
18
  toplevel
19
+ viewports
19
20
  vitest
20
21
  xlink