@erclx/canon 4.113.0 → 4.115.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 (63) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/codebase-layout/SKILL.md +1 -1
  3. package/claude/skills/document-health/references/axes.md +1 -1
  4. package/claude/skills/target-setup/SKILL.md +3 -1
  5. package/claude/skills/target-setup/references/detect.md +43 -9
  6. package/claude/skills/target-setup/references/verify.md +26 -10
  7. package/claude/skills/teach-workspace/SKILL.md +5 -3
  8. package/claude/skills/test-craft/SKILL.md +1 -1
  9. package/docs/agents/audits.md +4 -2
  10. package/docs/agents/commands.md +2 -2
  11. package/docs/agents/context-audit-checks.md +6 -6
  12. package/docs/agents/context-audit.md +1 -1
  13. package/docs/agents/context-classify.md +1 -1
  14. package/docs/agents/counts.md +3 -3
  15. package/docs/agents/install-and-sync.md +2 -0
  16. package/docs/agents/records.md +22 -15
  17. package/docs/agents/teach.md +12 -4
  18. package/docs/target-projects.md +11 -2
  19. package/docs/workflow/ai-workflow.md +1 -1
  20. package/governance/rules/lang/130-go.md +40 -0
  21. package/governance/rules/lang/140-php.md +31 -0
  22. package/governance/rules/lib/335-testing-go.md +35 -0
  23. package/governance/rules/lib/336-testing-php.md +30 -0
  24. package/governance/rules/lib/360-security-server.md +2 -0
  25. package/governance/rules/lib/370-database.md +2 -0
  26. package/governance/rules/ui/410-a11y.md +1 -0
  27. package/governance/rules/ui/440-surface-capture.md +2 -0
  28. package/governance/stacks/nextjs.toml +2 -0
  29. package/governance/stacks/react.toml +1 -1
  30. package/package.json +1 -1
  31. package/src/audits/catalog.ts +52 -0
  32. package/src/commands/context.ts +30 -10
  33. package/src/commands/records.ts +18 -1
  34. package/src/commands/teach.ts +4 -1
  35. package/src/commands/tooling.ts +25 -2
  36. package/src/context/audit.ts +54 -18
  37. package/src/design/components.ts +17 -0
  38. package/src/design/tokens.ts +1 -1
  39. package/src/gate/measures.ts +13 -2
  40. package/src/records/backup.ts +197 -27
  41. package/src/records/push-guard.ts +71 -0
  42. package/src/teach/nav.ts +278 -34
  43. package/src/tooling/inject.ts +43 -4
  44. package/src/tooling/scan.ts +35 -0
  45. package/src/tooling/subfolder.ts +110 -0
  46. package/standards/teach.md +13 -12
  47. package/tooling/base/configs/.husky/post-merge +5 -1
  48. package/tooling/go/configs/.golangci.yml +25 -0
  49. package/tooling/go/configs/scripts/verify.sh +95 -0
  50. package/tooling/go/manifest.toml +13 -0
  51. package/tooling/go/reference.md +60 -0
  52. package/tooling/go/seeds/.cspell/tech-stack.txt +16 -0
  53. package/tooling/go/seeds/smoke_test.go +9 -0
  54. package/tooling/php/configs/.php-cs-fixer.dist.php +18 -0
  55. package/tooling/php/configs/phpstan.neon +5 -0
  56. package/tooling/php/configs/phpunit.xml +18 -0
  57. package/tooling/php/configs/scripts/verify.sh +100 -0
  58. package/tooling/php/manifest.toml +15 -0
  59. package/tooling/php/reference.md +74 -0
  60. package/tooling/php/seeds/.cspell/tech-stack.txt +5 -0
  61. package/tooling/php/seeds/tests/SmokeTest.php +13 -0
  62. package/tooling/python/configs/scripts/verify.sh +32 -15
  63. package/tooling/web/configs/scripts/verify.sh +32 -15
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.113.0",
4
+ "version": "4.115.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -54,4 +54,4 @@ Read `${CLAUDE_SKILL_DIR}/references/adopted.md` only when extending this guidan
54
54
  - Which layer a test belongs at: `test-craft`. This skill decides where the test file sits once its layer is chosen.
55
55
  - Where a new path gets written into a plan, and the plan's shape: `plan-feature` and the plan standard
56
56
  - The directory principle every session loads: `000-constitution`
57
- - Runner, suffix, and top-level test folder conventions per language: `300-testing-ts` and `330-testing-py`
57
+ - Runner, suffix, and top-level test folder conventions per language: `300-testing-ts`, `330-testing-py`, `335-testing-go`, and `336-testing-php`
@@ -40,7 +40,7 @@ Weight is a judgment a reader settles rather than a defect, which the verb state
40
40
 
41
41
  **Evidence class:** `measured` for the audit's findings, `classified` for the sweep's verdicts.
42
42
 
43
- The audit answers required sections, entry length, citation resolution, reference form, catalog tables, provenance, and index drift. A citation resolving to nothing is a fact and gates. Entry length, reference form, table shape, and provenance are judgments under every flag.
43
+ The audit answers required sections, entry length, citation resolution, reference form, catalog tables, provenance, and index drift. Required sections reach wireframes as well as context entries, naming each wireframe short of the regions, states, copy, or exclusions section its standard requires, which is how a surface on an older wireframe shape reports rather than reading as clean. A citation resolving to nothing is a fact and gates. Entry length, reference form, table shape, and provenance are judgments under every flag.
44
44
 
45
45
  The sweep classifies each section as keep, replace or rewrite, history, or move. A `move` verdict is the placement finding proper, naming content sitting in the wrong document. Each verdict carries its own confidence, which is why the class is `classified` rather than `measured`, and a low-confidence `move` is a prompt to look rather than an instruction to cut.
46
46
 
@@ -92,7 +92,7 @@ Step 2: `canon tooling sync <tooling-stack> --write` installs stack deps, script
92
92
  CANON_NON_INTERACTIVE=1 canon tooling sync <tooling-stack> <target> --write
93
93
  ```
94
94
 
95
- Monorepo with multiple language roots: run `canon init` once at the repo root so `base` (husky, prettier, cspell, commitlint, CI) lands single, then sync each subtree with `--skip base` so the shared layer is not re-dropped.
95
+ Monorepo with multiple language roots: run `canon init` once at the repo root so `base` (husky, prettier, cspell, commitlint, CI) lands single, then sync each subtree with `--skip base` so the shared layer is not re-dropped. Take the subtree commands from the per-root rows the preview printed rather than picking folders by hand. Detection found each root and its stack, and the two commands below show the shape.
96
96
 
97
97
  ```bash
98
98
  CANON_NON_INTERACTIVE=1 canon tooling sync vite-react ./frontend --skip base --write
@@ -101,6 +101,8 @@ CANON_NON_INTERACTIVE=1 canon tooling sync python ./backend --skip base --write
101
101
 
102
102
  Without `--skip base`, each subtree re-drops husky, and git honors only one `core.hooksPath`, so the extra hook dirs silently break. Each subtree keeps its own framework configs, and its own stack reference reads through `canon tooling reference <stack>`.
103
103
 
104
+ A subtree sync withholds `.github/`, since GitHub reads workflows only at the repository root, and names each withheld file with the `working-directory` a root CI job needs. It also writes a nested `cspell.json` that registers the subtree's word lists. A subtree needs its own `bun init` before its scripts and dependencies land. The sync warns and skips them until then.
105
+
104
106
  Step 3: post-sync fixups. Golden configs arrive from sync, so no config generation is required. But a few items may need a one-time touch:
105
107
 
106
108
  - **ESLint version pin.** If `bun create vite` installed `eslint@^10` and the manifest pins `eslint@^9`, sync does not override a present dep. Run `bun add -d eslint@^9` if `bun run lint:fix` fails with `Class extends value undefined`.
@@ -27,19 +27,53 @@ since the chain reaches the tooling resolution a moment later.
27
27
 
28
28
  ## Detect
29
29
 
30
- Read these from the project root in parallel, skipping any that do not exist:
30
+ A monorepo keeps its languages in subfolders, so read the markers from every
31
+ folder to depth two rather than from the root alone. Collect the language roots
32
+ first:
33
+
34
+ ```bash
35
+ git ls-files --cached --others --exclude-standard \
36
+ | grep -E '(^|/)(package\.json|pyproject\.toml|go\.mod|composer\.json|Cargo\.toml)$' \
37
+ | awk -F/ 'NF <= 3'
38
+ ```
39
+
40
+ Every folder holding a marker is a language root, and the repository root is
41
+ one when it holds a marker itself. Going through git keeps ignored trees such as
42
+ `node_modules/`, `vendor/`, and `.venv/` off the list, since each carries
43
+ markers of its own. Outside a git repository, fall back to
44
+ `find . -maxdepth 3 \( -path '*/.*' -o -name node_modules -o -name vendor \) -prune -o -type f -print`
45
+ filtered to the same five names.
46
+
47
+ Depth two reaches a service folder at depth one and a workspace package under
48
+ `packages/<name>/` at depth two. Deeper, the walk starts finding fixtures and
49
+ examples as false roots.
50
+
51
+ Then read these per language root in parallel, skipping any that do not exist:
31
52
 
32
53
  - `package.json`: `dependencies` and `devDependencies`
33
- - Root configs: `astro.config.*`, `next.config.*`, `vite.config.*`, `tailwind.config.*`, `tsconfig.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`
54
+ - The root's own configs: `astro.config.*`, `next.config.*`, `vite.config.*`, `tailwind.config.*`, `tsconfig.json`, `pyproject.toml`, `go.mod`, `composer.json`, `Cargo.toml`
55
+
56
+ A non-JavaScript marker outranks a `package.json` in the same folder. A Go or
57
+ Composer service often carries a `package.json` only to host scripts, so that
58
+ folder is a Go or PHP root rather than a JavaScript one.
59
+
60
+ Read these once, from the repository root:
61
+
34
62
  - `canon/REQUIREMENTS.md` and `canon/ARCHITECTURE.md` if present
35
63
  - Directory structure via `ls -1` of the project root and `src/` if present
36
64
 
37
65
  ## Resolve arguments
38
66
 
39
- - **Stack:** pick the closest governance stack by matching detected runtime or framework against stack names in the catalog. If nothing matches, fall back to `base` and carry the fallback into the preview.
40
- - **Tooling stack:** pick the closest tooling stack from `canon tooling list --json` (e.g. `vite-react`, `astro`, `nextjs`). Distinct from the governance stack. Fall back to `base` if no framework match, and carry that fallback into the preview too.
41
- - **Next.js:** map `next` in `package.json` dependencies, or a root `next.config.ts`/`.js`/`.mjs` file, to the `nextjs` tooling stack. The dependency name does not match the stack name by itself.
42
- - **Extras:** identify technologies not already covered by the picked stack. For each, find a rule whose `description` or `paths` points at that technology and pass it via `--add`. Dedupe against the stack's own rules. Do not add a rule the stack already pulls in.
67
+ Governance installs once, at the repository root, and a rule's `paths:` globs
68
+ already reach every subfolder. Tooling installs per folder. So resolve one
69
+ governance stack for the whole repository and one tooling stack per language
70
+ root.
71
+
72
+ - **Stack:** pick the closest governance stack by matching the repository root's detected runtime or framework against stack names in the catalog. If the root matched nothing, `base` is the stack, including when subfolders matched, since no subfolder owns the repository and picking one by order would state a preference nobody gave. Mark it `fallback` in the preview only when no root matched anywhere.
73
+ - **Tooling stack:** for each language root, pick the closest tooling stack from `canon tooling list --json` (e.g. `vite-react`, `astro`, `nextjs`). Distinct from the governance stack. Fall back to `base` for a root with no framework or language match, and carry that fallback into its preview row.
74
+ - **Next.js:** map `next` in a root's `package.json` dependencies, or a `next.config.ts`/`.js`/`.mjs` file in that root, to the `nextjs` governance stack and the `nextjs` tooling stack. The dependency name does not match either stack name by itself.
75
+ - **Extras:** identify technologies not already covered by the picked governance stack, across every language root. A subfolder that matched a governance stack of its own contributes that stack's rules, read from `canon gov list --json`, and every root contributes its language rule. For each technology, find a rule whose `description` or `paths` points at it and pass it via `--add`. Dedupe against the picked stack's own rules. Do not add a rule the stack already pulls in.
76
+ - **Subfolder tooling:** a language root below the repository root takes its own `canon tooling sync <stack> <path> --skip base`, since `canon init` already lands `base` once at the root. Leave a root whose stack resolves to `base` out of the commands, since that sync would land nothing. A repository-root `package.json` hosting only formatters and hooks is that case, and it adds no row.
43
77
  - **Skip (`--skip`):** `wiki` installs by default. Add `--skip wiki` only when the user explicitly wants it left out.
44
78
 
45
79
  ## Gap handling
@@ -67,10 +101,10 @@ toolkit repository, and option 2 hands the project-local file to `create-rule`.
67
101
 
68
102
  Before executing, output:
69
103
 
70
- - **Detected:** each technology with its evidence file
104
+ - **Detected:** each technology with its evidence file, path included, so a subfolder marker reads as one
71
105
  - **Stack:** picked governance stack and resolved rule count. Mark it `fallback` when no detected runtime or framework matched a catalog name.
72
- - **Tooling stack:** picked tooling stack. Mark it `fallback` on the same test, and name what `base` lands: configs, seeds, and gitignore entries in every case, plus the JavaScript development dependencies, scripts, and hook activation wherever a `package.json` exists to carry them. A project outside that ecosystem runs none of the second group and keeps the first. Omit this line entirely on the `gov` phase, which syncs no tooling.
73
- - **Extras:** each `--add` rule with a one-line reason
106
+ - **Tooling stack:** one row per language root, each carrying its path, its picked tooling stack, a `fallback` mark on the same test, and its own command. On the repository root's row, name what `base` lands: configs, seeds, and gitignore entries in every case, plus the JavaScript development dependencies, scripts, and hook activation wherever a `package.json` exists to carry them. A project outside that ecosystem runs none of the second group and keeps the first. Omit these rows entirely on the `gov` phase, which syncs no tooling.
107
+ - **Extras:** each `--add` rule with a one-line reason, listed in full. A second matched stack reaches the install only through this list, since `canon init --stack` takes one stack, so a shortened list reads as that stack dropped.
74
108
  - **Skip:** any `--skip` entries with reason
75
109
  - **Target:** resolved target path
76
110
  - **Commands:** the full chain that will run
@@ -14,19 +14,33 @@ name and a stack exposing a different set is covered without an edit here.
14
14
 
15
15
  ## Guards
16
16
 
17
- - If `package.json` does not exist at the project root, stop: `❌ No package.json found. Cannot verify.`
18
- - If `node_modules/` does not exist, run `bun install` first, then proceed.
17
+ A monorepo syncs each language root into its own subfolder, so the project root
18
+ is one folder to verify among several rather than the only one. Collect the
19
+ folders first:
20
+
21
+ ```bash
22
+ canon targets list --sweep <root> --json
23
+ ```
24
+
25
+ - The root is a folder to verify when it carries a `package.json`.
26
+ - Each path in the record's `targets` that sits under the root and carries its own `package.json` is a folder to verify.
27
+ - A path under the root with no `package.json` is not verified. Name it as skipped with its fix, `run 'bun init' in <path>, then sync the stack again`, and carry on with the rest.
28
+ - If neither the root nor any stamped subfolder carries a `package.json`, stop: `❌ No package.json at the root or in any synced subfolder. Cannot verify.`
29
+ - In each folder to verify, if `node_modules/` does not exist, run `bun install` there first, then proceed.
19
30
 
20
31
  ## Step 1: read scripts
21
32
 
22
- Read `package.json` from the project root and extract the `scripts` block. Do
23
- not hardcode script names. Different stacks expose different scripts.
33
+ Read `package.json` from each folder and extract its `scripts` block. Do not
34
+ hardcode script names. Different stacks expose different scripts, and a
35
+ subfolder synced with `--skip base` declares none of the scripts the root owns.
24
36
 
25
37
  ## Step 2: run the chain
26
38
 
27
- Run the scripts for the requested depth in order. Stop on the first failure and
28
- surface the error. Skip any script not present in `package.json`. Do not invent
29
- a fallback command. Run each as `bun run <script>` from the project root.
39
+ Run the chain once per folder, root first. In each, run the scripts for the
40
+ requested depth in order, stop that folder on its first failure, and surface the
41
+ error. Skip any script not present in that folder's `package.json`. Do not
42
+ invent a fallback command. Run each as `bun run <script>` from the folder it was
43
+ read from.
30
44
 
31
45
  ### Default depth
32
46
 
@@ -79,15 +93,17 @@ port to poll across stacks, so port-probing is out of reach here.
79
93
 
80
94
  ## Step 3: report
81
95
 
82
- For each script run, report one of:
96
+ Report per folder, under a heading naming its path relative to the root, and
97
+ list each skipped folder with its `bun init` fix. For each script run, report
98
+ one of:
83
99
 
84
100
  - `✅ <script>`
85
101
  - `❌ <script>` followed by the failing output (last 40 lines) or, for a server script, the reason judged (process died, or a fatal string in its output)
86
102
 
87
103
  End with a summary line naming the depth that ran:
88
104
 
89
- - On pass: `✅ Scaffold verified at <depth> (<n> scripts passed).`
90
- - On fail: `❌ Scaffold failed at <script>. Fix the error and re-run the verify phase.`
105
+ - On pass: `✅ Scaffold verified at <depth> (<n> scripts passed across <m> folders).`
106
+ - On fail: `❌ Scaffold failed at <script> in <folder>. Fix the error and re-run the verify phase.`
91
107
 
92
108
  ## Out of scope
93
109
 
@@ -86,7 +86,7 @@ Reading the last record alone is what the schedule replaces. A topic missed thre
86
86
  Two outputs with two lifetimes, and the split decides the format.
87
87
 
88
88
  - A lesson is a self-contained page carrying its own quiz, a teach-back block, and the feedback for each question. It embeds one shared stylesheet rather than restating styles, and it is disposable and never promoted.
89
- - A reference page goes to `reference/<slug>.md`, written for a reader with no learner in it. This is the half that survives the workspace, so it is written in markdown to pass the authoring gates a promotion would put it through.
89
+ - A reference page goes to `reference/<slug>.md`, written for a reader with no learner in it. This is the half that survives the workspace, so it is written in markdown to pass the authoring gates a promotion would put it through. `canon teach nav` renders it to a `reference/<slug>.html` sibling, and a lesson links that sibling, since a served `.md` opens as plain text.
90
90
 
91
91
  Resolve the lesson before writing it, rather than composing its name or its quiz order by hand:
92
92
 
@@ -167,12 +167,14 @@ Follow `${CLAUDE_SKILL_DIR}/references/lesson-craft.md` for what makes a lesson
167
167
 
168
168
  ### Hand over a link, never a path
169
169
 
170
- A lesson is a page carrying a script, and an editor preview cannot run it, so a path alone opens markup with no working quiz. Serve the teach root and give the learner a link they can click:
170
+ A lesson is a page carrying a script, and an editor preview cannot run it, so a path alone opens markup with no working quiz. Serve the teach root and give the learner a link they can click. Take the serve line from the `Open` step of `canon teach list <topic>`, which resolves the teach folder against the directory the session runs in:
171
171
 
172
172
  ```bash
173
- canon serve .canon/teach --entry <nn>-<topic>/index.html --json
173
+ canon teach list <topic>
174
174
  ```
175
175
 
176
+ Run the line it prints with `--json` appended, rather than writing `canon serve .canon/teach` by hand. `canon serve` resolves its folder against the cwd, so that literal serves an absent folder from a linked worktree, where the list verb reads the main worktree root.
177
+
176
178
  Start it in the background so the session keeps going, and read `url` off the record rather than composing one. The verb walks past a port already in use, so the port it took is exactly the half a guessed URL gets wrong. Report the refusal and its `reason` when `ok` is false, and report it rather than proceeding silently when the verb does not resolve at all, which is an installed CLI predating it.
177
179
 
178
180
  Do this on every run that opens or resumes a workspace, including one that writes no lesson, since the learner's route into what is already there is the same link.
@@ -73,5 +73,5 @@ Check every test written this session against each line. A test that fails one i
73
73
  - The red, green, refactor procedure and its order: `test-first`, which loads this skill at its first step
74
74
  - Reproducing a failure with no known cause: `systematic-debugging`
75
75
  - Test structure floors such as arrange, act, assert, one behavior per test, isolation, and no snapshots: `010-testing`
76
- - Runner, file suffix, placement, and query conventions per language: `300-testing-ts` and `330-testing-py`
76
+ - Runner, file suffix, placement, and query conventions per language: `300-testing-ts`, `330-testing-py`, `335-testing-go`, and `336-testing-php`
77
77
  - Settle, guard, and run-scope rules inside an end to end file: `305-e2e-reliability` and `306-test-scope`
@@ -24,11 +24,13 @@ canon audits list --json
24
24
 
25
25
  ## What it runs
26
26
 
27
- Nineteen verbs, listed by `canon audits list`. Each runs once in its fullest form, and the aggregate reads that verb's own record rather than imposing a shared envelope on it. Every one of those records already has consumers naming its keys, so a common shape would be a breaking change bought for tidiness.
27
+ Twenty-one verbs, listed by `canon audits list`. Each runs once in its fullest form, and the aggregate reads that verb's own record rather than imposing a shared envelope on it. Every one of those records already has consumers naming its keys, so a common shape would be a breaking change bought for tidiness.
28
28
 
29
29
  The verbs walk separate trees and share no state, so they run together. Measured on the authoring machine at twelve verbs, a run finished in 0.8 seconds of wall clock against 4.4 seconds of processor, which is under every other stage in `bun run check`. `canon deps audit` is the one that changes that reading, since it reaches a network rather than a tree and its latency is the index's rather than this machine's: three clean runs measured 44.7, 77.6, and 62.9 seconds, and a stalled lookup runs to an explicit 120-second timeout rather than to bun's own 299-second ceiling. `--corpus` is what keeps a caller from paying that cost when it wants only the corpora on this disk.
30
30
 
31
- Sixteen of the nineteen read a tree on this disk, the one added by `restated.md` and the one added by `census.md` among them, since the first reads four such trees against each other and the second counts every file in one. The two added by `state-scoped-risk.md` read committed state rather than an arriving change, which is the gap every review surface here leaves by construction, and the one added by `label-coverage.md` reads a branch range against a map the project declares.
31
+ Eighteen of the twenty-one read a tree on this disk, the one added by `restated.md` and the one added by `census.md` among them, since the first reads four such trees against each other and the second counts every file in one. The two added by `state-scoped-risk.md` read committed state rather than an arriving change, which is the gap every review surface here leaves by construction, and the one added by `label-coverage.md` reads a branch range against a map the project declares.
32
+
33
+ The placement entry runs `canon context classify sweep` pinned to `--backend off`, so it reads the regex layer alone whatever classifier setting the project resolves. A count that moves with whichever model a machine runs is not comparable across clones, and the model layer turned a sub-second read into one measured at about 17 minutes over this repository's context and wireframe sections on the authoring machine. It retains one tally per verdict, `keep`, `rewrite`, and `move`, and gates on nothing, since each verdict is a judgment. `document-health` is the surface that reads the same sections with the model.
32
34
 
33
35
  Each is invoked as the CLI the caller is running rather than as a global `canon`. A globally installed binary resolves to the main checkout no matter which worktree is executing, so the aggregate would measure a tree the branch never touched and report a pass over it.
34
36
 
@@ -47,14 +47,14 @@ Full help: `canon <command> --help`. Bare `canon --help` lists every top-level c
47
47
  | `canon teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
48
48
  | `canon teach lesson` | Resolve the numbered path, shared stylesheet, mission success lines, and one quiz option order per question for the next lesson, writing nothing (`--json`) |
49
49
  | `canon teach stylesheet` | Seed a workspace stylesheet from the design source, leaving an existing one alone without `--force` (`--json`) |
50
- | `canon teach nav` | Rewrite the teach-root listing, a workspace's contents page, and each lesson's chrome from its four marker regions (`--json`) |
50
+ | `canon teach nav` | Rewrite the teach-root listing, a workspace's contents page, each lesson's chrome from its four marker regions, and each reference page's rendered HTML sibling (`--json`) |
51
51
  | `canon teach render` | Render a JSON block list (heading, paragraph, list, raw) from stdin to HTML through the shared components, refusing `bad-input` by block index (`--json`) |
52
52
  | `canon records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
53
53
  | `canon records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
54
54
  | `canon records ordinal` | Report or claim the next ordinal `intake` and `groundwork` share, creating the folder atomically (`--claim`, `--json`) |
55
55
  | `canon records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
56
56
  | `canon records prune-tmp` | Report scratch nobody has touched inside an age window and delete it (`--write`, `--older-than`, `--json`) |
57
- | `canon records push` | Commit every record folder less the exclusion set and push it to a private records remote (`--json`) |
57
+ | `canon records push` | Commit every record folder less the exclusion set and push it to a private records remote, refusing an oversized or credential-bearing file (`--json`) |
58
58
  | `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
59
59
  | `canon migrate rename` | Rewrite every unprotected `aitk` token to `canon` and move the paths that carry the name, reporting the plan without `--write` (`--scope`, `--json`) |
60
60
  | `canon migrate skill-names` | Move the prefixed skill folders onto their two-word names and rewrite every citation, reporting the plan without `--write` (`--root`, `--json`) |
@@ -9,11 +9,11 @@ What each finding from `canon context audit` means. The command surface, its fla
9
9
 
10
10
  ## Required sections
11
11
 
12
- The required-section check reports what does not declare `## Overview` and `## Layout`, the two sections `standards/context.md` marks required. The list is held in code beside the numeric checkpoints rather than parsed out of the standard, so it fails on a defect rather than on a rewrite of the wording around it. A heading at any level counts, because a domain that split into a folder carries its overview in a sibling named `overview.md` where the section is the `#` title and an `##` beneath it would repeat the filename. Matching exactly is what keeps `## Layout catalog` from satisfying `Layout`.
12
+ The required-section check reports what does not declare the sections its folder's standard requires. A context entry owes `## Overview` and `## Layout`, the two sections `standards/context.md` marks required. A wireframe owes `## Regions`, `## States`, `## Copy`, and `## Not on this surface`, the sections answering the four questions `standards/wireframes.md` tests a surface against. `## Behavior` is expected of a wireframe rather than required, since a static surface has no interaction to state. The lists are held in code beside the numeric checkpoints rather than parsed out of the standard, so it fails on a defect rather than on a rewrite of the wording around it. A heading at any level counts, because a domain that split into a folder carries its overview in a sibling named `overview.md` where the section is the `#` title and an `##` beneath it would repeat the filename. Matching exactly is what keeps `## Layout catalog` from satisfying `Layout`.
13
13
 
14
- Which unit answers depends on the folder. Entries of the folder named under `.claude/` are one domain each, so each answers for itself and a finding names the entry. Entries of a folder a domain split into describe that one domain between them, so any sibling answers and a finding names the folder. Holding a split folder to the rule per file would report every child beside its `overview.md`, and rolling the named folder up would let one conforming entry stand in for every other domain sitting next to it.
14
+ Which unit answers depends on the folder. Entries of the folder named under `.claude/` are one domain each, so each answers for itself and a finding names the entry. Entries of a folder a domain split into describe that one domain between them, so any sibling answers and a finding names the folder. Holding a split folder to the rule per file would report every child beside its `overview.md`, and rolling the named folder up would let one conforming entry stand in for every other domain sitting next to it. A wireframe answers for itself in every folder, nested or not, since its standard keeps one surface per file and a subfolder of wireframes groups surfaces rather than splitting one.
15
15
 
16
- It reports rather than gates by default, the closer call because a missing section reads more like a fact than the other judgments. What settles it is that the standard sanctions omitting `## Layout` from a domain owning no paths in the repo, and no measure separates that from an entry that forgot it. A domain covering only external tools is that case, and it reports on every run. The JSON record carries the findings as `missingSections` and the list as `checkpoints.requiredSections`.
16
+ It reports rather than gates by default, the closer call because a missing section reads more like a fact than the other judgments. What settles it is that the standard sanctions omitting `## Layout` from a domain owning no paths in the repo, and no measure separates that from an entry that forgot it. A domain covering only external tools is that case, and it reports on every run. The JSON record carries the findings as `missingSections`, the lists per folder name as `checkpoints.requiredSectionsByFolder`, and the context list alone as `checkpoints.requiredSections`, the flat key it carried before wireframes owed a section.
17
17
 
18
18
  `--gate` promotes the finding to a failing exit code, which the seed stage runs and no other caller does. That mode needs an answer to the sanctioned omission above, so a file declaring `stub: true` in its frontmatter is dropped before the check and reported nowhere. Both are described in `context-audit.md`.
19
19
 
@@ -127,11 +127,11 @@ All three wireframe findings are printed under a bare run. The states-mismatch f
127
127
 
128
128
  ## Which folders each check reaches
129
129
 
130
- The provenance, required-section, and narration checks cover `canon/context/` alone, the reference-form check covers the split folders inside it, length and the table finding reach every audited folder, and the wireframe states check covers `canon/wireframes/` alone.
130
+ The provenance and narration checks cover `canon/context/` alone, the reference-form check covers the split folders inside it, the required-section check covers `canon/context/` and `canon/wireframes/`, length and the table finding reach every audited folder, and the wireframe states check covers `canon/wireframes/` alone.
131
131
 
132
- What narrows the three is stated in `standards/context.md`, which opens its scope by handing diagrams and wireframes to `diagrams.md` and `wireframes.md`, and the sibling standards do not restate it. A marker reported in a diagram entry would cite a rule that entry's own standard routes elsewhere, and a diagram entry carries a heading per kind rather than a run of bullets deciding anything. The split is between kinds of rule rather than kinds of folder, and what decides it is which tier states the rule rather than what the check measures.
132
+ What narrows the first two is stated in `standards/context.md`, which opens its scope by handing diagrams and wireframes to `diagrams.md` and `wireframes.md`, and the sibling standards do not restate it. A marker reported in a diagram entry would cite a rule that entry's own standard routes elsewhere, and a diagram entry carries a heading per kind rather than a run of bullets deciding anything. The split is between kinds of rule rather than kinds of folder, and what decides it is which tier states the rule rather than what the check measures.
133
133
 
134
- Length and the table finding generalize as judgments about how far a reader travels, so both reach wherever the audit is pointed. Required sections narrow for a plainer reason: the names are the context standard's own, and neither sibling standard states a required section at all.
134
+ Length and the table finding generalize as judgments about how far a reader travels, so both reach wherever the audit is pointed. Required sections narrow for a plainer reason: each list is one standard's own, so it reaches the folder that standard governs. The context and wireframe standards each state a set, and the diagram standard states none.
135
135
 
136
136
  The same test is what moved depth and bullet weight out of this command entirely. A rule stated at the attribute tier reaches every markdown file, and a check reaching every markdown file has no reason to require a folder that resolves.
137
137
 
@@ -88,7 +88,7 @@ The pattern spells both record-root prefixes, so a citation into a folder that h
88
88
 
89
89
  ## The wireframe states gate
90
90
 
91
- Every entry under `canon/wireframes/` carrying a `## States` table is checked one-to-one against its evidence folders, matching the table's `State` and `Evidence` columns by header text rather than position, so a project reordering the template's own columns still resolves. An entry with no such table reports nothing, which is what leaves a project on an older wireframe shape silent until it adopts one.
91
+ Every entry under `canon/wireframes/` carrying a `## States` table is checked one-to-one against its evidence folders, matching the table's `State` and `Evidence` columns by header text rather than position, so a project reordering the template's own columns still resolves. An entry with no such table has no rows to match, and the required-section check reports it as short of `## States` instead, so a project on an older wireframe shape is told what it lacks rather than read as clean.
92
92
 
93
93
  An evidence cell resolves as the literal path it names, taken from the project root. A state whose cited folder does not exist is a states mismatch, and so is a folder sitting under a cited evidence root that no row names. A `not captured` cell is the one exception the standard admits, and it excuses that row from both readings. A `plaintext` sketch still carried once a state's evidence exists on disk is a third finding this check adds, reading the entry as a whole rather than one row.
94
94
 
@@ -96,4 +96,4 @@ Sweep mode's findings carry a `heading` field alongside `file` and use the sweep
96
96
  | `0` | the run completed, whatever the findings say |
97
97
  | `1` | refused: a bad range, an unreadable file, or a malformed flag |
98
98
 
99
- Findings never set a failing exit code, in either mode. Diff mode refuses on a git range it cannot resolve. Both modes refuse on a file the run could not read.
99
+ Findings never set a failing exit code, in either mode. `canon audits run` carries sweep mode as its placement entry for the same reason, pinned to `--backend off` so the tally it retains is the regex layer's on every machine. `docs/agents/audits.md` states why. Diff mode refuses on a git range it cannot resolve. Both modes refuse on a file the run could not read.
@@ -37,7 +37,7 @@ The set is closed rather than derived, so widening it is a deliberate change to
37
37
 
38
38
  A sentence has to carry an assertion verb (`loads`, `ships`, `carries`, `holds`, `counts`, `totals`, `documents`, `declares`, `installs`, `lists`, `contains`, `comprises`, `authors`), the quantifier `all`, or an article (`the`, `a`, `an`) immediately ahead of the number, which is immediately ahead of the catalog noun, with one optional qualifying word between the number and the noun (`sixty-one shipped skills`). The number reads either as digits or as a spelled-out cardinal through ninety-nine, since this corpus states a catalog size in words as often as in digits.
39
39
 
40
- Nothing stands between the trigger and the number, which is the rule an author writing a new count has to know. It separates `the toolkit authors 70 rules` and `took all 70 rules`, both read, from `a domain of 55 skills` and `denominator of sixty-one shipped skills`, neither of them read. A figure meant as a past state takes a date in the same sentence instead, which reads the sentence past whatever its shape.
40
+ Nothing stands between the trigger and the number, which is the rule an author writing a new count has to know. It separates `the toolkit authors 81 rules` and `took all 81 rules`, both read, from `a domain of 55 skills` and `denominator of sixty-one shipped skills`, neither of them read. A figure meant as a past state takes a date in the same sentence instead, which reads the sentence past whatever its shape.
41
41
 
42
42
  ## How the trigger set grew
43
43
 
@@ -45,7 +45,7 @@ The verb gate is not the first design tried. A bare number next to a catalog nou
45
45
 
46
46
  The article half was added after a first review of the shipped design found a live miss: `canon/context/development/gates.md` stated a stale audit total while the tree held 20, and the verb gate never reached it, since that sentence puts its verb after the noun rather than ahead of the number. Widening the trigger set to admit an article ahead of the number closed that gap.
47
47
 
48
- The quantifier `all` and the verb `authors` joined on the same evidence, two more live misses failing only the trigger test. Allowing words to stand between the trigger and the number was the other candidate, measured and declined: at widths of one, two, and three it reached 77, 104, and 132 sentences against a baseline of 65, missed `took all 70 rules` at every width, and reached `the toolkit authors 70 rules` only at a width admitting the indirect-noun shape below. The two words reach both at 75 sentences and no false positive.
48
+ The quantifier `all` and the verb `authors` joined on the same evidence, two more live misses failing only the trigger test. Allowing words to stand between the trigger and the number was the other candidate, measured and declined: at widths of one, two, and three it reached 77, 104, and 132 sentences against a baseline of 65, missed `took all 81 rules` at every width, and reached `the toolkit authors 81 rules` only at a width admitting the indirect-noun shape below. The two words reach both at 75 sentences and no false positive.
49
49
 
50
50
  ## The plausibility filter
51
51
 
@@ -59,7 +59,7 @@ The article gate is looser than the verb list and carries a real cost. Re-runnin
59
59
 
60
60
  A delta phrased as a transition (`from fourteen to fifteen`), a fraction (`thirteen of sixteen`), and a total reached through an indirect noun (`denominator of sixty-one shipped skills`) are all catalog-size claims this corpus carries, and none matches the trigger shape this reads. Each stays a known gap. The false-positive rate that gated closing them has a measurement behind it now, and what it showed is that the widening these three need is the one that costs a false positive rather than the one that does not.
61
61
 
62
- A second figure in a sentence whose first figure already matched is a fourth gap and a structural one, since one match is taken per catalog per sentence. `authors 70 rules under governance/rules/ and consumes 62 into .claude/rules/` is read for its 70 alone, which is correct here because the two figures name different populations, and a sentence stating one catalog twice would go unread the same way.
62
+ A second figure in a sentence whose first figure already matched is a fourth gap and a structural one, since one match is taken per catalog per sentence. `authors 81 rules under governance/rules/ and consumes 62 into .claude/rules/` is read for its 81 alone, which is correct here because the two figures name different populations, and a sentence stating one catalog twice would go unread the same way.
63
63
 
64
64
  A calendar date (`2026-08-21`) or a backticked commit reference in the same sentence reads the whole sentence past, since that is how this corpus already marks a figure as a historical record rather than a live claim. `canon/ARCHITECTURE.md` and the context entries carry a figure this way deliberately, and every one of them stays correct forever.
65
65
 
@@ -79,6 +79,8 @@ no copy to reconcile. `canon snippets list --json` carries the catalog and
79
79
 
80
80
  `canon tooling diff <stack> [target]` is the read-only comparison, and a session asking what differs reaches for it by default. It writes nothing and exits 1 when anything differs and 0 when nothing does, so it gates CI. `--json` adds the scan record on stdout with `ok: true`, and the frame goes to stderr. A refusal such as an unknown stack, an excluded stack, or a toolkit-root target still exits 1 and answers `{ ok: false, reason, message }`, so a caller branches on the record and reads a typo'd stack name as a refusal rather than as drift.
81
81
 
82
+ The record carries a `withheld` array beside `configs`, each entry `{ rel, stack, present }`. It fills only when the target sits below its git root: every `.github/` config moves there rather than into `configs`, and none of them counts toward the exit, since GitHub never reads a workflow outside the root. `present` marks a copy an earlier sync already left in the subfolder, which the sync reports and never deletes. At a root, and for a target outside any repository, `withheld` is empty.
83
+
82
84
  `canon tooling sync <stack> [target] --check` reports the same list and always exits 0. It stays for scripts that already call it. `sync` itself writes only under `--write`, and a headless run with neither flag reports and exits 1.
83
85
 
84
86
  ## Install guards
@@ -7,9 +7,9 @@ description: The two roots a record folder resolves at, validating the session r
7
7
 
8
8
  ## Record roots
9
9
 
10
- Every verb here resolves a record folder at two roots rather than one. `.canon/<folder>` is read first, `.claude/<folder>` second, and a folder neither root carries resolves to the creation default, which is `.claude/`. The scratch folder is the one name that differs by root, spelled `.canon/tmp` and `.canon/tmp`, since inside a dotted root the leading dot hides nothing already hidden.
10
+ Every verb here resolves a record folder at two roots rather than one. `.canon/<folder>` is read first, `.claude/<folder>` second, and a folder neither root carries resolves to the creation default, which is `.canon/`. The scratch folder is the one name that differs by root, spelled `.canon/tmp` and `.claude/.tmp`, since inside a dotted root the leading dot hides nothing already hidden. <!-- canon-keep-record-root -->
11
11
 
12
- The read order and the creation default disagree deliberately. The gitignored record folders are moving to a root of their own, and the CLI learns to read both roots in a release that ships ahead of the move, so the binary a session already holds knows where to look by the time a tree relocates. Creating under the new root before then would write records to a root whose ignore line may not have reached a project yet, and it would split one project's records across two roots with no verb able to reconcile them. The move flips the default and nothing else.
12
+ The read order and the creation default agree. While the record folders were moving to a root of their own, creation stayed at `.claude/` so no record landed under a root whose ignore line had yet to reach a project. The ignore line ships now, so a fresh project scaffolds `.canon/` alone, and `.claude/` stays as a read fallback for a project the move has not reached.
13
13
 
14
14
  A caller never spells a record root by hand for the same reason. A path written as `.canon/plans/...` resolves against one root and reports nothing when it is wrong, which is the quiet failure this ordering exists to prevent: a stale binary meeting a moved layout, writing to the old path, and reporting success. Read a folder through the verb that owns it, and where a skill needs the path itself, take it from that verb's record rather than composing one.
15
15
 
@@ -17,7 +17,7 @@ A refusal names every root it looked at, so a message reading `no-folder` says w
17
17
 
18
18
  ## Validate
19
19
 
20
- `canon records validate <kind>` reports where a file and the standard governing it disagree. Five kinds are gitignored folders under `.claude/`: `plans`, `groundwork`, `intake`, `memory`, and `teach`. The sixth is `standards`, the authoring corpus, which is tracked and installed rather than scratch.
20
+ `canon records validate <kind>` reports where a file and the standard governing it disagree. Five kinds are gitignored folders under the record root, resolved at `.canon/` first: `plans`, `groundwork`, `intake`, `memory`, and `teach`. The sixth is `standards`, the authoring corpus, which is tracked and installed rather than scratch.
21
21
 
22
22
  ```bash
23
23
  canon records validate plans
@@ -189,7 +189,9 @@ canon records push --json
189
189
  canon records pull
190
190
  ```
191
191
 
192
- At the `.canon` root, the backed folders are every top-level directory less three: `tmp`, which is deletable without loss, `ordinal-locks`, whose entries are transient per claim and would race the claim they guard, and `.records.git`, which is the history the rest are pushed into. Nothing bounds the set from outside, since the claude manifest ships one `.canon/` root entry and names no folder, so a record folder added later enters the payload on its own rather than waiting on a name written here. A push names each folder in scope that the records index has never tracked before, so a folder that picked up a name by mistake, such as a misrouted scratch write, is visible in the report rather than entering the payload silently. The legacy `.claude` root keeps a fixed allowlist instead, since that root also holds tracked `skills/`, `rules/`, and `hooks/` a push must never carry, and an exclusion set there would stage all three. Each name is a top-level record folder and every archive sits inside the one it archives, so the set stays at one entry per surface however many archives appear, and it deliberately does not match the six record kinds `validate` hardcodes.
192
+ At the `.canon` root, the backed folders are every top-level directory less three: `tmp`, which is deletable without loss, `ordinal-locks`, whose entries are transient per claim and would race the claim they guard, and `.records.git`, which is the history the rest are pushed into. Nothing bounds the set from outside, since the claude manifest ships one `.canon/` root entry and names no folder, so a record folder added later enters the payload on its own rather than waiting on a name written here. A push names each folder in scope that the records index has never tracked before, so a folder that picked up a name by mistake, such as a misrouted scratch write, is visible in the report rather than entering the payload silently. The report also lists every file new to the history under `added`, since a stray file inside a folder already tracked is visible only by name.
193
+
194
+ The three excluded names bound the records index as well as the disk. A name an older binary committed, such as `tmp`, is removed from the history once and reported under `dropped`, and no later push stages it again. That removes it from the tip going forward and purges nothing from earlier commits the remote already holds. A project's own drafts folder needs no config to be backed: moved to `.canon/<name>/`, it is a top-level directory like any other and enters the payload on the next push. The legacy `.claude` root keeps a fixed allowlist instead, since that root also holds tracked `skills/`, `rules/`, and `hooks/` a push must never carry, and an exclusion set there would stage all three. Each name is a top-level record folder and every archive sits inside the one it archives, so the set stays at one entry per surface however many archives appear, and it deliberately does not match the six record kinds `validate` hardcodes.
193
195
 
194
196
  Records are gitignored by design, so the history lives in a second git directory at `.records.git` inside the record root, with that root as its work tree. Both resolve off the root together rather than folder by folder, since a history opened at one root beside a work tree at the other would stage the deletion of every folder a move relocated. Every path stays where it is, which is what a separate checkout could not do. The verbs stage the backed folders by explicit pathspec with `--force`, so nothing outside them can enter the index however the ignore rules read, and the project working tree and its index are never touched. Each pathspec is a bare folder name and git reads it against the current directory rather than against the work tree the same call names, so the invocation carries `-C` at the work tree beside the other two flags. That is what lets either verb run from a linked worktree under `.claude/worktrees/`, which sits inside the records work tree and would otherwise prefix every name with its own path.
195
197
 
@@ -209,20 +211,25 @@ Point it at a private repository, and at one that is not a remote of the project
209
211
 
210
212
  ### Refusals
211
213
 
212
- | Reason | What fired |
213
- | ------------------- | ------------------------------------------------------------------------------------ |
214
- | `split-roots` | Record folders sit under both roots, so the resolved work tree is not the whole set |
215
- | `no-repository` | No `.canon/.records.git`, answered with the two setup commands |
216
- | `no-remote` | The records history has no `origin` |
217
- | `remote-unreadable` | The project's own remotes could not be read, so the shared-origin gate could not run |
218
- | `remote-shared` | The records origin is also a remote of the project |
219
- | `no-remote-records` | `pull` found no branch on the records origin |
220
- | `local-changes` | `pull` found records on disk that the history does not carry |
221
- | `local-ahead` | `pull` found local commits that never reached the origin |
222
- | `git-failed` | A git call failed, with its stderr in the message |
214
+ | Reason | What fired |
215
+ | ------------------- | -------------------------------------------------------------------------------------- |
216
+ | `split-roots` | Record folders sit under both roots, so the resolved work tree is not the whole set |
217
+ | `no-repository` | No `.canon/.records.git`, answered with the two setup commands |
218
+ | `no-remote` | The records history has no `origin` |
219
+ | `remote-unreadable` | The project's own remotes could not be read, so the shared-origin gate could not run |
220
+ | `remote-shared` | The records origin is also a remote of the project |
221
+ | `no-remote-records` | `pull` found no branch on the records origin |
222
+ | `local-changes` | `pull` found records on disk that the history does not carry |
223
+ | `local-ahead` | `pull` found local commits that never reached the origin |
224
+ | `unsafe-payload` | `push` found a pending file over 25 MB or carrying a credential, named under `blocked` |
225
+ | `git-failed` | A git call failed, with its stderr in the message |
223
226
 
224
227
  `split-roots` runs ahead of every gate below it and fires on a half-migrated tree, which is what a `canon migrate records` run that failed partway leaves. `recordRoot` answers for the whole tree on the first root that exists, so a folder left at the old root is absent from the work tree while the records index still names it, and an unguarded `add -A` would stage its deletion and drop it from the remote on the next push. Finish the move, or put the stranded folders back beside the others.
225
228
 
229
+ `unsafe-payload` runs before anything is staged, so a refusal leaves the index, the log, and the object store as it found them. It reads every new or changed file the push would stage and blocks one over 25 MB, which is read off the file's size without opening it, or one carrying a value `canon secrets scan` would report, through the same patterns and the same `canon-allow-secret` marker. It refuses the whole push rather than skipping the file, and `--json` lists each path with its cause and a detail that names the credential's kind and line, never its value. Move the file out of the record folders, such as into `.canon/tmp/`, or remove the credential, then push again. The guard reads only what this push would add, so a credential committed before it existed stays in the history unreported.
230
+
231
+ A push the remote rejects undoes the commit that run made and leaves the records on disk, so the next run does not send a payload the remote already refused. A history an older binary committed and left unpushed is outside that undo, and the guard never reads it either, so every push sends the same blob again. Reset it by hand from the project root to the last commit the origin holds, `git --git-dir=.canon/.records.git --work-tree=.canon reset <commit>`, and push again. The `--work-tree` is required, since the setup `init` marks the history bare and git refuses that reset on a bare repository.
232
+
226
233
  The two `pull` refusals exist because the directions are not symmetric. A push only adds, while a pull onto a machine holding work that never left it would discard that work. Resolve either by running `push` first, or by moving the local folders aside. A machine holding no backed folders has nothing to lose, so a restore onto a fresh checkout runs straight through.
227
234
 
228
235
  ### When it runs
@@ -172,13 +172,15 @@ canon teach nav regular-expressions --json
172
172
 
173
173
  With no topic it rewrites every workspace. The teach-root listing is always rewritten regardless, since it reports on every workspace and a scoped run leaving it stale would be a second kind of drift this verb exists to end.
174
174
 
175
- A lesson carries its chrome as four marker pairs the authoring skill writes empty: `canon:teach:style`, `canon:teach:header`, `canon:teach:footnav`, and `canon:teach:scripts`. This verb splices each one from the current workspace state, embedding the toolkit's favicon, the shared stylesheet, and the script that settles the course panel before the page paints, rebuilding the course sidebar, the breadcrumb and jump menus, the prev/next footer nav, and the behavior scripts, and leaves the authored `<h1>`, lede, body, and quiz between the header and the footnav untouched.
175
+ A lesson carries its chrome as four marker pairs the authoring skill writes empty: `canon:teach:style`, `canon:teach:header`, `canon:teach:footnav`, and `canon:teach:scripts`. This verb splices each one from the current workspace state, embedding the toolkit's favicon, the shared stylesheet, and the script that settles the course panel before the page paints, rebuilding the course sidebar, the breadcrumb and jump menus, the prev/next footer nav, and the behavior scripts. It leaves the authored `<h1>`, lede, body, and quiz between the header and the footnav as written, and wraps them in `<main>` when the lesson holds none, since the column width hangs off that element. A lesson that already carries a `<main>` keeps its own.
176
176
 
177
- It also deletes the one icon link the authoring skill used to have a session write by hand into a lesson's `<head>`, matched as that exact string, so the page keeps only the icon the `style` region carries. Any other link an author wrote stays.
177
+ Every workspace label in that chrome, being the breadcrumb, the sidebar heading, the workspace switcher, and the root listing's row, reads the mission's `title` and falls back to the folder topic in sentence case where the mission names none.
178
178
 
179
- A workspace carrying no `course.css` gets its stylesheet pair seeded first, so the contents page it links is never unstyled, and an existing `course.css` is never overwritten. The embedded copy drops the seed's `@import` line, which resolved against the lesson's own folder and never loaded anything.
179
+ It also deletes two links a session used to write by hand into a lesson's `<head>`, each matched as its exact string: the old icon link and `<link rel="stylesheet" href="../assets/course.css" />`. The page keeps only the icon and the stylesheets the `style` region carries. Any other link an author wrote stays.
180
180
 
181
- That seeding fires on the absent file alone, so a workspace already holding a `course.css` has neither half rewritten here and its lessons go on embedding the sheet they were built against. A change to the shared chrome therefore takes `canon teach stylesheet <topic>` per workspace first, which rewrites `assets/base.css`, and then this verb to embed the result. Running this one alone renders every lesson missing whatever the change added, and nothing reports it.
181
+ A workspace carrying no `course.css` gets its stylesheet pair seeded first, so the contents page it links is never unstyled, and an existing `course.css` is never overwritten. The `style` region links `assets/base.css` ahead of an embedded copy of `course.css`, so the workspace's own rules still win the cascade. The embedded copy drops the seed's `@import` line, which resolved against the lesson's own folder and never loaded anything, and the link is what reaches the base sheet in its place. A workspace with no `assets/base.css`, one holding a hand-authored `course.css` from before the seed, gets no link rather than one pointing at a missing file.
182
+
183
+ That seeding fires on the absent file alone, so a workspace already holding a `course.css` has neither half rewritten here. A change to the shared chrome therefore takes `canon teach stylesheet <topic>` per workspace, which rewrites `assets/base.css`, and every lesson picks it up through its link with no run of this verb. Running this one alone leaves every lesson on the `base.css` the workspace already holds, and nothing reports it. Only an edit to a workspace's own `course.css` needs this verb, since that half is embedded.
182
184
 
183
185
  A lesson missing one of the four marker pairs is refused by name rather than rewritten, and every other lesson in the run still rewrites. The record's `skipped` list carries the refused files and which marker each is missing.
184
186
 
@@ -190,6 +192,12 @@ One detector decides both regions. A lesson carrying `<button class="opt"`, the
190
192
 
191
193
  Splicing the stepper into a lesson of either shape was measured against a browser and reverted. Its rules match a button question, which can never hold a checked radio, so a lesson given both hides every question past the first for good and outranks the class the script reveals feedback with. Nothing else caught that: the markup is valid, the splice succeeds, and the page renders.
192
194
 
195
+ ### Reference pages
196
+
197
+ Each reference page is rendered to a `reference/<slug>.html` sibling carrying the same chrome, and the contents page links that sibling rather than the markdown, which `canon serve` hands over as plain text. The render drops the frontmatter, turns tables into `<table>` markup, and escapes raw HTML written in the markdown rather than passing it through. A body with no H1 gets one from the frontmatter `title`. A relative link to another reference page's markdown points at that page's `.html` sibling, and one to the workspace `GLOSSARY.md` points at the glossary on the contents page, `../index.html#gloss`. Every other link, such as `../RESOURCES.md`, stays as written. The markdown file is never touched.
198
+
199
+ Every rendered page carries a `<meta name="generator" content="canon teach nav">` tag. A `.html` file under `reference/` whose markdown is gone is deleted when it carries that tag, and a hand-written HTML file there, carrying none, is left alone. The record's `reference` field counts the pages rendered. On a Bun with no `Bun.markdown`, each reference page lands in `skipped` with `Bun.markdown` as what is missing, and the rest of the run goes on.
200
+
193
201
  ## Opening a workspace
194
202
 
195
203
  No `canon teach` verb serves the workspace. `canon serve` does, taking the teach root as its directory and the workspace contents page as its entry:
@@ -86,13 +86,15 @@ Each diagram entry records the commit and date it was last verified against, and
86
86
 
87
87
  The default path is `base`. `canon init` on `base` installs base tooling configs, Claude seeds, and governance core rules, and scaffolds an empty `.claude/wiki/`. Most projects need nothing more.
88
88
 
89
- Escalate only for real web apps. The `target-setup` skill reads `package.json` and root configs, then picks the matching tooling stack (`canon tooling list --json` names the current set) and the matching governance stack (`react`, `astro`, `node`).
89
+ Escalate only for real web apps. The `target-setup` skill reads `package.json` and the framework configs, then picks the matching tooling stack (`canon tooling list --json` names the current set) and the matching governance stack (`react`, `nextjs`, `astro`, `node`). A Next.js app lands on `nextjs`, which extends `react` with the App Router rule a Vite React app does not get.
90
+
91
+ In a monorepo the skill reads each folder holding a `package.json`, `pyproject.toml`, `go.mod`, `composer.json`, or `Cargo.toml` to depth two, skipping what git ignores. Each of those folders gets its own tooling stack and a `canon tooling sync <stack> <folder> --skip base` in the preview. Governance still installs once at the root, since a rule's `paths:` globs reach every subfolder, so a subfolder's language rules join the root install through `--add`.
90
92
 
91
93
  `node-server` is named rather than detected. It carries the server-side security and persistence rules for a project writing request handlers or a persistence layer in TypeScript, and the detect step matches a runtime or a framework against stack names, so nothing there marks a project as a backend. Pass it deliberately with `canon init --stack node-server` or `canon gov install node-server <target>`.
92
94
 
93
95
  Markdown-heavy projects, CLI tools, docs sites, research notebooks, and scripting repos stay on `base`. Escalation is a ceiling move, not a default.
94
96
 
95
- A project the toolkit ships no stack for lands on `base` the same way, and the skill marks that resolution as a fallback in its preview rather than reporting it as a match. Configs, seeds, and gitignore entries land either way, and the JavaScript development dependencies, scripts, and hook activation land only where a `package.json` exists to carry them. A project outside that ecosystem declines at the preview and takes `canon:target-setup gov` for the governance layer alone, which is language-neutral.
97
+ A project the toolkit ships no stack for lands on `base` the same way, and the skill marks that resolution as a fallback in its preview rather than reporting it as a match. Configs, seeds, and gitignore entries land either way, and the JavaScript development dependencies, scripts, and hook activation land only where a `package.json` exists to carry them. A project outside that ecosystem declines at the preview and takes `canon:target-setup gov` for the governance layer alone, which is language-neutral. Go and PHP ship tooling stacks but no governance stack, so a project in either takes the `go` or `php` tooling stack, lands on `base` for governance, and picks up its language and testing rules through `--add`, for example `canon gov install base --add 130-go,335-testing-go .`
96
98
 
97
99
  Run `canon tooling list --json` and `canon gov list --json` to see the current catalogs. Never hardcode stack names.
98
100
 
@@ -331,6 +333,13 @@ canon tooling sync python ./backend --skip base --write
331
333
 
332
334
  `--skip base` drops the `base` layer from each subtree sync, so husky, prettier, cspell, commitlint, and CI stay single at the repo root. Without it, every subtree re-drops husky, and since git honors only one `core.hooksPath` the extra hook dirs silently break. Each subtree still gets its own framework configs (eslint, vitest, tsconfig, vite), and its own stack reference reads through `canon tooling reference <stack>`.
333
335
 
336
+ A sync whose target sits below the git root knows it is writing into a subfolder:
337
+
338
+ - It withholds `.github/`, since GitHub reads workflows only at the repository root, and names each withheld file. Run the subtree from a job in the root workflow instead, with `working-directory` set to the subtree path the sync prints, such as `working-directory: frontend`.
339
+ - It writes a nested `cspell.json` that registers the subtree's word lists, so a root `cspell '**'` reads them for files under that folder. It leaves a spell config the subtree already has alone.
340
+ - It installs no dependencies and adds no scripts to a subtree without a `package.json`. It names what it skipped and ends on `run 'bun init' in <path>, then sync again`.
341
+ - The subtree's `scripts/verify.sh` skips the format, spell, and shell phases a subtree does not declare, since the root runs them, and still fails on a missing lint, typecheck, or test script.
342
+
334
343
  ## Running sync from an agent session
335
344
 
336
345
  `canon sync .` applies every installed domain sync, then offers to commit the result and open a pull request. That last step needs a terminal. Under `CANON_NON_INTERACTIVE=1`, which is how an agent runs it, the domain syncs still apply and the git workflow is refused: the command reports the branch and commit it would have created, writes nothing to git, and exits 0. Review the working tree and commit it yourself, or rerun interactively to reach the commit and pull request options.
@@ -107,7 +107,7 @@ An execution that picks other than the suggestion rewrites the `- Suggested:` li
107
107
 
108
108
  `canon records validate plans` reports where a plan and that standard disagree: a filename that is not `feature-<slug>.md`, a missing required section, a files-to-touch entry naming no file, and a question carrying a suggestion with no answer slot. The same verb takes `groundwork`, `intake`, `memory`, and `teach`, which are governed the same way and were unreachable for the same reason. Nothing fires it automatically, because all five folders are gitignored and every check the repository runs reads changed files from git. It reports and never writes, since the folders are per-machine scratch with no history to recover a wrong repair from.
109
109
 
110
- `canon records push` carries these folders off the disk they live on, and `canon records pull` brings them back. Every top-level entry under `.canon/` is backed except `tmp`, `ordinal-locks`, and `.records.git`, so a folder added later enters the payload on its own rather than waiting on a name written into a list. Push names each folder it carries the first time it sees it, which is what keeps a stray folder visible instead of silently entering the payload. The history lives in a second git directory at `.canon/.records.git` with `.canon/` as its work tree, so every path a task file cites stays where it is.
110
+ `canon records push` carries these folders off the disk they live on, and `canon records pull` brings them back. Every top-level entry under `.canon/` is backed except `tmp`, `ordinal-locks`, and `.records.git`, so a folder added later enters the payload on its own rather than waiting on a name written into a list. Push names each folder it carries the first time it sees it, which is what keeps a stray folder visible instead of silently entering the payload. It refuses the whole push, naming each path, when a new or changed file is over 25 MB or carries a credential `canon secrets scan` would report. The history lives in a second git directory at `.canon/.records.git` with `.canon/` as its work tree, so every path a task file cites stays where it is.
111
111
 
112
112
  A person points it at a private repository once and both verbs refuse until they have, and `push` refuses when that origin is also a remote of the project, since the payload is the memory pen and the groundwork trails. `.husky/post-merge` runs the push after its archive loop, on every merge rather than only on one that closed a task. See [records](../agents/records.md) for the refusal table.
113
113