@erclx/canon 5.38.0 → 5.40.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 (246) hide show
  1. package/README.md +5 -5
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/SKILL.md +12 -9
  4. package/claude/skills/auto-ship/SKILL.md +43 -23
  5. package/claude/skills/backlog-triage/SKILL.md +62 -53
  6. package/claude/skills/backlog-triage/references/apply.md +69 -0
  7. package/claude/skills/build-in-slices/SKILL.md +8 -4
  8. package/claude/skills/canon-cli/SKILL.md +5 -1
  9. package/claude/skills/canon-feedback/SKILL.md +18 -8
  10. package/claude/skills/canon-feedback-triage/SKILL.md +10 -3
  11. package/claude/skills/canvas-board/SKILL.md +37 -18
  12. package/claude/skills/ci-workflow/SKILL.md +5 -4
  13. package/claude/skills/code-craft/SKILL.md +10 -6
  14. package/claude/skills/codebase-layout/SKILL.md +3 -5
  15. package/claude/skills/context-fold/REQUIREMENT.md +4 -4
  16. package/claude/skills/context-fold/SKILL.md +42 -23
  17. package/claude/skills/context-fold/references/context-refresh.md +4 -3
  18. package/claude/skills/context-fold/references/context.md +79 -64
  19. package/claude/skills/create-rule/SKILL.md +7 -3
  20. package/claude/skills/create-skill/SKILL.md +27 -7
  21. package/claude/skills/create-skill/references/skill-reference.md +41 -17
  22. package/claude/skills/decision-escalate/SKILL.md +8 -5
  23. package/claude/skills/deploy-app/SKILL.md +1 -1
  24. package/claude/skills/deprecation-migration/SKILL.md +12 -4
  25. package/claude/skills/design-taste/SKILL.md +17 -11
  26. package/claude/skills/docs-sync/REQUIREMENT.md +1 -0
  27. package/claude/skills/docs-sync/SKILL.md +17 -6
  28. package/claude/skills/docs-sync/references/docs.md +54 -21
  29. package/claude/skills/docs-sync/references/readme.md +32 -8
  30. package/claude/skills/document-health/SKILL.md +3 -4
  31. package/claude/skills/draft-and-pick/SKILL.md +73 -47
  32. package/claude/skills/draft-doc/SKILL.md +9 -4
  33. package/claude/skills/draft-doc/references/context.md +4 -4
  34. package/claude/skills/draft-doc/references/docs.md +1 -0
  35. package/claude/skills/draft-doc/references/wiki.md +3 -2
  36. package/claude/skills/draft-figure/SKILL.md +9 -7
  37. package/claude/skills/draft-identity/SKILL.md +24 -52
  38. package/claude/skills/draft-identity/references/card-route.md +46 -0
  39. package/claude/skills/draft-ready/SKILL.md +19 -7
  40. package/claude/skills/draft-screencast/SKILL.md +6 -112
  41. package/claude/skills/draft-screencast/references/beats-template.md +119 -0
  42. package/claude/skills/draft-slides/SKILL.md +9 -7
  43. package/claude/skills/draft-slides/references/deck.md +183 -0
  44. package/claude/skills/git-branch/SKILL.md +3 -3
  45. package/claude/skills/git-commit/SKILL.md +5 -3
  46. package/claude/skills/git-followup/SKILL.md +16 -9
  47. package/claude/skills/git-issue/SKILL.md +6 -5
  48. package/claude/skills/git-pr/SKILL.md +56 -44
  49. package/claude/skills/git-pr/references/evidence.md +2 -2
  50. package/claude/skills/git-pr/references/final-command.md +1 -1
  51. package/claude/skills/git-pr/references/labels.md +1 -1
  52. package/claude/skills/git-pr/references/publish.md +1 -1
  53. package/claude/skills/git-ship/SKILL.md +43 -34
  54. package/claude/skills/git-ship/references/ci-watch.md +31 -0
  55. package/claude/skills/git-split/SKILL.md +28 -100
  56. package/claude/skills/git-split/references/independent.md +40 -0
  57. package/claude/skills/git-split/references/stacked.md +73 -0
  58. package/claude/skills/git-stage/SKILL.md +9 -7
  59. package/claude/skills/git-worktree/SKILL.md +27 -49
  60. package/claude/skills/git-worktree/references/cleanup.md +55 -0
  61. package/claude/skills/markdown-craft/SKILL.md +2 -2
  62. package/claude/skills/markdown-craft/references/markdown.md +82 -53
  63. package/claude/skills/plan-feature/SKILL.md +52 -46
  64. package/claude/skills/plan-feature/references/batches.md +46 -0
  65. package/claude/skills/plan-groundwork/SKILL.md +60 -74
  66. package/claude/skills/plan-groundwork/references/output.md +48 -0
  67. package/claude/skills/plan-intake/SKILL.md +28 -21
  68. package/claude/skills/plan-intake-answer/SKILL.md +14 -13
  69. package/claude/skills/read-frames/SKILL.md +2 -0
  70. package/claude/skills/record-screencast/SKILL.md +31 -11
  71. package/claude/skills/review-address/SKILL.md +76 -158
  72. package/claude/skills/review-address/references/rebase-only.md +34 -0
  73. package/claude/skills/review-craft/SKILL.md +27 -11
  74. package/claude/skills/review-pr/SKILL.md +91 -40
  75. package/claude/skills/review-ui/SKILL.md +63 -37
  76. package/claude/skills/review-ui/references/hosted-preview.md +30 -0
  77. package/claude/skills/role-orchestrator/SKILL.md +105 -74
  78. package/claude/skills/role-orchestrator/references/orchestrator-filing.md +26 -0
  79. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
  80. package/claude/skills/role-planner/SKILL.md +66 -64
  81. package/claude/skills/role-reviewer/SKILL.md +57 -46
  82. package/claude/skills/role-worker/SKILL.md +68 -63
  83. package/claude/skills/search-craft/SKILL.md +7 -5
  84. package/claude/skills/session-compact/SKILL.md +16 -11
  85. package/claude/skills/session-map/SKILL.md +31 -10
  86. package/claude/skills/session-relay/SKILL.md +32 -65
  87. package/claude/skills/session-resume/SKILL.md +18 -13
  88. package/claude/skills/session-worktree/SKILL.md +70 -42
  89. package/claude/skills/systematic-debugging/SKILL.md +6 -1
  90. package/claude/skills/target-check/SKILL.md +13 -6
  91. package/claude/skills/target-setup/SKILL.md +39 -27
  92. package/claude/skills/task-board/SKILL.md +68 -60
  93. package/claude/skills/task-board/references/board.md +1 -1
  94. package/claude/skills/task-board/references/create-report.md +36 -0
  95. package/claude/skills/task-board/references/tasks.md +1 -1
  96. package/claude/skills/task-complete/SKILL.md +20 -18
  97. package/claude/skills/teach-workspace/SKILL.md +61 -33
  98. package/claude/skills/test-craft/SKILL.md +2 -1
  99. package/claude/skills/test-first/SKILL.md +15 -9
  100. package/claude/skills/ui-checklist/SKILL.md +18 -16
  101. package/claude/skills/ux-audit/SKILL.md +9 -10
  102. package/claude/skills/ux-measure/SKILL.md +23 -19
  103. package/claude/skills/video-craft/SKILL.md +15 -7
  104. package/claude/skills/write-human/SKILL.md +9 -1
  105. package/claude/skills/youtube-transcripts/SKILL.md +2 -1
  106. package/docs/agents/audits.md +61 -60
  107. package/docs/agents/context-audit.md +40 -58
  108. package/docs/agents/index.md +11 -61
  109. package/docs/agents/indexes.md +25 -29
  110. package/docs/agents/install-and-sync.md +125 -235
  111. package/docs/agents/markdown-audit.md +54 -96
  112. package/docs/agents/migrate.md +41 -16
  113. package/docs/agents/output-shape.md +60 -45
  114. package/docs/agents/overview.md +36 -12
  115. package/docs/agents/records.md +37 -79
  116. package/docs/agents/sessions-transfer.md +26 -70
  117. package/docs/agents/targets.md +25 -77
  118. package/docs/target/index.md +0 -1
  119. package/docs/target/migrations.md +57 -66
  120. package/docs/target/projects.md +36 -78
  121. package/docs/target/scenarios.md +9 -19
  122. package/docs/target/sync.md +27 -68
  123. package/docs/workflow/ai-workflow.md +22 -68
  124. package/docs/workflow/operating-model.md +32 -186
  125. package/docs/workflow/parallel-features.md +29 -66
  126. package/docs/workflow/skill-map-on-request.md +22 -24
  127. package/docs/workflow/skill-map.md +72 -76
  128. package/docs/workflow/visual-design-workflow.md +25 -118
  129. package/docs/workflow/zshrc-aliases.md +18 -37
  130. package/package.json +1 -1
  131. package/src/canvas/client/inspector/fields.css +4 -0
  132. package/src/canvas/index.html +6 -0
  133. package/src/claude/skills-reach.ts +2 -2
  134. package/src/cli/ui.ts +3 -1
  135. package/src/commands/canvas.ts +23 -0
  136. package/src/commands/capture.ts +14 -0
  137. package/src/commands/claude.ts +14 -1
  138. package/src/commands/context.ts +106 -7
  139. package/src/commands/demo.ts +22 -0
  140. package/src/commands/deps.ts +6 -0
  141. package/src/commands/driver.ts +28 -2
  142. package/src/commands/gov.ts +52 -18
  143. package/src/commands/hooks.ts +63 -0
  144. package/src/commands/intake.ts +17 -0
  145. package/src/commands/labels.ts +38 -5
  146. package/src/commands/markdown.ts +169 -32
  147. package/src/commands/pr.ts +160 -43
  148. package/src/commands/records.ts +8 -0
  149. package/src/commands/secrets.ts +6 -0
  150. package/src/commands/sessions.ts +8 -0
  151. package/src/commands/tasks.ts +52 -0
  152. package/src/commands/teach.ts +13 -0
  153. package/src/commands/upstream.ts +25 -1
  154. package/src/commands/worktrees.ts +5 -0
  155. package/src/context/audit.ts +274 -5
  156. package/src/context/classify/prompts.ts +1 -1
  157. package/src/context/folders.ts +1 -1
  158. package/src/context/gate.ts +23 -7
  159. package/src/context/shape-gated.ts +30 -0
  160. package/src/counts/scan.ts +1 -1
  161. package/src/design/components.ts +1 -1
  162. package/src/design/document.ts +1 -1
  163. package/src/design/tokens.ts +1 -1
  164. package/src/gate/boundaries.ts +4 -1
  165. package/src/gate/measures.ts +16 -191
  166. package/src/gate/stages.ts +14 -30
  167. package/src/git/env.ts +6 -0
  168. package/src/hooks/reply-links.ts +110 -0
  169. package/src/markdown/density.ts +258 -0
  170. package/src/markdown/gate.ts +57 -18
  171. package/src/markdown/reference-shape.ts +103 -0
  172. package/src/markdown/scope.ts +162 -0
  173. package/src/markdown/structure.ts +9 -59
  174. package/src/pr/lift.ts +153 -19
  175. package/src/pr/painted.ts +87 -0
  176. package/src/roots/record.ts +18 -1
  177. package/src/sandbox/census.ts +1 -1
  178. package/src/sandbox/headless/run.ts +3 -2
  179. package/src/sandbox/headless/watch.ts +3 -4
  180. package/src/shipped/references.ts +1 -1
  181. package/src/slides/convert/master.ts +3 -0
  182. package/src/sync/format.ts +46 -0
  183. package/src/sync/stamp.ts +6 -0
  184. package/tooling/astro/reference.md +2 -2
  185. package/tooling/base/reference.md +1 -1
  186. package/tooling/claude/seeds/.claude/hooks/reply-links.sh +72 -0
  187. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +22 -1
  188. package/tooling/claude/seeds/.claude/settings.json +4 -0
  189. package/tooling/go/reference.md +1 -1
  190. package/tooling/nextjs/reference.md +1 -1
  191. package/tooling/php/reference.md +1 -1
  192. package/tooling/python/reference.md +1 -1
  193. package/tooling/vite-react/reference.md +1 -1
  194. package/tooling/web/reference.md +1 -1
  195. package/docs/agents/canvas.md +0 -86
  196. package/docs/agents/capture.md +0 -65
  197. package/docs/agents/census.md +0 -23
  198. package/docs/agents/commands.md +0 -154
  199. package/docs/agents/comments.md +0 -34
  200. package/docs/agents/context-audit-checks.md +0 -90
  201. package/docs/agents/context-classify.md +0 -101
  202. package/docs/agents/counts.md +0 -74
  203. package/docs/agents/demo.md +0 -140
  204. package/docs/agents/docs.md +0 -27
  205. package/docs/agents/drift-surfaces.md +0 -201
  206. package/docs/agents/driver.md +0 -103
  207. package/docs/agents/gate.md +0 -90
  208. package/docs/agents/gov-detect.md +0 -47
  209. package/docs/agents/hooks.md +0 -58
  210. package/docs/agents/intake.md +0 -81
  211. package/docs/agents/key-changes.md +0 -116
  212. package/docs/agents/label-coverage.md +0 -86
  213. package/docs/agents/markdown-audit-weight.md +0 -81
  214. package/docs/agents/pr-evidence-layout.md +0 -70
  215. package/docs/agents/pr-evidence.md +0 -273
  216. package/docs/agents/pr-frames.md +0 -131
  217. package/docs/agents/pr-lift.md +0 -132
  218. package/docs/agents/pr-local.md +0 -111
  219. package/docs/agents/pr-preview.md +0 -155
  220. package/docs/agents/pr-reads.md +0 -111
  221. package/docs/agents/pr-tick.md +0 -72
  222. package/docs/agents/records-push.md +0 -61
  223. package/docs/agents/records-stale.md +0 -40
  224. package/docs/agents/records-validate.md +0 -57
  225. package/docs/agents/restated.md +0 -83
  226. package/docs/agents/routing.md +0 -61
  227. package/docs/agents/rule-citations.md +0 -90
  228. package/docs/agents/sandbox.md +0 -126
  229. package/docs/agents/scripting.md +0 -142
  230. package/docs/agents/sessions.md +0 -128
  231. package/docs/agents/skills-audit.md +0 -120
  232. package/docs/agents/skills-reach.md +0 -64
  233. package/docs/agents/slides.md +0 -176
  234. package/docs/agents/state-scoped-risk.md +0 -105
  235. package/docs/agents/superseded.md +0 -85
  236. package/docs/agents/tasks-archive.md +0 -83
  237. package/docs/agents/tasks-plans.md +0 -152
  238. package/docs/agents/tasks-validate.md +0 -133
  239. package/docs/agents/tasks.md +0 -123
  240. package/docs/agents/teach-authoring.md +0 -118
  241. package/docs/agents/teach.md +0 -157
  242. package/docs/agents/test-order.md +0 -58
  243. package/docs/agents/upstream.md +0 -94
  244. package/docs/agents/worktrees.md +0 -118
  245. package/docs/target/standards-moved.md +0 -34
  246. package/src/markdown/growth.ts +0 -43
@@ -6,15 +6,13 @@ category: Workflow
6
6
 
7
7
  # Zshrc aliases for Claude Code
8
8
 
9
- Claude Code auto-discovers the toolkit plugin from `claude/.claude-plugin/plugin.json` when run inside the toolkit repository. Outside the toolkit, that discovery does not fire, so a separate set of aliases bakes in `--plugin-dir` explicitly. Two parallel sets cover both cases.
9
+ Claude Code auto-discovers the toolkit plugin inside the toolkit repository. Outside it, the `clp` aliases pass `--plugin-dir` explicitly.
10
10
 
11
11
  ## The aliases
12
12
 
13
- `bun run bootstrap` installs this block. It owns the canonical copy in `scripts/core/bootstrap.sh`, appends it to `~/.zshrc` wrapped in the two marker comments below, and sets `TOOLKIT` to the cloned path. The markers are how a re-run recognizes its own block and skips.
13
+ `bun run bootstrap` appends this block to `~/.zshrc` between the two marker comments and sets `TOOLKIT` to the cloned path. A re-run skips a block it finds and never updates it. To pick up a newer alias, delete the marked block and re-run, or paste the new lines in.
14
14
 
15
- Skipping means a re-run never updates a block already installed, so a shell that ran the bootstrap before the canonical block grew keeps whatever it got that day. To pick up an alias added since, delete the marked block and re-run the bootstrap, or paste the new lines into the block by hand.
16
-
17
- A block installed by hand before the managed one has no markers. Bootstrap detects that case through the `alias clp=` line instead, warns, and leaves the block untouched rather than appending a second copy. To switch over, delete the hand-rolled block and re-run the bootstrap.
15
+ A hand-written block without markers is detected through its `alias clp=` line, and bootstrap warns and leaves it. Delete it and re-run to switch.
18
16
 
19
17
  ```zsh
20
18
  # >>> canon aliases >>>
@@ -34,46 +32,29 @@ alias clpac='clpa --cwd .'
34
32
  # <<< canon aliases <<<
35
33
  ```
36
34
 
37
- The block sits after any `PATH` mutations and the `claude` CLI install. Zsh expands aliases recursively on the first word, so `clr`, `clc`, `clw`, and `cls` inherit their base through `cl`, and `clpc`, `clps`, and `clpa` inherit `--plugin-dir` through `clp`. `clpac` inherits it one level further, through `clpa`. `$TOOLKIT` expands at invocation time, so updating the variable and re-sourcing reroutes all `clp` calls without touching the alias definitions.
38
-
39
- To opt out, delete the block between the two markers. Bootstrap re-adds it on the next run, so skip that step by running `bun install` and `bun link` yourself instead.
40
-
41
- The block itself works unchanged in bash, and what differs is the file it goes in and who writes it. Paste it into `~/.bashrc` by hand, since `bun run bootstrap` appends to `~/.zshrc` alone and a run from a bash shell installs nothing that shell reads. Bash re-checks the first word of an alias expansion the way zsh does, so the `cl` and `clp` chains below still inherit their base, and the `bun install` and `bun link` step above is the opt-out either shell takes.
35
+ To opt out, delete the block and run `bun install` and `bun link` yourself. Bash takes the block unchanged in `~/.bashrc`, pasted by hand. Bootstrap writes `~/.zshrc` alone.
42
36
 
43
37
  ## What each one does
44
38
 
45
- `cl`, `clw`, and `cls` carry no explicit plugin dir and start a fresh session. Use them inside the toolkit repository, where Claude Code auto-discovers the plugin from `claude/.claude-plugin/plugin.json`. Loading `--plugin-dir` on top of auto-discovery registers every skill twice and produces duplicate entries in the slash command list.
46
-
47
- `clr` and `clc` resume a session rather than launching one, so auto-discovery never fires for them. A resume restores whatever plugin registration the session started with and recomputes nothing, which a bare `claude --resume` demonstrated by holding a stale registration through a full process restart until `--plugin-dir` was passed on the resume itself.
48
-
49
39
  - `cl`: plain session in the current directory
50
- - `clr`: opens the `/resume` picker scoped to the current directory. Trailing arguments filter by name. `clr auth` limits results to sessions containing "auth". Resuming does not reload the plugin, so reach for `clpc` when the session needs it.
51
- - `clc`: jumps straight into the most recent session for the current directory. No picker. Faster than `clr` when the terminal closed and you want back into the same session. Resuming does not reload the plugin here either.
52
- - `clw`: creates a worktree under `.claude/worktrees/<name>/` on a fresh branch and starts a Claude Code session in it. Pass the worktree name as the trailing arg: `clw feat-auth`.
53
- - `cls`: pins the session to Sonnet instead of the default Opus. Use for routine work where Opus cost is not justified.
40
+ - `clr`: opens the `/resume` picker for the current directory. `clr auth` filters to sessions containing "auth".
41
+ - `clc`: resumes the most recent session for the current directory, no picker
42
+ - `clw`: creates a worktree under `.claude/worktrees/<name>/` on a fresh branch and starts a session there, as in `clw feat-auth`
43
+ - `cls`: pins the session to Sonnet
54
44
 
55
- The `clp` family bakes in `--plugin-dir`. Use it outside the toolkit repository, where auto-discovery does not fire, and for any resume that needs the plugin.
45
+ The `clp` family loads the plugin explicitly.
56
46
 
57
- - `clp`: session with the toolkit plugin loaded explicitly
58
- - `clpc`: jumps straight into the most recent session for the current directory with the plugin loaded. The `clp` mirror of `clc`, and the one resume that carries the plugin.
47
+ - `clp`: session with the toolkit plugin loaded
48
+ - `clpc`: resumes the most recent session with the plugin loaded
59
49
  - `clps`: `clp` pinned to Sonnet
60
- - `clpa`: opens the agent view with the plugin loaded, covering background sessions from every directory
61
- - `clpac`: the same view filtered to background sessions started under the current directory
50
+ - `clpa`: opens the agent view with the plugin loaded, across every directory
51
+ - `clpac`: the same view filtered to the current directory
62
52
 
63
53
  ## When to use which
64
54
 
65
- Use `cl` by default when working in the toolkit repository. Auto-discovery loads the plugin and the two-key alias keeps it short.
66
-
67
- Use `clp` in any other repository where you want the toolkit skills available. Without the flag, those repos do not load the plugin.
68
-
69
- Use `cls` or `clps` to save Opus usage on routine sessions. Switch mid-session with `/model` to avoid restarting.
70
-
71
- Use `clw <name>` for features that will take more than one session. A worktree isolates the branch, the transcripts, and the `/resume` history. See [Claude Code and git worktrees](../../wiki/claude/worktrees.md) for the worktree mechanism, and [Parallel features](parallel-features.md) for fan-out rules.
72
-
73
- Use `clc` to resume the last session without a picker. Use `clr` when you have several sessions and need to pick by name or recency. Reach for `clpc` over `clc` wherever the resumed session needs the plugin, inside the toolkit repository as well as outside it, since neither resume re-runs discovery.
74
-
75
- Use `clpa` to see what is running across every directory, and `clpac` when only the current project matters.
76
-
77
- ## Why not a function
55
+ Use `cl` inside the toolkit repository and `clp` in any other repository that needs the toolkit skills. A resume does not reload the plugin, so use `clpc` over `clc` wherever the resumed session needs it.
78
56
 
79
- A function with subcommand dispatch (`cl r`, `cl c`, `cl w foo`) was considered and rejected. It loses shell completion and adds a layer of indirection. Separate aliases are self-contained and the shared `cl`/`clp` prefixes make them easy to recall.
57
+ - Use `cls` or `clps` for routine sessions, and `/model` to switch mid-session
58
+ - Use `clw <name>` for features that take more than one session. See [Claude Code and git worktrees](../../wiki/claude/worktrees.md) and [Parallel features](parallel-features.md).
59
+ - Use `clc` to resume without a picker and `clr` to pick by name
60
+ - Use `clpa` for every directory and `clpac` for the current project
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.38.0",
4
+ "version": "5.40.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": {
@@ -2,6 +2,10 @@
2
2
  * The inspector's own rules. Every value reads a chrome token, the same set
3
3
  * shell.css reads. Each field's glyph sits inside its box, so two fields share
4
4
  * a row without a label column eating the width.
5
+ *
6
+ * These rules share one class namespace with shell.css, where the Theme tab
7
+ * owns `.token`, so a new class here takes its own prefix, as `.token-option`
8
+ * does.
5
9
  */
6
10
 
7
11
  .inspector-section {
@@ -4,6 +4,12 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>Canvas</title>
7
+ <!--
8
+ Bun's bundler resolves every href and src here at start. A root-relative
9
+ route the server answers, such as /api/icon.svg, fails with "Could not
10
+ resolve" and leaves / serving an empty 200, so main.tsx points the icon
11
+ at it on load.
12
+ -->
7
13
  <link rel="icon" href="data:," />
8
14
  <link rel="stylesheet" href="./client/shell.css" />
9
15
  </head>
@@ -13,8 +13,8 @@ import { SURFACE_ROOTS } from '@/roots/surface'
13
13
  * `src/`, `scripts/`, and bare `docs/` are deliberately absent. A body naming
14
14
  * one of those is describing the reader's own tree, so listing them would
15
15
  * report a correct citation on every run and bury the defect this measures.
16
- * `docs/agents/` is the exception, being the CLI contract pages that exist
17
- * here alone.
16
+ * `docs/agents/` is the exception, being the reference pages for verbs run by
17
+ * hand, which exist here alone.
18
18
  *
19
19
  * The context folder carries both surface-root spellings, since this list
20
20
  * decides whether a shipped body names this repository's own tree and a body
package/src/cli/ui.ts CHANGED
@@ -162,6 +162,8 @@ export async function select<Value>(opts: {
162
162
  message: string
163
163
  options: { value: Value; label: string; hint?: string }[]
164
164
  nonInteractiveDefault?: boolean
165
+ /** Replaces the "Pass an argument" clause of the TTY refusal. */
166
+ ttyHint?: string
165
167
  }): Promise<Value> {
166
168
  const { message, options } = opts
167
169
  const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
@@ -196,7 +198,7 @@ export async function select<Value>(opts: {
196
198
  return new Promise<Value>((resolve) => {
197
199
  if (!process.stdin.isTTY) {
198
200
  process.stderr.write(
199
- `${GREY}│${NC} ${RED}✗${NC} ${message} requires a TTY. Pass an argument or set CANON_NON_INTERACTIVE=1.\n${GREY}└${NC}\n`,
201
+ `${GREY}│${NC} ${RED}✗${NC} ${message} requires a TTY. ${opts.ttyHint ?? 'Pass an argument'} or set CANON_NON_INTERACTIVE=1.\n${GREY}└${NC}\n`,
200
202
  )
201
203
  process.exit(1)
202
204
  }
@@ -77,6 +77,11 @@ const CLIENT_DEPS = ['preact', '@preact/signals'] as const
77
77
  * the bundler then answers `/` with an empty page rather than an error. The
78
78
  * caller passes the package root the binary runs from, never the served
79
79
  * project's, so a global install serving a target finds its own copies.
80
+ *
81
+ * `Bun.resolveSync` and `createRequire(...).resolve` fall back to Bun's global
82
+ * install cache when no ancestor of the starting directory holds a
83
+ * `node_modules`, so a presence check from such a directory passes where the
84
+ * bundler fails. The package root always sits under one.
80
85
  */
81
86
  export function missingClientDeps(from: string): Refused | undefined {
82
87
  const missing = CLIENT_DEPS.filter((name) => {
@@ -161,6 +166,24 @@ export function register(program: Command): void {
161
166
  'it, and layout.json beside them holds each frame box. Edit a frame by',
162
167
  'editing its file, and an open canvas reloads that frame.',
163
168
  '',
169
+ 'Reasons, on stderr or in the JSON record:',
170
+ ' no-root no .canon/canvas/ folder under the root',
171
+ ' no-page, no-frame the target is not on disk',
172
+ ' invalid-name a name or target breaks the page or frame form',
173
+ ' invalid-size a width or height is not a whole number of pixels',
174
+ ' invalid-position an x or y is not a whole number',
175
+ ' exists the page or frame is already there',
176
+ ' malformed-layout layout.json does not read as a frame map',
177
+ ' busy another write holds the page',
178
+ ' invalid-address an element address is not a path of positions',
179
+ ' address-mismatch the file builds elements the address does not',
180
+ ' stale-address the file changed since the address was served',
181
+ ' invalid-edit an edit key or value is not one the writer takes',
182
+ ' not-text-only text targets an element holding other elements',
183
+ ' missing-client-deps preact or @preact/signals does not resolve, so run bun install',
184
+ ' no-server no canvas server answers at the address',
185
+ ' capture-failed a frame did not render, with the font or browser reason',
186
+ '',
164
187
  'Examples:',
165
188
  ' canon canvas serve',
166
189
  ' canon canvas page add drafts',
@@ -58,6 +58,20 @@ export function register(program: Command): void {
58
58
  '--width <px>',
59
59
  'Viewport width in pixels, so a media query resolves at that width',
60
60
  )
61
+ .addHelpText(
62
+ 'after',
63
+ [
64
+ '',
65
+ 'The selector has no default, since the element to crop to belongs to the',
66
+ 'page. Each element holding text inside it must resolve its first font',
67
+ 'family, and a generic keyword such as monospace or system-ui fails,',
68
+ 'since it resolves differently on each machine. The user agent sets',
69
+ 'monospace on pre, code, kbd, and samp, so each needs its own',
70
+ 'font-family. --width takes one whole number and sets the viewport, so',
71
+ 'run it once per breakpoint with its own --out.',
72
+ '',
73
+ ].join('\n'),
74
+ )
61
75
  .action(
62
76
  async (
63
77
  source: string,
@@ -319,8 +319,13 @@ export function register(program: Command): void {
319
319
  ' 0 the file was read',
320
320
  ' 1 refused, with the reason on stderr',
321
321
  '',
322
+ 'Reasons:',
323
+ ' no-claude-md the tree has no always-loaded file',
324
+ ' no-rules no path-scoped rule under .claude/rules/, so every path would',
325
+ ' report as uncovered',
326
+ '',
322
327
  'Reports rather than gates. Whether a bullet belongs in a rule is the',
323
- 'judgment canon/context/context-model/overview.md states, and naming a path is evidence for it',
328
+ 'judgment canon/context/context-model.md states, and naming a path is evidence for it',
324
329
  'rather than the answer.',
325
330
  '',
326
331
  'Examples:',
@@ -432,6 +437,8 @@ export function register(program: Command): void {
432
437
  ' 1 refused, with the reason on stderr',
433
438
  ' 2 a skill folder carries no REQUIREMENT.md',
434
439
  '',
440
+ 'Refuses when neither claude/skills/ nor .claude/skills/ resolves.',
441
+ '',
435
442
  'Only a missing REQUIREMENT.md sets a failing exit code. Name, description,',
436
443
  'folder, and requirement-section findings are advisory.',
437
444
  '',
@@ -460,6 +467,10 @@ export function register(program: Command): void {
460
467
  ' 0 history was read, whether or not a body moved',
461
468
  ' 1 the question could not be answered, with the reason on stderr',
462
469
  '',
470
+ 'Refuses on an absent shipped corpus, an absent repository, and a ref',
471
+ 'matching no commit, each with its own reason. A target loading the',
472
+ 'plugin from a marketplace cache has no history, so it refuses there.',
473
+ '',
463
474
  'A moved body means the file changed, not that a session holds a stale',
464
475
  'copy. Passing a ref older than the oldest load over-reports, which is',
465
476
  'the safe direction. Confirm a name by reading the body.',
@@ -502,6 +513,8 @@ export function register(program: Command): void {
502
513
  ' 1 refused, with the reason on stderr',
503
514
  ' 2 at least one citation is unqualified',
504
515
  '',
516
+ 'Refuses a tree carrying neither claude/skills/ nor .claude/skills/.',
517
+ '',
505
518
  'Reports rather than gates. A toolkit-scoped instruction is sometimes',
506
519
  'meant for a session in this repository, so the verdict is a reading',
507
520
  'and the repair is to name the owner in the sentence.',
@@ -1,4 +1,4 @@
1
- import { resolve } from 'node:path'
1
+ import { join, relative, resolve } from 'node:path'
2
2
  import type { Command } from 'commander'
3
3
  import {
4
4
  type ContextAuditRefusal,
@@ -12,14 +12,22 @@ import {
12
12
  matchesSiblings,
13
13
  measureFolders,
14
14
  extraSections,
15
+ GOTCHA_CAP,
16
+ LAYOUT_BULLET_CAP,
17
+ LAYOUT_PARAGRAPH_CAP,
15
18
  missingSections,
19
+ OTHER_FOLDERS,
16
20
  PROVENANCE_FOLDER,
17
21
  ALLOWED_SECTIONS_BY_FOLDER,
18
22
  REQUIRED_SECTIONS,
19
23
  REQUIRED_SECTIONS_BY_FOLDER,
20
24
  requiredSections,
25
+ RETIRED_SECTIONS_BY_FOLDER,
26
+ shapeFindings,
21
27
  type ExtraSectionFinding,
22
28
  type SectionFinding,
29
+ type ShapeFinding,
30
+ type ShapeIssue,
23
31
  } from '@/context/audit'
24
32
  import { auditCitations, type CitationReport } from '@/context/citations'
25
33
  import {
@@ -49,6 +57,8 @@ import {
49
57
  resolveFolders,
50
58
  } from '@/context/folders'
51
59
  import { isGating } from '@/context/gate'
60
+ import { isShapeGated, SHAPE_GATED_FOLDERS } from '@/context/shape-gated'
61
+ import { currentWorktreeRoot } from '@/git/worktree'
52
62
  import {
53
63
  isOverWordCap,
54
64
  measureRequirements,
@@ -103,7 +113,7 @@ export function register(program: Command): void {
103
113
  .option('--citations-only', 'Run the gating citation check alone')
104
114
  .option(
105
115
  '--gate',
106
- 'Also fail on a missing required section or index drift, the findings that are facts',
116
+ 'Also fail on a missing required section, an H2 outside the set, a shape break in a gated folder, or index drift, the findings that are facts',
107
117
  )
108
118
  .addHelpText(
109
119
  'after',
@@ -118,10 +128,13 @@ export function register(program: Command): void {
118
128
  'stating a word cap gates past it, on any run except',
119
129
  '--citations-only, which never measures it. A record stating no',
120
130
  'cap is reported and never gated. --gate widens the gate to the',
121
- 'other two findings that are facts rather than judgments: a',
122
- 'missing required section and index drift. A context entry',
123
- 'requires Overview and Layout. Entry length, reference form,',
124
- 'table, and provenance are judgments under both.',
131
+ 'findings that are facts rather than judgments: a missing',
132
+ 'required section, an H2 outside the closed set, and index drift.',
133
+ 'A context entry requires Overview and Layout and allows Gotchas.',
134
+ 'A shape break, such as a Decisions section or a sixth gotcha, is',
135
+ 'reported everywhere and gated only under a shape-gated folder.',
136
+ 'Entry length, reference form, table, and provenance are',
137
+ 'judgments under both.',
125
138
  '',
126
139
  'Depth and bullet weight are stated over every markdown file rather',
127
140
  'than over a context entry, so `canon markdown audit` measures them.',
@@ -644,6 +657,15 @@ async function runAudit(
644
657
  const drift = gateOnly ? [] : await auditIndexes(folders)
645
658
  const sections = gateOnly ? [] : missingSections(root, folders, entries)
646
659
  const extras = gateOnly ? [] : extraSections(entries)
660
+ // The gated prefixes name paths from the repository root, while an entry's
661
+ // path runs from the audit root, which is a seed tree's own root when one is
662
+ // passed. Resolving here keeps a seed tree listed under its toolkit path.
663
+ const top = gateOnly ? root : await currentWorktreeRoot(root)
664
+ const shape = gateOnly
665
+ ? []
666
+ : shapeFindings(entries, (rel) =>
667
+ isShapeGated(relative(top, join(root, rel))),
668
+ )
647
669
  const length = gateOnly ? undefined : lengthFindings(entries)
648
670
  // Absent under `--citations-only` and null when the project carries no
649
671
  // record. A run that never looked and a project with nothing to look at are
@@ -658,6 +680,7 @@ async function runAudit(
658
680
  reportCitations(citations, cited)
659
681
  reportReferenceForm(entries, folders)
660
682
  reportSections(sections, extras, folders)
683
+ reportShape(shape)
661
684
  reportLength(length ?? [])
662
685
  reportTables(entries)
663
686
  reportProvenance(entries, folders)
@@ -696,6 +719,7 @@ async function runAudit(
696
719
  length,
697
720
  missingSections: sections,
698
721
  extraSections: extras,
722
+ shapeFindings: shape,
699
723
  indexDrift: drift,
700
724
  // Null says the run opened the project and found no record, which a
701
725
  // target that never wrote one is entitled to. Absent says the run
@@ -711,6 +735,11 @@ async function runAudit(
711
735
  // caller without an error.
712
736
  requiredSectionsByFolder: REQUIRED_SECTIONS_BY_FOLDER,
713
737
  allowedSectionsByFolder: ALLOWED_SECTIONS_BY_FOLDER,
738
+ retiredSectionsByFolder: RETIRED_SECTIONS_BY_FOLDER,
739
+ gotchaCap: GOTCHA_CAP,
740
+ layoutBulletCap: LAYOUT_BULLET_CAP,
741
+ layoutParagraphCap: LAYOUT_PARAGRAPH_CAP,
742
+ shapeGatedFolders: SHAPE_GATED_FOLDERS,
714
743
  },
715
744
  })}\n`,
716
745
  )
@@ -722,6 +751,7 @@ async function runAudit(
722
751
  requirements !== undefined && isOverWordCap(requirements),
723
752
  sections,
724
753
  extraSections: extras,
754
+ shape,
725
755
  drift,
726
756
  widened,
727
757
  })
@@ -958,7 +988,7 @@ function reportSections(
958
988
  logInfo(
959
989
  `The set is closed: ${Object.entries(ALLOWED_SECTIONS_BY_FOLDER)
960
990
  .map(([name, set]) => `canon/${name}/ allows ${set.join(', ')}`)
961
- .join('. ')} as H2. Any other topic is an H3 under one of them.`,
991
+ .join('. ')} as H2. A folder is an H3 under Layout.`,
962
992
  )
963
993
 
964
994
  if (extras.length === 0) {
@@ -974,6 +1004,75 @@ function reportSections(
974
1004
  )
975
1005
  }
976
1006
 
1007
+ /** Characters of a paragraph the report quotes, enough to find it by. */
1008
+ const QUOTE_WIDTH = 60
1009
+
1010
+ function clip(text: string): string {
1011
+ return text.length > QUOTE_WIDTH ? `${text.slice(0, QUOTE_WIDTH)}...` : text
1012
+ }
1013
+
1014
+ /** How each shape break reads in the report. */
1015
+ function readIssue(issue: ShapeIssue): string {
1016
+ switch (issue.kind) {
1017
+ case 'retired-section':
1018
+ return `retired section: ${issue.text}`
1019
+ case 'gotcha-count':
1020
+ return `${issue.count ?? 0} gotchas, past ${GOTCHA_CAP}`
1021
+ case 'gotcha-heading':
1022
+ return `heading inside Gotchas: ${issue.text}`
1023
+ case 'gotcha-paragraph':
1024
+ return `paragraph inside Gotchas: ${clip(issue.text)}`
1025
+ case 'layout-heading':
1026
+ return `Layout H3 titled neither a path nor ${OTHER_FOLDERS}: ${issue.text}`
1027
+ case 'layout-bullets':
1028
+ return `${issue.count ?? 0} bullets under ${issue.text}, past ${LAYOUT_BULLET_CAP}`
1029
+ case 'layout-paragraphs':
1030
+ return `${issue.count ?? 0} paragraphs under ${issue.text}, past ${LAYOUT_PARAGRAPH_CAP}`
1031
+ case 'layout-depth':
1032
+ return `heading below H3 under Layout: ${issue.text}`
1033
+ }
1034
+ }
1035
+
1036
+ /**
1037
+ * Names each entry breaking the section shape, gated ones first, and states
1038
+ * which prefixes gate on every run, since an empty gated list and a corpus that
1039
+ * conforms would otherwise print the same report.
1040
+ */
1041
+ function reportShape(shape: readonly ShapeFinding[]): void {
1042
+ logStep('Shape')
1043
+
1044
+ logInfo(
1045
+ SHAPE_GATED_FOLDERS.length === 0
1046
+ ? 'No folder is shape-gated yet, so every shape break reports and none gates.'
1047
+ : `Gated under --gate: ${SHAPE_GATED_FOLDERS.join(', ')}. A break elsewhere reports and never gates.`,
1048
+ )
1049
+
1050
+ if (shape.length === 0) {
1051
+ logInfo('Every entry holds the section shape.')
1052
+ return
1053
+ }
1054
+
1055
+ const gated = shape.filter((finding) => finding.gated)
1056
+ if (gated.length > 0) {
1057
+ logWarn(
1058
+ `${gated.length} gated ${gated.length === 1 ? 'entry' : 'entries'} breaking the shape`,
1059
+ )
1060
+ }
1061
+ logInfo(
1062
+ `${shape.length} ${shape.length === 1 ? 'entry' : 'entries'} breaking the shape in all`,
1063
+ )
1064
+ pipeOutput(
1065
+ [...gated, ...shape.filter((finding) => !finding.gated)]
1066
+ .map(
1067
+ (finding) =>
1068
+ `${finding.rel}${finding.gated ? ' gated' : ''}\n${finding.issues
1069
+ .map((issue) => ` :${issue.line} ${readIssue(issue)}`)
1070
+ .join('\n')}`,
1071
+ )
1072
+ .join('\n'),
1073
+ )
1074
+ }
1075
+
977
1076
  /** How each question reads in the report, in the standard's own order. */
978
1077
  const QUESTION_LABEL: Record<LengthQuestion, string> = {
979
1078
  domain: 'one domain',
@@ -76,6 +76,12 @@ export function register(program: Command): void {
76
76
  'wait condition, and the timing a beat lacks, and the timing is a',
77
77
  'starting point you tune, which is why a recompile refuses to overwrite.',
78
78
  '',
79
+ 'Reasons:',
80
+ ' draft-missing no file at the draft path',
81
+ ' draft-unreadable no beat sheet in the draft, or a sheet with no beats',
82
+ ' plan-exists a plan is already there and --force was not passed',
83
+ ' plan-unresolved a URL or a target is still empty, named field by field',
84
+ '',
79
85
  'Exit codes:',
80
86
  ' 0 a plan was written',
81
87
  ' 1 refused, with the reason on stderr',
@@ -114,9 +120,20 @@ export function register(program: Command): void {
114
120
  'Writes mp4 beside the webm when ffmpeg is on PATH, and skips it',
115
121
  `otherwise without failing the run. Install it with: ${INSTALL_CONVERTER}`,
116
122
  '',
123
+ 'Beside the webm it writes <video>.timeline.json, an entries array with',
124
+ 'one entry per step carrying index, kind, and startMs and endMs from the',
125
+ 'first frame. A click, fill, or hover entry adds box, the target in',
126
+ 'viewport pixels. The still is the hero beat, or the last beat.',
127
+ '',
117
128
  'A gif is opt-in behind --gif, since it is far larger than the webm',
118
129
  'and only a host that strips video needs one. GitHub is that host.',
119
130
  '',
131
+ 'Reasons:',
132
+ ' no-output-requested --no-video with --no-still, which would write nothing',
133
+ ' plan-unreadable the plan is not JSON, or a step names a kind nothing drives',
134
+ ' browser-missing the browser binary is not installed',
135
+ ' engine-missing the browser package did not resolve',
136
+ '',
120
137
  'Exit codes:',
121
138
  ' 0 the recording and the still were written',
122
139
  ' 1 refused, with the reason on stderr',
@@ -149,6 +166,11 @@ export function register(program: Command): void {
149
166
  'Needs ffmpeg on PATH, the same binary demo run already shells to for',
150
167
  `mp4 and gif conversion. Install it with: ${INSTALL_CONVERTER}`,
151
168
  '',
169
+ 'Reasons:',
170
+ ' video-missing no file at the video path',
171
+ ' converter-missing ffmpeg is not on PATH, so nothing was extracted',
172
+ ' extraction-failed ffmpeg exited non-zero, with its stderr in the message',
173
+ '',
152
174
  'Exit codes:',
153
175
  ' 0 frames were written',
154
176
  ' 1 refused, with the reason on stderr',
@@ -56,6 +56,12 @@ export function register(program: Command): void {
56
56
  ' 1 refused, with the reason on stderr',
57
57
  ' 2 at least one advisory was published',
58
58
  '',
59
+ 'Reasons:',
60
+ ' no-manifest no package.json in the project',
61
+ ' no-lockfile dependencies were never resolved, so install before auditing',
62
+ ' no-record the lookup returned no record that parses, as an unreachable',
63
+ ' index does',
64
+ '',
59
65
  'Examples:',
60
66
  ' canon deps audit',
61
67
  ' canon deps audit --json',
@@ -4,7 +4,7 @@ import type { Command } from 'commander'
4
4
  import { INSTALL_BROWSER, isEngineMissing } from '@/browser/engine'
5
5
  import type { DriverRefusal } from '@/driver/drive'
6
6
  import { describeViewport } from '@/driver/probes/viewport'
7
- import { PROBE_NAMES, readDriverPlan } from '@/driver/steps'
7
+ import { PROBE_NAMES, readDriverPlan, type ProbeName } from '@/driver/steps'
8
8
  import type { PlanRefusal } from '@/driver/steps'
9
9
  import {
10
10
  intro,
@@ -55,6 +55,15 @@ const DRIVE_REFUSALS: Record<DriverRefusal, string> = {
55
55
  'drive-failed': 'The drive failed against a reachable page.',
56
56
  }
57
57
 
58
+ const PROBE_NOTES: Record<ProbeName, string> = {
59
+ focus: 'keyboard focus that changes nothing visible, read after a Tab press',
60
+ details:
61
+ 'a details panel off the viewport or a row under the tap minimum, read open',
62
+ 'diagram-geometry':
63
+ 'an SVG label outside its frame, covered by a fill, or on another label',
64
+ 'diagram-strokes': 'an SVG label a stroke crosses, in either paint order',
65
+ }
66
+
58
67
  export function register(program: Command): void {
59
68
  program
60
69
  .command('drive')
@@ -103,7 +112,24 @@ export function register(program: Command): void {
103
112
  ` ${INSTALL_BROWSER}`,
104
113
  '',
105
114
  'Probes:',
106
- ...PROBE_NAMES.map((name) => ` ${name}`),
115
+ ...PROBE_NAMES.map(
116
+ (name) => ` ${name.padEnd(18)}${PROBE_NOTES[name]}`,
117
+ ),
118
+ '',
119
+ 'Reasons:',
120
+ ' no-run-file nothing readable at the run path',
121
+ ' unreadable-plan the run file is not valid JSON',
122
+ ' no-steps the run names no step',
123
+ ' no-probes the run names an empty probe list',
124
+ ' unknown-probe the run names a probe this build does not ship',
125
+ ' bad-step a step is missing what the driver needs to perform it',
126
+ ' no-viewport the run declares no viewport',
127
+ ' no-width the viewport has no width to wrap at',
128
+ ' no-heights the viewport names no height, which is never defaulted',
129
+ ' engine-missing the browser engine is not installed in this project',
130
+ ' browser-missing the engine is installed and its browser binary is not',
131
+ ' server-unreachable nothing answered at the URL, so no state was reached',
132
+ ' drive-failed the drive failed against a reachable page',
107
133
  '',
108
134
  'Step kinds:',
109
135
  ' click <target> press the first element the selector matches',