@erclx/canon 5.39.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 (131) hide show
  1. package/README.md +5 -5
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/canvas-board/SKILL.md +1 -1
  4. package/claude/skills/draft-slides/SKILL.md +2 -3
  5. package/claude/skills/draft-slides/references/deck.md +183 -0
  6. package/claude/skills/git-pr/references/evidence.md +2 -2
  7. package/claude/skills/git-pr/references/final-command.md +1 -1
  8. package/claude/skills/git-pr/references/labels.md +1 -1
  9. package/claude/skills/git-pr/references/publish.md +1 -1
  10. package/claude/skills/review-ui/SKILL.md +1 -0
  11. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
  12. package/claude/skills/task-board/references/board.md +1 -1
  13. package/claude/skills/task-board/references/tasks.md +1 -1
  14. package/claude/skills/ux-measure/SKILL.md +1 -1
  15. package/docs/agents/audits.md +61 -60
  16. package/docs/agents/context-audit.md +40 -58
  17. package/docs/agents/index.md +5 -51
  18. package/docs/agents/install-and-sync.md +1 -0
  19. package/docs/agents/markdown-audit.md +53 -99
  20. package/docs/agents/records.md +37 -79
  21. package/docs/agents/sessions-transfer.md +26 -70
  22. package/docs/target/index.md +0 -1
  23. package/docs/target/migrations.md +57 -66
  24. package/docs/target/projects.md +36 -78
  25. package/docs/target/scenarios.md +9 -19
  26. package/docs/target/sync.md +27 -68
  27. package/docs/workflow/ai-workflow.md +22 -68
  28. package/docs/workflow/operating-model.md +32 -186
  29. package/docs/workflow/parallel-features.md +29 -66
  30. package/docs/workflow/skill-map-on-request.md +22 -24
  31. package/docs/workflow/skill-map.md +72 -76
  32. package/docs/workflow/visual-design-workflow.md +25 -118
  33. package/docs/workflow/zshrc-aliases.md +18 -37
  34. package/package.json +1 -1
  35. package/src/canvas/client/inspector/fields.css +4 -0
  36. package/src/canvas/index.html +6 -0
  37. package/src/claude/skills-reach.ts +2 -2
  38. package/src/cli/ui.ts +3 -1
  39. package/src/commands/canvas.ts +23 -0
  40. package/src/commands/capture.ts +14 -0
  41. package/src/commands/claude.ts +13 -0
  42. package/src/commands/demo.ts +22 -0
  43. package/src/commands/deps.ts +6 -0
  44. package/src/commands/driver.ts +28 -2
  45. package/src/commands/gov.ts +52 -18
  46. package/src/commands/intake.ts +17 -0
  47. package/src/commands/labels.ts +38 -5
  48. package/src/commands/pr.ts +160 -43
  49. package/src/commands/records.ts +8 -0
  50. package/src/commands/secrets.ts +6 -0
  51. package/src/commands/sessions.ts +8 -0
  52. package/src/commands/tasks.ts +52 -0
  53. package/src/commands/teach.ts +13 -0
  54. package/src/commands/upstream.ts +25 -1
  55. package/src/commands/worktrees.ts +5 -0
  56. package/src/context/audit.ts +22 -4
  57. package/src/context/folders.ts +1 -1
  58. package/src/context/shape-gated.ts +4 -2
  59. package/src/counts/scan.ts +1 -1
  60. package/src/design/components.ts +1 -1
  61. package/src/design/document.ts +1 -1
  62. package/src/design/tokens.ts +1 -1
  63. package/src/gate/boundaries.ts +4 -1
  64. package/src/gate/measures.ts +1 -1
  65. package/src/gate/stages.ts +7 -7
  66. package/src/git/env.ts +6 -0
  67. package/src/markdown/scope.ts +5 -17
  68. package/src/pr/lift.ts +153 -19
  69. package/src/pr/painted.ts +87 -0
  70. package/src/roots/record.ts +18 -1
  71. package/src/sandbox/census.ts +1 -1
  72. package/src/sandbox/headless/run.ts +3 -2
  73. package/src/sandbox/headless/watch.ts +3 -4
  74. package/src/slides/convert/master.ts +3 -0
  75. package/src/sync/format.ts +46 -0
  76. package/src/sync/stamp.ts +6 -0
  77. package/tooling/astro/reference.md +2 -2
  78. package/tooling/base/reference.md +1 -1
  79. package/tooling/go/reference.md +1 -1
  80. package/tooling/nextjs/reference.md +1 -1
  81. package/tooling/php/reference.md +1 -1
  82. package/tooling/python/reference.md +1 -1
  83. package/tooling/vite-react/reference.md +1 -1
  84. package/tooling/web/reference.md +1 -1
  85. package/docs/agents/canvas.md +0 -86
  86. package/docs/agents/capture.md +0 -65
  87. package/docs/agents/census.md +0 -23
  88. package/docs/agents/comments.md +0 -34
  89. package/docs/agents/context-audit-checks.md +0 -108
  90. package/docs/agents/context-classify.md +0 -101
  91. package/docs/agents/counts.md +0 -74
  92. package/docs/agents/demo.md +0 -140
  93. package/docs/agents/driver.md +0 -103
  94. package/docs/agents/gate.md +0 -90
  95. package/docs/agents/hooks.md +0 -78
  96. package/docs/agents/intake.md +0 -81
  97. package/docs/agents/key-changes.md +0 -116
  98. package/docs/agents/label-coverage.md +0 -86
  99. package/docs/agents/markdown-audit-density.md +0 -39
  100. package/docs/agents/markdown-audit-weight.md +0 -83
  101. package/docs/agents/pr-evidence-layout.md +0 -70
  102. package/docs/agents/pr-evidence.md +0 -273
  103. package/docs/agents/pr-frames.md +0 -131
  104. package/docs/agents/pr-lift.md +0 -132
  105. package/docs/agents/pr-local.md +0 -111
  106. package/docs/agents/pr-preview.md +0 -155
  107. package/docs/agents/pr-reads.md +0 -111
  108. package/docs/agents/pr-tick.md +0 -72
  109. package/docs/agents/records-push.md +0 -61
  110. package/docs/agents/records-stale.md +0 -40
  111. package/docs/agents/records-validate.md +0 -57
  112. package/docs/agents/restated.md +0 -83
  113. package/docs/agents/routing.md +0 -61
  114. package/docs/agents/rule-citations.md +0 -90
  115. package/docs/agents/sandbox.md +0 -126
  116. package/docs/agents/sessions.md +0 -128
  117. package/docs/agents/skills-audit.md +0 -120
  118. package/docs/agents/skills-reach.md +0 -64
  119. package/docs/agents/slides.md +0 -176
  120. package/docs/agents/state-scoped-risk.md +0 -105
  121. package/docs/agents/superseded.md +0 -85
  122. package/docs/agents/tasks-archive.md +0 -83
  123. package/docs/agents/tasks-plans.md +0 -152
  124. package/docs/agents/tasks-validate.md +0 -133
  125. package/docs/agents/tasks.md +0 -123
  126. package/docs/agents/teach-authoring.md +0 -118
  127. package/docs/agents/teach.md +0 -157
  128. package/docs/agents/test-order.md +0 -58
  129. package/docs/agents/upstream.md +0 -94
  130. package/docs/agents/worktrees.md +0 -118
  131. package/docs/target/standards-moved.md +0 -34
@@ -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.39.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,6 +319,11 @@ 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
328
  'judgment canon/context/context-model.md states, and naming a path is evidence for it',
324
329
  'rather than the answer.',
@@ -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.',
@@ -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',
@@ -84,6 +84,7 @@ const CANON_RULES_REL = join('.claude', 'rules', 'canon')
84
84
 
85
85
  interface InstallOptions {
86
86
  readonly add?: string
87
+ readonly yes?: boolean
87
88
  }
88
89
 
89
90
  interface RegenOptions {
@@ -147,6 +148,7 @@ export function register(program: Command): void {
147
148
  .argument('[target]', 'Target directory', '.')
148
149
  .helpOption('-h, --help', 'Show this help message')
149
150
  .option('--add <rules>', 'Comma-separated rules to layer on the stack')
151
+ .option('--yes', 'Answer the install confirm without a prompt')
150
152
  .addHelpText(
151
153
  'after',
152
154
  [
@@ -156,6 +158,7 @@ export function register(program: Command): void {
156
158
  ' canon gov install node ../my-app',
157
159
  ' canon gov install node --add astro,shadcn ../my-app',
158
160
  ' canon gov install --add tasks,plan ../my-app',
161
+ ' canon gov install node --yes ../my-app',
159
162
  '',
160
163
  'With --add and no stack, the named rules install alone. A first',
161
164
  'positional naming a stack is read as the stack, so pass . or a path',
@@ -164,6 +167,10 @@ export function register(program: Command): void {
164
167
  'Copies each rule into .claude/rules/canon/, replacing a copy already',
165
168
  'there. Rules under .claude/rules/project/ are never read.',
166
169
  '',
170
+ 'The install confirm needs a TTY. --yes answers it, so a headless call',
171
+ 'installs without setting CANON_NON_INTERACTIVE=1. It does not pick a',
172
+ 'stack, so a headless call still names one.',
173
+ '',
167
174
  'Exit codes:',
168
175
  ' 0 installed, the stack held no rules, or the prompt was cancelled',
169
176
  ' 1 refused: no stack named headlessly, an unknown stack, no --add',
@@ -349,6 +356,10 @@ export function register(program: Command): void {
349
356
  ' 1 refused, with the reason on stderr or in the JSON record',
350
357
  ' 2 at least one stated figure disagrees with the tree',
351
358
  '',
359
+ 'Reasons:',
360
+ ' no-git git could not list the tree',
361
+ ' no-markdown the tree holds no markdown file',
362
+ '',
352
363
  'It reports and never gates. The false-positive rate is read off the',
353
364
  'first run rather than assumed, and a push failing on a stated figure',
354
365
  'that reads correctly to a person would teach a contributor to route',
@@ -415,6 +426,9 @@ export function register(program: Command): void {
415
426
  ' 1 refused, with the reason on stderr or in the JSON record',
416
427
  ' 2 at least one declaration asserts either',
417
428
  '',
429
+ 'Refuses an empty superseded value, a superseded value equal to its',
430
+ 'replacement, and a tree git cannot list. An empty replacement is allowed.',
431
+ '',
418
432
  'Examples:',
419
433
  ' canon gov superseded feature-feat- feature-',
420
434
  ' canon gov superseded feature-feat- feature- --json',
@@ -496,6 +510,9 @@ export function register(program: Command): void {
496
510
  ' 2 at least one cited path resolves to nothing, or one glob matches',
497
511
  ' nothing in this tree',
498
512
  '',
513
+ 'Refuses a tree holding neither rule corpus, and a `git check-ignore`',
514
+ 'read that fails.',
515
+ '',
499
516
  'Examples:',
500
517
  ' canon gov citations',
501
518
  ' canon gov citations --json',
@@ -543,6 +560,10 @@ export function register(program: Command): void {
543
560
  ' 1 refused, with the reason on stderr or in the JSON record',
544
561
  ' 2 at least one instruction is restated outside a declared mirror',
545
562
  '',
563
+ 'Reasons:',
564
+ ' no-instructions the tree holds no always-loaded file or seed to read',
565
+ ' no-surfaces the tree holds no skills tree or rules tree to match against',
566
+ '',
546
567
  'Examples:',
547
568
  ' canon gov restated',
548
569
  ' canon gov restated --json',
@@ -1268,8 +1289,9 @@ async function runInstall(
1268
1289
  intro('canon gov install')
1269
1290
 
1270
1291
  const add = opts.add ?? ''
1292
+ const yes = opts.yes === true
1271
1293
  if (add !== '' && isAddWithoutStack(stack, target)) {
1272
- return installExtras(add, stack ?? target)
1294
+ return installExtras(add, stack ?? target, yes)
1273
1295
  }
1274
1296
 
1275
1297
  const resolved = resolveTarget(target, PROJECT_ROOT)
@@ -1314,14 +1336,7 @@ async function runInstall(
1314
1336
  for (const rule of missing) logWarn(`${rule} (source not found, skipping)`)
1315
1337
  for (const source of found) logInfo(source.rule)
1316
1338
 
1317
- const shouldInstall = await select({
1318
- message: `Install ${found.length} rules to ${displayPath(target, CANON_RULES_REL)}?`,
1319
- options: [
1320
- { value: true, label: 'Yes' },
1321
- { value: false, label: 'No' },
1322
- ],
1323
- nonInteractiveDefault: true,
1324
- })
1339
+ const shouldInstall = await confirmInstall(found.length, target, yes)
1325
1340
 
1326
1341
  if (!shouldInstall) {
1327
1342
  logWarn('Cancelled')
@@ -1347,6 +1362,28 @@ async function runInstall(
1347
1362
  return 0
1348
1363
  }
1349
1364
 
1365
+ /**
1366
+ * The install confirm. `--yes` answers it without a prompt, so the line it
1367
+ * would have printed is skipped rather than duplicated under
1368
+ * `CANON_NON_INTERACTIVE=1`.
1369
+ */
1370
+ async function confirmInstall(
1371
+ count: number,
1372
+ target: string,
1373
+ yes: boolean,
1374
+ ): Promise<boolean> {
1375
+ if (yes) return true
1376
+ return select({
1377
+ message: `Install ${count} rules to ${displayPath(target, CANON_RULES_REL)}?`,
1378
+ options: [
1379
+ { value: true, label: 'Yes' },
1380
+ { value: false, label: 'No' },
1381
+ ],
1382
+ nonInteractiveDefault: true,
1383
+ ttyHint: 'Pass --yes',
1384
+ })
1385
+ }
1386
+
1350
1387
  /**
1351
1388
  * Whether `--add` runs with no stack under it. Commander fills the first
1352
1389
  * positional before the second, so `--add a,b ../my-app` arrives with the
@@ -1366,7 +1403,11 @@ function isAddWithoutStack(stack: string | undefined, target: string): boolean {
1366
1403
  * extras are not a stack, and overwriting the chain would cost a target the
1367
1404
  * missing-rule report its stack install recorded.
1368
1405
  */
1369
- async function installExtras(add: string, target: string): Promise<number> {
1406
+ async function installExtras(
1407
+ add: string,
1408
+ target: string,
1409
+ yes: boolean,
1410
+ ): Promise<number> {
1370
1411
  const resolved = resolveTarget(target, PROJECT_ROOT)
1371
1412
  if (typeof resolved === 'number') return resolved
1372
1413
 
@@ -1382,14 +1423,7 @@ async function installExtras(add: string, target: string): Promise<number> {
1382
1423
  return 1
1383
1424
  }
1384
1425
 
1385
- const shouldInstall = await select({
1386
- message: `Install ${found.length} rules to ${displayPath(target, CANON_RULES_REL)}?`,
1387
- options: [
1388
- { value: true, label: 'Yes' },
1389
- { value: false, label: 'No' },
1390
- ],
1391
- nonInteractiveDefault: true,
1392
- })
1426
+ const shouldInstall = await confirmInstall(found.length, target, yes)
1393
1427
 
1394
1428
  if (!shouldInstall) {
1395
1429
  logWarn('Cancelled')
@@ -64,6 +64,12 @@ export function register(program: Command): void {
64
64
  .addHelpText(
65
65
  'after',
66
66
  [
67
+ '',
68
+ 'Refusals, read off `reason`:',
69
+ ' no-intake the project holds no intake folder',
70
+ ' no-folder no folder matches the slug',
71
+ ' ambiguous-slug a bare slug matches more than one ordinal-prefixed',
72
+ ' folder, all named in `detail`',
67
73
  '',
68
74
  'Exit codes:',
69
75
  ' 0 the folders or items were listed',
@@ -105,6 +111,17 @@ export function register(program: Command): void {
105
111
  .addHelpText(
106
112
  'after',
107
113
  [
114
+ '',
115
+ 'Refusals, read off `reason`:',
116
+ ' no-intake the project holds no intake folder',
117
+ ' no-folder no folder matches the slug',
118
+ ' ambiguous-slug a bare slug matches more than one ordinal-prefixed',
119
+ ' folder, all named in `detail`',
120
+ ' no-cluster the cluster file named does not exist',
121
+ ' no-item the item number named does not exist in the cluster',
122
+ ' answered the item already carries an answer',
123
+ ' bad-input a malformed command line, such as no selection, an empty',
124
+ ' answer, or two answers for one item',
108
125
  '',
109
126
  'Exit codes:',
110
127
  ' 0 every named item now carries its answer',
@@ -111,6 +111,10 @@ export function register(program: Command): void {
111
111
  ' 1 refused, with the reason on stderr or in the JSON record',
112
112
  ' 2 at least one changed path is reached by no row',
113
113
  '',
114
+ 'Reasons:',
115
+ ' no-map the project declares no label map, so it labels nothing. An',
116
+ ' expected absence for `canon audits run`, not a fault',
117
+ '',
114
118
  'Examples:',
115
119
  ' canon labels audit',
116
120
  ' canon labels audit --json',
@@ -130,7 +134,10 @@ export function register(program: Command): void {
130
134
  )
131
135
  .argument('<number>', "This branch's open pull request")
132
136
  .helpOption('-h, --help', 'Show this help message')
133
- .option('--base <ref>', 'Far side of the range, defaulting to the trunk')
137
+ .option(
138
+ '--base <ref>',
139
+ "Read the git range back to this ref instead of the pull request's files",
140
+ )
134
141
  .option('--root <path>', 'Repository to read, defaulting to the cwd')
135
142
  .option('--check', 'Report the labels owed and write nothing')
136
143
  .option('--json', 'Add a machine-readable record on stdout')
@@ -138,9 +145,10 @@ export function register(program: Command): void {
138
145
  'after',
139
146
  [
140
147
  '',
141
- 'Resolves the earned set the way `canon labels audit` does, binds to the',
142
- 'pull request only when its head is the current branch and it is open,',
143
- 'and adds each earned label the pull request lacks over REST.',
148
+ "Resolves the earned set from the pull request's own files listing, or",
149
+ 'from the git range when --base is passed, binds to the pull request only',
150
+ 'when its head is the current branch and it is open, and adds each earned',
151
+ 'label the pull request lacks over REST.',
144
152
  '',
145
153
  'It adds and never removes, so a label a person applied by hand stays.',
146
154
  'A label the remote does not carry is reported under missingOnRemote and',
@@ -155,6 +163,12 @@ export function register(program: Command): void {
155
163
  ' 2 labels are still owed, being any under --check, or a label the',
156
164
  ' remote lacks or a failed add otherwise',
157
165
  '',
166
+ 'Reasons:',
167
+ ' head-mismatch the pull request is not open on the current branch, under',
168
+ ' --check as well',
169
+ ' gh-failed the files listing or the labels could not be read, and the',
170
+ ' run does not fall back to git',
171
+ '',
158
172
  'Examples:',
159
173
  ' canon labels apply 123 --json',
160
174
  ' canon labels apply 123 --check',
@@ -396,7 +410,26 @@ async function runApply(number: string, opts: ApplyOptions): Promise<number> {
396
410
  if (target === null) return refuse('gh-failed')
397
411
  if (target.trim() !== `${head} open`) return refuse('head-mismatch')
398
412
 
399
- const report = await auditLabels(root, { base: opts.base })
413
+ // An explicit --base is an explicit request for the git route. Otherwise the
414
+ // pull request's own file listing is the diff the labels describe, and a
415
+ // failed read refuses rather than falling back, since binding needed gh too.
416
+ let paths: string[] | undefined
417
+ if (opts.base === undefined) {
418
+ const listing = await gh(root, [
419
+ 'api',
420
+ '--paginate',
421
+ `repos/{owner}/{repo}/pulls/${number}/files`,
422
+ '--jq',
423
+ '.[].filename',
424
+ ])
425
+ if (listing === null) return refuse('gh-failed')
426
+ paths = nonEmptyLines(listing)
427
+ }
428
+
429
+ const report = await auditLabels(root, {
430
+ base: opts.base,
431
+ ...(paths !== undefined && { paths }),
432
+ })
400
433
 
401
434
  // A project declaring no map earns nothing and is labelled silently, which
402
435
  // is an answer and exits clean rather than reading as a break.