@erclx/canon 5.39.0 → 5.41.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 (162) hide show
  1. package/README.md +39 -22
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
  4. package/claude/skills/auto-ship/SKILL.md +6 -0
  5. package/claude/skills/auto-ship/references/fan-in.md +59 -0
  6. package/claude/skills/canvas-board/SKILL.md +1 -1
  7. package/claude/skills/create-skill/references/skill.md +1 -1
  8. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
  9. package/claude/skills/decision-escalate/SKILL.md +1 -2
  10. package/claude/skills/draft-slides/SKILL.md +2 -3
  11. package/claude/skills/draft-slides/references/deck.md +183 -0
  12. package/claude/skills/git-branch/REQUIREMENT.md +17 -6
  13. package/claude/skills/git-branch/SKILL.md +63 -7
  14. package/claude/skills/git-pr/references/evidence.md +2 -2
  15. package/claude/skills/git-pr/references/final-command.md +1 -1
  16. package/claude/skills/git-pr/references/labels.md +1 -1
  17. package/claude/skills/git-pr/references/publish.md +1 -1
  18. package/claude/skills/plan-feature/references/batches.md +48 -1
  19. package/claude/skills/plan-feature/references/plan.md +2 -0
  20. package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
  21. package/claude/skills/plan-intake-answer/SKILL.md +1 -2
  22. package/claude/skills/review-address/references/rebase-conflicts.md +7 -0
  23. package/claude/skills/review-ui/SKILL.md +3 -0
  24. package/claude/skills/review-ui/references/close-out.md +47 -0
  25. package/claude/skills/review-ui/references/post.md +3 -1
  26. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -0
  27. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
  28. package/claude/skills/role-worker/REQUIREMENT.md +1 -0
  29. package/claude/skills/role-worker/SKILL.md +9 -0
  30. package/claude/skills/task-board/references/board.md +1 -1
  31. package/claude/skills/task-board/references/tasks.md +1 -1
  32. package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
  33. package/claude/skills/teach-workspace/SKILL.md +1 -2
  34. package/claude/skills/ux-measure/SKILL.md +1 -1
  35. package/docs/agents/audits.md +61 -60
  36. package/docs/agents/context-audit.md +40 -58
  37. package/docs/agents/index.md +5 -51
  38. package/docs/agents/install-and-sync.md +1 -0
  39. package/docs/agents/markdown-audit.md +53 -99
  40. package/docs/agents/records.md +37 -79
  41. package/docs/agents/sessions-transfer.md +26 -70
  42. package/docs/target/index.md +0 -1
  43. package/docs/target/migrations.md +57 -66
  44. package/docs/target/projects.md +36 -78
  45. package/docs/target/scenarios.md +9 -19
  46. package/docs/target/sync.md +27 -68
  47. package/docs/workflow/ai-workflow.md +22 -68
  48. package/docs/workflow/operating-model.md +32 -186
  49. package/docs/workflow/parallel-features.md +30 -66
  50. package/docs/workflow/skill-map-on-request.md +22 -24
  51. package/docs/workflow/skill-map.md +72 -76
  52. package/docs/workflow/visual-design-workflow.md +25 -118
  53. package/docs/workflow/zshrc-aliases.md +18 -37
  54. package/package.json +1 -1
  55. package/src/canvas/client/inspector/fields.css +4 -0
  56. package/src/canvas/index.html +6 -0
  57. package/src/claude/cases/git.ts +4 -0
  58. package/src/claude/skills-reach.ts +2 -2
  59. package/src/cli/ui.ts +3 -1
  60. package/src/commands/canvas.ts +23 -0
  61. package/src/commands/capture.ts +14 -0
  62. package/src/commands/claude.ts +13 -0
  63. package/src/commands/demo.ts +22 -0
  64. package/src/commands/deps.ts +6 -0
  65. package/src/commands/driver.ts +28 -2
  66. package/src/commands/gov.ts +52 -18
  67. package/src/commands/intake.ts +17 -0
  68. package/src/commands/labels.ts +38 -5
  69. package/src/commands/pr.ts +192 -52
  70. package/src/commands/records.ts +19 -3
  71. package/src/commands/secrets.ts +6 -0
  72. package/src/commands/sessions.ts +8 -0
  73. package/src/commands/tasks.ts +52 -0
  74. package/src/commands/teach.ts +13 -0
  75. package/src/commands/upstream.ts +25 -1
  76. package/src/commands/worktrees.ts +5 -0
  77. package/src/context/audit.ts +22 -4
  78. package/src/context/folders.ts +1 -1
  79. package/src/context/shape-gated.ts +5 -2
  80. package/src/counts/scan.ts +1 -1
  81. package/src/design/components.ts +1 -1
  82. package/src/design/document.ts +1 -1
  83. package/src/design/tokens.ts +1 -1
  84. package/src/gate/boundaries.ts +4 -1
  85. package/src/gate/measures.ts +1 -1
  86. package/src/gate/stages.ts +7 -7
  87. package/src/git/env.ts +6 -0
  88. package/src/markdown/scope.ts +11 -19
  89. package/src/pr/frames.ts +53 -9
  90. package/src/pr/lift.ts +153 -19
  91. package/src/pr/painted.ts +87 -0
  92. package/src/records/validate.ts +1 -0
  93. package/src/roots/record.ts +18 -1
  94. package/src/sandbox/census.ts +1 -1
  95. package/src/sandbox/headless/run.ts +3 -2
  96. package/src/sandbox/headless/watch.ts +3 -4
  97. package/src/slides/convert/master.ts +3 -0
  98. package/src/sync/format.ts +46 -0
  99. package/src/sync/stamp.ts +6 -0
  100. package/src/tasks/answers.ts +79 -2
  101. package/src/tasks/fan-in.ts +176 -0
  102. package/src/tasks/stack.ts +1 -1
  103. package/tooling/astro/reference.md +80 -39
  104. package/tooling/base/reference.md +63 -32
  105. package/tooling/base/seeds/canon/context/ci.md +7 -15
  106. package/tooling/base/seeds/canon/context/development.md +14 -14
  107. package/tooling/claude/reference.md +40 -22
  108. package/tooling/cloudflare/reference.md +31 -12
  109. package/tooling/go/reference.md +47 -23
  110. package/tooling/nextjs/reference.md +52 -19
  111. package/tooling/php/reference.md +35 -16
  112. package/tooling/python/reference.md +47 -30
  113. package/tooling/shell/reference.md +6 -5
  114. package/tooling/vite-react/reference.md +49 -21
  115. package/tooling/web/reference.md +131 -71
  116. package/docs/agents/canvas.md +0 -86
  117. package/docs/agents/capture.md +0 -65
  118. package/docs/agents/census.md +0 -23
  119. package/docs/agents/comments.md +0 -34
  120. package/docs/agents/context-audit-checks.md +0 -108
  121. package/docs/agents/context-classify.md +0 -101
  122. package/docs/agents/counts.md +0 -74
  123. package/docs/agents/demo.md +0 -140
  124. package/docs/agents/driver.md +0 -103
  125. package/docs/agents/gate.md +0 -90
  126. package/docs/agents/hooks.md +0 -78
  127. package/docs/agents/intake.md +0 -81
  128. package/docs/agents/key-changes.md +0 -116
  129. package/docs/agents/label-coverage.md +0 -86
  130. package/docs/agents/markdown-audit-density.md +0 -39
  131. package/docs/agents/markdown-audit-weight.md +0 -83
  132. package/docs/agents/pr-evidence-layout.md +0 -70
  133. package/docs/agents/pr-evidence.md +0 -273
  134. package/docs/agents/pr-frames.md +0 -131
  135. package/docs/agents/pr-lift.md +0 -132
  136. package/docs/agents/pr-local.md +0 -111
  137. package/docs/agents/pr-preview.md +0 -155
  138. package/docs/agents/pr-reads.md +0 -111
  139. package/docs/agents/pr-tick.md +0 -72
  140. package/docs/agents/records-push.md +0 -61
  141. package/docs/agents/records-stale.md +0 -40
  142. package/docs/agents/records-validate.md +0 -57
  143. package/docs/agents/restated.md +0 -83
  144. package/docs/agents/routing.md +0 -61
  145. package/docs/agents/rule-citations.md +0 -90
  146. package/docs/agents/sandbox.md +0 -126
  147. package/docs/agents/sessions.md +0 -128
  148. package/docs/agents/skills-audit.md +0 -120
  149. package/docs/agents/skills-reach.md +0 -64
  150. package/docs/agents/slides.md +0 -176
  151. package/docs/agents/state-scoped-risk.md +0 -105
  152. package/docs/agents/superseded.md +0 -85
  153. package/docs/agents/tasks-archive.md +0 -83
  154. package/docs/agents/tasks-plans.md +0 -152
  155. package/docs/agents/tasks-validate.md +0 -133
  156. package/docs/agents/tasks.md +0 -123
  157. package/docs/agents/teach-authoring.md +0 -118
  158. package/docs/agents/teach.md +0 -157
  159. package/docs/agents/test-order.md +0 -58
  160. package/docs/agents/upstream.md +0 -94
  161. package/docs/agents/worktrees.md +0 -118
  162. package/docs/target/standards-moved.md +0 -34
@@ -1,103 +0,0 @@
1
- ---
2
- title: Driver
3
- description: Walking a page through named interactions, the probe catalog and the false finding each one carries, why viewport heights are never defaulted, and what each refusal reports
4
- ---
5
-
6
- # Driver
7
-
8
- `canon drive <url> <run>` walks a page through a sequence of interactions and measures every state it reaches. A render answers about a page as it loads, so a defect that exists only after a menu opens, an answer is chosen, or the page scrolls is invisible to one. That second axis is what this adds.
9
-
10
- ```bash
11
- canon drive http://localhost:4173 run.json
12
- canon drive http://localhost:4173 run.json --json
13
- ```
14
-
15
- | Option | Behavior |
16
- | -------- | --------------------------------------- |
17
- | `--json` | Add a machine-readable record on stdout |
18
-
19
- ## The run file
20
-
21
- The run is a JSON file rather than a key in a project config, because a route catalog is state a project holds and an interaction sequence is a script written for one question. It carries the viewport, the probes to run by default, and the steps.
22
-
23
- ```json
24
- {
25
- "viewport": { "width": 1440, "heights": [900, 1200, 1500] },
26
- "probes": ["focus", "details"],
27
- "steps": [
28
- { "name": "on load", "kind": "wait", "ms": 0 },
29
- { "name": "open the menu", "kind": "click", "target": "#menu summary" },
30
- {
31
- "name": "reach the diagram",
32
- "kind": "scroll",
33
- "target": "figure",
34
- "probes": ["diagram-geometry", "diagram-strokes"]
35
- }
36
- ]
37
- }
38
- ```
39
-
40
- Every step names what it did, and that name is carried onto each finding it produced, alongside the viewport. A step naming its own `probes` overrides the run-level list for that step alone. Omitting `probes` at the run level runs all four.
41
-
42
- | Step kind | Fields | What it does |
43
- | --------- | ------------------ | ---------------------------------------------- |
44
- | `click` | `target` | Presses the first element the selector matches |
45
- | `scroll` | `target` | Brings the first match into view |
46
- | `fill` | `target`, `text` | Types into the first match |
47
- | `tab` | `count` (optional) | Advances keyboard focus, once by default |
48
- | `wait` | `ms` | Holds, for a state the page reaches alone |
49
-
50
- Probes run after a step and never on arrival, so a run reaches the load state by opening with a `wait` step of its own, as the example above does. Nothing probes before the first step runs, which makes that leading step the only way to measure the page as it first painted, and naming it is what puts the load state on its own findings rather than under whatever ran next.
51
-
52
- Write one wherever no capture runs against the page. `canon capture` renders a single state from a source on disk or a URL, so where it runs it already answers about arrival and a leading `wait` duplicates it. Where it does not, this command is the only thing measuring the page at all, and a run without that step reports every driven state and nothing about the one a visitor sees first.
53
-
54
- Each height is driven in a context of its own from a fresh navigation, rather than by resizing the page the previous height already drove, since a sweep asks the same question of each height rather than a later question of an already-driven page.
55
-
56
- ## The probes
57
-
58
- Four probes, a fixed catalog rather than a subject system a project extends. Each carries a class of false finding that a throwaway version of this command produced before it produced true ones, and a probe a project authored for itself would re-pay those.
59
-
60
- | Probe | What it reports |
61
- | ------------------ | ------------------------------------------------------------------------------------------ |
62
- | `focus` | An element that takes keyboard focus and changes nothing visible |
63
- | `details` | A `details` panel off the viewport, or a row under the tap minimum, both read open |
64
- | `diagram-geometry` | An SVG label outside its frame, covered by a filled shape, or colliding with another label |
65
- | `diagram-strokes` | An SVG label a stroke crosses, sampled along the stroke's own geometry |
66
-
67
- `focus` presses Tab before it reads anything. `:focus-visible` does not match a scripted `.focus()` once the page has taken a pointer interaction, and a driver clicks by definition, so a reader without that press reports every correctly styled element as unstyled. It also ignores an outline width, color, or offset moving underneath an outline that resolves to `none`, which paints nothing.
68
-
69
- `details` discovers every `details` on the page rather than taking a selector, measures each shut and again open, and judges only the open reading. A shut menu still reports a layout box and the box describes the trigger rather than the panel, so the shut numbers travel in the record as context. Discovery is automatic because the one real menu defect in the originating run was missed by a hand-picked probe: nobody thought to name that menu.
70
-
71
- Both diagram probes read after `document.fonts.ready` and never compute from the markup. A generated page is routinely authored against one font and restyled to another, so a label that cleared a line at its authored width can overlap it once rendered. Every rect either probe compares is inset vertically and never horizontally, since a text rect is loose above and below the glyphs and tight to them left and right, and a horizontal inset passes a real overrun.
72
-
73
- `diagram-strokes` is separate from `diagram-geometry` because a bounding box cannot answer for a stroke. A probe comparing filled boxes filters out every `line` and every `fill="none"` panel border before it compares, and a diagonal connector's box covers most of a diagram while the stroke itself touches almost none of it. Both paint orders are findings: a stroke drawn after a label crosses the glyphs out, and one drawn before it shows through them, since SVG text carries no plate of its own.
74
-
75
- ## Viewport heights are never defaulted
76
-
77
- A run with no `viewport.heights` refuses. The heights that separate a passing render from a failing one belong to the layout being driven, and the only evidence behind any default this could ship is one fixture's failure range, too narrow a sample to hand every later caller a number they never chose. A scroll rail that skipped its middle sections passed at 900 and failed at 1200 and 1500, so one height reports clean over a live defect.
78
-
79
- ## What it reports
80
-
81
- It reports findings and never gates, and the exit code says only whether the drive completed. Every probe here carries a class of false finding already paid for once, so a command ending a build on its own reading makes a claim this catalog has not earned. A caller that wants a verdict reads `findings` off the `--json` record and decides for itself.
82
-
83
- | Reason | What it means |
84
- | -------------------- | ------------------------------------------------------ |
85
- | `no-run-file` | Nothing readable at the path given |
86
- | `unreadable-plan` | The run file is not valid JSON |
87
- | `no-steps` | The run names no step |
88
- | `no-probes` | The run names an empty probe list |
89
- | `unknown-probe` | The run names a probe this build does not ship |
90
- | `bad-step` | A step is missing what the driver needs to perform it |
91
- | `no-viewport` | The run declares no viewport |
92
- | `no-width` | The viewport has no width to wrap at |
93
- | `no-heights` | The viewport names no height, which is never defaulted |
94
- | `engine-missing` | The browser engine is not installed in this project |
95
- | `browser-missing` | The engine is installed and its browser binary is not |
96
- | `server-unreachable` | Nothing answered at the URL, so no state was reached |
97
- | `drive-failed` | The drive failed against a reachable page |
98
-
99
- An unreachable page refuses rather than returning an empty report, since nothing measured and nothing found read the same to anything counting findings.
100
-
101
- The browser binary installs separately from the package, once, with `bunx playwright install chromium`. The command ships to targets like `demo`, `inventory`, and `capture`, because its whole purpose is measuring a page inside someone else's project. It reaches nothing under `src/capture/`, so the two surfaces move independently.
102
-
103
- See `canon --help` for where this sits among the browser commands, and `capture.md` for the single-state render it adds an axis to.
@@ -1,90 +0,0 @@
1
- ---
2
- title: Merge gate
3
- description: Running the gate this repository verifies a branch with, what the stage table holds and what stays a script, how the changed set scopes three stages, and why a stage that cannot read its input reports rather than passing
4
- ---
5
-
6
- # Merge gate
7
-
8
- `canon gate run` runs every stage that guards a branch here, in order, stopping at the first stage that finds a fact. `bun run check` and `bun run check:ci` resolve to it, and the `update` package script runs `bun run check` after a dependency update.
9
-
10
- ```bash
11
- canon gate run
12
- canon gate run --all
13
- canon gate run --json
14
- ```
15
-
16
- | Option | Behavior |
17
- | ---------- | ----------------------------------------------------------------------------- |
18
- | `--all` | Run every stage instead of scoping shell, types, and tests to the changed set |
19
- | `--nested` | Suppress the outer frame when a calling script has already opened one |
20
- | `--json` | Add a machine-readable record on stdout |
21
-
22
- ## What the command owns and what it runs
23
-
24
- Three things sit in the command: the stage table in `src/gate/stages.ts`, the changed-file scoping and the run loop in `src/gate/sequencer.ts`, and every threshold comparison in `src/gate/measures.ts`. Each individual check is the script or the verb it already was, under `scripts/core/` or behind another `canon` command, and the move changed none of them. Sequencing, scoping, and comparison are where the recurring defects were, and a check whose behavior changed while its sequencing moved would make any regression impossible to attribute.
25
-
26
- A stage is a list of checks and a check is one of four kinds:
27
-
28
- | Kind | What it is |
29
- | --------- | ------------------------------------------------------------------------------- |
30
- | `command` | Any binary, run from the project root |
31
- | `cli` | This checkout's own `src/cli.ts`, never a globally installed `canon` |
32
- | `drift` | A regenerated pathspec asserted against the index and against the untracked set |
33
- | `measure` | A reading whose verdict is a comparison rather than an exit code |
34
-
35
- A `cli` check runs the source rather than the binary because a globally installed `canon` resolves to the main checkout no matter which worktree is running, so a gate reading through it would measure the wrong tree and pass a branch it never opened.
36
-
37
- Every check is an argument vector rather than a shell line, so no stage carries a quoting hazard and a `drift` pathspec reaches git exactly as the table spells it.
38
-
39
- ## Scoping
40
-
41
- Shell, types, and tests read the changed set. Everything else always runs, because its input is diffuse enough that no path predicts it.
42
-
43
- The changed set unions the branch diff against the merge base with `origin/main`, the working tree, and untracked files, which is what a pull request will contain. The baseline is the remote ref and not local `main`, since on `main` itself the local ref is HEAD and every unpushed commit would drop out. Every fallback widens rather than narrows: no merge base at all runs every stage, and a local baseline equal to HEAD does the same. `--all` turns scoping off outright, which is what `bun run check:ci` passes so CI stays the backstop for a wrong local scoping decision.
44
-
45
- ## A stage that cannot read its input
46
-
47
- A stage reports one of four states. It passed, it was scoped out, it found a fact, or it could not measure its input at all.
48
-
49
- The fourth is the one worth naming. An absent tool, a catalog that did not report, a corpus with nothing under it: each used to print a line that read like a pass. Now the run records it as unmeasured, the closing line says how many stages measured nothing, and the reader is not told a verdict nobody took.
50
-
51
- What happens next depends on where the run is. On a contributor's machine it warns and the run still exits 0, because an absent tool there is somebody mid-setup. Under CI, read off `CI=true`, it refuses, because the same absence on a runner is a broken workflow step.
52
-
53
- ## Exit codes
54
-
55
- | Code | Meaning |
56
- | ---- | ------------------------------------------------------------- |
57
- | `0` | every stage that ran reported, and none found a fact |
58
- | `1` | a stage found a fact, or could not measure its input under CI |
59
-
60
- One code for a failure, which is what a `bun run` caller and a git hook both read. An unmeasured stage takes no code of its own, since it has already refused under CI and reports on a contributor's machine, so a second code would name a state no caller branches on.
61
-
62
- ## The record
63
-
64
- `--json` puts one record on stdout and keeps every diagnostic on stderr:
65
-
66
- ```json
67
- {
68
- "ok": true,
69
- "root": "/path/to/checkout",
70
- "scoped": true,
71
- "changed": 12,
72
- "summary": {
73
- "ran": 23,
74
- "passed": 22,
75
- "skipped": 1,
76
- "unmeasured": 0,
77
- "failed": 0
78
- },
79
- "ms": 44453,
80
- "stages": [
81
- { "id": "indexes", "label": "Indexes", "status": "passed", "ms": 210 }
82
- ]
83
- }
84
- ```
85
-
86
- A failing stage carries its remedy in `failure`, and the same line goes to stderr so a caller reading neither the record nor the frame is still told what to fix.
87
-
88
- `ms` carries the wall time each stage took, including every process spawn, and the top-level `ms` sums them. Neither reading changes what a stage measures. Both exist so a slow run is attributable to a stage rather than read off a stopwatch held against the whole thing.
89
-
90
- The Tests stage holds a machine lock so one box runs one suite at a time, and its `ms` includes any time it queued behind another worktree's suite. A stage that queued carries `queuedMs`, the part of `ms` spent waiting, which is how to separate a queue from a slow suite. The field is absent on a stage that never waited. The wait is also announced on stderr when it starts, naming the holder's pid and worktree, in every mode.
@@ -1,78 +0,0 @@
1
- ---
2
- title: Hooks
3
- description: Running the post-merge steps through one verb, the order and the child call behind each, the root and working directory each step reads, the skip switches, the line each record maps to, and the reply link check
4
- ---
5
-
6
- # Hooks
7
-
8
- `canon hooks post-merge` runs the steps a git `post-merge` hook drives, so the hook itself stays a guard plus one call. It reads which pull requests the merge brought in, runs each step as a child `canon` call with `--json`, and prints one line per outcome worth reporting.
9
-
10
- ```bash
11
- canon hooks post-merge
12
- canon hooks post-merge --root "$root"
13
- ```
14
-
15
- | Option | Behavior |
16
- | --------------- | ---------------------------------------------------------------------------------------------------- |
17
- | `--root <path>` | Main worktree root holding the board and the records, resolved from `git worktree list` when omitted |
18
-
19
- The base stack ships the same call as its `.husky/post-merge`, behind the same board guard and upgrade bootstrap, so a target that syncs it runs these five steps.
20
-
21
- Exit codes: `0` on every path, since a hook failing aborts nothing useful. Lines go to stderr, and stdout stays empty. An ordinary merge prints nothing.
22
-
23
- ## The steps
24
-
25
- The steps run in this order, and the verb's `--help` lists the child calls from the same table it runs them from.
26
-
27
- | Step | Child call | Runs from |
28
- | ------------------------ | ------------------------------------------------------------- | ---------------- |
29
- | Archive the merged task | `canon tasks archive --pull-request <n> --root <root> --json` | the caller's cwd |
30
- | Push the records | `canon records push --root <root> --json` | the caller's cwd |
31
- | Reclaim merged worktrees | `canon worktrees reclaim --json` | the caller's cwd |
32
- | Reinstall the CLI | `canon upgrade --json`, under `CANON_NON_INTERACTIVE=1` | the caller's cwd |
33
- | Update the plugin cache | `canon claude plugin-update --json` | the caller's cwd |
34
-
35
- The archive runs once per pull request number, read as the trailing `(#NNN)` on every subject in `ORIG_HEAD..HEAD`, or on the tip alone when the merge left no `ORIG_HEAD`.
36
-
37
- The reclaim takes no `--root`. It refuses the worktree its own working directory names, so a root would let a pull inside a linked worktree remove the worktree it stands in. The archive and the push take the root because the board and the records live at the main worktree root.
38
-
39
- Each step is a separate process rather than an in-process call, so the plugin update runs the binary the upgrade just installed. Every child, and every `git` read, runs with the repository variables a hook exports stripped, since `GIT_DIR` beats `-C`.
40
-
41
- ## Reply links
42
-
43
- `canon hooks reply-links` checks that every markdown link in a reply names a file on disk. The `Stop` hook `reply-links.sh` runs it on the closing message of a turn, so a dead link is corrected in the same turn.
44
-
45
- ```bash
46
- printf '%s' "$reply" | canon hooks reply-links
47
- canon hooks reply-links --json < reply.md
48
- ```
49
-
50
- | Option | Behavior |
51
- | --------------- | ------------------------------------------------------------------------------------------------- |
52
- | `--root <path>` | Root a relative link resolves against, the main worktree root from `git worktree list` if omitted |
53
- | `--json` | Write `{ ok, missing, message }` to stdout. `message` is present only when `ok` is false |
54
-
55
- The reply comes on stdin, and a terminal on stdin is refused. A relative link resolves against the root, an absolute one is checked as written. The check skips placeholders such as `<path>`, globs, URLs, anchors, and any link inside a code span or fence. A `:42`, `:42-50`, or `#L42` tail is stripped before the check.
56
-
57
- Each entry in `missing` carries the `destination` as written and the `path` that was checked. A link into `.claude/worktrees/<name>/.canon/` also carries a `suggestion` with the main-root path when that file exists there.
58
-
59
- Exit codes: `0` when every link resolves or the reply holds none, `1` when at least one does not. The missing list goes to stderr. The hook turns a finding into exit 2 so the session continues, and exits 1 without blocking when it could not run the check, such as with no `jq` or no `canon` on `PATH`.
60
-
61
- ## Skip switches
62
-
63
- | Variable | Skips |
64
- | ---------------------------- | ----------------------- |
65
- | `CANON_SKIP_RECLAIM=1` | the worktree reclaim |
66
- | `CANON_SKIP_UPGRADE=1` | the CLI reinstall |
67
- | `CANON_SKIP_PLUGIN_UPDATE=1` | the plugin cache update |
68
-
69
- Any non-empty value skips the step, and each switch skips its own step only.
70
-
71
- ## What prints
72
-
73
- Each line is decided from the child's parsed record. A child producing no parseable record is an older binary carrying no such subcommand, and that step stays quiet.
74
-
75
- - Archive: the archived task and its number, plus a `Links not repointed in:` line naming the files in a non-empty `relinkFailed`, or the refusal's `message` with a pointer to the task board. A record carrying no `relinkFailed` reads as empty. `no-match` and `no-board` stay quiet.
76
- - Records push: the count of changed paths when above zero, or the refusal reason. `no-repository` stays quiet.
77
- - Reclaim: the removal count and the failure count, both read from the fields whatever the exit, or an unreadable reason. `gh-missing` stays quiet.
78
- - Upgrade and plugin update: the record's `message` on any state but `current`, or on a failure. The plugin update stays quiet on `no-claude` and `no-plugin`.
@@ -1,81 +0,0 @@
1
- ---
2
- title: Intake
3
- description: Reading intake folder counts and items, the three read states an item can be in, landing a batch of selections in one cluster, the refusal reasons, and why a call is scoped to one file
4
- ---
5
-
6
- # Intake
7
-
8
- ## List
9
-
10
- `canon intake list` reports the intake folders under `.canon/intake/`, or the items one folder holds. It reads and never writes, because an answer belongs to the operator and a verb that filled one would decide what the folder exists to ask.
11
-
12
- A folder carries a two-digit ordinal ahead of its slug, as in `21-toolkit-overview`, so a listing sorts by when each opened. A bare slug still resolves: passing `toolkit-overview` matches the one folder whose name is an ordinal ahead of it, and the folder's real name is what every command reports back.
13
-
14
- ```bash
15
- canon intake list
16
- canon intake list toolkit-overview --unread --json
17
- ```
18
-
19
- | Option | Behavior |
20
- | --------------- | ------------------------------------------------------------------------- |
21
- | `--unread` | Keep only what is unread: folders carrying one, or a folder's empty slots |
22
- | `--json` | Emit a machine-readable record on stdout |
23
- | `--root <path>` | Intake root, defaulting to the main worktree |
24
-
25
- With no slug it reports per-folder counts. With one it reports every item grouped by the cluster file holding it, each carrying its label, title, open question, suggestion, and answer.
26
-
27
- An item sits in one of three read states, and the counts keep them apart:
28
-
29
- | State | What it means |
30
- | --------- | ------------------------------------------------------- |
31
- | unread | The slot is present and empty, so nobody has reached it |
32
- | answered | The slot carries text, which is a decision already made |
33
- | malformed | The item carries no slot, so no verb here can answer it |
34
-
35
- The third is counted apart from both rather than folded into either. Counted as answered it hides a file that needs fixing behind a folder reading as fully worked through, and counted as unread it joins a list whose every entry `answer` then refuses.
36
-
37
- The index is skipped, since it points at items and answers nothing itself. So is any item a file displays inside a fence, which is how a folder copying the item format into its own overview is read as the sample it is rather than as an item offering a slot no reader owns.
38
-
39
- ## Answer
40
-
41
- `canon intake answer` writes selections into the answer slots of one cluster file.
42
-
43
- ```bash
44
- canon intake answer toolkit-overview --cluster 05-coverage.md --set 3=ok
45
- canon intake answer toolkit-overview --cluster 11-intake-skill.md --set 3d=ok --set 9="not worth it" --json
46
- ```
47
-
48
- | Option | Behavior |
49
- | --------------------- | ----------------------------------------------------- |
50
- | `--cluster <file>` | Cluster file the items live in, with or without `.md` |
51
- | `--set <item=answer>` | Answer to land on an item, repeatable |
52
- | `--json` | Emit a machine-readable record on stdout |
53
- | `--root <path>` | Intake root, defaulting to the main worktree |
54
-
55
- Items are labeled per cluster file, so a selection names the cluster and the label together. A label alone names an item in every cluster at once.
56
-
57
- A label may carry a letter suffix, as in `3a` beside `3`, which is how a pass records a finding split after the fact rather than renumbering every item below it. Pass it exactly as the heading spells it.
58
-
59
- One call writes one cluster. A call per selection is the alternative, and several of those against the same file read it before any of them writes, so every answer but the last is lost with nothing reporting it. The selections split on the first `=`, so an answer carrying its own survives intact.
60
-
61
- An item already carrying an answer refuses rather than being overwritten, and one filled item refuses the whole batch, so a partly applied write never lands. Drop the named item and send the rest.
62
-
63
- Exit codes: `0` every named item now carries its answer, `1` refused. The `reason` field carries `no-intake`, `no-folder`, `ambiguous-slug`, `no-cluster`, `no-item`, `answered`, or `bad-input`. A bare slug matching more than one ordinal-prefixed folder refuses as `ambiguous-slug` rather than `no-folder`, naming every match in `detail`.
64
-
65
- An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the record's `reason` rather than the exit when a skill consumes this, which matters most here because the verb writes.
66
-
67
- `bad-input` covers a malformed command line: no cluster, no selection, a selection that parses to no label and answer, an empty answer, an answer carrying a line break, or two answers for one item. It is separate from the reasons describing the folder, so a caller that mistyped a flag is not sent to repair a file that is fine.
68
-
69
- An empty answer refuses rather than writing an empty slot. The slot means unread while it is empty, so writing one back would report an item as answered that nobody decided.
70
-
71
- An answer occupies one line, and one carrying a line break refuses before anything is read. Writing it splices a bare continuation into the item that matches none of the patterns the reader tests, so the slot reads back as the text before the break while the item counts as answered, and the refusal on an already-answered item then leaves hand-editing the file as the only correction. The whole batch refuses, so a good selection beside a broken one never lands half applied.
72
-
73
- The folder is shared scratch at the main worktree root, so `--root` defaults to the first entry of `git worktree list` rather than the working directory. The write is an edit inside a file that already exists, which `Edit` and `Write` refuse from a linked worktree and a shell stream editor may not do, so the verb resolves the root in-process and rewrites whole lines.
74
-
75
- Skills branch on the reason rather than on the exit code:
76
-
77
- ```bash
78
- canon intake list toolkit-overview --unread --json | jq -r '.clusters[] | .cluster'
79
- ```
80
-
81
- For the folder layout, the item format, the answer contract, and retrieval, see `claude/skills/plan-intake/references/intake.md`.
@@ -1,116 +0,0 @@
1
- ---
2
- title: Key changes bijection
3
- description: Comparing the files a pull request body's Key changes names against its own diff, the two directions and the split inside each that decides what is worth raising, the span rules the extractor was measured into, and the three refusals that separate a clean pass from a read that produced nothing
4
- ---
5
-
6
- # Key changes bijection
7
-
8
- `canon pr key-changes` reads the `## Key changes` section of a pull request body, lifts the paths its bullets claim, and compares that set against the files the pull request actually changed. This repository squash-merges, so the body becomes the commit message and the record on the trunk once the branch is gone.
9
-
10
- ```bash
11
- canon pr key-changes
12
- canon pr key-changes 1265 --json
13
- canon pr key-changes --body .canon/tmp/pr/body.md --base origin/main
14
- ```
15
-
16
- The positional is the pull request to read, defaulting to the one open on this branch.
17
-
18
- | Option | Behavior |
19
- | --------------- | ------------------------------------------------------------------ |
20
- | `--body <path>` | Read the body from a file, taking the changed set from git instead |
21
- | `--base <ref>` | Ref the range runs back to when `--body` supplies the body |
22
- | `--root <path>` | Repository to read, defaulting to the cwd |
23
- | `--json` | Add a machine-readable record on stdout, keeping the frame |
24
-
25
- `--body` decides where both halves come from, so a number passed beside it is never read. The body comes off disk and the changed set from the local range, which is the shape a fixture and a body still being drafted both need. A relative `--body` path resolves against the working directory the command runs from, never against `--root`. `--base` and the changed-file read still resolve against `--root`.
26
-
27
- `--base` names the far side of that range and never the commit the comparison runs against directly. The reader resolves the merge base between `HEAD` and the ref, so `--base origin/main` measures what the branch wrote rather than what the trunk gained while the branch was open. Passing the trunk as it stands today is therefore safe on a branch of any age, and a ref sharing no history with `HEAD` refuses as `bad-base`.
28
-
29
- Without `--body` the body and the head commit come off the REST pull read and the file list off the paginated files endpoint. Every read runs on REST, since a cloud session's GitHub proxy refuses the GraphQL that `gh pr view` runs on. A push landing between the two reads compares a body against the next head's files, a window the verb accepts for the cloud reach.
30
-
31
- A failed page refuses as `gh-failed` rather than comparing against a set known to be short. A set silently one file short is the worst input this takes, since the missing file is exactly what a correct bullet would then be accused of inventing. With no number named, the verb reads the one pull request open on the checkout's branch, and refuses as `ambiguous-pull` when two are open against different bases.
32
-
33
- ## The two directions
34
-
35
- They are reported apart because they want different tolerances.
36
-
37
- - **`unmet`** is a whole path the body claims ahead of its bullet's first comma and the diff does not carry. This is the graded direction and it sets the exit code. A bullet naming an untouched file is wrong more often than not, and it corrupts the record that reaches the trunk.
38
- - **`unnamed`** is a changed file no bullet reached that a reader might have wanted one for. Reported with no grade, since a change can be too small to describe and still be correctly absent. Grading it would fire on nearly every branch.
39
- - **`incidental`** is a changed file no bullet reached that owes none: a test beside its subject, anything under a fixture or snapshot folder, and a lockfile a package manager writes. Held apart so the count above reads, and reported rather than dropped so a run still says what it set aside.
40
- - **`unresolved`** is a path the reading could not judge either way. Three causes reach it. A path written partially, such as `role-worker/SKILL.md` for a file under `claude/skills/`. A path past its bullet's first comma. And, for a live pull request, a claim landing here rather than `unmet` because the rename or `.gitignore`-addition evidence that might have credited it could not be read, which `evidenceUnread` on the record names.
41
-
42
- Each direction splits on one question, which is whether the evidence is strong enough to raise with a person. A partial path and a trailing path can each credit a changed file and can never accuse one, because nothing separates a path written short from a path written wrong, or a second claim from a file cited for context. Neither split drops anything: what comes out of `unmet` lands in `unresolved` and what comes out of `unnamed` lands in `incidental`, so a count a reader can act on never costs a file the run stayed silent about.
43
-
44
- A generated asset and a regenerated index belong in the incidental class and are deliberately absent from it, because neither has a spelling that holds outside one project. Guessing at one would set aside a file that did owe a bullet, which hides a real omission, where leaving them out only leaves the count where it already was.
45
-
46
- ## What counts as a claim
47
-
48
- Only `## Key changes` is read. `## Technical context` legitimately names files a branch never touched, such as an install stamp inside a target, so widening the read manufactures a finding out of every argument an author made for the change.
49
-
50
- Inside the section, every backticked span in a bullet is read, and the bullet's first comma outside a span divides the ones that can accuse from the ones that can only credit. That one lever was chosen by measurement. Over the 23 merged pull requests in this repository carrying the section, reading whole bullets reported 16 paths as claimed-but-untouched and every one was a file the body named for context. Cutting at the comma left 110 claims of the original 149 and took the false reports to 2. A list of sixteen clause-opening words tried beside it removed nothing the comma had not already removed, because this corpus punctuates every one of them.
51
-
52
- The cut used to decide whether a span was read at all, and the paths past the comma fell out of the claim set into `unnamed`. That was accepted on the ground that the ungraded direction does no damage, and it did: `review-pr` read `unnamed` as a question to put to the branch author, so on 2026-09-01 the question went to three pull requests over bullets that had named the files all along. Reading the whole bullet as claims outright is the obvious repair and the corpus refuses it, taking `unmet` from 10 to 19 over the 40 most recent merged pull requests carrying the section, with all nine additions in the context class the cut exists to exclude. Reading the whole bullet and gating the accusation on the cut gives claims 243 to 314 and unnamed 1124 to 1058 with `unmet` identical entry for entry.
53
-
54
- A span anywhere in the bullet has to survive all of these:
55
-
56
- | Rule | What it keeps out |
57
- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
58
- | No whitespace, `<`, `>`, `$`, `*`, `\|`, `?`, `^` | A backticked command, a placeholder, a glob, a pattern |
59
- | No `://` and no leading `/`, `~`, `@`, `#`, `!` | A URL, an absolute path, a module alias |
60
- | Contains `/` | A bare filename with no folder around it |
61
- | Not a dotted-decimal segment, or ends with `/` | A dotted number such as an address |
62
- | Not a single top-level folder | `src/`, which nobody claims to have rewritten whole |
63
- | A `file:line` span leads its bullet, or the bullet carries no bare line-only companion | A citation into a file the bullet already named, such as `verify.sh:634` beside `:642` |
64
- | The region carries no no-change marker | A bullet recording a file it deliberately left alone |
65
-
66
- The last three rows earn their place from the corpus rather than from a rule. Every body that spelled a lone top-level folder was naming where something lives, and a line citation that follows another claim in the same bullet points into the file being described only when a bare line-only span sits beside it, such as `:642` following `verify.sh:634`. With no such companion the citation names a second, genuinely distinct file and stays a claim. A line citation leading its bullet stays a claim either way, which is how a body names the exact line it rewrote.
67
-
68
- The no-change marker is the one rule the region cut cannot substitute for. A body writes "Leave `x` untouched, since the decision keeps it" to record a change it declined, and the path sits ahead of the first comma, so a stricter cut would not reach it and a looser one would find more. Since `keep` and `leave` each open a real claim often enough, the marker decides it rather than the leading verb. Three words carry the set: `untouched`, `unchanged`, and `as written`. `in place` was measured and dropped, because rewriting a file in place is an ordinary claim, and `no other line` was dropped because a correct bullet writes "as one insertion that touches no other line" about a change it did make. `alone` shipped in the set too, until review found every corpus occurrence sitting past the first comma, where the cut already excludes it, so the word voided no true claim there. Restrictive use is the more common one in this repository's own prose, and a comma-free bullet exposed it: "Move the threshold read into `src/gate/stages.ts` alone" asserts an edit, and the marker voided it while the word was still in the set. Over the 40-pull-request corpus the rule still voids no true claim.
69
-
70
- A folder claim covers every changed file beneath it. A bare filename drops outright: resolved as a sibling of a path earlier in its bullet it produced seven wrong paths across this corpus against two right ones, because a compound bullet names a sibling folder as often as a sibling file.
71
-
72
- ## Anchoring
73
-
74
- A claim is anchored when its first segment names a folder the tree holds. The roots come from the tracked file list joined with the first segment of every changed path, and the second half is what admits a folder the branch created. Reading the tree alone would mark every claim under a new top-level directory unanchored, and an unanchored claim never accuses, so the first branch to open one would lose the graded direction with nothing reporting it.
75
-
76
- ## Exit codes and refusals
77
-
78
- | Code | Meaning |
79
- | ---- | ------------------------------------------------- |
80
- | `0` | every claimed path is in the diff |
81
- | `1` | refused, with `reason` naming the cause |
82
- | `2` | at least one claimed path is absent from the diff |
83
-
84
- Branch on the record rather than on the exit code. A shell function wrapping `canon` takes its status from whatever it runs last, so every non-zero exit can reach a caller as zero.
85
-
86
- Three refusals separate a clean pass from a read that produced nothing:
87
-
88
- - `no-section`. The body carries no such heading, so it claims nothing.
89
- - `no-claims`. The heading is there and no span resolved, which is the extractor failing over prose rather than the body being wrong. Nothing is raised.
90
- - `no-changes`. The pull request changed no files, so no claim has anything to answer.
91
-
92
- An empty extraction read as a clean pass is the failure shape this repository has recorded twice, which is why the middle one is its own reason rather than a zero count.
93
-
94
- `gh-missing`, `gh-failed`, `ambiguous-pull`, `unreadable-body`, `unreadable-tree`, `no-base`, `bad-base`, and `unreadable-changes` cover the reads that never reached a comparison.
95
-
96
- ## What it was measured against
97
-
98
- Driven over the 40 most recent merged pull requests in this repository at the time, 23 of which carry the section, the shipped verb reported zero unmet paths and one `no-claims` refusal. The naive extractor it replaced reported 16.
99
-
100
- The re-measurement behind the trailing-path split is a wider corpus and its numbers do not compare to those. It takes the 40 most recent merged pull requests that carry the section, which reaches back through 90 merges rather than 40, and the reader reports 10 unmet over it. Those 10 predate the split and survive it entry for entry, which is the check that the widening moved nothing into the graded direction. Three name a gitignored path a diff can never carry and one is the definition-site class named below. The remaining six were not run down, so the residual over that corpus is 10 rather than zero and how much of it is the reader rather than the bodies is unmeasured.
101
-
102
- The first body written after that corpus closed produced two, which is what the no-change marker and the open class above come from. One was the disclaiming bullet and the rule now covers it at no cost to the corpus. The other is the definition-site class, left open with the report naming it.
103
-
104
- The bound is that the corpus is one repository writing to one house style. A project that punctuates differently loses claims to the comma cut rather than gaining false ones, since every rule above drops rather than invents, so the check degrades toward silence rather than toward noise.
105
-
106
- What it cannot see is a bullet claiming a change to a file the branch did touch for another reason, and a path written without backticks. A claim in a second coordinated clause after the comma is read and credited, and it stays out of the graded direction, since nothing here separates it from a file the same clause cites for context.
107
-
108
- Two shapes credit against evidence the changed-file list itself never carries. A bullet naming the path a git rename moved away from, such as "Move `claude/skills/identity/` to `claude/skills/draft-identity/`", is credited against a local rename read (`git diff --name-status -M`) or, for a live pull request, `gh api repos/{owner}/{repo}/pulls/{number}/files`, keyed on the old path git's own detection reports. A bullet naming a pattern newly added to `.gitignore`, such as "Ignore `web/screenshots/` and `web/evidence/`", is credited against the added, non-comment lines of that file's own diff, with the trailing slash a directory claim carries normalized against the raw pattern. Both credits reach past the anchored-and-leading split that decides `unmet`, and the ignore-addition credit marks `.gitignore` itself as named, so the file also stops reading as `unnamed`.
109
-
110
- Neither credit can tell a claim that is genuinely stale from one the evidence would have credited had the read gone through, since a failed fetch and a confirmed absence both produce the same empty result. `evidenceUnread` on the record separates the two: when the read that could have credited a claim fails, that claim lands in `unresolved` instead of `unmet`, rather than risk the exact false accusation this section exists to remove.
111
-
112
- One class stays open and is named rather than closed. A bullet can cite where something is defined while claiming an edit somewhere else, as `erclx/canon#1274` does with "Name the slug transform in `standards/slug.md` inline in the same step". The path is a definition site, the edit target is the step, and separating the two needs the sentence parsed rather than cut. A finding on such a bullet is this class rather than a stale claim, and a reader weighing an `unmet` path checks whether the bullet's real target is a locative the path does not name. <!-- canon-allow-reference: illustrates the input shape the rule reads -->
113
-
114
- ## Where it runs
115
-
116
- `review-pr` Step 3 calls it through its `references/key-changes.md` and files an `unmet` path as a `should-fix` finding under the `**PR body**` block the stale ticked box already takes, since what both corrupt is the merge record rather than a file in the diff. A body is edited between pushes, so a finding names the head the comparison ran at. `unnamed` is read there and raised off no count, since the step's own history is a question sent over bullets that already answered it. `unresolved` and `incidental` are reported nowhere.
@@ -1,86 +0,0 @@
1
- ---
2
- title: Label coverage
3
- description: Reading a changed set against the pull request label map, the two tables it matches, how a gap is separated from a decision, and why an absent map is an answer
4
- ---
5
-
6
- # Label coverage
7
-
8
- `canon labels audit` resolves the paths a branch changed against the label map a project declares, reports the labels the set earns, and names every path no row reaches. It closes a gap the map's own comment had predicted since the map shipped: nothing detected a surface added after the rows were written, so such a branch merged carrying no label and nobody heard about it.
9
-
10
- ```bash
11
- canon labels audit
12
- canon labels audit --json
13
- canon labels audit --base origin/main
14
- canon labels audit src/cli.ts docs/index.md --json
15
- ```
16
-
17
- | Option | Behavior |
18
- | --------------- | ---------------------------------------------------------- |
19
- | `--base <ref>` | Ref the range runs back to, defaulting to the trunk |
20
- | `--root <path>` | Repository to read, defaulting to the current directory |
21
- | `--json` | Add a machine-readable record on stdout, keeping the frame |
22
-
23
- `--base` names the far side of the range rather than the commit the diff runs against. The audit resolves the merge base between `HEAD` and the ref before reading anything, so `--base origin/main` on a branch the trunk has moved past still earns labels for the paths that branch wrote. Reading the ref literally is what once handed a long-open branch the labels for somebody else's merge, and it named those paths as uncovered surfaces on top.
24
-
25
- Positional paths replace the range entirely. A caller holding a changed set already passes it rather than paying for a second git read, and the record then omits `base` to say the range was never resolved.
26
-
27
- Under `--json` the record holds stdout alone and the frame still renders on stderr, refusals included, which is the split `output-shape.md` fixes for every mode.
28
-
29
- ## The map it reads
30
-
31
- The map sits at `canon/config/pr-labels.toml`, falling back to `.claude/canon/pr-labels.toml` for a project that has not moved, and carries two tables. `[domains]` keys a label name to the path prefixes that earn it, and `[declined]` keys a reason to the prefixes that earn no label on purpose.
32
-
33
- ```toml
34
- [domains]
35
- api = ["services/api/"]
36
- web = ["apps/web/", "packages/ui/"]
37
-
38
- [declined]
39
- release-managed = ["CHANGELOG.md", "package.json"]
40
- generated = ["build/manifest.json"]
41
- ```
42
-
43
- Matching is prefix-anchored, so a row written for an authoring root reaches nothing under the copy a project consumes. A path claimed by rows in both tables takes the label, since it already has a subject and reporting it as deliberately unlabelled would contradict the label the same run applies.
44
-
45
- ## A gap and a decision are different findings
46
-
47
- The verb sorts every changed path into three buckets and keeps two of them apart:
48
-
49
- - **Labelled.** At least one `[domains]` row reaches the path. It contributes its label and nothing else.
50
- - **Declined.** A `[declined]` row reaches it. Somebody already decided the path earns no label, so the run names it and moves no exit code.
51
- - **Uncovered.** Neither table reaches it. This is the finding, and it wants either a prefix on the row that owns its subject or a `[declined]` row carrying the reason.
52
-
53
- Folding the last two together was the shape this replaced. A report that cannot tell a surface nobody has gotten to from one somebody refused is useful about neither, which is why the declined half moved out of the map's trailing comment and became data the command reads.
54
-
55
- ## The range it reads
56
-
57
- The default range is the branch against the trunk, resolved as the merge base against `origin/main` and then local `main`. The changed set is that base diffed against the working tree, plus untracked files git does not ignore, rather than against `HEAD`.
58
-
59
- Reading the working tree is what lets the check run before the branch commits, which is the moment a session adds the surface nobody covered. The set is a superset of `base..HEAD`, so a caller running after the commits still sees the whole branch.
60
-
61
- ## Exit codes and refusals
62
-
63
- | Code | Meaning |
64
- | ---- | ---------------------------------------------- |
65
- | `0` | every changed path is labelled or declined |
66
- | `1` | refused, with `reason` naming the cause |
67
- | `2` | at least one changed path is reached by no row |
68
-
69
- It reports and never gates. Whether an uncovered surface deserves a label is a judgment only whoever owns that surface can make, and a push failing on one teaches a contributor to route around the check while nothing about the surface has changed.
70
-
71
- A project declaring no map refuses with `no-map`, which is an answer rather than a fault. Such a project is labelled silently by design, and treating the absence as a break would make the map mandatory for every target. `canon audits run` reads that one reason as an expected absence and every other refusal as a measure that did not run.
72
-
73
- ## Applying the set
74
-
75
- `canon labels apply <number>` reads the same earned set and adds each label the open pull request lacks, so a pull request opened outside `git-pr` takes one command to label.
76
-
77
- ```bash
78
- canon labels apply 123 --json
79
- canon labels apply 123 --check
80
- ```
81
-
82
- It binds to the pull request before anything is read or written, and refuses with `head-mismatch` unless the head is the current branch and the state is open, under `--check` as well. It adds and never removes, so a label a person applied stays. A label the remote does not carry lands in `missingOnRemote` and is never created. `--check` writes nothing and exits 2 when labels are owed. A project declaring no map adds nothing and exits 0.
83
-
84
- ## What it does not measure
85
-
86
- A prefix reaching no path is invisible here. A row left behind by a deleted folder stays in the map forever, and this verb has nothing to say about it. That is the map going stale from the other side and a second measure rather than this one.
@@ -1,39 +0,0 @@
1
- ---
2
- title: Markdown audit density
3
- description: The six layout limits the markdown audit measures, what each one counts, which paths take a finding, and how a folder's gate turns on
4
- ---
5
-
6
- # Markdown audit density
7
-
8
- `canon markdown audit` measures six layout limits beside its bans. Each one is a count against a fixed number, so a finding is a fact rather than a judgment, and a gated path fails the run on it.
9
-
10
- ## The limits
11
-
12
- Six limits report on the paths that take them, under `density` in each `--json` entry.
13
-
14
- | Limit | Finding |
15
- | ------------------- | ---------------------------------------------------------- |
16
- | Paragraph length | a prose paragraph past 350 characters |
17
- | Paragraphs in a row | more than 2 paragraphs with nothing else between them |
18
- | Bullet length | a top-level bullet past 200 characters, marker excluded |
19
- | Bullets in a row | more than 5 plain top-level bullets |
20
- | Heading depth | any heading past H3 |
21
- | H3 opening | an H3 whose first block is neither a paragraph nor a table |
22
-
23
- ## What counts as a block
24
-
25
- A paragraph indented under a list item counts toward that bullet's length and never as a paragraph. A table, a fence, a list, a heading, and a bold section marker each break a paragraph run. Each finding carries `kind`, `line`, `value`, `limit`, and `gating`, and the H3 opening carries `null` for both numbers.
26
-
27
- ## Which paths take a finding
28
-
29
- Only context entries, docs pages, skill bodies and references, tooling seeds and stack references, the wiki, `README.md`, and `CONTRIBUTING.md` take a finding. A rule, `CLAUDE.md`, a requirement or design record, the changelog, `examples/`, and every `.canon/` record take none, so a plan never reports one.
30
-
31
- ## When a finding gates
32
-
33
- A finding gates when its path sits under a prefix in `GATED_PATHS` in `src/markdown/scope.ts`, and each rewrite batch adds its folders at zero, or one file per entry where it covers part of a folder. A run of more than 5 plain bullets gates in context, docs, and skills from the start, since it failed the push there before this measure existed.
34
-
35
- Outside a canon checkout every density finding reports and none gates, since the list describes this repository's own rewrite.
36
-
37
- ## What reads the finding
38
-
39
- A gating finding sets exit `2`, the same code a ban hit takes, and `markdown-audit.md` carries the full exit contract. The `Markdown` stage of `canon gate run` reads that exit over the whole corpus, and the edit-time hook hands every finding back, gating or not.