@erclx/canon 4.83.0 → 4.85.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.
@@ -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.83.0",
4
+ "version": "4.85.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: draft-and-pick
3
- description: Drafts several candidates for a decision judged by looking, renders them side by side on one page, hands the operator the addresses, takes the pick through the structured question surface, and loops on the pick until they stop. Use when asked to "draft some options", "show me a few versions", "try a few variations", "mock up alternatives", "give me candidates for X", or when a choice is taste rather than correctness. Do NOT use when the request already names the answer and asks for it to be built, which is `plan-feature`. Do NOT use to read source for roughness, which is `ux-audit`, to measure what a running interface costs to paint, which is `ux-measure`, to write tests for a change already made, which is `ui-test`, or to script a recording, which is `draft-screencast`.
3
+ description: Drafts several candidates for a decision judged by looking, renders them side by side on one page, hands the operator the addresses, takes the pick through the structured question surface, and loops on the pick until they stop. Use when asked to "draft some options", "show me a few versions", "try a few variations", "mock up alternatives", "give me candidates for X", or when a choice is taste rather than correctness. Do NOT use when the request already names the answer and asks for it to be built, which is `plan-feature`. Do NOT use to read source for roughness, which is `ux-audit`, to measure what a running interface costs to paint, which is `ux-measure`, to write tests for a change already made, which is `ui-test`, to script a recording, which is `draft-screencast`, or to inspect a running app across many findings, which is `ux-walkthrough`.
4
4
  ---
5
5
 
6
6
  # Draft and pick
@@ -48,7 +48,7 @@ An installed binary carrying no `plan-reach` subcommand reports the reach unread
48
48
 
49
49
  Capture leads the sequence because a routed fact lands in a context entry, which is a tracked file. Running it after the pull request opens leaves that edit off the branch entirely, so the fact reaches nothing. Memory files are gitignored either way, which is what hid the ordering while capture wrote only those.
50
50
 
51
- Stop at the Propose phase. Do not run Apply. Promoting an entry to `CLAUDE.md` or a skill body ships as its own change, separate from this feature.
51
+ Stop at the Propose phase. Do not run Apply. Promoting an entry to an always-loaded rule or a skill body ships as its own change, separate from this feature.
52
52
 
53
53
  ## After completion
54
54
 
@@ -29,7 +29,7 @@ A pen the caller cannot face is the same gap wearing a different shape. Routing
29
29
  ## Must not
30
30
 
31
31
  - Apply anything the user has not approved by item
32
- - Author a governance rule inline. Coding-standards rules have an owner and a scaffolding path, and a rule written here bypasses both.
32
+ - Author a toolkit rule inline. A toolkit rule under `internal/rules/` or `governance/rules/` has an owner and a scaffolding path, `internal-governance`, and a rule written here bypasses it. A project rule under `.claude/rules/project/` is project-owned and takes an `Edit` appending to an existing file, or a handoff to `create-rule` when none fits.
33
33
  - Mutate tracked files from the main worktree
34
34
  - Answer a question raised in a decision slot while applying. Discussion and application are separate passes so an approval is never inferred from a reply.
35
35
  - Delete a memory entry at all. Every exit from the pen is a move into the archive.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: memory-review
3
- description: Reviews `.canon/memory/` and proposes per-entry actions (promote to `CLAUDE.md`, move into a skill body, route to a context entry, hand off to governance, or retire as stale). Also runs the discuss, challenge, apply, and cleanup phases on an existing review file. Use when asked to "review memory", "discuss memory questions", "challenge the promotes", "apply memory decisions", "cleanup memory review", "promote memory", or "consolidate memories". Do NOT auto-apply. Output a grouped proposal and wait for block-by-block approval.
3
+ description: Reviews `.canon/memory/` and proposes per-entry actions (promote to an always-loaded rule, move into a skill body, route to a context entry, hand off to governance, or retire as stale). Also runs the discuss, challenge, apply, and cleanup phases on an existing review file. Use when asked to "review memory", "discuss memory questions", "challenge the promotes", "apply memory decisions", "cleanup memory review", "promote memory", or "consolidate memories". Do NOT auto-apply. Output a grouped proposal and wait for block-by-block approval.
4
4
  ---
5
5
 
6
6
  # Memory review
@@ -48,11 +48,12 @@ Read in parallel from the project root:
48
48
 
49
49
  Read in parallel from the project root. Skip any file or folder that does not exist.
50
50
 
51
- - `CLAUDE.md`: project behavior rules and Content ownership section
51
+ - `CLAUDE.md`: project behavior rules and Content ownership section, still a read target for the absorbed-already check even though it takes no new promotion
52
52
  - every `SKILL.md` under `.claude/skills/`: domain-scoped internal skill bodies
53
53
  - every `SKILL.md` under `claude/skills/`: plugin skill bodies
54
54
  - every `*.md` under `${CLAUDE_SKILL_DIR}/../../standards/`: authoring references
55
55
  - every `*.md` under `governance/rules/` in the toolkit repo, or `.claude/rules/` in a target project: coding-standards rules
56
+ - every `*.md` under `internal/rules/` in the toolkit repo, or `.claude/rules/project/` in a target project: always-loaded rules a promote lands in
56
57
 
57
58
  ### Step 3: classify each entry
58
59
 
@@ -62,11 +63,13 @@ Read in parallel from the project root. Skip any file or folder that does not ex
62
63
 
63
64
  For each in-scope entry (see Scope), pick one action:
64
65
 
65
- - **Promote to `CLAUDE.md`**: the rule is cross-domain behavior or a design principle applied across the whole project.
66
+ - **Promote to an always-loaded rule**: the rule is cross-domain behavior or a design principle applied across the whole project, passing `592-claude-md.md`'s test (applies every session regardless of what is being edited). Do not author a toolkit rule file inline.
67
+ - In the toolkit repo, hand off to `internal-governance` and `${CLAUDE_SKILL_DIR}/../../standards/rule.md`, which own `internal/rules/core/` (this repo only, never ships) and `governance/rules/core/` (ships to every target).
68
+ - In a target project, append the rule to an existing file under `.claude/rules/project/` with `Edit`, since a project rule is project-owned and sync never overwrites it, or hand off to the `create-rule` skill when no existing file fits.
66
69
  - **Promote to a skill body**: the rule fires only when editing a specific path-scoped domain. Name the target skill.
67
70
  - **Promote to a standards file**: the rule is an authoring reference that belongs in the project's own standards folder as `<domain>.md`.
68
- - **Promote to a context entry**: the entry states a fact about a domain carrying an entry in `canon/context/index.md`. Append it to `.canon/tmp/memory-routing/<slug>.md` at the main worktree root, in the format `memory-capture` writes, and tell the user to run `/docs-fold` from a branch. Do not edit the context entry here.
69
- - **Hand off to governance**: the rule is coding-standards class (typescript, testing, naming, error-handling, performance, logging, concurrency, planning). Do not author the rule file inline. Never edit the synced `.claude/rules/` copies of toolkit rules, because `canon gov sync` overwrites them. Stop at handoff.
71
+ - **Promote to a context entry**: the entry states a fact about a domain carrying an entry in `canon/context/index.md`. Append it to `.canon/tmp/memory-routing/<slug>.md` at the main worktree root, in the format `memory-capture` writes, and tell the user to run `/docs-fold` from a branch. Do not edit the context entry here. A project-identity or command fact takes this action when a context entry owns the subject, such as a development entry for commands, and **Retire** otherwise, naming a hand edit to `CLAUDE.md` as the reason. Memory review does not write the root file itself.
72
+ - **Hand off to governance**: the rule is coding-standards class (typescript, testing, naming, error-handling, performance, logging, concurrency, planning), never a cross-domain behavior rule. Do not author the rule file inline. Never edit the synced `.claude/rules/` copies of toolkit rules, because `canon gov sync` overwrites them. Stop at handoff. This and **Promote to an always-loaded rule** never both claim one entry: class names the topic (coding-standards routes here), firing axis names the rest (applies-every-session routes to the rule promote).
70
73
  - In the toolkit repo, point the user at `internal-governance` and `${CLAUDE_SKILL_DIR}/../../standards/rule.md`, which own the source-of-truth rules under `governance/rules/`.
71
74
  - In a target project, point the user at the `create-rule` skill, which scaffolds a project-local rule under `.claude/rules/`.
72
75
  - **Retire**: the rule is stale, already absorbed into a durable surface, too vague to phrase as a rule, or a one-time incident narrative. Apply moves the file to `.canon/tmp/memory-archive/` rather than deleting it.
@@ -83,7 +86,7 @@ The check covers implication, not only keyword match. If an adjacent bullet in t
83
86
 
84
87
  #### Crispness check
85
88
 
86
- Rules that resist crisp one-line phrasing default to **Retire** over promote. Never promote a memory unchanged. Rewrite to match the destination surface's tone. Use terser phrasing for `CLAUDE.md` and imperative phrasing for skill bodies.
89
+ Rules that resist crisp one-line phrasing default to **Retire** over promote. Never promote a memory unchanged. Rewrite to match the destination surface's tone. Use single-directive rule-bullet phrasing for an always-loaded rule and imperative phrasing for skill bodies.
87
90
 
88
91
  ### Step 4: write the proposal to the review file
89
92
 
@@ -134,9 +137,9 @@ Before applying any item, check the worktree state:
134
137
 
135
138
  If the result is `MAIN`, stop and tell the user: `❌ Apply phase mutates tracked files. Run /session-worktree first.` Discuss and Challenge phases only touch `.canon/review/` scratch and run from anywhere.
136
139
 
137
- Before applying a promote to root `CLAUDE.md`, load `internal-claude` so its seed-mirror rule fires on the edit.
140
+ Before applying a promote to an always-loaded rule in the toolkit repo, load `internal-governance`, which owns `internal/rules/core/` and `governance/rules/core/` and is the only route that may author into either. In a target project, hand off to `create-rule` with no load, since `internal-governance` is a toolkit-internal skill under `.claude/skills/` that a target project never has. An `Edit` appending to an existing file under `.claude/rules/project/` needs no load either.
138
141
 
139
- Promotions are a separate concern from any feature in flight. Keep the promoted edits on their own commit. Do not fold a `CLAUDE.md` or skill-body change into a feature's commits, because a feature reviewer should not have to vet a change to how the agent operates.
142
+ Promotions are a separate concern from any feature in flight. Keep the promoted edits on their own commit. Do not fold an always-loaded-rule or skill-body change into a feature's commits, because a feature reviewer should not have to vet a change to how the agent operates.
140
143
 
141
144
  For each item, parse the `Decision:` line:
142
145
 
@@ -149,7 +152,7 @@ Free-form text after the verb is a reason. Capture it in the receipt but do not
149
152
 
150
153
  Action by action type:
151
154
 
152
- - **Promote**: use `Edit` to insert the rewritten rule into the target surface, then archive the memory file.
155
+ - **Promote**: use `Edit` to insert the rewritten rule into the target surface, then archive the memory file. A promote to an always-loaded rule in the toolkit repo never reaches this line: it stops as a handoff to `internal-governance`, the same as **Hand off** below, and archives only on the user's explicit confirmation.
153
156
  - **Promote to a context entry**: append the fact to `.canon/tmp/memory-routing/<slug>.md` at the main worktree root, then archive the memory file. `docs-fold` folds it in on its next run from a branch, which is what keeps one skill writing context entries.
154
157
  - **Hand off**: do not edit governance. Archive the memory file only if the user confirmed the handoff explicitly. Otherwise leave it in place.
155
158
  - **Retire**: archive the memory file.
@@ -45,4 +45,4 @@ Decision:
45
45
 
46
46
  ## Variation by action
47
47
 
48
- For Hand off items, the body is a pointer to the governance target instead of a rewritten rule: `internal-governance` and `${CLAUDE_SKILL_DIR}/../../standards/rule.md` in the toolkit repo, or the `create-rule` skill in a target project. For Retire items, skip the rewrite block. Every item gets a `Decision:` slot regardless of action. `Take:` is added only when a question response is needed.
48
+ For Hand off items, the body is a pointer to the governance target instead of a rewritten rule: `internal-governance` and `${CLAUDE_SKILL_DIR}/../../standards/rule.md` in the toolkit repo, or the `create-rule` skill in a target project. For a Promote to an always-loaded rule item, the H2 target names the rule file path the rule lands in, `<target>` in the template above, being an existing path under `internal/rules/core/` or `governance/rules/core/` in the toolkit repo, or under `.claude/rules/project/` in a target project, or the `create-rule` pointer when no existing file fits. For Retire items, skip the rewrite block. Every item gets a `Decision:` slot regardless of action. `Take:` is added only when a question response is needed.
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: ux-walkthrough
3
+ description: Why a multi-finding inspection walkthrough with the operator needs its own surface beside draft-and-pick, and where the boundary against building and auditing falls
4
+ ---
5
+
6
+ # UX walkthrough requirement
7
+
8
+ ## Gap
9
+
10
+ Without this skill, a session running an inspection walkthrough with the operator:
11
+
12
+ - Files every round's evidence into one flat shared folder, so nothing shows which walkthrough a folder came from or in what order the rounds ran.
13
+ - Hands over screenshots or file paths the operator cannot open, where a served localhost page is the one form that reaches them.
14
+ - Asks the pick question before the operator has the page, so the answer is taken from a description.
15
+ - Sends the link inside the same message as the question, where the structured question surface draws over it and the operator never sees the link.
16
+ - Draws arms as hand-written mock-ups of the app, which drift a few pixels and a few words from what ships, rather than lifting the built page's own markup and stylesheet.
17
+ - Measures the mark and not the text inside it, or the gap and not what shows above it, so a pick ships with half its condition unchecked.
18
+ - Quotes figures in a pick question from memory, and records them without reading the computed values back.
19
+ - Relays each pick as it is taken when the operator wants picks batched on their own call.
20
+ - Serves a rebuilt export from a server whose working directory was deleted by the rebuild, and measures a page that is not there.
21
+ - Applies a pick to the tracked tree, or deletes the candidate pages, because the single-decision loop it borrowed does both.
22
+ - Leaves findings, numbers and build criteria in chat, so whoever files the work does it from a summary rather than from a record.
23
+
24
+ ## Must
25
+
26
+ - Record conditions, findings with their measurements, picks with the arms they beat, evidence paths and build criteria in one walkthrough file under `.canon/walkthroughs/`.
27
+ - Measure each finding off the built page before drafting any arm.
28
+ - Build candidate pages from the app's rendered markup and built stylesheet, with a theme toggle, served on localhost.
29
+ - Capture every arm in every theme the app ships to its walkthrough's own evidence folder, and look at the captures before handing the link over.
30
+ - Number the walkthrough folder and each round's folders so they sort in the order they ran, and let a round's number match its finding's.
31
+ - Send the localhost link in its own message, ending the turn, before every pick question is asked.
32
+ - Hold picks and relay them only when the operator calls a batch, as one message, to the controller where one exists and to the operator otherwise.
33
+ - Route a finding with no visible choice into the batch as a proposed row rather than drafting arms for it.
34
+
35
+ ## Must not
36
+
37
+ - Change a tracked file, create a branch or commit, or file a task row.
38
+ - Apply a winning arm or delete the evidence.
39
+ - Draft arms for a finding the operator has not raised.
40
+ - Restate `draft-and-pick`'s arm rules or the `canon capture` and `canon serve` mechanics.
41
+ - Fire on a request naming one decision alone, which `draft-and-pick` covers.
42
+ - Review criterion, not a gate: whether anything other than the operator or a controller's launch brief invokes this skill, and which lines a second project found in its way. Read both back after it has run outside the project it was written in.
43
+
44
+ ## Guards
45
+
46
+ The refusal strings sit in the body. Two conditions stop a run: no build to measure against, and a request to change a tracked file.
47
+
48
+ ## Out of scope
49
+
50
+ - `draft-and-pick` runs one decision end to end and applies it. This runs many decisions and applies none.
51
+ - `ux-audit` reads source for roughness. This takes its findings from what the operator saw.
52
+ - `plan-feature` and a worker build a pick. This stops at the record and the batch.
53
+ - The controller, or the operator where none exists, decides rows, order and pull request boundaries. This proposes and does not file.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: ux-walkthrough
3
+ description: Runs an inspection walkthrough over a running app with the operator. Measures each finding off the built page, drafts arms as served HTML pages lifted from the app's own markup and stylesheet, hands over the localhost link before every pick question, records findings and picks with the arms they beat in one walkthrough file, and relays picks only in batches the operator calls. Use when asked to "run a first-use walkthrough", "do an operator walkthrough", "go through my findings one by one", "inspection walkthrough over the app", or "walk through everything I listed with me". Do NOT use for one decision on its own, which is `draft-and-pick`, to read source for roughness, which is `ux-audit`, or to build a pick, which is `plan-feature` and a worker.
4
+ ---
5
+
6
+ # UX walkthrough
7
+
8
+ A walkthrough turns what the operator sees in a running app into findings and picks a builder can act on without the conversation. The render is the decision and the record carries the measurements, so every pick here is taken by looking and written down with the numbers behind it.
9
+
10
+ ## Guards
11
+
12
+ - If no running build or build command exists to measure against, stop: `❌ Nothing to inspect. A walkthrough measures a running build.`
13
+ - If the session is asked to change a tracked file, stop and route it: `❌ A walkthrough records and does not build. Hand the pick to whoever dispatches the build.`
14
+ - Draft no arm for a finding the operator has not raised or agreed to take up.
15
+
16
+ ## Posture
17
+
18
+ - Write only under `.canon/`, never a tracked file, and create no branch, commit or task row.
19
+ - Stay in the checkout the session started in. Enter no worktree for a walkthrough, since nothing it writes is tracked.
20
+ - Read the `walkthrough.md` of every earlier walkthrough under `.canon/walkthroughs/` first, and raise nothing they already decided.
21
+ - Name every folder a walkthrough writes with a two-digit prefix, per `${CLAUDE_SKILL_DIR}/references/record.md`, so the walkthrough and its rounds sort in the order they ran.
22
+ - Put builds, generator scripts and logs in a scratch folder outside the tracked tree, and anything the operator opens under `.canon/tmp/`.
23
+
24
+ ## Steps
25
+
26
+ 1. **Bring up both builds and record the conditions.** Follow `${CLAUDE_SKILL_DIR}/references/builds.md`. Start the walkthrough file with the commit, the ports and the build commands before the first finding.
27
+ 2. **Take the operator's list in their order.** Name each item as a finding with the walkthrough letter and a number, such as T1, and confirm the order once rather than per item.
28
+ 3. **Measure before drafting.** Read the component behind the finding and pull the numbers off the built page, per `${CLAUDE_SKILL_DIR}/references/measuring.md`. Write the finding into the walkthrough file with those numbers before any arm exists.
29
+ 4. **Route a finding with no visible choice.** A parse defect, a stale figure or a broken invariant gets recorded as a finding with no draft and goes into the batch as a proposed row, not as a pick.
30
+ 5. **Draft three or four arms.** Follow `draft-and-pick` Steps 1 and 2 for the arms, with arm 0 the shipped state, one property varied and a cost on each. Build the page from the app's own rendered markup, per `${CLAUDE_SKILL_DIR}/references/candidate-pages.md`.
31
+ 6. **Look before handing anything over.** Capture every arm in every theme the app ships into `.canon/walkthroughs/<nn>-<slug>/evidence/<nn>-<slug>/`, with the finding's number as the prefix, open the captures, and fix what rendered wrong before the operator sees the page.
32
+ 7. **Hand the link, then ask.** Emit `http://localhost:<port>/<nn>-<slug>/candidates.html` in a message that ends the turn, confirmed with a `200`, carrying no question. Take any reply after that message as the operator having looked, an explicit "go" included, rather than holding for a stated confirmation, and only then put the choice through the structured question surface with the recommendation first.
33
+ 8. **Record the pick.** Write what won, what it beat and by which numbers, where the evidence is, and the build criteria, per `${CLAUDE_SKILL_DIR}/references/record.md`. Read every figure you quote back from the page or the data file first, and correct the record where the question quoted one wrong.
34
+ 9. **Hold picks for the batch.** Relay nothing per pick. When the operator calls the batch, send it once per `${CLAUDE_SKILL_DIR}/references/relay.md`.
35
+ 10. **Close on the operator's word.** Add the walkthrough summary table and the handoff, leave the evidence in place, and report every file written by its path.
36
+
37
+ ## Rules
38
+
39
+ - Answer a question the operator asks mid-walkthrough in prose first, with a recommendation, and offer a draft rather than drafting unasked.
40
+ - Say when a pick revises an earlier walkthrough's pick, and record it as a revision naming the pick it revises.
41
+ - Check a claim against the code or the data before an arm makes it, since an arm drawn on a wrong fact is a pick on nothing.
42
+ - Measure both halves of a pick whose condition has two, such as a gap and what shows above it.
43
+ - Keep a finding's measured numbers and the pick's build criteria in the record, never only in chat.
44
+
45
+ ## What this delegates
46
+
47
+ - `draft-and-pick` owns the arm discipline and the structured question. This walkthrough departs from its Step 2 inlining and its Step 6 apply and delete, for the reasons `${CLAUDE_SKILL_DIR}/references/candidate-pages.md` states.
48
+ - `write-human` carries the voice of every recorded passage and any copy an arm puts in front of a reader.
49
+ - `plan-feature` and whoever dispatches builds turn a batch into plans and code.
50
+ - `canon capture`, `canon serve` and `canon sessions list` own the render, the address and the roster.
@@ -0,0 +1,15 @@
1
+ # Builds and servers
2
+
3
+ Read when bringing the app up at the start of a walkthrough, and whenever a measurement returns nothing it should.
4
+
5
+ ## Bring up
6
+
7
+ - Run the live build and whatever service it calls from the checkout the session started in, and record the commit, ports and any state that changes a reading as the walkthrough file's conditions.
8
+ - Build the artifact the deploy ships in a scratch folder outside the tracked tree, so a build never touches the project.
9
+ - Serve that artifact the way the host resolves it. A plain file server can answer a clean route the host rewrites with a 404, such as `/about` for `about.html`.
10
+ - Rebuild from the current main whenever main moves mid-walkthrough, and say so in the record. Measuring a stale build reports on code that no longer ships.
11
+
12
+ ## Traps
13
+
14
+ - Restart a server after rebuilding what it serves. A server started inside an output folder the rebuild replaced keeps serving a deleted directory, and every selector comes back empty.
15
+ - Note what a build cannot show, such as live timing a recorded build does not carry or recompile pauses a dev server adds. Neither is a finding.
@@ -0,0 +1,26 @@
1
+ # Candidate pages
2
+
3
+ Read when drafting arms for a finding. Skip it for a finding with no visible choice.
4
+
5
+ ## Build from the app
6
+
7
+ - Dump the rendered markup of the surface under test from the built page, after it has loaded, into a JSON file in scratch. Strip scripts from the dump.
8
+ - Copy the built stylesheet and font files under `.canon/tmp/`, at the path the dumped markup links them from, so a candidate page renders with the stylesheet the app ships. Every arm then differs from the shipped page only by what the arm names.
9
+ - Trim a large dump to the part the finding needs, and keep the wrapper classes intact so layout rules keyed to them still resolve.
10
+ - Size each frame to the content width the finding names rather than the window width, since a container query reads the frame.
11
+ - Write copy an arm introduces with `write-human`, and take every other word from the dump.
12
+
13
+ ## The page
14
+
15
+ - Generate the page with a short script, one per round, so a shared change is one edit and a rerun.
16
+ - Put a theme button on the page that flips the app's own theme switch on the root, rather than drawing each frame once per theme. Read `?arm=<id>&theme=<name>` to strip every other arm and the button for capture. Skip the button for an app that ships one theme.
17
+ - Print each frame's measurement under it from a script in the page, so the numbers the operator reads are the browser's.
18
+ - Write an index page linking one page per arm when an arm is a whole page or a route rather than a frame, with thumbnails copied beside it.
19
+ - Name anything the preview cannot reproduce, such as an asset whose colors follow the browser rather than the page's theme button, rather than fixing the page around it.
20
+
21
+ ## Where this departs from draft-and-pick
22
+
23
+ - Capture into the walkthrough's own numbered evidence folder instead of the folder `draft-and-pick` Step 6 archives arms into, per `${CLAUDE_SKILL_DIR}/references/record.md`.
24
+ - Link the app's built stylesheet instead of inlining every asset. Lifted markup needs the real stylesheet, and an inlined copy is the drift this avoids.
25
+ - Keep `candidates.html` and never apply the winning arm. The walkthrough records and a build applies.
26
+ - Leave `.canon/tmp/<nn>-<slug>/` in place until the picks are built, and never delete the round's evidence folder.
@@ -0,0 +1,22 @@
1
+ # Measuring
2
+
3
+ Read before writing a finding and before recording a pick.
4
+
5
+ ## Off the page
6
+
7
+ - Measure on the built page in a real browser at the widths the finding names, in every theme the app ships, and record the viewport.
8
+ - Read computed styles rather than class lists. A class list names intent, and the computed value is what paints.
9
+ - Measure text contrast against the ground it sits on, and a mark's contrast against its own ground, as two numbers. A mark that sets a background and no text color inherits the browser default, and only the text reading catches it.
10
+ - Measure empty space by text extent, the union of each text node's client rects, rather than by leaf element boxes. A block element spans its row whatever its text covers.
11
+ - Measure a landing by the landed element's top against the bar's bottom edge and by whether the element before it is visible. A pick with a two-part condition needs both readings.
12
+ - Reproduce the shipped value inside the candidate page before trusting an arm's numbers. Arm 0 reading the same as the live page is what shows the page matches.
13
+
14
+ ## Off the data
15
+
16
+ - Re-derive a figure from the data file that produced it rather than from any document quoting it. Two documents can disagree, and neither is the source.
17
+ - Count a defect across the whole corpus or tree before recording its size, and say whether it sits only at the end, only in one version, or throughout.
18
+ - Test a proposed fix without editing code where the question is its effect, by patching the function in a throwaway script and comparing before and after on counts the fix could move.
19
+
20
+ ## Before quoting
21
+
22
+ - Read every figure a pick question or a record quotes back from the page or the script that computed it. Correct the record in place when the question quoted one wrong, and say so.
@@ -0,0 +1,36 @@
1
+ # The walkthrough record
2
+
3
+ Read when starting the walkthrough file, recording a finding, and recording a pick.
4
+
5
+ ## File
6
+
7
+ - Write to `.canon/walkthroughs/<nn>-<slug>/walkthrough.md`, with `<nn>-<slug>` the walkthrough's own folder.
8
+ - Open with one paragraph naming the walkthrough, the date, the commit and what landed since the last walkthrough, then `## Conditions`, `## Findings`, `## Picks`, `## Walkthrough summary` and `## Handoff`.
9
+ - Number findings with one letter per walkthrough and a counter, and never reuse a number.
10
+
11
+ ## Folder names
12
+
13
+ - With no earlier walkthrough on this topic, a fresh folder takes the next ordinal in `.canon/walkthroughs/`'s own sequence: list the folders present, take the highest `<nn>`, and increment it, starting at `01` when none exist.
14
+ - Put a round's captures at `.canon/walkthroughs/<nn>-<slug>/evidence/<nn>-<slug>/` and its candidate pages at `.canon/tmp/<nn>-<slug>/`, where the inner `<nn>` is the finding's own number, so T1's folders start `01-`.
15
+ - Leave a gap where a finding has no draft. The missing number is what maps each folder to its entry in the record.
16
+ - Take `<slug>` from the decision sentence the way `draft-and-pick` Step 1 derives it, and add only the prefix.
17
+ - Rename no folder from a walkthrough that predates this rule. Tracked documents cite those paths, and a rename breaks every citation.
18
+
19
+ ## A finding
20
+
21
+ - Head it `### T<n>: <what is wrong, as a claim>`.
22
+ - State what the operator saw, the code behind it by path and line, and the measurement in a table when it has more than two readings.
23
+ - Name an earlier pick the finding revises, and a collision with work in flight.
24
+
25
+ ## A pick
26
+
27
+ - Head it `### Pick <n>, T<n>: arm <id>, <what won>`.
28
+ - One paragraph on what the arm does and its measured result, then `It beat:` with one bullet per losing arm and the number that lost it.
29
+ - One `Evidence:` line naming the evidence folder and its file set.
30
+ - `Build criteria:` as bullets a builder checks against the built page, each measurable, including what the render shows that the prose does not.
31
+ - An answer to a question the operator asked with the pick goes under the criteria, stated as a decision with its reason.
32
+
33
+ ## Close
34
+
35
+ - `## Walkthrough summary` is one table row per finding: the finding, the pick, and the arms it beat.
36
+ - `## Handoff` proposes how the picks split into pull requests by the files each writes, and names shared files and the order they force.
@@ -0,0 +1,25 @@
1
+ # Relaying picks
2
+
3
+ Read when the operator calls a batch, and at the start of the walkthrough when a launch brief asks for collisions.
4
+
5
+ ## Where the batch goes
6
+
7
+ - Send it to the controller where the session was dispatched by one, resolved per the Channel rule, matched by name through `canon gov list --rules --json` rather than a path a target may not have installed.
8
+ - Give it to the operator as one list in the conversation where no controller exists. The walkthrough file is the handoff either way.
9
+
10
+ ## When
11
+
12
+ - Relay nothing per pick. Relay once when the operator calls the batch, covering every pick held since the last one.
13
+ - Put a finding with no visible choice in the same batch, as a proposed row.
14
+
15
+ ## The message
16
+
17
+ - Open with one line saying what the batch holds and where the record is.
18
+ - Give each pick its finding, the arm, the numbers that decided it, the arms beaten, and the files it writes.
19
+ - Close with overlaps and order: files two picks share, picks that must follow another, and collisions with open pull requests or unmerged branches.
20
+ - Name anything the operator holds that is not a row, such as a repository setting.
21
+
22
+ ## Collisions
23
+
24
+ - List open pull requests with their files, and unmerged remote branches with `git diff --stat origin/main...origin/<branch>`, and compare file sets rather than descriptions.
25
+ - Stamp an overlap with the commit it was measured against.
@@ -25,7 +25,7 @@ canon transcripts <url>
25
25
  ```
26
26
 
27
27
  - Pass `--keep-timestamps` when the user wants `[mm:ss]` markers per line instead of prose.
28
- - Pass `--out <dir>` to override the output directory. The default is `transcripts/` in the current directory.
28
+ - Pass `--out <dir>` to override the output directory, resolved against the current directory. The default is the backed `.canon/transcripts/` folder at the main worktree root, under a filename shaped `<fetch-date>--<title-slug>--<video-id>.md`.
29
29
  - The written file path prints to stdout. Surface it back to the user as a full relative path, in the form the project's instruction file sets under `## Output`.
30
30
 
31
31
  ## After the fetch
@@ -53,7 +53,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
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
- | `canon records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
56
+ | `canon records push` | Commit every record folder less the exclusion set and push it to a private records remote (`--json`) |
57
57
  | `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
58
58
  | `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`) |
59
59
  | `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`) |
@@ -126,7 +126,7 @@ Exit codes: `0` reported the next ordinal, or `--claim` created the folder. `1`
126
126
 
127
127
  ## Size
128
128
 
129
- `canon records size` reports what each record folder holds and how much of it is recent. It reads the ten backed folders named under Push and pull, plus the scratch folder, and it gates nothing.
129
+ `canon records size` reports what each record folder holds and how much of it is recent. It reads the same backed folders push carries, resolved the same way, plus the scratch folder, and it gates nothing.
130
130
 
131
131
  ```bash
132
132
  canon records size
@@ -138,7 +138,7 @@ canon records size --json
138
138
  | `--json` | Add a machine-readable record on stdout |
139
139
  | `--root <path>` | Project root, defaulting to the main worktree |
140
140
 
141
- The table carries one row per folder that exists, heaviest first, with the file count, the bytes, a count for each growth window, and the dates of the least and most recently written file. Those dates render in the machine's local time, which is the calendar day whoever wrote the file was living in, and the reading is per-machine already. Folders that do not exist are named on one line below it rather than printed as rows of zeros. The record a `--json` call emits carries every folder either way, each with a `present` flag, so a caller reading the record gets a stable set of keys and can tell an absent folder from one the reading skipped.
141
+ The table carries one row per folder that exists, heaviest first, with the file count, the bytes, a count for each growth window, and the dates of the least and most recently written file. Those dates render in the machine's local time, which is the calendar day whoever wrote the file was living in, and the reading is per-machine already. Folders that do not exist are named on one line below it rather than printed as rows of zeros. At the legacy `.claude` root, the record a `--json` call emits carries every folder in the fixed list either way, each with a `present` flag, so a caller reading the record gets a stable set of keys and can tell an absent folder from one the reading skipped. At a `.canon` root, the folder set is read off the directory itself rather than off a fixed list, so an absent folder is not listed at all: nothing enumerates a name nobody has created yet.
142
142
 
143
143
  Ordering by weight is what makes the reading worth taking. A folder listed alphabetically hides behind its neighbors, and the row a reader came for is the one that grew.
144
144
 
@@ -150,7 +150,7 @@ The scratch folder is read here and skipped by a backup, because deletable witho
150
150
 
151
151
  The window counts read `mtime`, so what they report is a file written inside the window rather than one created there. An entry edited long after it landed reads as recent, which overstates growth and never understates it, and these folders are append-mostly so the two readings agree on nearly every file. The one reading that is wrong rather than early is a machine restored by `canon records pull`, which resets the work tree hard and re-dates every file it writes, so a window taken there counts the restore. Nothing on the filesystem separates the two, since a restored file is new by every stamp it carries.
152
152
 
153
- Exit codes: `0` the reading completed, `1` refused. The one refusal is `no-folder`, raised when the project holds neither record root. A project holding a root and no records is empty rather than absent, and each folder's own `present` flag already says which of the ten it carries.
153
+ Exit codes: `0` the reading completed, `1` refused. The one refusal is `no-folder`, raised when the project holds neither record root. A project holding a root and no records is empty rather than absent: at the legacy `.claude` root each folder's own `present` flag says which ones it carries, and at a `.canon` root the folder list itself is already the answer, since nothing absent is named.
154
154
 
155
155
  ## Push and pull
156
156
 
@@ -162,9 +162,9 @@ canon records push --json
162
162
  canon records pull
163
163
  ```
164
164
 
165
- The backed folders are `diagrams`, `groundwork`, `intake`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`, all under whichever record root the project carries. They are the record root's own entries less three: `tmp`, which is deletable without loss, `.claude/worktrees/`, whose contents belong to the project repository already, and `.records.git/`, which is the history the rest are pushed into. Nothing bounds the list from outside any more, since the claude manifest ships one `.canon/` root entry and names no folder, so spelling the nine out is what keeps a record folder added later from silently entering the payload. Each name is a top-level record folder and every archive sits inside the one it archives, so the list stays at one entry per surface however many archives appear. It is a constant rather than configuration, and it deliberately does not match the six record kinds `validate` hardcodes.
165
+ 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.
166
166
 
167
- 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 nine 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.
167
+ 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.
168
168
 
169
169
  ### Setup
170
170
 
@@ -196,7 +196,7 @@ Point it at a private repository, and at one that is not a remote of the project
196
196
 
197
197
  `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.
198
198
 
199
- 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 none of the ten has nothing to lose, so a restore onto a fresh checkout runs straight through.
199
+ 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.
200
200
 
201
201
  ### When it runs
202
202
 
@@ -105,7 +105,7 @@ An execution that picks other than the suggestion rewrites the `- Suggested:` li
105
105
 
106
106
  `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.
107
107
 
108
- `canon records push` carries these folders off the disk they live on, and `canon records pull` brings them back. Nine of them are backed: `diagrams`, `groundwork`, `intake`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`, each carrying whatever it has archived inside it. 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.
108
+ `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.
109
109
 
110
110
  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.
111
111
 
@@ -242,6 +242,7 @@ This section is the corpus the coverage claim is measured against: every name `c
242
242
  | `canon:markdown-propose` | When a markdown claim needs rewriting and the change should wait for an answer per file |
243
243
  | `canon:ux-audit` | To read UI source for missing states, edge cases, and inconsistencies |
244
244
  | `canon:ux-measure` | To start the interface and measure paint, processor, and layout cost |
245
+ | `canon:ux-walkthrough` | To run a multi-finding inspection pass over a running app with the operator |
245
246
 
246
247
  ### Ship it
247
248
 
@@ -41,6 +41,7 @@ A cell no source anchors ends in `? verify`, and the preview shows that marker b
41
41
  - `canon:ux-audit` for UX gap detection on existing surfaces
42
42
  - `canon:ux-measure` for what a running surface costs to paint, read against published thresholds
43
43
  - `canon:draft-and-pick` for a call settled by looking, drafting several candidates onto one page and taking your pick
44
+ - `canon:ux-walkthrough` for a multi-finding inspection pass over a running app with the operator, drafting arms from the app's own markup and recording each pick for a builder
44
45
  - `canon:draft-identity` to draft a project's logo mark and compose it into an icon sequence and a social card, through `draft-and-pick`'s own render-and-pick loop
45
46
  - Anthropic's `frontend-design` plugin optional for light visual steering
46
47
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.83.0",
4
+ "version": "4.85.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -138,6 +138,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
138
138
  prompt: 'Tell me the paint and layout cost of this page right now.',
139
139
  expect: 'ux-measure',
140
140
  },
141
+ {
142
+ prompt:
143
+ 'Sit with me while I go through my whole list of app findings one at a time, and write down each pick with what it beat.',
144
+ expect: 'ux-walkthrough',
145
+ },
141
146
  {
142
147
  prompt:
143
148
  'I am building this branch for another session. What am I on the hook for, and what is off limits?',
@@ -1,7 +1,7 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
3
  import type { Command } from 'commander'
4
- import { BACKED_FOLDERS, pullRecords, pushRecords } from '@/records/backup'
4
+ import { pullRecords, pushRecords } from '@/records/backup'
5
5
  import { migrateRecord } from '@/records/migrate'
6
6
  import {
7
7
  type ClaimOutcome,
@@ -14,7 +14,7 @@ import {
14
14
  type FolderSize,
15
15
  formatBytes,
16
16
  GROWTH_WINDOWS,
17
- SIZED_FOLDERS,
17
+ sizedFolders,
18
18
  type SizeOutcome,
19
19
  sizeRecords,
20
20
  } from '@/records/size'
@@ -207,8 +207,9 @@ export function register(program: Command): void {
207
207
  'after',
208
208
  [
209
209
  '',
210
- 'Folders read under .claude/:',
211
- ` ${SIZED_FOLDERS.join(', ')}`,
210
+ 'Folders read, at whichever record root the project carries:',
211
+ ' every folder canon records push carries, plus the scratch folder',
212
+ ' (deletable without loss, so a backup skips it, but a reading does not)',
212
213
  '',
213
214
  'Exit codes:',
214
215
  ' 0 the reading completed',
@@ -274,8 +275,10 @@ function backupHelp(verb: 'push' | 'pull'): string {
274
275
 
275
276
  return [
276
277
  '',
277
- 'Backed folders under .claude/:',
278
- ` ${BACKED_FOLDERS.join(', ')}`,
278
+ 'Backed folders, at whichever record root the project carries:',
279
+ ' every top-level entry, less tmp, ordinal-locks, and .records.git',
280
+ ' (a legacy .claude root instead uses a fixed list; run this with --json',
281
+ ' to see what actually resolved, including any folder seen for the first time)',
279
282
  '',
280
283
  'Exit codes:',
281
284
  ' 0 the records remote and this machine agree',
@@ -404,6 +407,9 @@ async function runPush(opts: BackupCommandOptions): Promise<number> {
404
407
  logInfo(
405
408
  `${outcome.folders.length} folder(s), ${outcome.changed} path(s) changed`,
406
409
  )
410
+ if (outcome.firstSeen.length > 0) {
411
+ logWarn(`first seen: ${outcome.firstSeen.join(', ')}`)
412
+ }
407
413
  logStep(outcome.pushed ? 'Pushed' : 'Nothing to push')
408
414
  logInfo(
409
415
  outcome.commit
@@ -104,7 +104,7 @@ export const TOKENS: DesignTokens = {
104
104
  ].join('\n'),
105
105
 
106
106
  colorNote: [
107
- 'Every role clears WCAG AA at 4.5:1 against each ground it declares, asserted in `src/design/contrast.test.ts`. Two corrections landed with this record becoming the source. The light `muted` step moved from `#7A736A`, which read 4.38 and 4.09 against the two light grounds, and the dark `accent` moved off the `#C8602E` the slide theme carried, which read 4.36 and 3.99 against the two dark ones. Both now sit on the values below.',
107
+ 'Every role clears WCAG AA at 4.5:1 against each ground it declares, asserted in `src/design/contrast.test.ts`.',
108
108
  '',
109
109
  'Warning and error hold ANSI codes because that is what `scripts/lib/ui.sh` writes and no rendered surface implements an equivalent. Giving either a hex value would invent a mapping no file has, so they carry no contrast reading either.',
110
110
  '',
@@ -232,7 +232,7 @@ export const TOKENS: DesignTokens = {
232
232
  typographyNote: [
233
233
  'One family covers every role but `page-display`, which is the landing page hero and takes the proportional sibling of the same superfamily. The size scale runs from 11.5 to 52 pixels, and six values map onto a role. Five further values are adjustments inside a single component and get no role here, since a scale with five invented steps reads as a system the surfaces do not implement. They are 11.5, 12.5, 13, 14, and 15 pixels.',
234
234
  '',
235
- 'The 52 pixel step sits above the 34 the rest of the scale tops out at, and it is the one size no other surface reaches. A hero headline set at the display cap reads as a section heading rather than an opening, which four rendered arms measured on 2026-09-04 before the step was added.',
235
+ 'The 52 pixel step sits above the 34 the rest of the scale tops out at, and it is the one size no other surface reaches, since a hero headline set at the display cap reads as an opening rather than as a section heading.',
236
236
  '',
237
237
  'A tagged cell is one no rendering surface exercises yet, which is a declaration the system has not tested rather than one it has.',
238
238
  '',
@@ -353,8 +353,11 @@ export const TOKENS: DesignTokens = {
353
353
  motion:
354
354
  'Motion is not used. No transition, animation, or keyframe declaration appears on any rendered surface, and the capture pipeline screenshots a static frame.',
355
355
 
356
- iconography:
357
- "No icon library is installed. `assets/brand/mark.svg` is the one authored icon, embedded inline in the hero topbar, and the surfaces otherwise draw literal glyph characters: `│ ├ ✓ ! ✗ + - ◆ ◇ ❯` for the terminal framing. The same mark also ships as a favicon on every rendered surface, as three independently-maintained copies that track different accents by design rather than by drift: `regen-hero.sh` derives one from the live SVG colored with whatever `--color-accent` (`#e0724b`) the fetched token CSS carries, `src/design/render.ts` carries the path data as a hardcoded literal colored via `colorValue('light-accent')` (`#a4471c`), since a data URI has no CSS context and that page renders on light chrome, and `teach-workspace`'s `SKILL.md` names one in prose colored `rgb(224,114,75)`, the same value as the dark accent written as decimal rather than hex to clear the shipped-references gate's commit-sha check. Unifying the three or repairing the one that looks drifted would break the fit each was chosen for.",
356
+ iconography: [
357
+ 'No icon library is installed. `assets/brand/mark.svg` is the one authored icon, embedded inline in the hero topbar, and the surfaces otherwise draw literal glyph characters: `│ ├ ✓ ! ✗ + - ◆ ◇ ❯` for the terminal framing.',
358
+ '',
359
+ 'The same mark ships as a favicon on every rendered surface, as three independently-maintained copies that track different accents by design rather than by drift, colored to fit the chrome each renders on: the dark accent (`#e0724b`) for a dark-chrome surface and the light accent (`#a4471c`) for a light-chrome one. Unifying the three or repairing the one that looks drifted would break the fit each was chosen for. `canon/context/design.md` carries which file holds each copy.',
360
+ ].join('\n'),
358
361
  }
359
362
 
360
363
  /** A role's value, or `undefined` where the record declares no such role. */
@@ -26,7 +26,7 @@
26
26
  import { existsSync } from 'node:fs'
27
27
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
28
28
  import { dirname, join } from 'node:path'
29
- import { BACKED_FOLDERS } from '@/records/backup'
29
+ import { presentFolders } from '@/records/backup'
30
30
  import { recordDir, SCRATCH } from '@/record-root'
31
31
 
32
32
  /**
@@ -168,13 +168,13 @@ function applyRewrites(
168
168
  return { text: rewritten.join('\n'), count }
169
169
  }
170
170
 
171
- /** The files under every `BACKED_FOLDERS` entry, archives included. */
171
+ /** The files under every present backed folder at `root`, archives included. */
172
172
  export async function walkScratchEvidenceCorpus(
173
173
  root: string,
174
174
  ): Promise<string[]> {
175
175
  const files: string[] = []
176
176
 
177
- for (const folder of BACKED_FOLDERS) {
177
+ for (const folder of presentFolders(root)) {
178
178
  const dir = recordDir(root, folder)
179
179
  if (!existsSync(dir)) continue
180
180
 
@@ -46,8 +46,9 @@ const CANON_SCRATCH = 'tmp'
46
46
  /**
47
47
  * Every entry that lives under the record root, at the name `.claude/` gave it.
48
48
  *
49
- * These are the twelve ignore patterns the move to `.canon/` collapsed into one,
50
- * so the list counts entries rather than record folders: `.records.git` is the
49
+ * These are the twelve ignore patterns the move to `.canon/` collapsed into
50
+ * one, plus every record folder added since, so the list counts entries
51
+ * rather than record folders: `.records.git` is the
51
52
  * backup history rather than a record, and `README.md` is a file a records pull
52
53
  * writes back. `worktrees` is absent because the harness creates a worktree
53
54
  * under `.claude/` and requires its target to sit there.
@@ -72,6 +73,8 @@ export const RECORD_ENTRIES: readonly string[] = [
72
73
  'review',
73
74
  'tasks',
74
75
  'teach',
76
+ 'transcripts',
77
+ 'walkthroughs',
75
78
  ]
76
79
 
77
80
  /** Whether a name under `.claude/` is one the record root owns. */
@@ -1,27 +1,25 @@
1
- import { existsSync } from 'node:fs'
1
+ import { existsSync, readdirSync } from 'node:fs'
2
2
  import { basename, join, relative, resolve } from 'node:path'
3
3
  import { $ } from 'bun'
4
4
  import { gitEnv } from '@/git-env'
5
5
  import { RECORD_ROOTS, recordRoot } from '@/record-root'
6
6
 
7
7
  /**
8
- * The folders a backup carries, relative to the record root `workTree` resolves
9
- * rather than to either root specifically, since the same ten names sit under
10
- * whichever one a tree holds.
8
+ * The legacy `.claude`-root allowlist, read only when a project has not moved
9
+ * to `.canon/` yet.
11
10
  *
12
- * Nothing bounds this list any more, and the move is what took the bound away.
13
- * The claude manifest used to ship a folder apiece, so the `# Claude` group
14
- * named a superset this could be read against; it ships one `.canon/` root
15
- * entry now and names no folder at all. Spelling the list out is therefore the
16
- * whole of the protection rather than half of it, since a record folder added
17
- * under `.canon/` is ignored the moment it exists and enters no payload until
18
- * a name is written here.
11
+ * That root still tracks `skills/`, `rules/`, and `hooks/` a backup must never
12
+ * carry, so it cannot take the exclusion-based reading `foldersAt` applies at
13
+ * `.canon/`: every top-level entry there is fair game and an allowlist is the
14
+ * only thing separating a record folder from the vendor's own. A `.canon/` root
15
+ * carries no such mix, since nothing else lives there, which is what let the
16
+ * bound move from this list to `EXCLUDED_ENTRIES`.
19
17
  *
20
- * Three counts describe this surface and each is right about a different
21
- * question, so they are stated apart rather than reconciled. Ten is what a
22
- * disk loss would take, which is this list. Twelve is what sat under `.claude/`
23
- * as an ignored folder before the move, which adds the scratch folder that is
24
- * deletable without loss and `worktrees/`, whose contents belong to the
18
+ * Three counts describe what this list once bounded and each is right about a
19
+ * different question, so they are stated apart rather than reconciled. Eleven
20
+ * is what a disk loss would take, which is this list. Twelve is what sat under
21
+ * `.claude/` as an ignored folder before the move, which adds the scratch folder
22
+ * that is deletable without loss and `worktrees/`, whose contents belong to the
25
23
  * enclosing repository already. Thirteen is what the move relocated, which counts
26
24
  * ignore entries rather than folders: the twelve less `worktrees/`, which stayed,
27
25
  * plus `.records.git/` and the `README.md` a records pull writes back.
@@ -47,6 +45,7 @@ export const BACKED_FOLDERS = [
47
45
  'tasks',
48
46
  'teach',
49
47
  'transcripts',
48
+ 'walkthroughs',
50
49
  ] as const
51
50
 
52
51
  /**
@@ -72,6 +71,53 @@ const RETIRED_FOLDERS = [
72
71
  /** The history directory's own name, which keeps its dot at either record root. */
73
72
  const RECORDS_GIT_NAME = '.records.git'
74
73
 
74
+ /**
75
+ * Top-level `.canon/` entries a backup never carries, named rather than
76
+ * matched by pattern so a reader can see the whole exemption in one place.
77
+ *
78
+ * `tmp` is deletable without loss, per the scratch standard. `ordinal-locks`
79
+ * is transient per claim, and pushing one races the claim it guards.
80
+ * `.records.git` is the history itself, and reading directories rather than
81
+ * every entry already keeps a pull's `README.md` out, so this is the one name
82
+ * that still has to be said: without it a listing would stage the history
83
+ * into itself.
84
+ */
85
+ export const EXCLUDED_ENTRIES = [
86
+ 'tmp',
87
+ 'ordinal-locks',
88
+ RECORDS_GIT_NAME,
89
+ ] as const
90
+
91
+ /**
92
+ * The record folders sitting at `dir` right now, at either root spelling.
93
+ *
94
+ * A `.claude`-spelled directory reads the fixed allowlist, filtered to what
95
+ * exists, since that root also holds `skills/`, `rules/`, and `hooks/` no
96
+ * exclusion set names. A `.canon`-spelled directory reads its own top-level
97
+ * directories less `EXCLUDED_ENTRIES` instead, since nothing else shares that
98
+ * root and a folder added there is a record folder by construction.
99
+ *
100
+ * Takes a directory rather than a project root so `strandedFolders` can ask it
101
+ * about a candidate root other than the one `workTree` resolved.
102
+ */
103
+ function foldersAt(dir: string): string[] {
104
+ if (basename(dir) === '.claude') {
105
+ return BACKED_FOLDERS.filter((folder) => existsSync(join(dir, folder)))
106
+ }
107
+
108
+ const entries = existsSync(dir)
109
+ ? readdirSync(dir, { withFileTypes: true })
110
+ : []
111
+
112
+ return entries
113
+ .filter((entry) => entry.isDirectory())
114
+ .map((entry) => entry.name)
115
+ .filter(
116
+ (name) =>
117
+ !EXCLUDED_ENTRIES.includes(name as (typeof EXCLUDED_ENTRIES)[number]),
118
+ )
119
+ }
120
+
75
121
  /**
76
122
  * The tree a backup stages, which is the record root itself.
77
123
  *
@@ -166,6 +212,8 @@ export interface PushReport {
166
212
  readonly ok: true
167
213
  readonly root: string
168
214
  readonly folders: readonly string[]
215
+ /** A folder in scope this push found on disk but the records index had never tracked. */
216
+ readonly firstSeen: readonly string[]
169
217
  readonly changed: number
170
218
  readonly commit?: string
171
219
  readonly pushed: boolean
@@ -371,9 +419,10 @@ async function resolveRemote(
371
419
  * work tree hard and leaves the stranded copy beside it, which is not a loss but
372
420
  * is a tree where two roots disagree and neither is wrong.
373
421
  *
374
- * Only `BACKED_FOLDERS` is read. The scratch folder is deletable by definition
375
- * and `worktrees/` belongs to the enclosing repository, so neither stranded
376
- * anywhere costs a record.
422
+ * Reads `foldersAt` on each candidate, so a `.canon` candidate is checked
423
+ * against its own exclusion set rather than the `.claude` allowlist. The
424
+ * scratch folder is deletable by definition and `worktrees/` belongs to the
425
+ * enclosing repository, so neither stranded anywhere costs a record.
377
426
  */
378
427
  function strandedFolders(root: string): string[] {
379
428
  const resolved = resolve(workTree(root))
@@ -382,9 +431,7 @@ function strandedFolders(root: string): string[] {
382
431
  const dir = join(root, candidate)
383
432
  if (resolve(dir) === resolved) return []
384
433
 
385
- return BACKED_FOLDERS.filter((folder) => existsSync(join(dir, folder))).map(
386
- (folder) => join(candidate, folder),
387
- )
434
+ return foldersAt(dir).map((folder) => join(candidate, folder))
388
435
  })
389
436
  }
390
437
 
@@ -403,9 +450,15 @@ function refuseSplitRoots(root: string): BackupRefused | undefined {
403
450
  )
404
451
  }
405
452
 
453
+ /** A pathspec-safe subset, plus which of it never entered the records index before. */
454
+ interface FolderScope {
455
+ readonly scope: readonly string[]
456
+ readonly firstSeen: readonly string[]
457
+ }
458
+
406
459
  /**
407
- * The subset of the backed and retired names a pathspec can name: on disk, or
408
- * already in the records index.
460
+ * The subset of the present, retired, and previously-tracked names a pathspec
461
+ * can name.
409
462
  *
410
463
  * A pathspec matching neither fails the whole `add`, which is why the subset
411
464
  * exists. The index half is what covers a folder deleted in full. Reading disk
@@ -414,29 +467,64 @@ function refuseSplitRoots(root: string): BackupRefused | undefined {
414
467
  * every other unpushed deletion.
415
468
  *
416
469
  * The retired names are the same case one level up, where the folder left the
417
- * backed list rather than the disk, and the index is the only side that still
418
- * knows it existed.
470
+ * disk before the index caught up, and the index is the only side that still
471
+ * knows it existed. At a `.canon` root a folder can leave the same way under a
472
+ * name `RETIRED_FOLDERS` never anticipated, which is what folding the index
473
+ * into the candidate set (rather than only using it as a filter) covers.
474
+ *
475
+ * `firstSeen` is a folder in scope that disk carries but the index has never
476
+ * tracked, which is the read a caller reports rather than acts on: a folder
477
+ * `.canon` picked up that should have been excluded is visible in the push
478
+ * report instead of entering the payload silently.
419
479
  */
420
- async function scopedFolders(root: string): Promise<string[]> {
480
+ async function scopedFolders(root: string): Promise<FolderScope> {
421
481
  const tracked = await records(root, ['ls-files'])
422
482
  const indexed = new Set(
423
483
  tracked.ok ? tracked.text.split('\n').filter(Boolean).map(topSegment) : [],
424
484
  )
425
485
 
426
- return [...BACKED_FOLDERS, ...RETIRED_FOLDERS].filter(
486
+ const present = new Set(foldersAt(workTree(root)))
487
+ const retired = RETIRED_FOLDERS.filter(
427
488
  (folder) => existsSync(join(workTree(root), folder)) || indexed.has(folder),
428
489
  )
490
+ const scope = [...new Set([...present, ...retired, ...indexed])].sort()
491
+ const firstSeen = scope.filter(
492
+ (folder) => present.has(folder) && !indexed.has(folder),
493
+ )
494
+
495
+ return { scope, firstSeen }
429
496
  }
430
497
 
431
498
  function topSegment(path: string): string {
432
499
  return path.split('/')[0]
433
500
  }
434
501
 
435
- /** What a report names, which is the folders a reader can go and open. */
436
- function presentFolders(root: string): string[] {
437
- return BACKED_FOLDERS.filter((folder) =>
438
- existsSync(join(workTree(root), folder)),
439
- )
502
+ /**
503
+ * What a report names, which is the folders a reader can go and open.
504
+ *
505
+ * Exported so the scratch-evidence walk and the push and pull help text read
506
+ * the same resolved set rather than each re-deriving it against
507
+ * `BACKED_FOLDERS`, which is silently wrong at a `.canon` root.
508
+ */
509
+ export function presentFolders(root: string): string[] {
510
+ return foldersAt(workTree(root))
511
+ }
512
+
513
+ /**
514
+ * The record folder names a project could ever carry, without regard to
515
+ * whether each currently exists: `BACKED_FOLDERS` at the legacy `.claude`
516
+ * root, since nothing else names a folder there, and the live directory
517
+ * listing at `.canon`, since nothing bounds that root's names from outside
518
+ * any more and a name nobody has created yet cannot be listed.
519
+ *
520
+ * Exported for `canon records size`, whose own contract reports every folder
521
+ * whether or not it exists so a caller reading the record gets a stable set
522
+ * of keys. `presentFolders` filters by existence instead, which is right for
523
+ * a push report naming what a reader can go and open and wrong here.
524
+ */
525
+ export function candidateFolders(root: string): string[] {
526
+ const tree = workTree(root)
527
+ return basename(tree) === '.claude' ? [...BACKED_FOLDERS] : foldersAt(tree)
440
528
  }
441
529
 
442
530
  function countLines(text: string): number {
@@ -458,7 +546,7 @@ export async function pushRecords(root: string): Promise<PushOutcome> {
458
546
  const remote = await resolveRemote(root, enclosing)
459
547
  if (typeof remote !== 'string') return remote
460
548
 
461
- const scope = await scopedFolders(root)
549
+ const { scope, firstSeen } = await scopedFolders(root)
462
550
 
463
551
  if (scope.length > 0) {
464
552
  // `-f` is what carries the payload: every backed folder is ignored by the
@@ -494,7 +582,7 @@ export async function pushRecords(root: string): Promise<PushOutcome> {
494
582
  const folders = presentFolders(root)
495
583
  const head = await records(root, ['rev-parse', '--short', 'HEAD'])
496
584
  if (!head.ok) {
497
- return { ok: true, root, folders, changed, pushed: false }
585
+ return { ok: true, root, folders, firstSeen, changed, pushed: false }
498
586
  }
499
587
 
500
588
  const branch = await projectBranch(root, enclosing)
@@ -505,7 +593,15 @@ export async function pushRecords(root: string): Promise<PushOutcome> {
505
593
  ])
506
594
  if (!pushed.ok) return failed('push', pushed)
507
595
 
508
- return { ok: true, root, folders, changed, commit: head.text, pushed: true }
596
+ return {
597
+ ok: true,
598
+ root,
599
+ folders,
600
+ firstSeen,
601
+ changed,
602
+ commit: head.text,
603
+ pushed: true,
604
+ }
509
605
  }
510
606
 
511
607
  /**
@@ -556,7 +652,7 @@ export async function pullRecords(root: string): Promise<PullOutcome> {
556
652
  const target = await records(root, ['rev-parse', 'FETCH_HEAD'])
557
653
  if (!target.ok) return failed('rev-parse', target)
558
654
 
559
- const scope = await scopedFolders(root)
655
+ const { scope } = await scopedFolders(root)
560
656
 
561
657
  if (scope.length > 0) {
562
658
  const dirty = await records(root, ['status', '--porcelain', '--', ...scope])
@@ -2,20 +2,24 @@ import { existsSync, type Stats } from 'node:fs'
2
2
  import { readdir, stat } from 'node:fs/promises'
3
3
  import { join } from 'node:path'
4
4
  import { RECORD_ROOTS, recordDir, SCRATCH } from '@/record-root'
5
- import { BACKED_FOLDERS } from '@/records/backup'
5
+ import { candidateFolders } from '@/records/backup'
6
6
 
7
7
  /**
8
- * The folders a size reading covers, named at the record root they sit under.
8
+ * The folders a size reading covers at `root`, resolved the same way a push
9
+ * resolves its scope rather than pinned to `BACKED_FOLDERS`, which is silently
10
+ * wrong at a `.canon` root.
9
11
  *
10
- * It is the backed set plus the scratch folder, which a backup skips because it is
11
- * deletable without loss and a reading covers because deletable is not the same
12
- * as empty: the routing handoffs and the memory archive both sit there and both
13
- * accumulate. `.records.git` stays out because it is the backup history rather
14
- * than a record, and `worktrees/` stays out because each entry there is a
15
- * checkout of the enclosing repository with its own removal verb, and one of
16
- * them outweighs every record folder combined.
12
+ * It is the present backed set plus the scratch folder, which a backup skips
13
+ * because it is deletable without loss and a reading covers because deletable
14
+ * is not the same as empty: the routing handoffs and the memory archive both
15
+ * sit there and both accumulate. `.records.git` stays out because it is the
16
+ * backup history rather than a record, and `worktrees/` stays out because each
17
+ * entry there is a checkout of the enclosing repository with its own removal
18
+ * verb, and one of them outweighs every record folder combined.
17
19
  */
18
- export const SIZED_FOLDERS = [...BACKED_FOLDERS, SCRATCH] as const
20
+ export function sizedFolders(root: string): string[] {
21
+ return [...candidateFolders(root), SCRATCH]
22
+ }
19
23
 
20
24
  /**
21
25
  * The windows a reading reports, in days.
@@ -227,9 +231,9 @@ export async function sizeRecords(
227
231
 
228
232
  // Each folder is walked independently, and the report is ordered by the
229
233
  // caller rather than by arrival, so `Promise.all` keeps the input order while
230
- // the ten walks overlap.
234
+ // the walks overlap.
231
235
  const folders = await Promise.all(
232
- SIZED_FOLDERS.map((folder) => measure(root, folder, now)),
236
+ sizedFolders(root).map((folder) => measure(root, folder, now)),
233
237
  )
234
238
 
235
239
  return {
@@ -210,7 +210,7 @@ Without this skill, a session <observed failure>, <observed failure>.
210
210
  ### Output and tuning
211
211
 
212
212
  - Skill success lines emit the full relative path from the project root (`<dir>/<file>`) for any file written, updated, or deleted. A bare filename names a file the reader cannot open. The `## Output` section of the project's instruction file sets the form that path takes, so a skill body states which path is emitted and leaves the form to that section.
213
- - Before a skill writes anything, decide whether the output is a deliverable the project keeps or a toolkit session record. A deliverable lands among the project's own tracked files. A session record lands under `.canon/`, in the named subfolder for its kind (`tasks/`, `plans/`, `review/`, `memory/`, `groundwork/`, `intake/`, `proposals/`, `diagrams/`, or `teach/`, with `tmp/` for scratch nothing else claims), never in a folder the body invents. `canon/ARCHITECTURE.md`'s per-folder decisions are the precedent for which kind takes which folder.
213
+ - Before a skill writes anything, decide whether the output is a deliverable the project keeps or a toolkit session record. A deliverable lands among the project's own tracked files. A session record lands under `.canon/`, in the named subfolder for its kind (`tasks/`, `plans/`, `review/`, `memory/`, `groundwork/`, `intake/`, `proposals/`, `diagrams/`, `teach/`, or `walkthroughs/`, with `tmp/` for scratch nothing else claims), never in a folder the body invents. `canon/ARCHITECTURE.md`'s per-folder decisions are the precedent for which kind takes which folder.
214
214
  - Codify a skill's posted or generated output as a fenced template, and keep the body consistent with every capability the frontmatter description names.
215
215
  - When a skill gathers user input or pre-seeds a template, attach a concrete proposed default to every question, derived from project context. Accept "use defaults" as a bulk-confirm.
216
216
  - Separate correctness axes (routing, sourcing, escalation, decline) from shape axes (line count, formatting, variant sprawl) when tuning a skill. Tighten only on correctness regressions. Do not convert soft caps to hard caps for aesthetic drift when correctness passes.