@erclx/canon 5.74.0 → 5.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/create-rule/SKILL.md +5 -1
  4. package/claude/skills/create-rule/references/ledger.md +73 -0
  5. package/claude/skills/create-rule/references/rule.md +1 -1
  6. package/claude/skills/create-skill/references/skill-practice.md +1 -1
  7. package/claude/skills/pr-evidence/references/reel.md +9 -9
  8. package/claude/skills/proposal-critique/references/adopted.md +44 -0
  9. package/claude/skills/ui-film/SKILL.md +8 -7
  10. package/docs/agents/audits.md +24 -24
  11. package/docs/agents/migrate.md +10 -10
  12. package/docs/migrations/by-verb.md +2 -0
  13. package/governance/detect.toml +4 -0
  14. package/governance/ledger/claude/rule.md +16 -0
  15. package/governance/rules/claude/rule.md +13 -0
  16. package/package.json +1 -1
  17. package/scripts/ci/refresh-frames-pr.sh +11 -12
  18. package/src/audits/catalog.ts +7 -2
  19. package/src/capture/stamp.ts +3 -5
  20. package/src/claude/skills-audit.ts +65 -0
  21. package/src/cli/help.ts +1 -1
  22. package/src/commands/claude.ts +6 -2
  23. package/src/commands/gov.ts +12 -7
  24. package/src/commands/migrate.ts +6 -2
  25. package/src/commands/take.ts +36 -7
  26. package/src/counts/catalogs.ts +3 -5
  27. package/src/gate/measures.ts +1 -144
  28. package/src/gate/stages.ts +0 -23
  29. package/src/gov/audit.ts +59 -13
  30. package/src/markdown/scope.ts +2 -0
  31. package/src/migrate/session-defaults.ts +114 -4
  32. package/src/take/encode.ts +9 -1
  33. package/src/take/film.ts +17 -2
  34. package/src/take/fixtures/busy.html +107 -0
  35. package/src/take/perform.ts +99 -25
  36. package/src/take/screencast.ts +4 -3
  37. package/src/take/stepper.ts +33 -20
  38. package/tooling/claude/reference.md +4 -2
  39. package/tooling/claude/seeds/.claude/hooks/plan-guard.sh +239 -0
  40. package/tooling/claude/seeds/.claude/settings.json +8 -0
  41. package/scripts/core/regen-hero.ts +0 -113
  42. package/src/capture/frame-values.ts +0 -326
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.75.0](https://github.com/erclx/canon/compare/v5.74.0...v5.75.0) (2026-10-29)
4
+
5
+
6
+ ### Features
7
+
8
+ * **claude:** audit proposal-critique against its ledger ([#2845](https://github.com/erclx/canon/issues/2845)) ([397699b](https://github.com/erclx/canon/commit/397699bdeb7da47a39fb210d2850e8502d503a1f))
9
+ * **claude:** seed the plan guard and wire installed projects ([#2851](https://github.com/erclx/canon/issues/2851)) ([e312aab](https://github.com/erclx/canon/commit/e312aabaa69941ce01f656a635b98ef9e01680f1))
10
+ * **gov:** check a project's ledgers under canon/ledger/ ([#2848](https://github.com/erclx/canon/issues/2848)) ([65fb0e6](https://github.com/erclx/canon/commit/65fb0e6e60ef22c16a7743964865fc62d6b55ed8))
11
+ * **governance:** route a rule edit to create-rule ([#2847](https://github.com/erclx/canon/issues/2847)) ([010b639](https://github.com/erclx/canon/commit/010b63970a874905ec9ee3db520b7cb5f96db778))
12
+ * **take:** refuse a take past --max-seconds as over-cap ([#2855](https://github.com/erclx/canon/issues/2855)) ([d9d6424](https://github.com/erclx/canon/commit/d9d642434d082f9f7d4b1508723576044e55f6a4))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **take:** cut a screencast clip at the stage clock ([#2853](https://github.com/erclx/canon/issues/2853)) ([3588832](https://github.com/erclx/canon/commit/3588832a8263079ff8340b2cf7af3c492bf4cc66))
18
+ * **take:** hold canon take steady on a page whose frames run slow ([#2852](https://github.com/erclx/canon/issues/2852)) ([f2b55f9](https://github.com/erclx/canon/commit/f2b55f9a2f948c46fb4c84720da7596df31107a5))
19
+ * **take:** scroll a target taller than the window ([#2854](https://github.com/erclx/canon/issues/2854)) ([976b85d](https://github.com/erclx/canon/commit/976b85d27391106d2cb7e03072451de5a128d2fd))
20
+
3
21
  ## [5.74.0](https://github.com/erclx/canon/compare/v5.73.0...v5.74.0) (2026-10-28)
4
22
 
5
23
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
4
- "version": "5.74.0",
4
+ "version": "5.75.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -86,6 +86,10 @@ Read `${CLAUDE_SKILL_DIR}/references/checklist.md` and run every bullet of the w
86
86
 
87
87
  ## After writing
88
88
 
89
- Emit the full path on its own line: `.claude/rules/project/<subdir>/<slug>.md`. Remind the user that Claude Code loads path-scoped rules when it reads or writes a matching file, and always-on rules every session. When a bullet claims outside practice, tell them to cite its source in the commit that adds the rule.
89
+ Emit the full path on its own line: `.claude/rules/project/<subdir>/<slug>.md`. Remind the user that Claude Code loads path-scoped rules when it reads or writes a matching file, and always-on rules every session. When a bullet claims outside practice, tell them to cite its source, in a ledger or in the commit that adds the rule.
90
90
 
91
91
  Show the new rule as a diff in chat and commit nothing until the operator says OK.
92
+
93
+ ### When a bullet cites outside practice
94
+
95
+ Offer to record each source in a ledger at `canon/ledger/rules/<subdir>/<slug>.md`, which `canon gov audit` checks row for bullet. Read `${CLAUDE_SKILL_DIR}/references/ledger.md` for the layout and the row shape before writing one.
@@ -0,0 +1,73 @@
1
+ ---
2
+ title: Project ledger reference
3
+ description: Where a project's rule and skill ledgers live under canon/ledger/, and the row shape both audits check, one table per H2 and one row per bullet
4
+ ---
5
+
6
+ # Project ledger reference
7
+
8
+ ## Contents
9
+
10
+ - [Overview](#overview)
11
+ - [Where it lives](#where-it-lives)
12
+ - [Shape](#shape)
13
+ - [Rows](#rows)
14
+ - [Example](#example)
15
+
16
+ ## Overview
17
+
18
+ A ledger names what stands behind each bullet of a rule or a skill file, kept beside the file rather than inside it, so the reasoning costs no load. A bullet claiming outside practice names its source, and a bullet recording a project decision names what it prevents.
19
+
20
+ A project writes a ledger where it wants one and owes none. `canon gov audit` and `canon claude skills audit` check each ledger that exists against its file and report a ledger whose file is gone.
21
+
22
+ ## Where it lives
23
+
24
+ Every ledger sits under `canon/ledger/`, which Claude Code never loads. A ledger under `.claude/rules/` would load as a rule on every matching read.
25
+
26
+ - A rule at `.claude/rules/project/<subdir>/<slug>.md` takes `canon/ledger/rules/<subdir>/<slug>.md`
27
+ - A skill body at `.claude/skills/<skill>/SKILL.md` takes `canon/ledger/skills/<skill>/SKILL.md`
28
+ - A skill reference at `.claude/skills/<skill>/references/<file>.md` takes `canon/ledger/skills/<skill>/<file>.md`, dropping the `references/` segment
29
+ - Move or delete a ledger in the change that moves or deletes its file, since the audits report one left behind
30
+
31
+ ## Shape
32
+
33
+ The audits match rows to bullets by count under a heading of the same name, so the shape is fixed and whether a row supports its bullet is a review judgment.
34
+
35
+ - Write one `## <heading>` per H2 in the file, spelled as the file spells it
36
+ - Put one table under each heading, with one row per bullet in the file's order
37
+ - Give a file with no H2 one table under the title
38
+ - Write no other table in the ledger, since every table row under a heading counts. A sample table inside a code fence is not counted.
39
+
40
+ A skill file counts a few lines differently from a rule.
41
+
42
+ - Give each top-level ordered step a row, the way a bullet takes one
43
+ - Give `## Contents` no table, since it repeats the headings
44
+ - Stop a `SKILL.md` ledger's tables at the body's first closing section, such as `## Excuses and rebuttals` or `## What this delegates`, since a table under one reads as rows with no bullets
45
+
46
+ ## Rows
47
+
48
+ Each row names the bullet and the one thing behind it.
49
+
50
+ - **Sourced:** a link to the primary source, with the line it says, such as the framework's own docs or the language spec
51
+ - **House choice:** a cell opening `House choice:` and naming the bug class the bullet prevents, when no outside source takes a side
52
+ - **Decision:** a house choice naming what the bullet prevents and the option it beat, with the date it was decided
53
+ - Cut a bullet that has neither a source nor a bug class behind it, rather than writing a row that says so
54
+
55
+ ## Example
56
+
57
+ A project React rule in the `framework/` subdir, with two bullets under `## Component patterns`, takes this ledger at `canon/ledger/rules/framework/react.md`:
58
+
59
+ ```markdown
60
+ ---
61
+ title: React rule ledger
62
+ description: Sources for the project React rule, one row per bullet
63
+ ---
64
+
65
+ # React rule ledger
66
+
67
+ ## Component patterns
68
+
69
+ | Bullet | Source |
70
+ | ---------------------------- | --------------------------------------------------------------------------------------------- |
71
+ | Stable keys over array index | [Rendering lists](https://react.dev/learn/rendering-lists): index keys cause subtle bugs |
72
+ | Named exports | House choice: a default export renamed at import drifts from the component's name in a search |
73
+ ```
@@ -101,7 +101,7 @@ A bullet claiming outside practice, such as a language idiom or a library's corr
101
101
 
102
102
  - Cite the source in the commit that adds the rule, naming a primary page or the bug class the bullet prevents
103
103
  - Cut a bullet that has neither a source nor a bug class behind it
104
- - The toolkit holds its outside-practice rules to a source ledger kept outside the installed tree, so the citation costs no load
104
+ - A project can hold its rules to a source ledger kept outside the installed tree, so the citation costs no load, laid out in `${CLAUDE_SKILL_DIR}/references/ledger.md`
105
105
  - A bullet resting on how Claude Code behaves ends with a `Revisit when` sentence, per `### Phrasing`
106
106
 
107
107
  ## Location
@@ -73,4 +73,4 @@ The audit counts a few lines in a ledgered file apart from a rule's.
73
73
  - It keeps a wider ledger list beside it, adding the skills that teach a craft drawn from outside sources without a practice's closing shape
74
74
  - A skill on the wider list alone owes the ledger and none of the three sections
75
75
  - Never mark a skill as a practice skill in its own frontmatter, since a self-declared kind lets a skill exempt itself.
76
- - Both lists name this toolkit's shipped skills alone, so a project's own practice skill takes this shape with no check behind it.
76
+ - Both lists name this toolkit's shipped skills alone. A project's own skill owes no ledger, and one it writes is checked per `${CLAUDE_SKILL_DIR}/../create-rule/references/ledger.md`.
@@ -62,23 +62,23 @@ Write one plan entry for each beat chosen, then skip when none was.
62
62
 
63
63
  The negative test is a judgment over the box's text. A misread box costs a weaker reel and never a wrong pass, since the still check below guards the footage and the UI reviewer drives every box regardless.
64
64
 
65
- Keep the whole reel at or under 12 seconds. Each beat opens and closes on a 0.6 s hold, so give each action's `wait` only what its result needs to settle.
65
+ Keep the whole reel at or under 12 seconds, which the take below enforces. Each beat opens and closes on a 0.6 s hold, so give each action's `wait` only what its result needs to settle.
66
66
 
67
67
  ## Film the reel
68
68
 
69
69
  ```bash
70
- canon take <url> .canon/tmp/pr/reel/<number>/plan.json --out .canon/tmp/pr/reel/<number>/takes --reel .canon/tmp/pr/reel/<number>/reel.webp --json
70
+ canon take <url> .canon/tmp/pr/reel/<number>/plan.json --out .canon/tmp/pr/reel/<number>/takes --reel .canon/tmp/pr/reel/<number>/reel.webp --max-seconds 12 --json
71
71
  ```
72
72
 
73
73
  Branch on the record's `reason`, never on the exit code.
74
74
 
75
- | Record | Route |
76
- | --------------- | ------------------------------------------------------------------ |
77
- | No `reason` | Sum the beats' `seconds` and continue to the stills |
78
- | `reel-failed` | Skip as `reel-failed` with the record's `message` |
79
- | Any other value | Skip under that `reason`, such as `target-missing` with its `beat` |
80
-
81
- When the beats sum past 12 seconds, drop the last beat from the plan and film once more. A second run past the cap skips as `over-cap`.
75
+ | Record | Route |
76
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
77
+ | No `reason` | Continue to the stills |
78
+ | `reel-failed` | Skip as `reel-failed` with the record's `message` |
79
+ | `target-offscreen` | Add a setup step that brings the target into the window, being a scroll over an element already inside it, such as a fixed header, with `dy` covering the distance, or a click on a link to it, and film once more. Skip as `target-offscreen` on a second refusal |
80
+ | `over-cap` | Drop `beat` and each beat after it, then film once more. Skip as `over-cap` if `beat` is first or it refuses again |
81
+ | Any other value | Skip under that `reason`, such as `target-missing` with its `beat` |
82
82
 
83
83
  ## Check each still
84
84
 
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: Adopted and declined
3
+ description: Which designs this skill considered and dropped, and what would revive each, so a later session extends the position instead of re-deriving it
4
+ ---
5
+
6
+ # Adopted and declined
7
+
8
+ The skill came out of a groundwork track that measured two shipped fixes against the questions a critique would have asked. This file records the options the track dropped. A later session extending the skill adds to it rather than re-arguing an item settled here.
9
+
10
+ A bullet's source is linked in its row of the toolkit's per-bullet ledger at `governance/ledger/skills/proposal-critique/`, outside the plugin, and this file links no source a row cites.
11
+
12
+ ## Declined
13
+
14
+ Each item names the design that stays out, the reason, and what would bring it back.
15
+
16
+ ### A default call from every planning skill
17
+
18
+ An authoring session questioning its own plan is the weak form Kahneman, Lovallo, and Sibony warn against when they ask for reviewers independent of the team that made the recommendation. The planning skills name the critique in one line for a one-way change or a fix with an unverified cause, and never call it.
19
+
20
+ Revisit when a replay finds a case where the on-demand form would not have been invoked in time.
21
+
22
+ ### Folding it into `decision-escalate`
23
+
24
+ That skill batches preference questions. Most critique questions are answered from evidence, and only the remainder goes to the operator.
25
+
26
+ Revisit when critique passes show every question turning out to be preference.
27
+
28
+ ### Running it as a forked subagent
29
+
30
+ A fork reads cold, which is the independence Kahneman, Lovallo, and Sibony ask for. It also loses the conversation, and a background fork loses the tools the operator questions need.
31
+
32
+ Revisit when a critique keeps agreeing with the session that wrote the proposal.
33
+
34
+ ### A written critique section in the plan
35
+
36
+ A plan already carries `## Risks` and `## Questions`, and its validator owns the sections. The verdict goes to chat, and the operator decides what moves into a record.
37
+
38
+ Revisit when critiques are lost to compaction before anyone acts on them.
39
+
40
+ ## Revisit the whole design when
41
+
42
+ - Replaying the chain that shipped two fixes and two or three past plans finds the on-demand form would not have been invoked in time
43
+ - One body cannot hold questions that differ so much per record, whether plan, fix, or triage
44
+ - A replayed case shows a cheap, reversible change still doing real harm
@@ -70,13 +70,14 @@ canon take <url> .canon/video/<slug>/plan.json --out .canon/video/<slug>/takes -
70
70
 
71
71
  Branch on the record's `reason`, never on the exit code, which a shell function wrapping `canon` can flatten to zero.
72
72
 
73
- | Record | Route |
74
- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
75
- | No `reason` | Continue to the stills |
76
- | `target-missing` with `fallback` | Check the access before the selector, per the note below the table |
77
- | `target-missing` | Open the record's `still`, find the target the record's `beat` names on the page, fix the plan, and film once more |
78
- | `server-unreachable` | Restart the server from Step 1 and film once more |
79
- | Any other `reason` | Report the `reason` and `message` to the operator and stop, since each is a setup step |
73
+ | Record | Route |
74
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
75
+ | No `reason` | Continue to the stills |
76
+ | `target-missing` with `fallback` | Check the access before the selector, per the note below the table |
77
+ | `target-missing` | Open the record's `still`, find the target the record's `beat` names on the page, fix the plan, and film once more |
78
+ | `target-offscreen` | Add a setup step that brings the target into the window, being a scroll over an element already inside it, such as a fixed header, with `dy` covering the distance, or a click on a link to it, and film once more |
79
+ | `server-unreachable` | Restart the server from Step 1 and film once more |
80
+ | Any other `reason` | Report the `reason` and `message` to the operator and stop, since each is a setup step |
80
81
 
81
82
  A refusal carrying `fallback` means the screencast opened the address a second time, so a single-use link was spent before the step ran. Read `${CLAUDE_SKILL_DIR}/references/config.md` for the access the take opens with, and fix that before touching the target.
82
83
 
@@ -63,30 +63,30 @@ An audit that finds no corpus reports `absent`, which moves neither the verdict
63
63
 
64
64
  `canon audits list` reports the first 18 rows. The rows marked `outside` run by hand or from a skill and sit beside the aggregate.
65
65
 
66
- | Id | Verb | Measures | Finding |
67
- | -------------------- | ----------------------------------- | ---------------------------------------------------------------------------------- | -------------- |
68
- | `context` | `canon context audit` | Context folder structure, citations, and the requirements record's word cap | Gates |
69
- | `markdown` | `canon markdown audit` | Bans, dead links, density, and reference shape | Gates |
70
- | `skills` | `canon claude skills audit` | Both skill corpora against the skill standards | Gates |
71
- | `skills-reach` | `canon claude skills reach` | Skill bodies citing a path no target receives | Reports |
72
- | `skills-rank` | `canon claude skills rank` | Whether skill descriptions separate against a routing case corpus | Reports |
73
- | `tasks` | `canon tasks validate` | Board rows nothing resolves | Reports |
74
- | `records-plans` | `canon records validate plans` | Plan records against their shape | Reports |
75
- | `records-groundwork` | `canon records validate groundwork` | Groundwork folders against their shape | Reports |
76
- | `records-intake` | `canon records validate intake` | Intake folders against their shape | Reports |
77
- | `comments` | `canon comments scan` | Comment density by language and kind, and `TODO`, `FIXME`, `HACK`, `XXX` | Reports |
78
- | `test-order` | `canon gov test-order` | Implementations that reached branch history before their test | Reports |
79
- | `secrets` | `canon secrets scan` | Credential-shaped values in the shipped tree | Gates |
80
- | `labels` | `canon labels audit` | Changed paths no row of the label map reaches | Reports |
81
- | `restated` | `canon gov restated` | Instructions `CLAUDE.md` states that a second surface states too | Reports |
82
- | `rules` | `canon gov audit` | Rules past a shape limit, and here rules short of a ledger or out of step with one | Reports |
83
- | `counts` | `canon gov counts` | Sentences stating a closed catalog's size that disagree with the tree | Reports |
84
- | `census` | `canon census` | File count, extension breakdown, and line total | Reports |
85
- | `deps` | `canon deps audit` | Advisories against the resolved dependencies | Reports |
86
- | `outside` | `canon gov citations` | Rule citations and internal frontmatter globs reaching nothing | Gates in check |
87
- | `outside` | `canon gov superseded <old> <new>` | Where the tree still asserts a value a changed convention no longer produces | Reports |
88
- | `outside` | `canon claude skills drift <ref>` | Shipped skill bodies rewritten since a ref | Reports |
89
- | `outside` | `canon claude routing` | Per `CLAUDE.md` section, how many bullets name a path and whether a rule covers it | Reports |
66
+ | Id | Verb | Measures | Finding |
67
+ | -------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------- | -------------- |
68
+ | `context` | `canon context audit` | Context folder structure, citations, and the requirements record's word cap | Gates |
69
+ | `markdown` | `canon markdown audit` | Bans, dead links, density, and reference shape | Gates |
70
+ | `skills` | `canon claude skills audit` | Both skill corpora against the skill standards, and any ledger under `canon/ledger/skills/` | Gates |
71
+ | `skills-reach` | `canon claude skills reach` | Skill bodies citing a path no target receives | Reports |
72
+ | `skills-rank` | `canon claude skills rank` | Whether skill descriptions separate against a routing case corpus | Reports |
73
+ | `tasks` | `canon tasks validate` | Board rows nothing resolves | Reports |
74
+ | `records-plans` | `canon records validate plans` | Plan records against their shape | Reports |
75
+ | `records-groundwork` | `canon records validate groundwork` | Groundwork folders against their shape | Reports |
76
+ | `records-intake` | `canon records validate intake` | Intake folders against their shape | Reports |
77
+ | `comments` | `canon comments scan` | Comment density by language and kind, and `TODO`, `FIXME`, `HACK`, `XXX` | Reports |
78
+ | `test-order` | `canon gov test-order` | Implementations that reached branch history before their test | Reports |
79
+ | `secrets` | `canon secrets scan` | Credential-shaped values in the shipped tree | Gates |
80
+ | `labels` | `canon labels audit` | Changed paths no row of the label map reaches | Reports |
81
+ | `restated` | `canon gov restated` | Instructions `CLAUDE.md` states that a second surface states too | Reports |
82
+ | `rules` | `canon gov audit` | Rules past a shape limit or out of step with a ledger, and here rules short of one | Reports |
83
+ | `counts` | `canon gov counts` | Sentences stating a closed catalog's size that disagree with the tree | Reports |
84
+ | `census` | `canon census` | File count, extension breakdown, and line total | Reports |
85
+ | `deps` | `canon deps audit` | Advisories against the resolved dependencies | Reports |
86
+ | `outside` | `canon gov citations` | Rule citations and internal frontmatter globs reaching nothing | Gates in check |
87
+ | `outside` | `canon gov superseded <old> <new>` | Where the tree still asserts a value a changed convention no longer produces | Reports |
88
+ | `outside` | `canon claude skills drift <ref>` | Shipped skill bodies rewritten since a ref | Reports |
89
+ | `outside` | `canon claude routing` | Per `CLAUDE.md` section, how many bullets name a path and whether a rule covers it | Reports |
90
90
 
91
91
  A deliberate secret takes a `canon-allow-secret: <reason>` marker. A deliberate citation takes `canon-allow-citation: <reason>` and a deliberate superseded value takes `canon-allow-superseded: <reason>`, each on its own line or the one above. Run any verb with `--help` for its flags and refusal reasons.
92
92
 
@@ -21,7 +21,7 @@ kind: reference
21
21
  | `record-layout` | Review receipts, memory archives, feedback, renders, picks, evidence, and the design board into their current folders |
22
22
  | `session-defaults` | The seeded session defaults into an existing `.claude/settings.json`, listed below the table |
23
23
 
24
- `record-tree` follows citations in `diagrams`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`. `session-defaults` denies `Agent`, `Task`, and both merge commands, and turns auto memory off. The bullets below add detail per verb.
24
+ `record-tree` follows citations in `diagrams`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`. `session-defaults` denies `Agent`, `Task`, and both merge commands, turns auto memory off, and wires an installed `plan-guard.sh` into the PreToolUse hooks. The bullets below add detail per verb.
25
25
 
26
26
  - `skill-names` takes no `--scope`, and skips the changelog, eval transcripts, replay fixtures, and the pointer each renamed `draft-` skill leaves at its old name
27
27
  - `surface-roots` moves `context/`, `wireframes/`, `ARCHITECTURE.md`, `REQUIREMENTS.md`, `DESIGN.md`, and the stamp folder, and leaves `rules/`, `skills/`, `hooks/`, and `settings.json` in place
@@ -56,15 +56,15 @@ Three markers keep an old spelling on a line. Each also covers the nearest non-b
56
56
 
57
57
  ## Refusals
58
58
 
59
- | Verb | Refuses when |
60
- | ------------------ | ------------------------------------------------------------------------------------- |
61
- | `records` | The project does not ignore `.canon/`. Run `canon tooling sync --write` first. |
62
- | `records` | A record folder already exists at the destination |
63
- | `surface-roots` | Any destination already exists under `canon/`, refusing the whole plan |
64
- | `scratch-evidence` | A destination is occupied |
65
- | `record-layout` | A destination already exists, each move refused on its own |
66
- | `rule-layout` | A destination holds different bytes. Neither file is touched and other moves apply. |
67
- | `session-defaults` | The settings file is not valid JSON, or `permissions` or its `deny` has another shape |
59
+ | Verb | Refuses when |
60
+ | ------------------ | --------------------------------------------------------------------------------------------------------- |
61
+ | `records` | The project does not ignore `.canon/`. Run `canon tooling sync --write` first. |
62
+ | `records` | A record folder already exists at the destination |
63
+ | `surface-roots` | Any destination already exists under `canon/`, refusing the whole plan |
64
+ | `scratch-evidence` | A destination is occupied |
65
+ | `record-layout` | A destination already exists, each move refused on its own |
66
+ | `rule-layout` | A destination holds different bytes. Neither file is touched and other moves apply. |
67
+ | `session-defaults` | The settings file is not valid JSON, or `permissions`, `deny`, `hooks`, or `PreToolUse` has another shape |
68
68
 
69
69
  A `rule-layout` destination holding the same bytes marks a duplicate, which `--write` deletes.
70
70
 
@@ -108,6 +108,8 @@ It moves each surface with `git mv` and repoints every tracked citation. No othe
108
108
 
109
109
  The seeded `.claude/settings.json` now denies the `Agent` and `Task` tools and both merge commands, and sets `autoMemoryEnabled` to `false`. `canon claude sync` never rewrites a settings file you hold, so it warns while the file holds no default or lacks a merge entry. Removing `Agent` and `Task` later draws no warning.
110
110
 
111
+ The plan guard takes two steps. `canon claude init` adds the absent `.claude/hooks/plan-guard.sh`, and the verb below then wires it into `hooks.PreToolUse` for `Bash` and `Write|Edit`. It skips a tool whose matcher already runs the guard, and wires nothing while the script is absent.
112
+
111
113
  ```bash
112
114
  canon migrate session-defaults --json
113
115
  canon migrate session-defaults --write --json
@@ -20,6 +20,10 @@
20
20
  deps = []
21
21
  files = [".claude/skills", "claude/skills"]
22
22
 
23
+ ["rule"]
24
+ deps = []
25
+ files = [".claude/rules/project"]
26
+
23
27
  ["ci-workflow"]
24
28
  deps = []
25
29
  files = [".github/workflows"]
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: Rule rule ledger
3
+ description: Decisions behind governance/rules/claude/rule.md, one row per bullet
4
+ ---
5
+
6
+ # Rule rule ledger
7
+
8
+ The rule is a core rule, so every row is a house choice rather than a citation. Each names what goes wrong without its bullet and the option the bullet beat, with the date it was decided.
9
+
10
+ ## Rule files
11
+
12
+ | Bullet | Source | Read |
13
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
14
+ | Load create-rule before an edit | House choice: a session hand-editing rules without the skill shipped defects `canon gov audit` passed (issue 2842). It beat relying on the audit alone | 2026-10-29 |
15
+ | Report an unresolved skill | House choice: a project that installed governance without the plugin has no `create-rule`, and a silent skip hides that. It beat omitting the line | 2026-10-29 |
16
+ | Audit each touched rule | House choice: the audit catches the shape and ledger defects the skill's checklist names, so running it before the commit closes the loop. It beat leaving the audit to the pre-push gate alone | 2026-10-29 |
@@ -0,0 +1,13 @@
1
+ ---
2
+ description: Route edits to a rule file to the create-rule skill for its shape and checklist
3
+ paths:
4
+ - '.claude/rules/project/**/*.md'
5
+ ---
6
+
7
+ # Rule standards
8
+
9
+ ## Rule files
10
+
11
+ - Load the `canon:create-rule` skill before a hand edit to a rule, since it carries the frontmatter, the shape limits, and the bullet checklist.
12
+ - Report it rather than proceeding silently when `canon:create-rule` does not resolve, as in a project that installed governance without the plugin.
13
+ - Run `canon gov audit` over each rule the edit touched before committing it.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.74.0",
4
+ "version": "5.75.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": {
@@ -1,29 +1,29 @@
1
1
  #!/usr/bin/env bash
2
- # Commits the regenerated capture frames onto the refresh branch and opens or
2
+ # Commits the recaptured home baseline onto the refresh branch and opens or
3
3
  # updates its pull request, called by refresh-capture-frames.yml after the
4
- # capture steps. The step holds GH_TOKEN and HUSKY, since the token scope is
4
+ # capture step. The step holds GH_TOKEN and HUSKY, since the token scope is
5
5
  # read off the workflow.
6
6
  #
7
7
  # Hooks stay off, since pre-push runs a check whose format step needs shfmt,
8
8
  # which the runner lacks. verify.yml checks the pull request.
9
9
  #
10
- # The frames commit where they rendered, then only the files that commit wrote
10
+ # The baseline commits where they rendered, then only the files that commit wrote
11
11
  # or dropped carry onto main fetched now, so a merge landing mid-run neither
12
- # leaves the branch on an older base nor loses a frame it moved. The pull
12
+ # leaves the branch on an older base nor loses a baseline frame it moved. The pull
13
13
  # request resolves by number, since every merged refresh leaves a closed one on
14
14
  # the same head.
15
15
  set -e
16
16
 
17
17
  branch=chore/refresh-capture-frames
18
- title="chore(web): refresh capture frames"
18
+ title="chore(web): refresh home baseline"
19
19
 
20
- if [ -z "$(git status --porcelain assets/captures assets/frames assets/evidence)" ]; then
21
- echo "Frames are current, nothing to refresh."
20
+ if [ -z "$(git status --porcelain assets/evidence)" ]; then
21
+ echo "The home baseline is current, nothing to refresh."
22
22
  exit 0
23
23
  fi
24
24
  git config user.name "github-actions[bot]"
25
25
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com" # canon-allow-reference: the bot account id, not a commit
26
- git add assets/captures assets/frames assets/evidence
26
+ git add assets/evidence
27
27
  git commit -m "$title"
28
28
  rendered=$(git rev-parse HEAD)
29
29
  written=$(git diff --name-only --no-renames --diff-filter=d HEAD~1 HEAD)
@@ -36,8 +36,8 @@ fi
36
36
  if [ -n "$dropped" ]; then
37
37
  printf '%s\n' "$dropped" | xargs -d '\n' git rm -q --ignore-unmatch --
38
38
  fi
39
- if git diff --cached --quiet -- assets/captures assets/frames assets/evidence; then
40
- echo "Main already carries these frames, nothing to refresh."
39
+ if git diff --cached --quiet -- assets/evidence; then
40
+ echo "Main already carries this baseline, nothing to refresh."
41
41
  exit 0
42
42
  fi
43
43
  git commit -m "$title"
@@ -50,11 +50,10 @@ if [ -z "$number" ]; then
50
50
  --title "$title" \
51
51
  --body "## Summary
52
52
 
53
- - Refreshes the capture frames from main so their counts and tokens stay current.
53
+ - Refreshes the home page baseline from main so canon pr evidence compares against a current render.
54
54
 
55
55
  ## Key changes
56
56
 
57
- - Regenerates the count and token bearing frames, their images, and their stamps from main.
58
57
  - Recaptures the home page baseline under assets/evidence/home.
59
58
  - Merges itself by squash once its required check passes."
60
59
  number=$(gh pr list --head "$branch" --state open --json number --jq '.[0].number')
@@ -379,7 +379,8 @@ function restatedCounts(record: unknown): Record<string, number> | undefined {
379
379
  * a short one are fixed by different passes.
380
380
  */
381
381
  function ruleCounts(record: unknown): Record<string, number> | undefined {
382
- const rules = asObject(record)?.rules
382
+ const root = asObject(record)
383
+ const rules = root?.rules
383
384
  if (!Array.isArray(rules)) return undefined
384
385
 
385
386
  let shapeRules = 0
@@ -397,7 +398,11 @@ function ruleCounts(record: unknown): Record<string, number> | undefined {
397
398
  if ((lengthOf(ledger.mismatches) ?? 0) > 0) ledgerMismatches += 1
398
399
  }
399
400
 
400
- return { shapeRules, missingLedgers, ledgerMismatches }
401
+ // A target ledger with no rule sits beside `rules` rather than in it. A
402
+ // binary predating it carries no `orphans` key, which reads as none.
403
+ const orphanLedgers = lengthOf(root?.orphans) ?? 0
404
+
405
+ return { shapeRules, missingLedgers, ledgerMismatches, orphanLedgers }
401
406
  }
402
407
 
403
408
  function findingsOnly(record: unknown): Record<string, number> | undefined {
@@ -6,13 +6,11 @@ const STAMP_EXTENSION = '.stamp'
6
6
 
7
7
  /**
8
8
  * Provenance for one rendered PNG: the markup it came from and a digest over
9
- * each side of the pair. `assertStampField` in `src/gate/measures.ts` reads both
10
- * digests back off the first whitespace-separated token, so the field names, the
11
- * colon, and the space after it are a contract between the writer and the gate
12
- * rather than formatting.
9
+ * each side of the pair. Nothing reads the digests back today, so no check
10
+ * fails when either side moves.
13
11
  *
14
12
  * Both sides are recorded because either can move alone. The markup digest
15
- * catches an edit committed with no capture, and the image digest catches a PNG
13
+ * would show an edit committed with no capture, and the image digest a PNG
16
14
  * replaced under markup that never changed.
17
15
  */
18
16
  export interface CaptureStamp {
@@ -82,6 +82,7 @@ export const LEDGER_SKILLS: readonly string[] = [
82
82
  join(SHIPPED_CORPUS, 'write-human'),
83
83
  join(SHIPPED_CORPUS, 'test-first'),
84
84
  join(SHIPPED_CORPUS, 'ui-craft'),
85
+ join(SHIPPED_CORPUS, 'proposal-critique'),
85
86
  ]
86
87
 
87
88
  function references(...names: string[]): string[] {
@@ -125,6 +126,7 @@ export const BULLET_LEDGER_SKILLS: Readonly<Record<string, BulletLedgerScope>> =
125
126
  [join(SHIPPED_CORPUS, 'write-human')]: { exempt: [], body: true },
126
127
  [join(SHIPPED_CORPUS, 'systematic-debugging')]: { exempt: [], body: true },
127
128
  [join(SHIPPED_CORPUS, 'build-in-slices')]: { exempt: [], body: true },
129
+ [join(SHIPPED_CORPUS, 'proposal-critique')]: { exempt: [], body: true },
128
130
  }
129
131
 
130
132
  /** Where a per-bullet ledger lives, as `<skill>/<file>.md` below it. */
@@ -139,6 +141,15 @@ export function skillLedgerPath(folder: string, file: string): string {
139
141
  return join(SKILL_LEDGERS, folder, inside.startsWith('..') ? file : inside)
140
142
  }
141
143
 
144
+ /**
145
+ * Where a target keeps the ledgers behind its own skills, as `<skill>/<file>.md`
146
+ * below it. A tree holding `governance/rules/` is the toolkit, which keeps its
147
+ * ledgers at `SKILL_LEDGERS`, so this folder is never read there.
148
+ */
149
+ export const PROJECT_SKILL_LEDGERS = join('canon', 'ledger', 'skills')
150
+
151
+ const TOOLKIT_MARKER = join('governance', 'rules')
152
+
142
153
  /** The closing H2s a practice skill carries, matched exactly outside fences. */
143
154
  export const PRACTICE_SECTIONS: readonly string[] = [
144
155
  'Excuses and rebuttals',
@@ -312,6 +323,9 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
312
323
  present.map((corpus) => readCorpus(root, corpus.rel, corpus.dir)),
313
324
  )
314
325
  const sources = perCorpus.flat()
326
+ const projectLedgers = existsSync(join(root, TOOLKIT_MARKER))
327
+ ? []
328
+ : await readProjectLedgers(root, sources)
315
329
 
316
330
  return {
317
331
  corpora: present.map((corpus, index) => ({
@@ -344,6 +358,7 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
344
358
  sources,
345
359
  ),
346
360
  ...sources.flatMap(practiceFindings),
361
+ ...projectLedgers,
347
362
  ],
348
363
  referenceContents: sources.flatMap((source) =>
349
364
  source.bodies.flatMap(contentsFindings),
@@ -483,6 +498,56 @@ async function readReferenceLedgers(
483
498
  )
484
499
  }
485
500
 
501
+ /**
502
+ * Pairs each file a target wrote under `PROJECT_SKILL_LEDGERS` with the skill
503
+ * file of that name, `SKILL.md` with the body and any other with the reference
504
+ * at `references/<file>`. Nothing is owed, since a project has no list to owe a
505
+ * ledger against, so only a ledger that exists is read, and one naming a skill
506
+ * or a file that is gone is reported rather than skipped.
507
+ */
508
+ async function readProjectLedgers(
509
+ root: string,
510
+ sources: readonly SkillSource[],
511
+ ): Promise<SkillFinding[]> {
512
+ const dir = join(root, PROJECT_SKILL_LEDGERS)
513
+ if (!existsSync(dir)) return []
514
+ const paths = [
515
+ ...new Bun.Glob('**/*.md').scanSync({ cwd: dir, onlyFiles: true }),
516
+ ].sort()
517
+
518
+ const perLedger = await Promise.all(
519
+ paths.map(async (path): Promise<SkillFinding[]> => {
520
+ const ledgerRel = join(PROJECT_SKILL_LEDGERS, path)
521
+ const [folder, ...rest] = path.split('/')
522
+ const owners = sources.filter((source) => source.folder === folder)
523
+ if (rest.length === 0 || owners.length === 0) {
524
+ return [{ rel: ledgerRel, detail: `missing skill: ${folder}` }]
525
+ }
526
+ const file = rest.join('/')
527
+ const target = file === 'SKILL.md' ? file : join('references', file)
528
+ const rows = countLedgerRows(await Bun.file(join(dir, path)).text())
529
+
530
+ return owners.flatMap((owner) => {
531
+ const rel = join(owner.rel, target)
532
+ const body = owner.bodies.find((entry) => entry.rel === rel)
533
+ if (body === undefined) {
534
+ return [{ rel: ledgerRel, detail: `missing file: ${rel}` }]
535
+ }
536
+ return referenceLedgerFindings({
537
+ rel,
538
+ ledgerRel,
539
+ mismatches: compareLedger(
540
+ ledgerGroups(body.text, target === 'SKILL.md'),
541
+ rows,
542
+ ),
543
+ sources: [],
544
+ })
545
+ })
546
+ }),
547
+ )
548
+ return perLedger.flat()
549
+ }
550
+
486
551
  /**
487
552
  * Directives per H2, `null` for any above the first. A contents list repeats
488
553
  * the headings rather than stating a rule, so `## Contents` takes no rows. A