@erclx/canon 5.39.0 → 5.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +39 -22
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
  4. package/claude/skills/auto-ship/SKILL.md +6 -0
  5. package/claude/skills/auto-ship/references/fan-in.md +59 -0
  6. package/claude/skills/canvas-board/SKILL.md +1 -1
  7. package/claude/skills/create-skill/references/skill.md +1 -1
  8. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
  9. package/claude/skills/decision-escalate/SKILL.md +1 -2
  10. package/claude/skills/draft-slides/SKILL.md +2 -3
  11. package/claude/skills/draft-slides/references/deck.md +183 -0
  12. package/claude/skills/git-branch/REQUIREMENT.md +17 -6
  13. package/claude/skills/git-branch/SKILL.md +63 -7
  14. package/claude/skills/git-pr/references/evidence.md +2 -2
  15. package/claude/skills/git-pr/references/final-command.md +1 -1
  16. package/claude/skills/git-pr/references/labels.md +1 -1
  17. package/claude/skills/git-pr/references/publish.md +1 -1
  18. package/claude/skills/plan-feature/references/batches.md +48 -1
  19. package/claude/skills/plan-feature/references/plan.md +2 -0
  20. package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
  21. package/claude/skills/plan-intake-answer/SKILL.md +1 -2
  22. package/claude/skills/review-address/references/rebase-conflicts.md +7 -0
  23. package/claude/skills/review-ui/SKILL.md +3 -0
  24. package/claude/skills/review-ui/references/close-out.md +47 -0
  25. package/claude/skills/review-ui/references/post.md +3 -1
  26. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -0
  27. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
  28. package/claude/skills/role-worker/REQUIREMENT.md +1 -0
  29. package/claude/skills/role-worker/SKILL.md +9 -0
  30. package/claude/skills/task-board/references/board.md +1 -1
  31. package/claude/skills/task-board/references/tasks.md +1 -1
  32. package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
  33. package/claude/skills/teach-workspace/SKILL.md +1 -2
  34. package/claude/skills/ux-measure/SKILL.md +1 -1
  35. package/docs/agents/audits.md +61 -60
  36. package/docs/agents/context-audit.md +40 -58
  37. package/docs/agents/index.md +5 -51
  38. package/docs/agents/install-and-sync.md +1 -0
  39. package/docs/agents/markdown-audit.md +53 -99
  40. package/docs/agents/records.md +37 -79
  41. package/docs/agents/sessions-transfer.md +26 -70
  42. package/docs/target/index.md +0 -1
  43. package/docs/target/migrations.md +57 -66
  44. package/docs/target/projects.md +36 -78
  45. package/docs/target/scenarios.md +9 -19
  46. package/docs/target/sync.md +27 -68
  47. package/docs/workflow/ai-workflow.md +22 -68
  48. package/docs/workflow/operating-model.md +32 -186
  49. package/docs/workflow/parallel-features.md +30 -66
  50. package/docs/workflow/skill-map-on-request.md +22 -24
  51. package/docs/workflow/skill-map.md +72 -76
  52. package/docs/workflow/visual-design-workflow.md +25 -118
  53. package/docs/workflow/zshrc-aliases.md +18 -37
  54. package/package.json +1 -1
  55. package/src/canvas/client/inspector/fields.css +4 -0
  56. package/src/canvas/index.html +6 -0
  57. package/src/claude/cases/git.ts +4 -0
  58. package/src/claude/skills-reach.ts +2 -2
  59. package/src/cli/ui.ts +3 -1
  60. package/src/commands/canvas.ts +23 -0
  61. package/src/commands/capture.ts +14 -0
  62. package/src/commands/claude.ts +13 -0
  63. package/src/commands/demo.ts +22 -0
  64. package/src/commands/deps.ts +6 -0
  65. package/src/commands/driver.ts +28 -2
  66. package/src/commands/gov.ts +52 -18
  67. package/src/commands/intake.ts +17 -0
  68. package/src/commands/labels.ts +38 -5
  69. package/src/commands/pr.ts +192 -52
  70. package/src/commands/records.ts +19 -3
  71. package/src/commands/secrets.ts +6 -0
  72. package/src/commands/sessions.ts +8 -0
  73. package/src/commands/tasks.ts +52 -0
  74. package/src/commands/teach.ts +13 -0
  75. package/src/commands/upstream.ts +25 -1
  76. package/src/commands/worktrees.ts +5 -0
  77. package/src/context/audit.ts +22 -4
  78. package/src/context/folders.ts +1 -1
  79. package/src/context/shape-gated.ts +5 -2
  80. package/src/counts/scan.ts +1 -1
  81. package/src/design/components.ts +1 -1
  82. package/src/design/document.ts +1 -1
  83. package/src/design/tokens.ts +1 -1
  84. package/src/gate/boundaries.ts +4 -1
  85. package/src/gate/measures.ts +1 -1
  86. package/src/gate/stages.ts +7 -7
  87. package/src/git/env.ts +6 -0
  88. package/src/markdown/scope.ts +11 -19
  89. package/src/pr/frames.ts +53 -9
  90. package/src/pr/lift.ts +153 -19
  91. package/src/pr/painted.ts +87 -0
  92. package/src/records/validate.ts +1 -0
  93. package/src/roots/record.ts +18 -1
  94. package/src/sandbox/census.ts +1 -1
  95. package/src/sandbox/headless/run.ts +3 -2
  96. package/src/sandbox/headless/watch.ts +3 -4
  97. package/src/slides/convert/master.ts +3 -0
  98. package/src/sync/format.ts +46 -0
  99. package/src/sync/stamp.ts +6 -0
  100. package/src/tasks/answers.ts +79 -2
  101. package/src/tasks/fan-in.ts +176 -0
  102. package/src/tasks/stack.ts +1 -1
  103. package/tooling/astro/reference.md +80 -39
  104. package/tooling/base/reference.md +63 -32
  105. package/tooling/base/seeds/canon/context/ci.md +7 -15
  106. package/tooling/base/seeds/canon/context/development.md +14 -14
  107. package/tooling/claude/reference.md +40 -22
  108. package/tooling/cloudflare/reference.md +31 -12
  109. package/tooling/go/reference.md +47 -23
  110. package/tooling/nextjs/reference.md +52 -19
  111. package/tooling/php/reference.md +35 -16
  112. package/tooling/python/reference.md +47 -30
  113. package/tooling/shell/reference.md +6 -5
  114. package/tooling/vite-react/reference.md +49 -21
  115. package/tooling/web/reference.md +131 -71
  116. package/docs/agents/canvas.md +0 -86
  117. package/docs/agents/capture.md +0 -65
  118. package/docs/agents/census.md +0 -23
  119. package/docs/agents/comments.md +0 -34
  120. package/docs/agents/context-audit-checks.md +0 -108
  121. package/docs/agents/context-classify.md +0 -101
  122. package/docs/agents/counts.md +0 -74
  123. package/docs/agents/demo.md +0 -140
  124. package/docs/agents/driver.md +0 -103
  125. package/docs/agents/gate.md +0 -90
  126. package/docs/agents/hooks.md +0 -78
  127. package/docs/agents/intake.md +0 -81
  128. package/docs/agents/key-changes.md +0 -116
  129. package/docs/agents/label-coverage.md +0 -86
  130. package/docs/agents/markdown-audit-density.md +0 -39
  131. package/docs/agents/markdown-audit-weight.md +0 -83
  132. package/docs/agents/pr-evidence-layout.md +0 -70
  133. package/docs/agents/pr-evidence.md +0 -273
  134. package/docs/agents/pr-frames.md +0 -131
  135. package/docs/agents/pr-lift.md +0 -132
  136. package/docs/agents/pr-local.md +0 -111
  137. package/docs/agents/pr-preview.md +0 -155
  138. package/docs/agents/pr-reads.md +0 -111
  139. package/docs/agents/pr-tick.md +0 -72
  140. package/docs/agents/records-push.md +0 -61
  141. package/docs/agents/records-stale.md +0 -40
  142. package/docs/agents/records-validate.md +0 -57
  143. package/docs/agents/restated.md +0 -83
  144. package/docs/agents/routing.md +0 -61
  145. package/docs/agents/rule-citations.md +0 -90
  146. package/docs/agents/sandbox.md +0 -126
  147. package/docs/agents/sessions.md +0 -128
  148. package/docs/agents/skills-audit.md +0 -120
  149. package/docs/agents/skills-reach.md +0 -64
  150. package/docs/agents/slides.md +0 -176
  151. package/docs/agents/state-scoped-risk.md +0 -105
  152. package/docs/agents/superseded.md +0 -85
  153. package/docs/agents/tasks-archive.md +0 -83
  154. package/docs/agents/tasks-plans.md +0 -152
  155. package/docs/agents/tasks-validate.md +0 -133
  156. package/docs/agents/tasks.md +0 -123
  157. package/docs/agents/teach-authoring.md +0 -118
  158. package/docs/agents/teach.md +0 -157
  159. package/docs/agents/test-order.md +0 -58
  160. package/docs/agents/upstream.md +0 -94
  161. package/docs/agents/worktrees.md +0 -118
  162. package/docs/target/standards-moved.md +0 -34
@@ -1,61 +0,0 @@
1
- ---
2
- title: Records push
3
- description: Backing the record folders to a private remote and restoring them, which folders the payload carries, the one-time setup, the refusal reasons, and the hook that runs the push
4
- ---
5
-
6
- # Records push
7
-
8
- `canon records push` commits the backed record folders to a private remote and pushes them. `canon records pull` fetches the other direction and writes them back. Both take `--json` and `--root` the way `validate` does, and both exit `0` on agreement and `1` on a refusal.
9
-
10
- ```bash
11
- canon records push
12
- canon records push --json
13
- canon records pull
14
- ```
15
-
16
- 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.
17
-
18
- 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.
19
-
20
- 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.
21
-
22
- ## Setup
23
-
24
- A person creates the records repository once per machine, and the verbs refuse with the commands when it is absent:
25
-
26
- ```bash
27
- git --git-dir=.canon/.records.git init
28
- git --git-dir=.canon/.records.git remote add origin <private-repo-url>
29
- printf '.canon/\n' >> .gitignore
30
- ```
31
-
32
- The ignore line is repeated here rather than left to the install, because the person running these commands is the one who creates the directory and the rule is worth reading beside the command that needs it. The claude manifest ships `.canon/` too, so a project that ran `canon tooling sync` already carries it and this line is a no-op there. One root entry covers the history and every record beside it, which is what makes the rule worth stating once rather than per folder.
33
-
34
- Point it at a private repository, and at one that is not a remote of the project. Records carry the plans, the review reports, and the groundwork trails, so a public project publishes all of it to anyone who fetches all refs. `push` compares the configured origin against every remote of the project and refuses on a match. A read of that list which fails refuses as well, since an empty list clears the comparison for every origin and a gate that passes on its own failure is no gate.
35
-
36
- ## Refusals
37
-
38
- | Reason | What fired |
39
- | ------------------- | -------------------------------------------------------------------------------------- |
40
- | `split-roots` | Record folders sit under both roots, so the resolved work tree is not the whole set |
41
- | `no-repository` | No `.canon/.records.git`, answered with the two setup commands |
42
- | `no-remote` | The records history has no `origin` |
43
- | `remote-unreadable` | The project's own remotes could not be read, so the shared-origin gate could not run |
44
- | `remote-shared` | The records origin is also a remote of the project |
45
- | `no-remote-records` | `pull` found no branch on the records origin |
46
- | `local-changes` | `pull` found records on disk that the history does not carry |
47
- | `local-ahead` | `pull` found local commits that never reached the origin |
48
- | `unsafe-payload` | `push` found a pending file over 25 MB or carrying a credential, named under `blocked` |
49
- | `git-failed` | A git call failed, with its stderr in the message |
50
-
51
- `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.
52
-
53
- `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.
54
-
55
- 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.
56
-
57
- 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.
58
-
59
- ## When it runs
60
-
61
- `canon hooks post-merge`, which `.husky/post-merge` calls, runs `push` after the task archiving, on every merge rather than only on one that archived a task. A review report and a plan edit both land on runs that close nothing. The verb exits zero whatever the push answers, so an unreachable remote neither aborts the hook nor delays the archiving before it, and a checkout that never ran the setup reports nothing. `hooks.md` covers the verb. Anything the hook misses is covered by running the verb by hand.
@@ -1,40 +0,0 @@
1
- ---
2
- title: Records stale
3
- description: Counting the releases since the requirements record was last reviewed, where the review date is read from, and the exit codes and refusals
4
- ---
5
-
6
- # Records stale
7
-
8
- `canon records stale canonical` reports when the requirements record was last reviewed and how many releases have shipped since. A skill takes the count from the verb rather than re-deriving it from the tag list.
9
-
10
- ```bash
11
- canon records stale canonical
12
- canon records stale canonical --json
13
- ```
14
-
15
- | Option | Behavior |
16
- | --------------- | ----------------------------------------------------------------- |
17
- | `--json` | Add a machine-readable record on stdout |
18
- | `--root <path>` | Project root, defaulting to the current worktree rather than main |
19
-
20
- `canonical` is the one kind it reads. Any other argument refuses as `unknown-kind`, including `memory`, which the verb read until the memory pen retired.
21
-
22
- It reports and never writes, matching `validate` and `size`.
23
-
24
- ## Canonical records
25
-
26
- `canonical` reads `REQUIREMENTS.md` at whichever surface root carries it, `canon/` ahead of `.claude/`, and omits it when the project does not hold one. It is tracked, so `--root` defaults to the worktree the caller stands in rather than the main one, and a stamp a branch just wrote reads back from that branch.
27
-
28
- The review point is an optional `reviewed: YYYY-MM-DD` frontmatter field, set by whoever finishes a review, per `claude/skills/draft-doc/references/requirements.md`. It is a date rather than a commit, since a commit written on a feature branch never reaches a trunk that squash-merges. The field is read off the raw frontmatter block rather than a parsed value, since a YAML parser resolves a bare date to a date object on one schema and a string on another. A value that is not a calendar date reads as never reviewed, and the record carries the raw value as `invalidReviewed` so a review can see it rather than the verb throwing over one hand edit.
29
-
30
- The count is every release tag merged into `HEAD` whose creator date falls after the reviewed day. A release tag ends in a dotted version, bare as `v5.25.0` or behind a component prefix as `canon-v2.1.0`, so an eval, deploy, or snapshot tag on the trunk never inflates it. A lightweight tag's creator date is its commit's date and an annotated tag's is the tagging date, so a project mixing both counts by two clocks.
31
-
32
- The record carries `tagged`, false when `HEAD` has merged no release tag or the repository has no commit yet, and one `docs` row per doc with `path`, `reviewed`, `releasesSince`, and `latestRelease`. `releasesSince` is null on a doc never reviewed and `0` on one reviewed after every tag. With no tags it is also `0`, which is why `tagged` travels beside it.
33
-
34
- It gates nothing, and no hook, gate stage, or workflow runs it. `canon:document-health` reads it when asked to review the two records.
35
-
36
- ## Exit codes and refusals
37
-
38
- Exit codes: `0` the reading completed, whatever it found, and `1` refused. A release count is a prompt for the review to judge rather than a failure, so nothing here gates.
39
-
40
- A `reason` field carries which gate fired: `unknown-kind` for any kind but `canonical`, and `unreadable-tags` when the tags cannot be read.
@@ -1,57 +0,0 @@
1
- ---
2
- title: Records validate
3
- description: Validating the session records, the per-kind checks, which root they default to, the exit codes, and the refusal reasons
4
- ---
5
-
6
- # Records validate
7
-
8
- `canon records validate <kind>` reports where a file and the standard governing it disagree. Four kinds exist, each a gitignored folder under the record root resolved at `.canon/` first: `plans`, `groundwork`, `intake`, and `teach`.
9
-
10
- ```bash
11
- canon records validate plans
12
- canon records validate intake --json
13
- ```
14
-
15
- | Option | Behavior |
16
- | --------------- | --------------------------------------------- |
17
- | `--json` | Add a machine-readable record on stdout |
18
- | `--root <path>` | Project root, defaulting to the main worktree |
19
-
20
- It reads and never writes. A session record is per-machine scratch with no history behind it, so a repair that guessed wrong could not be undone.
21
-
22
- Nothing fires it automatically. The four record folders are gitignored, so the standards-audit hook exits early on them and any check reading changed files from git never lists one. The verb runs at the moment a session claims the record is finished, which is the same placement `canon tasks validate` takes over the board.
23
-
24
- The two record roots every kind resolves at are in `records.md`.
25
-
26
- ## What each kind checks
27
-
28
- | Kind | What it reports |
29
- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
- | `plans` | A filename that is not `feature-<slug>.md`, a missing `# Feature:` heading, a missing required section, a files-to-touch entry naming no file or saying nothing about one, a question carrying no suggestion or no answer slot, and a batch still staged with a `**Batch N**` sub-heading inside the one file rather than split into its own plan |
31
- | `groundwork` | A track with no `README.md` or no `01-current-state.md`, a file missing `title` or `description`, a `README.md` with no `date` as `YYYY-MM-DD`, an unnumbered file, and a track holding a decision without its handoff or the reverse |
32
- | `intake` | A dump with no `00-overview.md`, the same frontmatter and numbering checks, an item missing any of `Problem`, `Fix`, `Worth it`, or `You`, and an item carrying `Open` with no `Suggested` |
33
- | `teach` | A workspace folder carrying no two-digit ordinal, an absent `MISSION.md`, `RESOURCES.md`, or `GLOSSARY.md`, a file missing `title` or `description`, a mission with no `date` as `YYYY-MM-DD` or no `## Success looks like` section, an unnumbered learning record, and a reference page opening with an ordinal |
34
-
35
- The half-closed track is the groundwork check a reader cannot run by eye. A folder holding `06` without `07` reads as closed to anyone scanning filenames while the file a returning session actually opens is absent.
36
-
37
- The item check skips `00-overview.md` and `99-next-session.md`, since neither holds items and running it over the handoff would report every heading it carries.
38
-
39
- A plan section opens as a bold label or as an H2 and the check counts both, naming the reference's spelling when it reports one missing. Plans split roughly four to one between the two forms, so failing the variant would report nearly every plan on a rule that costs a reader nothing.
40
-
41
- A section runs to the next marker-shaped line whatever it names, so a plan carrying a label of its own closes the section above it rather than collecting into it. Fenced blocks are dropped before any of this, since a plan showing the shape it writes puts real-looking bullets and headings inside a fence.
42
-
43
- ## Exit codes and refusals
44
-
45
- Exit codes: `0` every check passed, `1` refused, `2` at least one record carries a finding. A `reason` field carries which gate fired: `no-folder` when none of the kind's directories exist, and `unknown-kind` when the argument names no published kind. A `no-folder` message names every candidate, so a refusal names both record roots.
46
-
47
- An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the record's `findings` array and its `reason` rather than the exit when a skill consumes this.
48
-
49
- The four record folders are shared scratch at the main worktree root, so `--root` defaults to the first entry of `git worktree list` rather than the working directory. A linked worktree validates the same records every other session reads.
50
-
51
- Skills branch on the findings rather than on the exit code:
52
-
53
- ```bash
54
- canon records validate plans --json | jq -r '.findings[] | "\(.kind): \(.subject)"'
55
- ```
56
-
57
- For the shapes each check enforces, see `claude/skills/plan-feature/references/plan.md`, `claude/skills/plan-groundwork/references/groundwork.md`, and `claude/skills/plan-intake/references/intake.md`.
@@ -1,83 +0,0 @@
1
- ---
2
- title: Restated instructions
3
- description: Counting the instructions the always-loaded file and every path-scoped rule share with the seed, the skill bodies, and each other, how a match is decided, the three classes, which surface a later edit starts from, and why the sweep reports rather than gates
4
- ---
5
-
6
- # Restated instructions
7
-
8
- `canon gov restated` reports every instruction the always-loaded file or a path-scoped rule states that a further surface states too. It answers a question nothing else here could: one rule was found written in three places, and it was found by accident when a shipped skill deleted a file the rule said to keep.
9
-
10
- ```bash
11
- canon gov restated
12
- canon gov restated --json
13
- canon gov restated --root ../my-app
14
- ```
15
-
16
- | Option | Behavior |
17
- | --------------- | ---------------------------------------------------------- |
18
- | `--root <path>` | Tree to read, defaulting to the current directory |
19
- | `--json` | Add a machine-readable record on stdout, keeping the frame |
20
-
21
- Under `--json` the record holds stdout alone and the frame still renders on stderr, refusals included, which is the split `output-shape.md` fixes for every mode.
22
-
23
- ## The corpus it reads
24
-
25
- Four surfaces, and they are not read symmetrically. Every top-level bullet in `CLAUDE.md` and in every path-scoped rule is a subject, and every surface below is searched, rules included a second time:
26
-
27
- - `tooling/claude/seeds/CLAUDE.md`, read as bullets, since the seed carries the same shape as the file it is authored from
28
- - `claude/skills/*/SKILL.md` and `.claude/skills/*/SKILL.md`, read as every prose line and bullet, since the motivating case was stated in a body as a paragraph rather than a list item. The second root holds the project's own skills, which are equally a place a rule gets restated
29
- - `governance/rules/**/*.md` and `internal/rules/**/*.md`, the authoring roots rather than the consumed copy under `.claude/rules/`, read as bullets the same way the seed is. Each record's `file` carries the root it was read from
30
-
31
- A rule reads as both a subject and a candidate, unlike the seed and the skill bodies. A stack ships a whole rule folder, so a bullet duplicated between two rules reaches a target exactly as a bullet duplicated between the always-loaded file and a rule does, and neither shape is visible from one side alone. A candidate sharing its subject's file is skipped, since two bullets inside one rule sharing anchors are adjacent instructions on one topic rather than the same rule shipped twice, and a pair found from one direction is not reported again from the other.
32
-
33
- Frontmatter, headings, tables, and fenced blocks are read past. A heading names a section instead of stating a rule, a fenced block is an example whose words belong to the prose around it, and a body's `description` restates that skill's own purpose, so sweeping it would match every subject naming its domain.
34
-
35
- A rule stated in two skill bodies and never in `CLAUDE.md` or another rule is outside this reading. A body is searched but never read as a subject, and a subject is what the search runs from.
36
-
37
- ## How a match is decided
38
-
39
- Matching is recall-first and keyed on shared distinctive tokens rather than on a phrase two surfaces spell the same way. The case this exists for was one rule written three different ways, so a near-exact matcher would ship a cheap report blind to the defect it was built for.
40
-
41
- A token is distinctive when it appears in at most 20 of the corpus's statements, which is under one percent of them. `.canon/plans/` sits at 14 and is the anchor the motivating case turns on, while `file` sits at 371 and would match most of the tree.
42
-
43
- A backticked token counts double. An author marking a span as code named an identifier rather than describing one, so `.canon/plans/archive/` says more about what a statement governs than any two prose words do. Two statements are one rule when their shared anchors reach a weight of 3.
44
-
45
- Every record names the anchors its match rested on, so a reader can weigh a finding instead of taking it.
46
-
47
- ## The three classes
48
-
49
- - **Mirror.** Both files sit on a declared path pair whose duplication is deliberate. `CLAUDE.md` and the seed are the one pair, since the seed is authored from it and a hand merge reconciles the two. Excluding by pair rather than by content is the point: the duplication is a location fact this repository already records, and a content test would rediscover it on every run.
50
- - **Repetition.** Two surfaces state one rule and neither is declared a copy of the other.
51
- - **Contradiction.** The prohibition falls on one surface alone, on a match strong enough to read that as a disagreement. This is a polarity reading rather than a judgment about meaning, so weigh each against the surfaces it names.
52
-
53
- The contradiction floor sits above the match floor deliberately. A thin match says two statements touch the same subject, which is not enough to claim one forbids what the other prescribes, so a weak pair reports as a repetition and the loudest class is reserved for a pair sharing real identity.
54
-
55
- Two further rules decide where a prohibition counts, and both came out of false positives rather than from reasoning ahead of the corpus.
56
-
57
- Polarity is read off the clause the anchors landed in, and off the densest such clause rather than every one carrying an anchor. A statement states one rule across several clauses, so a union answers true whenever any clause anywhere carries a marker, which is the whole statement again under another name.
58
-
59
- The marker also has to open its clause, because an instruction leads with its verb. `Never delete a task file` prohibits where `a fallback never fires` reports, and no test reading the marker anywhere in the clause tells those apart. What that costs is a prohibition written mid-clause, which now reads as description and lands the pair in the repetition class, so both surfaces still reach the report and only the label is weaker.
60
-
61
- One mid-clause shape still counts. A `no` opening a clause or a comma-set phrase directly on a code span prohibits, as in `pinned to major tags, no` followed by the pinned tag, since sitting on an identifier it names what is ruled out. The phrase boundary keeps description out: `returns no` then a code span puts the marker after a verb and reports what happens. `not` and `never` in the same position are left out, because admitting them turned agreeing pairs into false contradictions.
62
-
63
- A mirror that disagrees stays a finding. The exclusion reaches a repetition alone, because the two files on a declared pair are meant to agree.
64
-
65
- ## Which surface is authoritative
66
-
67
- Each restatement names where a later edit starts.
68
-
69
- - `claude-md` for a seed match, since the always-loaded file is authored first and the seed carries it to a target
70
- - `skill-body` where the subject names that skill, which is the content-ownership rule that behavior triggered only when editing one domain belongs to that domain's skill
71
- - `unknown` everywhere else, a rule match included, which is a first-class answer rather than a gap
72
-
73
- The ownership table assigns a cross-domain rule and a domain-triggered one, and reaches nothing stated in a skill body the always-loaded file never names. Guessing there would put a reader on a surface nobody decided.
74
-
75
- ## Exit codes
76
-
77
- Exit codes are `0` when no instruction is restated outside a declared mirror, `1` for a refusal, and `2` for at least one restatement outside one. Mirrors move no exit code.
78
-
79
- Nothing wires this into `bun run check` or into a hook. A restatement is legitimate more often than not, and gating a measure whose ordinary result is a finding is what teaches contributors to route around the stage. `canon gov test-order` and `canon labels audit` are the siblings.
80
-
81
- Both refusals are absences rather than breaks. A target may hold none of the seed, a skills tree, or a rules tree, so `canon audits run` reads `no-instructions` and `no-surfaces` as a corpus that is not there rather than a verb that failed.
82
-
83
- An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the record's `counts` rather than the exit when a skill consumes this.
@@ -1,61 +0,0 @@
1
- ---
2
- title: Routing report
3
- description: Reading per CLAUDE.md section how many bullets name a path, what counts as naming one, when a rule counts as covering it, the two refusals, and why the verb reports rather than gates
4
- ---
5
-
6
- # Routing report
7
-
8
- `canon claude routing` reports, per `CLAUDE.md` section, how many top-level bullets name a path and how many of those a path-scoped rule already covers. It answers the firing axis of the tier test in `canon/context/context-model.md`, whether a fact applies every session or fires on one path, and which until this verb existed was a judgment nothing counted. The tier test names two further axes, conditional presence and updatability, that this verb does not check.
9
-
10
- ```bash
11
- canon claude routing
12
- canon claude routing --json
13
- canon claude routing ../my-app
14
- ```
15
-
16
- | Option | Behavior |
17
- | -------- | ------------------------------------------------------------ |
18
- | `[path]` | Repository root to read, defaulting to the current directory |
19
- | `--json` | Add a machine-readable record on stdout, keeping the frame |
20
-
21
- Under `--json` the record holds stdout alone and the frame still renders on stderr, refusals included, which is the split `output-shape.md` fixes for every mode.
22
-
23
- The root is the argument rather than the toolkit, matching the reach and drift verbs, so a linked worktree reads its own branch and a target reads its own file.
24
-
25
- ## What it reads
26
-
27
- Every H2 and H3 owning at least one top-level bullet. An H3 is reported under the H2 containing it, as `Behavior / Scope discipline`, so a section and its subsections are counted apart rather than summed.
28
-
29
- Three things are read past. A nested bullet belongs to the one above it rather than to the section. A bullet inside a fenced block is example text rather than instruction. A heading carrying no bullet answers nothing the report asks, so it is dropped rather than listed at zero.
30
-
31
- ## What counts as naming a path
32
-
33
- A bullet is path-scoped here when it names a path. That is evidence for the tier judgment rather than the judgment itself, and the gap runs both ways: a bullet naming a folder can still apply every session, and one firing on a path it never spells is invisible to the count. Read a section's number as a place to look rather than as a verdict on it.
34
-
35
- A backticked token counts when it carries a separator or an alphabetic extension, which admits `src/cli.ts` and `tsconfig.json` while leaving a flag, a bare word, and a version string out.
36
-
37
- A shape counts as the folder above its placeholder. `canon/context/<domain>.md` names `canon/context/` and nothing narrower, so dropping the token whole would report the section carrying it as naming no path at all. A placeholder opening the first segment has no openable prefix and is dropped.
38
-
39
- ## When a rule counts as covering it
40
-
41
- A rule covers a named path only when its glob anchors to a location. A glob opening `**` reaches every folder in the tree, so it answers that a file type is governed and never that a named path is.
42
-
43
- The corpus-wide markdown rules are what force that. Counting one reports every markdown path as covered, which collapses the column to a constant and tells a reader nothing about which folder somebody actually scoped a rule to.
44
-
45
- A folder is probed with a handful of extensions rather than matched literally, because a glob narrowed by file type reaches under a folder without ever matching the folder's own name. Globs are read from each rule's frontmatter block alone, so a rule quoting a path in its body does not register a scope it never declared.
46
-
47
- The paths a section names that no rule reaches are listed beside it, which is where a reader looks first when deciding what a new rule would cover.
48
-
49
- ## Refusals
50
-
51
- Two, each naming what a reader does about it. `no-claude-md` is a tree with no always-loaded file. `no-rules` is a tree carrying no path-scoped rule under `.claude/rules/`, where every path would report as uncovered and the column would say nothing.
52
-
53
- An always-on rule declaring no `paths` is skipped rather than refused. It applies at the same priority as the always-loaded file, so it covers no path in particular.
54
-
55
- ## Exit codes
56
-
57
- Exit codes are `0` when the file was read and `1` for a refusal. No finding moves the exit code.
58
-
59
- Nothing wires this into `bun run check` or into a hook. Whether a bullet belongs in a rule is a judgment, and gating a measure that counts a judgment forces an escape hatch for every deliberate case.
60
-
61
- An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the JSON record's `sections` array rather than the exit when a skill consumes this.
@@ -1,90 +0,0 @@
1
- ---
2
- title: Rule citations
3
- description: Resolving every path a governance rule cites and every frontmatter glob the internal corpus declares, the one form a citation is written in, the shapes that look like citations and are not, the two classes where an absent path is correct, why the glob half reads one corpus, and why this one gates
4
- ---
5
-
6
- # Rule citations
7
-
8
- `canon gov citations` resolves every path a rule cites and every frontmatter glob the internal corpus declares, naming the ones reaching nothing. It answers a failure no other stage sees: a rule points a reader at a file, the file moves, and nothing reports it until a session opens the path and finds an absence.
9
-
10
- A glob fails the same way and more quietly. A rule scoped at a directory that moved stops matching, so it never loads again, and a rule that never fires looks exactly like a rule nobody violated.
11
-
12
- ```bash
13
- canon gov citations
14
- canon gov citations --json
15
- canon gov citations --root ../my-app
16
- ```
17
-
18
- | Option | Behavior |
19
- | --------------- | ---------------------------------------------------------- |
20
- | `--root <path>` | Tree to read, defaulting to the current directory |
21
- | `--json` | Add a machine-readable record on stdout, keeping the frame |
22
-
23
- Under `--json` the record holds stdout alone and the frame still renders on stderr, refusals included, which is the split `output-shape.md` fixes for every mode.
24
-
25
- ## What the drift check cannot see
26
-
27
- `bun run check` asserts that an authored rule and its consumed copy agree, and a dead citation passes that assertion because both copies carry it. The teach rule told its reader to open a `references/glossary.md` inside the teach skill that had never existed at that path. The rule shipped, survived a release, and fired during a real teach run before anyone noticed.
28
-
29
- Two files agreeing is what a drift check measures. Whether either one is right about the tree is a different question, and this is the stage that asks it.
30
-
31
- ## The one form
32
-
33
- A citation reaches a reader as a backticked path, reported under the form `path`, and it resolves against the root. A bare filename is never a citation, since with no folder around it nothing tells a rule from a convention such as `index.md`. A rule pointing at another rule writes its repository path.
34
-
35
- A call to the retired standards verb is no form here. A rule holding one fails the shipped-references stage of `canon gate run`, which reads every shipped file for it.
36
-
37
- A `path` is anchored on the whole backticked span rather than on a trailing pattern inside it. Cutting `tooling-reference.md` out of `.claude/skills/internal-tooling/references/tooling-reference.md` and resolving that against the root alone reports a file that exists as missing, which is a mistake made by hand while measuring this corpus before the stage was written.
38
-
39
- ## What is not a citation
40
-
41
- A rule body is full of backticked spans carrying a slash, and almost none of them names a file in this tree. The stage declines four shapes outright.
42
-
43
- - A placeholder or glob segment describes a shape rather than naming a file: `canon/context/<domain>.md`, `app/**/route.ts`, `${CLAUDE_SKILL_DIR}/../references/<name>.md`.
44
- - A bare filename names a convention: `route.ts`, `manifest.toml`, `components.json`, `playwright.config.ts`.
45
- - A span carrying no file extension is a folder or a module specifier: `src/pages/`, `next/font`, `try/except`, `react-hooks/set-state-in-effect`, `oven-sh/setup-bun@v2`, `@/lib/utils`.
46
- - A fenced block displays a path rather than pointing at one.
47
-
48
- The extension test is what carries most of the separation, and it costs the real paths that name a folder or a symlink, which this stage declines to check because nothing in the span says it is a file. Declining is the right direction for a gate, where a guess that goes wrong fails a push over prose.
49
-
50
- ## Where an absent path is correct
51
-
52
- Two classes resolve to nothing and are right to. Both are reported by name rather than dropped, so a reader can see what the verdict declined to judge.
53
-
54
- **Governed.** A rule spelling a path in its own frontmatter `paths:` is naming an artifact a target holds rather than a file here, so the file is correctly absent from this tree and correctly named in the rule. No shipped rule declares one today, and the stage's test fixtures carry the instance that keeps the class exercised.
55
-
56
- Only an exact declaration exempts, never a glob match against one. A glob declares a shape, so a body path sitting inside it is still a citation and a stale one is still a defect. A rule scoped at `docs/**` citing a `docs/agents/renamed.md` that moved is exactly the class this stage exists to catch, and matching the glob would excuse it.
57
-
58
- **Ignored.** A path git ignores is session scratch no clone is expected to hold. `governance/rules/canon/tasks.md` cites `.canon/tasks/index.md`, which is real in a live project and absent from a fresh clone and from every linked worktree. Resolving against the filesystem alone would make the verdict depend on which tree the stage ran in, so the unresolved paths go to one batched `git check-ignore` and an ignored one is excused. A read git cannot answer refuses rather than reporting those paths dead.
59
-
60
- ## Why the glob half reads one corpus
61
-
62
- Bodies are read across both corpora and frontmatter globs across `internal/rules/` alone.
63
-
64
- A rule under `governance/rules/` installs into a target, and its `paths:` entries name the shape that project holds rather than anything here. Measured over that corpus, 32 of its 72 globs match nothing in this tree and every one of them is correct. `src/pages/**` in the Astro rule cannot be told by pattern from a path this repository might hold, so a check cannot separate the two, and gating them would ship an exemption list the length of the corpus. Nothing reads the shipped globs, and the measurement is the reason rather than a gap left for someone to close.
65
-
66
- `internal/rules/` ships nowhere. The tree it governs is the tree present, which makes the question answerable, and all 14 of its globs across 7 rules match at the commit this shipped on.
67
-
68
- What that leaves unreached is a glob that matches real files and still reaches none of the work it was scoped at. `governance/rules/lib/e2e-reliability.md` scoped itself at `e2e/*.ts` and `e2e/**/*.ts` and now reads `**/e2e/**/*.ts`. No probe in this repository is written under `e2e/`, so the rule asking a session to watch a new guard fail never fired for the session writing guards. Resolution is mechanical and reach is a judgment about where the work happens, so only the first is here.
69
-
70
- ## The exemption marker
71
-
72
- Everything the classifier can separate mechanically is separated there. For the residue, a line carries `canon-allow-citation: <reason>` on itself or the one directly above, which moves it into the report's `Exempt` section. Only a marker naming a reason counts, since a bare token is a line that meant to say something and did not. This is the `canon-allow-superseded` shape, and both read the same placement rule through one helper.
73
-
74
- ## The blind spots
75
-
76
- A citation that resolves and points at the wrong file passes. Resolution is mechanical and correctness is a reading, and only the first is a gate's business.
77
-
78
- A path written into running prose without backticks is not read at all. Matching one would report every sentence that happens to name a file, and the backticks are what separate a reference a reader follows from a name in a sentence.
79
-
80
- A folder or a path carrying no extension is declined rather than guessed at, which is stated above as the cost of the test that carries the separation.
81
-
82
- A glob under `governance/rules/` is not read, and a glob that resolves while reaching none of the work it was scoped at is a reading rather than a resolution. Both are stated above.
83
-
84
- ## Exit codes
85
-
86
- Exit codes are `0` when every citation resolves or is excused and every glob read matches, `1` for a refusal, and `2` for at least one cited path reaching nothing or one glob matching nothing. It refuses a tree holding neither rule corpus, since a tree with no rules passes each of its zero rules, and a `git check-ignore` read that fails.
87
-
88
- This gates, and `bun run check` runs it as the `Rule citations` stage. The sibling sweeps report rather than gate because a value appears for reasons unrelated to the convention, so their output is a reading. A path resolving to nothing carries no judgment: either the file is there or the citation is stale, and the two classes where absence is correct are separated before the verdict rather than left for a reader to settle. The corpus is clean at the commit this shipped on, so the gate starts green and stays that way until something breaks.
89
-
90
- An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the JSON record's `citations` array and filter on `status` rather than the exit when a skill consumes this.
@@ -1,126 +0,0 @@
1
- ---
2
- title: Sandbox
3
- description: Scenario routing, the expectation scoring surface, and the coverage census over scenarios and skills
4
- ---
5
-
6
- # Sandbox
7
-
8
- `canon sandbox` provisions isolated project states, scores a provisioned one against a declared expectation, and reports which scenarios declare an expectation at all.
9
-
10
- ## Scenarios
11
-
12
- Scenarios live under `sandbox/`, one folder per category and one file per command, each a TypeScript module. `sandbox/fixtures/` is the exception, holding file content that scenarios stage rather than scenarios of its own, so the picker and the coverage report both filter it out. That tree sits outside `files` in `package.json`, so an installed `canon` carries the command, reports it as toolkit-only on one line, and exits 1 rather than failing on the missing directory. Route non-interactively with `SANDBOX_SCENARIO`:
13
-
14
- ```bash
15
- SANDBOX_SCENARIO=sync canon sandbox infra:tooling
16
- ```
17
-
18
- Scenario categories: `infra:*` (domain flows), `git:*`, `scaffold:*`. `create` scenarios require interactive input and loop on empty input, so skip them in automated runs.
19
-
20
- ## Scenario expectations
21
-
22
- `canon sandbox check <category>:<command> [arm]` scores a provisioned sandbox against the arm's `expect.toml`, printing a verdict on stderr and, with `--json`, the same verdict as a record on stdout.
23
-
24
- ```bash
25
- canon sandbox check claude:context-fold drift --json
26
- ```
27
-
28
- | Flag | Effect |
29
- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
30
- | `--envelope <file>` | Read `is_error`, `num_turns`, denials, and the reply text |
31
- | `--writes <file>` | Newline-delimited paths the session wrote, for write scope |
32
- | `--escapes <file>` | Newline-delimited paths written to a watched toolkit root, for escape scope |
33
- | `--escapes-watched` | At least one watched root held a target this run |
34
- | `--concurrent-sessions <file>` | Newline-delimited sessions live in the registry both before and after this run, a witness for an unbounded escape |
35
- | `--json` | Emit the verdict record on stdout |
36
- | `--strict` | Exit 1 on `unchecked` instead of 0 |
37
-
38
- The verdict `state` is `pass`, `fail`, or `unchecked`. An arm with no `expect.toml` is `unchecked` and exits 0, so the harness stays usable while expectations roll out. A declaration that exists but asserts nothing is a failure, since an expectation file that asserts nothing passes every run.
39
-
40
- Omitting `--writes`, `--escapes`, or `--envelope` does not silently drop the assertion kinds that need them. Write scope, escape scope, the turn ceiling, and the reply assertion report as unchecked and appear in the count, so the standalone command cannot claim more coverage than it had. Supplying `--escapes` without `--escapes-watched` reports the same way: a zero-escape result with no root confirmed watched is unmeasured rather than a pass. A verdict never reports `pass` with zero assertions.
41
-
42
- An arm declaring `escape_scope` asserts a bound on the headless runner's own escape watch rather than on the sandbox tree. `write_scope` skips when the run wrote nothing, since a required output missing is itself a finding, but `escape_scope` passes on zero escapes outright, since a clean run producing none is the expected outcome for a destination nothing requires a skill to touch, provided `--escapes-watched` confirms a root held something to watch. Declaring `escape_scope = []` asserts that a correct run reaches none of the watched destinations at all. `canon/context/sandbox/isolation.md` names what the watch reaches and what it cannot.
43
-
44
- An envelope that parses but carries no `result` field skips the reply assertion the same way an absent file does. An envelope carrying an empty `result` fails it, since a run that returned no text is a finding rather than a gap in the input.
45
-
46
- Exit 0 means `pass` or `unchecked`. Exit 1 means `fail`, or a caller error: a malformed target, or a sandbox that was never provisioned. A missing sandbox reports as an error rather than a failed verdict, because failing every path assertion would read as a skill that did nothing. `--strict` moves `unchecked` to exit 1 for a caller that has finished arming its scenarios.
47
-
48
- ## Headless runs
49
-
50
- `canon sandbox run` provisions a scenario, drives one `claude -p` session over it, and scores the result with `canon sandbox check`.
51
-
52
- ```bash
53
- canon sandbox run <category>:<command> "<prompt>" [arm]
54
- canon sandbox run claude:plan-feature "/canon:plan-feature add a widget" small
55
- ```
56
-
57
- Every run bills a real session through the `claude` binary on `PATH`. The session runs with `--plugin-dir` pointed at this checkout's `claude/`, so it loads the branch's skills rather than an installed copy, and provisioning injects no skill into the tree for the same reason. Four variables override the flags it hands to `claude -p`:
58
-
59
- | Variable | Default |
60
- | ---------------------------------- | -------------------------------- |
61
- | `CANON_SKILL_TEST_MODEL` | `sonnet` |
62
- | `CANON_SKILL_TEST_TOOLS` | `Bash,Read,Glob,Grep,Edit,Write` |
63
- | `CANON_SKILL_TEST_MAX_TURNS` | `30` |
64
- | `CANON_SKILL_TEST_PERMISSION_MODE` | `bypassPermissions` |
65
-
66
- The max-turns value is the only budget a run has. An arm's `max_turns` is asserted after the run and cannot raise it.
67
-
68
- Stdout carries one JSON record: the session's envelope with `verdict`, `escapes`, and `sessions` merged in, described under the skills census below. Stderr carries the framed timeline, the provisioning output, the session's own stderr, the verdict report, and a warning for each escape, new session record, or reaped process. An envelope that does not parse still prints its verdict, under `is_error: true`.
69
-
70
- Exit codes follow whichever step stopped the run. Provisioning's code ends a run before the session starts, and a session that exits non-zero ends it with that code after writing a dead-run record, `{is_error, exit_code, raw_output}`, to the same runs folder the merged record uses. Otherwise the verdict's code stands, except that an envelope that does not parse forces 1. An interrupt reaps the session's process group before the runner exits.
71
-
72
- ## Provisioning equivalence
73
-
74
- `canon sandbox equivalence` proves a change moved nothing in what scenarios provision. It provisions every arm of the named targets on a base ref and on the working checkout, builds a manifest of each tree, and names what differs. It runs offline and never calls `canon sandbox run` or a `claude` binary, so no arm it runs bills a model.
75
-
76
- ```bash
77
- canon sandbox equivalence claude --base origin/main --json
78
- canon sandbox equivalence claude:plan-feature/full --base HEAD
79
- ```
80
-
81
- A target is `<category>`, `<category>:<command>`, or `<category>:<command>/<arm>`, defaulting to every category.
82
-
83
- | Flag | Effect |
84
- | ------------------ | -------------------------------------------------------------------------------------------------- |
85
- | `--base <ref>` | Ref the baseline is provisioned from, `main` by default |
86
- | `--include-anchor` | Also run the arms that push to the shared GitHub anchor, one at a time, for an attended run |
87
- | `--stub-remote` | Run the anchor arms against a local bare repository and a stub `gh`, writing nothing to the anchor |
88
- | `--out <dir>` | Keep the provisioned trees, logs, and per-side manifests in this folder |
89
- | `--no-masks` | Compare every path, which lists what each mask would hide |
90
- | `--json` | Emit the record on stdout |
91
-
92
- The baseline is a detached worktree with full history, removed on exit including after `SIGINT`. Each arm provisions on the base side, is read into a manifest, is deleted, then provisions on the head side at the same path with the run id pinned. A manifest holds every file's mode and hash, every symlink's target, the exit status, the narration, and for the outer repository and each nested one the commit subjects, the staged index, and the branch.
93
-
94
- The record lists per arm `identical`, `differs` with the manifest keys that differ, `red-on-base` when both sides fail the same way, or `skipped-anchor`. It also carries `canonVersion`, `baseCommit`, `masksApplied`, `errors`, and `counts`. A declared arm adds a `check` entry from `canon sandbox check` on the head tree, whose `asserted` count shows its `expect.toml` resolved from `sandbox/fixtures/`.
95
-
96
- Exit 0 means every compared arm is identical or red the same way on both sides. Exit 1 means any `differs`, a base and head exit that disagree, an armed arm that `canon sandbox coverage` lists and enumeration lacks, or a `.ts` scenario that fails to load.
97
-
98
- ## Scenario coverage
99
-
100
- `canon sandbox coverage` reports which scenarios declare expectations and which only provision a state. It reads the fixture tree, so it needs no provisioned sandbox and runs nothing. Where that tree does not ship it exits 1 and prints no percentage, since a denominator nobody looked at is not a coverage result. A tree that is present and holds no scenarios is a real zero and still reports one.
101
-
102
- ```bash
103
- canon sandbox coverage --json
104
- ```
105
-
106
- | Flag | Effect |
107
- | ---------- | ----------------------------------------------------------- |
108
- | `--json` | Emit the coverage record on stdout |
109
- | `--strict` | Exit 1 while any scenario declares no expectation |
110
- | `--skills` | Add a per-skill asserted, should-be-asserted, exempt census |
111
-
112
- The record carries every scenario with the arms that declare, plus `totalScenarios`, `armedScenarios`, and `armedArms`. Scenarios and arms count separately, since several arms can share one scenario and dividing one by the other overstates the rollout.
113
-
114
- ### The skills census
115
-
116
- `--skills` answers what the scenario count cannot, which is whether anything can fail a given skill. It adds `skills`, `totalSkills`, `asserted`, `shouldBeAsserted`, `exempt`, `staleExemptions`, and `supersededExemptions` to the record, and keeps the scenario view rather than replacing it. The two denominators disagree on purpose: an armed scenario under `infra/` or `tooling/` exercises a CLI domain and pairs with no skill at all.
117
-
118
- A skill pairs to a scenario by filename, `<category>-<command>` first and bare `<command>` second, so `claude/target-setup.ts` reaches the `target-setup` skill. `should-be-asserted` is the default rather than a queue to drain, and which of those skills earns an arm is a project decision the census does not make.
119
-
120
- `exempt` means no arm should be written and holds only with a reason, declared in `sandbox/exempt.toml` and limited to a harness limit the checker cannot reach past or a skill that writes no artifact. An armed arm outranks an exemption. An exemption naming no shipped skill, or naming one an arm now asserts, exits 1 without `--strict`. Each armed arm reports as `<category>:<command>/<arm>`, so two same-named arms under different scenarios stay distinct.
121
-
122
- `canon sandbox run` calls this after a headless run and merges the verdict into the envelope it prints. It also writes that merged record to `.canon/tmp/runs/sandbox/<target>-<arm>-<timestamp>.json` with a `writes` array appended, and logs the path on stderr. Both fields are what a later re-score needs, since `--envelope` and `--writes` read files the run deletes on exit.
123
-
124
- Two more fields ride alongside the verdict rather than inside it. `escapes` lists what the run wrote under a watched toolkit root, which the verdict cannot assert over because those files sit outside the sandbox tree. `sessions` reports the nested-dispatch bound, carrying `watched` for whether the client's session registry was there to read, `new` for the records that appeared while the run was in flight, `concurrent` for the records present both before and after, and `reap` for what the run found in the session's process group afterwards. Neither field fails a run on its own.
125
-
126
- `canon sandbox run` passes `concurrent` through `--concurrent-sessions` to `sandbox check`, and `checkEscapeScope` appends a witness count to an `unbounded escape:` message rather than lets it soften the verdict.