@erclx/canon 5.35.0 → 5.36.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 (146) hide show
  1. package/README.md +11 -11
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/canon-cli/REQUIREMENT.md +2 -2
  4. package/claude/skills/canon-cli/SKILL.md +2 -1
  5. package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
  6. package/claude/skills/canvas-board/SKILL.md +2 -2
  7. package/claude/skills/canvas-board/references/design.md +53 -36
  8. package/claude/skills/codebase-layout/REQUIREMENT.md +1 -1
  9. package/claude/skills/codebase-layout/SKILL.md +2 -2
  10. package/claude/skills/create-rule/REQUIREMENT.md +6 -8
  11. package/claude/skills/create-rule/SKILL.md +8 -8
  12. package/claude/skills/create-rule/references/rule.md +11 -23
  13. package/claude/skills/create-skill/references/skill.md +1 -1
  14. package/claude/skills/design-taste/REQUIREMENT.md +6 -6
  15. package/claude/skills/design-taste/SKILL.md +2 -2
  16. package/claude/skills/design-taste/references/craft.md +1 -1
  17. package/claude/skills/design-taste/references/preflight.md +2 -2
  18. package/claude/skills/design-taste/references/tells.md +1 -1
  19. package/claude/skills/draft-identity/SKILL.md +3 -2
  20. package/claude/skills/markdown-craft/references/markdown.md +3 -2
  21. package/claude/skills/plan-feature/SKILL.md +1 -1
  22. package/claude/skills/review-craft/references/ui.md +1 -1
  23. package/claude/skills/test-craft/SKILL.md +2 -2
  24. package/claude/skills/ux-audit/REQUIREMENT.md +1 -1
  25. package/claude/skills/ux-audit/SKILL.md +2 -2
  26. package/claude/skills/ux-measure/REQUIREMENT.md +1 -1
  27. package/claude/skills/write-human/references/machine-tells.md +12 -0
  28. package/docs/agents/commands.md +12 -9
  29. package/docs/agents/comments.md +1 -1
  30. package/docs/agents/gov-detect.md +1 -1
  31. package/docs/agents/index.md +1 -1
  32. package/docs/agents/install-and-sync.md +4 -4
  33. package/docs/agents/markdown-audit.md +12 -0
  34. package/docs/agents/output-shape.md +1 -1
  35. package/docs/agents/overview.md +2 -2
  36. package/docs/agents/rule-citations.md +5 -10
  37. package/docs/agents/scripting.md +1 -1
  38. package/docs/target/projects.md +2 -2
  39. package/docs/target/scenarios.md +5 -5
  40. package/docs/workflow/ai-workflow.md +1 -1
  41. package/docs/workflow/visual-design-workflow.md +3 -3
  42. package/governance/detect.toml +32 -32
  43. package/governance/renames.toml +44 -0
  44. package/governance/rules/lib/{305-e2e-reliability.md → e2e-reliability.md} +1 -1
  45. package/governance/rules/lib/{306-test-scope.md → test-scope.md} +2 -2
  46. package/governance/rules/ui/{410-a11y.md → a11y.md} +1 -1
  47. package/governance/rules/ui/{440-surface-capture.md → surface-capture.md} +3 -3
  48. package/governance/stacks/base.toml +6 -6
  49. package/governance/stacks/go.toml +1 -1
  50. package/governance/stacks/node.toml +1 -1
  51. package/governance/stacks/php.toml +1 -1
  52. package/governance/stacks/python.toml +1 -1
  53. package/package.json +1 -1
  54. package/scripts/core/install-check.sh +2 -7
  55. package/src/capture/frame-values.ts +3 -6
  56. package/src/cli/help-all.ts +96 -0
  57. package/src/cli/help.ts +27 -26
  58. package/src/cli.ts +8 -1
  59. package/src/commands/claude.ts +29 -0
  60. package/src/commands/design.ts +22 -1
  61. package/src/commands/gov.ts +25 -3
  62. package/src/commands/init.ts +10 -1
  63. package/src/commands/markdown.ts +96 -3
  64. package/src/commands/records.ts +1 -1
  65. package/src/commands/sync.ts +11 -0
  66. package/src/commands/tooling.ts +8 -0
  67. package/src/design/budget.ts +14 -10
  68. package/src/design/document.ts +5 -5
  69. package/src/design/parse.ts +21 -4
  70. package/src/design/render.ts +3 -3
  71. package/src/gate/measures.ts +1 -1
  72. package/src/gov/citations.ts +10 -33
  73. package/src/gov/stacks.ts +3 -3
  74. package/src/inventory/subjects.ts +1 -1
  75. package/src/markdown/ceiling.ts +6 -1
  76. package/src/markdown/colons.ts +86 -0
  77. package/src/markdown/gate.ts +15 -0
  78. package/src/markdown/heading-case.ts +74 -0
  79. package/src/shipped/references.ts +14 -14
  80. package/src/sync/copy.ts +4 -5
  81. package/tooling/astro/manifest.toml +0 -1
  82. package/tooling/astro/reference.md +0 -8
  83. package/tooling/base/configs/scripts/verify.json +1 -1
  84. package/tooling/base/configs/scripts/verify.ts +22 -6
  85. package/tooling/base/manifest.toml +6 -5
  86. package/tooling/base/reference.md +15 -22
  87. package/tooling/base/seeds/.github/workflows/verify.yml +0 -6
  88. package/tooling/base/seeds/.lintstagedrc +1 -2
  89. package/tooling/base/seeds/canon/context/ci.md +4 -5
  90. package/tooling/base/seeds/canon/context/development.md +2 -2
  91. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +1 -1
  92. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +27 -2
  93. package/tooling/claude/seeds/CLAUDE.md +1 -1
  94. package/tooling/claude/seeds/canon/DESIGN.md +24 -37
  95. package/tooling/shell/configs/.github/workflows/shell.yml +30 -0
  96. package/tooling/shell/manifest.toml +12 -0
  97. package/tooling/shell/reference.md +46 -0
  98. package/tooling/vite-react/manifest.toml +0 -1
  99. package/tooling/vite-react/reference.md +0 -8
  100. package/tooling/web/configs/.github/workflows/readme-screenshot.yml +3 -5
  101. package/tooling/web/configs/.github/workflows/verify.yml +0 -6
  102. package/tooling/web/manifest.toml +1 -1
  103. package/tooling/web/reference.md +3 -3
  104. package/tooling/base/configs/scripts/verify.sh +0 -2
  105. /package/governance/rules/canon/{606-git.md → git.md} +0 -0
  106. /package/governance/rules/canon/{611-indexes.md → indexes.md} +0 -0
  107. /package/governance/rules/canon/{604-scratch.md → scratch.md} +0 -0
  108. /package/governance/rules/canon/{602-tasks.md → task-scope.md} +0 -0
  109. /package/governance/rules/canon/{605-worktrees.md → worktrees.md} +0 -0
  110. /package/governance/rules/claude/{566-output.md → output.md} +0 -0
  111. /package/governance/rules/code/{000-code.md → routing.md} +0 -0
  112. /package/governance/rules/code/{010-testing.md → testing.md} +0 -0
  113. /package/governance/rules/framework/{210-astro.md → astro.md} +0 -0
  114. /package/governance/rules/framework/{220-fastapi.md → fastapi.md} +0 -0
  115. /package/governance/rules/framework/{230-nextjs.md → nextjs.md} +0 -0
  116. /package/governance/rules/framework/{200-react.md → react.md} +0 -0
  117. /package/governance/rules/framework/{260-shadcn.md → shadcn.md} +0 -0
  118. /package/governance/rules/framework/{250-tailwind.md → tailwind.md} +0 -0
  119. /package/governance/rules/lang/{130-go.md → go.md} +0 -0
  120. /package/governance/rules/lang/{140-php.md → php.md} +0 -0
  121. /package/governance/rules/lang/{110-python.md → python.md} +0 -0
  122. /package/governance/rules/lang/{100-typescript.md → typescript.md} +0 -0
  123. /package/governance/rules/lib/{370-database.md → database.md} +0 -0
  124. /package/governance/rules/lib/{340-pydantic.md → pydantic.md} +0 -0
  125. /package/governance/rules/lib/{360-security-server.md → security-server.md} +0 -0
  126. /package/governance/rules/lib/{350-security-web.md → security-web.md} +0 -0
  127. /package/governance/rules/lib/{320-tanstack-query.md → tanstack-query.md} +0 -0
  128. /package/governance/rules/lib/{335-testing-go.md → testing-go.md} +0 -0
  129. /package/governance/rules/lib/{336-testing-php.md → testing-php.md} +0 -0
  130. /package/governance/rules/lib/{330-testing-py.md → testing-py.md} +0 -0
  131. /package/governance/rules/lib/{300-testing-ts.md → testing-ts.md} +0 -0
  132. /package/governance/rules/lib/{310-zod.md → zod.md} +0 -0
  133. /package/governance/rules/tooling/{700-ci-workflow.md → ci-workflow.md} +0 -0
  134. /package/governance/rules/tooling/{790-code-comments.md → code-comments.md} +0 -0
  135. /package/governance/rules/tooling/{780-config-comments.md → config-comments.md} +0 -0
  136. /package/governance/rules/tooling/{775-dependencies.md → dependencies.md} +0 -0
  137. /package/governance/rules/ui/{480-canvas.md → canvas.md} +0 -0
  138. /package/governance/rules/ui/{400-ui.md → copy.md} +0 -0
  139. /package/governance/rules/ui/{460-design-taste.md → design-taste.md} +0 -0
  140. /package/governance/rules/ui/{420-forms.md → forms.md} +0 -0
  141. /package/governance/rules/ui/{465-interface-casing.md → interface-casing.md} +0 -0
  142. /package/governance/rules/ui/{450-link-behavior.md → link-behavior.md} +0 -0
  143. /package/governance/rules/ui/{470-motion.md → motion.md} +0 -0
  144. /package/governance/rules/ui/{430-ux-completeness.md → ux-completeness.md} +0 -0
  145. /package/tooling/{base → shell}/configs/.editorconfig +0 -0
  146. /package/tooling/{base → shell}/configs/.shellcheckrc +0 -0
@@ -32,3 +32,47 @@
32
32
  "655-tasks" = "tasks"
33
33
  "570-skill" = "skill"
34
34
  "801-markdown" = "markdown"
35
+ "602-tasks" = "task-scope"
36
+ "604-scratch" = "scratch"
37
+ "605-worktrees" = "worktrees"
38
+ "606-git" = "git"
39
+ "611-indexes" = "indexes"
40
+ "566-output" = "output"
41
+ "000-code" = "routing"
42
+ "010-testing" = "testing"
43
+ "200-react" = "react"
44
+ "210-astro" = "astro"
45
+ "220-fastapi" = "fastapi"
46
+ "230-nextjs" = "nextjs"
47
+ "250-tailwind" = "tailwind"
48
+ "260-shadcn" = "shadcn"
49
+ "100-typescript" = "typescript"
50
+ "110-python" = "python"
51
+ "130-go" = "go"
52
+ "140-php" = "php"
53
+ "300-testing-ts" = "testing-ts"
54
+ "305-e2e-reliability" = "e2e-reliability"
55
+ "306-test-scope" = "test-scope"
56
+ "310-zod" = "zod"
57
+ "320-tanstack-query" = "tanstack-query"
58
+ "330-testing-py" = "testing-py"
59
+ "335-testing-go" = "testing-go"
60
+ "336-testing-php" = "testing-php"
61
+ "340-pydantic" = "pydantic"
62
+ "350-security-web" = "security-web"
63
+ "360-security-server" = "security-server"
64
+ "370-database" = "database"
65
+ "700-ci-workflow" = "ci-workflow"
66
+ "775-dependencies" = "dependencies"
67
+ "780-config-comments" = "config-comments"
68
+ "790-code-comments" = "code-comments"
69
+ "400-ui" = "copy"
70
+ "410-a11y" = "a11y"
71
+ "420-forms" = "forms"
72
+ "430-ux-completeness" = "ux-completeness"
73
+ "440-surface-capture" = "surface-capture"
74
+ "450-link-behavior" = "link-behavior"
75
+ "460-design-taste" = "design-taste"
76
+ "465-interface-casing" = "interface-casing"
77
+ "470-motion" = "motion"
78
+ "480-canvas" = "canvas"
@@ -32,4 +32,4 @@ paths:
32
32
 
33
33
  ## Authority
34
34
 
35
- - Follow `.claude/rules/canon/lib/300-testing-ts.md` for framework choice, file placement, and test naming.
35
+ - Follow `.claude/rules/canon/lib/testing-ts.md` for framework choice, file placement, and test naming.
@@ -20,5 +20,5 @@ paths:
20
20
 
21
21
  - Answer a question about the running page with a script against the dev server rather than with the suite.
22
22
  - Do not enable `fullyParallel` in `playwright.config.ts`.
23
- - Follow `.claude/rules/canon/ui/440-surface-capture.md` for capture scope.
24
- - Follow `.claude/rules/canon/lib/305-e2e-reliability.md` for waits and guards.
23
+ - Follow `.claude/rules/canon/ui/surface-capture.md` for capture scope.
24
+ - Follow `.claude/rules/canon/lib/e2e-reliability.md` for waits and guards.
@@ -60,4 +60,4 @@ paths:
60
60
 
61
61
  - Let content reflow at a viewport 320 CSS pixels wide with no loss of content or function and no two-dimensional scrolling. Only content whose meaning depends on both axes, such as a data table, a map, or a diagram, may scroll sideways (WCAG Reflow, 1.4.10, Level AA).
62
62
  - Treat 320 as the criterion's own test condition rather than a device width. It is equivalent to a 1280 pixel window at 400 percent zoom, so a layout that breaks there breaks for a person who zooms.
63
- - Hold the same 320 floor for the content region beside persistent chrome a person can resize. The criterion measures the viewport, so this extension is this rule's own rather than the criterion's: a panel widened to 480 in a 768 window leaves 288 for the content, under the floor at a window the criterion passes. `440-surface-capture` states where the frame for it gets taken.
63
+ - Hold the same 320 floor for the content region beside persistent chrome a person can resize. The criterion measures the viewport, so this extension is this rule's own rather than the criterion's: a panel widened to 480 in a 768 window leaves 288 for the content, under the floor at a window the criterion passes. `surface-capture` states where the frame for it gets taken.
@@ -26,12 +26,12 @@ paths:
26
26
  ## What a capture covers
27
27
 
28
28
  - Capture the full page. Do not capture a component in isolation.
29
- - Declare the widths a layout case captures. Always include 320, the reflow floor from the reflow criterion in `410-a11y`, then add each breakpoint bucket the project writes rules for, read from the `## Layout` section of `DESIGN.md`.
29
+ - Declare the widths a layout case captures. Always include 320, the reflow floor from the reflow criterion in `a11y`, then add each breakpoint bucket the project writes rules for, read from the `## Layout` section of `DESIGN.md`.
30
30
  - Fall back to the 320 floor alone when the project has no `DESIGN.md` or its record carries no `## Layout` section. Do not impose another project's breakpoints.
31
31
  - Add a layout case at each bound when a surface renders persistent chrome a person can resize, such as a draggable sidebar or a split pane, one with the chrome at its narrowest and one at its widest. Take both at the narrowest declared width where the chrome stays persistent rather than collapsing, since a frame where it has turned into an overlay shows none of the width it takes.
32
- - Read those bounds from the code that clamps the chrome, since `## Layout` has no slot for them. Judge each frame by the content width left beside the chrome rather than by the window width, against the 320 floor `410-a11y` holds for that region.
32
+ - Read those bounds from the code that clamps the chrome, since `## Layout` has no slot for them. Judge each frame by the content width left beside the chrome rather than by the window width, against the 320 floor `a11y` holds for that region.
33
33
  - Apply the declaration to a case that covers layout only. A case driving a menu open or an answer chosen tests a state, so it takes one width rather than the full set.
34
- - Read a capture at 320 as proof that a frame was taken there, not that the page reflows correctly. The criterion is stated in `410-a11y` and nothing here enforces it.
34
+ - Read a capture at 320 as proof that a frame was taken there, not that the page reflows correctly. The criterion is stated in `a11y` and nothing here enforces it.
35
35
  - Write the capture into a folder named `evidence`. The image is collected for the pull request comment when any segment of its path is literally `evidence` and its filename carries an image extension, so the folder name is fixed and its position in the tree is the project's own.
36
36
  - Several `evidence/` folders in one project is the ordinary shape rather than a problem to consolidate. One per surface is how a project with more than one rendered surface ends up.
37
37
  - Drive a state a screenshot cannot reach. An initial render reports nothing about a menu that opens, an answer that is chosen, or a rail that tracks scrolling.
@@ -3,12 +3,12 @@ extends = ""
3
3
  # `canon` or `claude` reaches no project until it is added here or proposed by
4
4
  # `canon gov detect`. Every other rule is opt-in.
5
5
  rules = [
6
- "566-output",
7
- "602-tasks",
8
- "604-scratch",
9
- "605-worktrees",
10
- "606-git",
11
- "611-indexes",
6
+ "output",
7
+ "task-scope",
8
+ "scratch",
9
+ "worktrees",
10
+ "git",
11
+ "indexes",
12
12
  "context",
13
13
  "tasks",
14
14
  "plan",
@@ -1,2 +1,2 @@
1
1
  extends = "base"
2
- rules = ["code", "130-go", "335-testing-go"]
2
+ rules = ["code", "go", "testing-go"]
@@ -1,2 +1,2 @@
1
1
  extends = "base"
2
- rules = ["code", "100-typescript", "300-testing-ts"]
2
+ rules = ["code", "typescript", "testing-ts"]
@@ -1,2 +1,2 @@
1
1
  extends = "base"
2
- rules = ["code", "140-php", "336-testing-php"]
2
+ rules = ["code", "php", "testing-php"]
@@ -1,2 +1,2 @@
1
1
  extends = "base"
2
- rules = ["code", "110-python", "330-testing-py"]
2
+ rules = ["code", "python", "testing-py"]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.35.0",
4
+ "version": "5.36.0",
5
5
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -101,18 +101,13 @@ log_step "Assert scaffold"
101
101
  # skills rather than installing into a target. The two
102
102
  # rule paths stand for the `claude` and `canon` groups `base` takes whole.
103
103
  for path in "CLAUDE.md" ".claude/wiki/index.md" ".claude" "canon/context/index.md" \
104
- ".prettierrc" ".editorconfig" ".lintstagedrc" ".husky/pre-commit" ".github/workflows/verify.yml" "scripts/verify.sh" \
105
- ".claude/rules/canon/claude/566-output.md" ".claude/rules/canon/canon/604-scratch.md"; do
104
+ ".prettierrc" ".lintstagedrc" ".husky/pre-commit" ".github/workflows/verify.yml" "scripts/verify.ts" \
105
+ ".claude/rules/canon/claude/output.md" ".claude/rules/canon/canon/scratch.md"; do
106
106
  if [ ! -e "$TARGET_DIR/$path" ]; then
107
107
  log_error "Missing after canon init: $path"
108
108
  fi
109
109
  log_info "Found: $path"
110
110
  done
111
111
 
112
- if [ ! -x "$TARGET_DIR/scripts/verify.sh" ]; then
113
- log_error "Not executable after canon init: scripts/verify.sh"
114
- fi
115
- log_info "Executable: scripts/verify.sh"
116
-
117
112
  log_step "Verification passed"
118
113
  log_info "Manual check still needed: bun link and global canon invocation"
@@ -121,9 +121,6 @@ const plural = (count: number, noun: string): string =>
121
121
  const frameRow = (cells: string): string =>
122
122
  `<span class="frame">│</span> <span class="ok">✓</span> ${cells}`
123
123
 
124
- /** Rule names carry a numeric prefix that orders the load, not the identity. */
125
- const slug = (name: string): string => name.replace(/^\d+-/, '')
126
-
127
124
  /** Indented to the depth a rule inside the `<style>` block sits at. */
128
125
  const indentTokens = (css: string): string =>
129
126
  css
@@ -194,13 +191,13 @@ export function buildFrameValues(catalogs: FrameCatalogs): FrameValues {
194
191
  }
195
192
 
196
193
  const tokenCss = indentTokens(catalogs.tokenCss)
197
- const rules = gov.rules.map((entry) => slug(entry.name))
194
+ const rules = gov.rules.map((entry) => entry.name)
198
195
  // A rule no stack names is opt-in behind `--add`, so featuring one advertises
199
196
  // an entry no ordinary `canon gov install` delivers. The column samples the
200
197
  // stack-reached subset while its count and remainder describe the whole.
201
198
  const stacked = new Set(gov.stacks.flatMap((stack) => stack.rules))
202
199
  const deliveredEntries = gov.rules.filter((entry) => stacked.has(entry.name))
203
- const deliveredRules = deliveredEntries.map((entry) => slug(entry.name))
200
+ const deliveredRules = deliveredEntries.map((entry) => entry.name)
204
201
 
205
202
  for (const [label, list] of [
206
203
  ['skills', skills],
@@ -236,7 +233,7 @@ export function buildFrameValues(catalogs: FrameCatalogs): FrameValues {
236
233
  const scope =
237
234
  globs.length > 0 ? clip(globs.join(' '), GLOB_WIDTH) : 'every session'
238
235
  return frameRow(
239
- `<span class="name">${escape(pad(slug(entry.name), 24))}</span>` +
236
+ `<span class="name">${escape(pad(entry.name, 24))}</span>` +
240
237
  `<span class="domain">${escape(pad(`[${entry.domain}]`, 12))}</span>` +
241
238
  `<span class="muted">${escape(scope)}</span>`,
242
239
  )
@@ -0,0 +1,96 @@
1
+ import type { Command } from 'commander'
2
+
3
+ export type HelpRoute =
4
+ | { kind: 'parse' }
5
+ | { kind: 'top' }
6
+ | { kind: 'all'; command: Command }
7
+ | { kind: 'unknown'; name: string }
8
+
9
+ const HELP_FLAGS = new Set(['-h', '--help'])
10
+
11
+ // Commander exposes no getter for a disabled help option, and `canon sandbox`
12
+ // disables its own to forward `--help` to a bash script.
13
+ function hasHelpOption(command: Command): boolean {
14
+ return command.helpInformation().includes('--help')
15
+ }
16
+
17
+ // A group with its own action, or with a default subcommand such as
18
+ // `inventory run`, takes an operand that names no subcommand. Commander keeps
19
+ // both on private fields.
20
+ function takesOperand(command: Command): boolean {
21
+ const internals = command as unknown as {
22
+ _actionHandler: unknown
23
+ _defaultCommandName: unknown
24
+ }
25
+ return (
26
+ command.registeredArguments.length > 0 ||
27
+ internals._actionHandler !== null ||
28
+ internals._defaultCommandName !== null
29
+ )
30
+ }
31
+
32
+ function visibleChildren(command: Command): Command[] {
33
+ return command
34
+ .createHelp()
35
+ .visibleCommands(command)
36
+ .filter((child) => command.commands.includes(child))
37
+ }
38
+
39
+ function leaves(command: Command): Command[] {
40
+ return visibleChildren(command).flatMap((child) =>
41
+ visibleChildren(child).length === 0 ? [child] : leaves(child),
42
+ )
43
+ }
44
+
45
+ // `outputHelp` rather than `helpInformation`, since only the former emits the
46
+ // blocks `addHelpText` registers, which carry every verb's exit codes and
47
+ // examples.
48
+ function captureHelp(command: Command): string {
49
+ const config = command.configureOutput()
50
+ const { writeOut } = config
51
+ let text = ''
52
+ command.configureOutput({ writeOut: (chunk) => (text += chunk) })
53
+ try {
54
+ command.outputHelp()
55
+ } finally {
56
+ command.configureOutput({ writeOut })
57
+ }
58
+ return text.trimEnd()
59
+ }
60
+
61
+ // Each block opens on its own `Usage:` line, so a blank line is the whole
62
+ // separator and a reader splits on the usage header.
63
+ export function renderAll(command: Command): string {
64
+ const blocks = [command, ...leaves(command)].map(captureHelp)
65
+ return `${blocks.join('\n\n')}\n`
66
+ }
67
+
68
+ // Reads argv ahead of commander, which prints a group's help for an unknown
69
+ // subcommand because it answers `--help` before it resolves operands. Walking
70
+ // stops at the first option, so an option's value is never read as a name.
71
+ export function routeHelp(program: Command, args: string[]): HelpRoute {
72
+ if (!args.some((arg) => HELP_FLAGS.has(arg))) return { kind: 'parse' }
73
+
74
+ let command = program
75
+ let depth = 0
76
+ for (const arg of args) {
77
+ if (arg.startsWith('-')) break
78
+ const child = command.commands.find((c) => c.name() === arg)
79
+ if (child === undefined) {
80
+ const isGroupOnly =
81
+ command !== program &&
82
+ command.commands.length > 0 &&
83
+ !takesOperand(command) &&
84
+ arg !== 'help'
85
+ if (isGroupOnly) return { kind: 'unknown', name: arg }
86
+ break
87
+ }
88
+ command = child
89
+ depth += 1
90
+ }
91
+
92
+ if (!args.includes('--all')) return { kind: 'parse' }
93
+ if (depth === 0) return { kind: 'top' }
94
+ if (!hasHelpOption(command)) return { kind: 'parse' }
95
+ return { kind: 'all', command }
96
+ }
package/src/cli/help.ts CHANGED
@@ -20,61 +20,61 @@ export const COMMAND_GROUPS: { title: string; rows: CommandRow[] }[] = [
20
20
  ['sync [path]', 'Sync all installed domains in a project'],
21
21
  ['upgrade', 'Reinstall the CLI with the manager that installed it'],
22
22
  ['migrate [cmd]', 'Move a project off a retired name or layout'],
23
- ['targets [cmd]', 'Projects this toolkit installed into (list, pulls)'],
23
+ ['targets [cmd]', 'Projects this toolkit installed into'],
24
24
  ],
25
25
  },
26
26
  {
27
27
  title: 'Domains',
28
28
  rows: [
29
- ['gov [cmd]', 'Governance rules (install, sync, list)'],
30
- ['tooling [cmd]', 'Tooling stacks (sync, ref, create)'],
31
- ['claude [cmd]', 'Claude workflow (init, sync, setup)'],
32
- ['wiki [cmd]', 'Wiki pages (init)'],
33
- ['indexes [cmd]', 'Regenerate index.md files (regen)'],
34
- ['docs [cmd|topic]', 'Emit toolkit reference docs (list, <topic>)'],
29
+ ['gov [cmd]', 'Governance rules'],
30
+ ['tooling [cmd]', 'Tooling stacks'],
31
+ ['claude [cmd]', 'Claude workflow'],
32
+ ['wiki [cmd]', 'Wiki pages'],
33
+ ['indexes [cmd]', 'Regenerate index.md files'],
34
+ ['docs [cmd|topic]', 'Emit toolkit reference docs'],
35
35
  ],
36
36
  },
37
37
  {
38
38
  title: 'Author and render',
39
39
  rows: [
40
- ['design [cmd]', 'Design system (render, regen)'],
41
- ['slides [cmd]', 'Slide decks (render, list)'],
40
+ ['design [cmd]', 'Design system'],
41
+ ['slides [cmd]', 'Slide decks'],
42
42
  ['capture [source]', 'Render HTML capture sources to PNG'],
43
43
  ['serve [dir]', 'Serve a directory on localhost, print the link'],
44
- ['demo [cmd]', 'Record a running app (compile, run)'],
44
+ ['demo [cmd]', 'Record a running app'],
45
45
  ['inventory [subj]', 'Report one computed property across every route'],
46
46
  ['drive <url> <run>', 'Walk a page through interactions, measure each'],
47
47
  ['transcripts <url>', 'Fetch a YouTube transcript with frontmatter'],
48
- ['teach [cmd]', 'Learning workspaces (list, open, resource, glossary)'],
49
- ['canvas [cmd]', 'Local canvas of pages and frames (serve, list)'],
48
+ ['teach [cmd]', 'Learning workspaces'],
49
+ ['canvas [cmd]', 'Local canvas of pages and frames'],
50
50
  ],
51
51
  },
52
52
  {
53
53
  title: 'Session and ship',
54
54
  rows: [
55
55
  ['sandbox [cat:cmd]', 'Provision and run sandbox scenarios'],
56
- ['tasks [cmd]', 'Task board (validate, archive)'],
57
- ['intake [cmd]', 'Intake folders under .canon/intake/ (list, answer)'],
58
- ['records [cmd]', 'Session records (validate, size, push, pull)'],
56
+ ['tasks [cmd]', 'Task board'],
57
+ ['intake [cmd]', 'Intake folders under .canon/intake/'],
58
+ ['records [cmd]', 'Session records'],
59
59
  ['sessions [cmd]', 'Resolve live sessions to worktree and branch'],
60
60
  ['worktrees [cmd]', 'Reclaim worktrees whose branches merged'],
61
- ['hooks [cmd]', 'Run the steps a git hook drives (post-merge)'],
62
- ['pr [cmd]', 'Read a pull request (key-changes, head, checks)'],
63
- ['upstream [cmd]', 'Read Claude Code releases (fetch, catalog, advance)'],
61
+ ['hooks [cmd]', 'Run the steps a git hook drives'],
62
+ ['pr [cmd]', 'Read a pull request'],
63
+ ['upstream [cmd]', 'Read Claude Code releases'],
64
64
  ['feedback', 'File toolkit feedback as a GitHub issue'],
65
65
  ],
66
66
  },
67
67
  {
68
68
  title: 'Repo reports',
69
69
  rows: [
70
- ['audits [cmd]', 'Run every health check as one set (run, list)'],
71
- ['gate [cmd]', 'Run the merge gate stage by stage (run)'],
72
- ['secrets [cmd]', 'Scan the shipped tree for credentials (scan)'],
73
- ['deps [cmd]', 'Read the dependency set for advisories (audit)'],
74
- ['labels [cmd]', 'Read a changed set against the label map (audit)'],
75
- ['comments [cmd]', 'Measure comment density and trend (scan)'],
76
- ['context [cmd]', 'Report context folder health (audit)'],
77
- ['markdown [cmd]', 'Report markdown against the shipped bans (audit)'],
70
+ ['audits [cmd]', 'Run every health check as one set'],
71
+ ['gate [cmd]', 'Run the merge gate stage by stage'],
72
+ ['secrets [cmd]', 'Scan the shipped tree for credentials'],
73
+ ['deps [cmd]', 'Read the dependency set for advisories'],
74
+ ['labels [cmd]', 'Read a changed set against the label map'],
75
+ ['comments [cmd]', 'Measure comment density and trend'],
76
+ ['context [cmd]', 'Report context folder health'],
77
+ ['markdown [cmd]', 'Report markdown against the shipped bans'],
78
78
  ['census [path]', 'Report file count, extensions, and line totals'],
79
79
  ],
80
80
  },
@@ -111,6 +111,7 @@ const EXAMPLES = [
111
111
  'canon drive http://localhost:4173 run.json --json',
112
112
  'pbpaste | canon feedback',
113
113
  'canon transcripts https://youtu.be/VIDEO_ID',
114
+ 'canon tasks --help --all',
114
115
  'canon tasks archive --pull-request 673 --json',
115
116
  'canon intake list toolkit-overview --unread --json',
116
117
  'canon teach list --json',
package/src/cli.ts CHANGED
@@ -43,6 +43,7 @@ import { register as targets } from '@/commands/targets'
43
43
  import { register as upgrade } from '@/commands/upgrade'
44
44
  import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
45
45
  import { renderHelp } from '@/cli/help'
46
+ import { renderAll, routeHelp } from '@/cli/help-all'
46
47
 
47
48
  function showHelp(): void {
48
49
  console.log(renderHelp(process.stdout))
@@ -102,4 +103,10 @@ audits(program)
102
103
  gate(program)
103
104
  upgrade(program)
104
105
 
105
- program.parse()
106
+ const route = routeHelp(program, process.argv.slice(2))
107
+ if (route.kind === 'top') showHelp()
108
+ else if (route.kind === 'all') process.stdout.write(renderAll(route.command))
109
+ else if (route.kind === 'unknown') {
110
+ console.error(`error: unknown command '${route.name}'`)
111
+ process.exitCode = 1
112
+ } else program.parse()
@@ -212,6 +212,20 @@ export function register(program: Command): void {
212
212
  .description('Seed .claude/ workflow docs into a project')
213
213
  .argument('[target]', 'Target directory', '.')
214
214
  .helpOption('-h, --help', 'Show this help message')
215
+ .addHelpText(
216
+ 'after',
217
+ [
218
+ '',
219
+ 'Copies each workflow seed the target lacks, CLAUDE.md included, and',
220
+ 'leaves one already present alone. Then adds the .gitignore entries',
221
+ 'the claude manifest lists.',
222
+ '',
223
+ 'Exit codes:',
224
+ ' 0 initialized, already initialized, or the prompt was cancelled',
225
+ ' 1 the target is missing or is the toolkit root',
226
+ '',
227
+ ].join('\n'),
228
+ )
215
229
  .action(async (target: string) => {
216
230
  process.exitCode = await runInit(target)
217
231
  })
@@ -221,6 +235,21 @@ export function register(program: Command): void {
221
235
  .description('Reconcile .gitignore against the claude manifest')
222
236
  .argument('[target]', 'Target directory', '.')
223
237
  .helpOption('-h, --help', 'Show this help message')
238
+ .addHelpText(
239
+ 'after',
240
+ [
241
+ '',
242
+ 'Writes .gitignore alone: adds the entries the claude manifest lists',
243
+ 'and drops ones under its headers that the manifest no longer lists.',
244
+ 'A seeded file is reported, never rewritten, and a missing one points',
245
+ 'at `canon claude init`.',
246
+ '',
247
+ 'Exit codes:',
248
+ ' 0 synced, already up to date, or the prompt was cancelled',
249
+ ' 1 the target is missing or is the toolkit root',
250
+ '',
251
+ ].join('\n'),
252
+ )
224
253
  .action(async (target: string) => {
225
254
  process.exitCode = await runSync(target)
226
255
  })
@@ -161,7 +161,13 @@ export function register(program: Command): void {
161
161
  `overrides a value by writing ${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/`,
162
162
  'itself, which sync never touches. Nothing arrives on a project that',
163
163
  'has not run this, so a design value reaches a target on an install',
164
- 'rather than on the next sync.',
164
+ 'rather than on the next sync. A base stylesheet already installed is',
165
+ 'replaced.',
166
+ '',
167
+ 'Exit codes:',
168
+ ' 0 the stylesheet was installed',
169
+ ' 1 the target is missing or is the toolkit root, or the toolkit',
170
+ ' source is absent',
165
171
  '',
166
172
  ].join('\n'),
167
173
  )
@@ -176,6 +182,21 @@ export function register(program: Command): void {
176
182
  )
177
183
  .argument('[target]', 'Target directory', '.')
178
184
  .helpOption('-h, --help', 'Show this help message')
185
+ .addHelpText(
186
+ 'after',
187
+ [
188
+ '',
189
+ 'Replaces the shipped design files under .claude/design/ with the',
190
+ 'toolkit copy, edited or not, and leaves',
191
+ `${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/ alone. It deletes nothing it finds no`,
192
+ 'source for.',
193
+ '',
194
+ 'Exit codes:',
195
+ ' 0 synced, already up to date, or the prompt was cancelled',
196
+ ' 1 the target is missing or is the toolkit root',
197
+ '',
198
+ ].join('\n'),
199
+ )
179
200
  .action(async (target: string) => {
180
201
  process.exitCode = await runDomainSync(
181
202
  createDesignAdapter(PROJECT_ROOT),
@@ -154,13 +154,21 @@ export function register(program: Command): void {
154
154
  'Examples:',
155
155
  ' canon gov install node',
156
156
  ' canon gov install node ../my-app',
157
- ' canon gov install node --add 210-astro,260-shadcn ../my-app',
157
+ ' canon gov install node --add astro,shadcn ../my-app',
158
158
  ' canon gov install --add tasks,plan ../my-app',
159
159
  '',
160
160
  'With --add and no stack, the named rules install alone. A first',
161
161
  'positional naming a stack is read as the stack, so pass . or a path',
162
162
  'carrying a slash to force a target that shares a stack name.',
163
163
  '',
164
+ 'Copies each rule into .claude/rules/canon/, replacing a copy already',
165
+ 'there. Rules under .claude/rules/project/ are never read.',
166
+ '',
167
+ 'Exit codes:',
168
+ ' 0 installed, the stack held no rules, or the prompt was cancelled',
169
+ ' 1 refused: no stack named headlessly, an unknown stack, no --add',
170
+ ' rule resolved, or a missing target or the toolkit root',
171
+ '',
164
172
  ].join('\n'),
165
173
  )
166
174
  .action(
@@ -178,6 +186,21 @@ export function register(program: Command): void {
178
186
  .description('Update rules already installed under .claude/rules/canon/')
179
187
  .argument('[target]', 'Target directory', '.')
180
188
  .helpOption('-h, --help', 'Show this help message')
189
+ .addHelpText(
190
+ 'after',
191
+ [
192
+ '',
193
+ 'Replaces an edited rule under .claude/rules/canon/ with the shipped',
194
+ 'copy, deletes a rule the toolkit retired, and moves a renamed one.',
195
+ 'It adds no new rule, which is `canon gov install`, and never touches',
196
+ '.claude/rules/project/.',
197
+ '',
198
+ 'Exit codes:',
199
+ ' 0 synced, already up to date, or the prompt was cancelled',
200
+ ' 1 the target is missing or is the toolkit root',
201
+ '',
202
+ ].join('\n'),
203
+ )
181
204
  .action(async (target: string) => {
182
205
  process.exitCode = await runDomainSync(
183
206
  createGovAdapter(PROJECT_ROOT),
@@ -440,10 +463,9 @@ export function register(program: Command): void {
440
463
  'internal corpus ships nowhere, which makes the tree it governs the tree',
441
464
  'present and the question answerable.',
442
465
  '',
443
- 'Forms read, with where each resolves:',
466
+ 'Form read, with where it resolves:',
444
467
  ' path a backticked path carrying a directory segment and a file',
445
468
  ' extension, against the root',
446
- " sibling a bare <nnn>-<slug>.md , against the citing rule's folder",
447
469
  '',
448
470
  'Not read, each a shape the corpus writes and none of them a citation:',
449
471
  ' a placeholder or glob segment, which describes a shape rather than',
@@ -37,10 +37,19 @@ export function register(program: Command): void {
37
37
  '',
38
38
  'Passing any option skips the confirmation prompt.',
39
39
  '',
40
+ 'Runs `canon tooling sync base --write` first, which replaces every',
41
+ 'base golden config the target holds, then `canon claude init`,',
42
+ '`canon gov install`, and `canon wiki init`. Refuses a target that is',
43
+ 'not a directory or is the toolkit root.',
44
+ '',
45
+ 'Exit codes:',
46
+ ' 0 every domain installed, or the prompt was cancelled',
47
+ ' 1 the target was refused, or a domain failed, named on stderr',
48
+ '',
40
49
  'Examples:',
41
50
  ' canon init',
42
51
  ' canon init ../my-app',
43
- ' canon init --stack node --add 210-astro,260-shadcn ../my-app',
52
+ ' canon init --stack node --add astro,shadcn ../my-app',
44
53
  ' canon init --skip governance ../my-app',
45
54
  '',
46
55
  ].join('\n'),