@erclx/canon 4.73.0 → 4.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/create-skill/SKILL.md +0 -1
  3. package/claude/skills/docs-fold/SKILL.md +1 -1
  4. package/claude/skills/git-followup/SKILL.md +9 -5
  5. package/claude/skills/git-pr/SKILL.md +21 -0
  6. package/claude/skills/markdown-propose/REQUIREMENT.md +1 -1
  7. package/claude/skills/markdown-propose/SKILL.md +9 -7
  8. package/claude/skills/markdown-propose/references/format.md +5 -3
  9. package/claude/skills/plan-feature/SKILL.md +3 -3
  10. package/claude/skills/plan-groundwork/SKILL.md +6 -5
  11. package/claude/skills/plan-intake/SKILL.md +1 -1
  12. package/claude/skills/review-pr/SKILL.md +2 -2
  13. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -0
  14. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -1
  15. package/claude/skills/role-orchestrator/scripts/poll.sh +16 -10
  16. package/claude/skills/task-board/SKILL.md +37 -3
  17. package/claude/skills/teach-workspace/references/lesson-craft.md +1 -6
  18. package/docs/agents/commands.md +2 -0
  19. package/docs/agents/index.md +2 -1
  20. package/docs/agents/install-and-sync.md +7 -4
  21. package/docs/agents/pr-evidence.md +108 -0
  22. package/docs/agents/records.md +29 -9
  23. package/docs/agents/tasks.md +2 -0
  24. package/docs/target-projects.md +4 -0
  25. package/governance/rules/ui/440-surface-capture.md +1 -0
  26. package/package.json +1 -1
  27. package/src/commands/design.ts +10 -2
  28. package/src/commands/pr.ts +220 -0
  29. package/src/commands/records.ts +138 -0
  30. package/src/commands/transcripts.ts +20 -3
  31. package/src/design/base.css +59 -0
  32. package/src/design/components.ts +80 -50
  33. package/src/design/fonts.ts +21 -0
  34. package/src/init/plan.ts +8 -1
  35. package/src/init/steps.ts +17 -0
  36. package/src/intake/folder.ts +1 -1
  37. package/src/pr/evidence.ts +171 -0
  38. package/src/records/backup.ts +110 -38
  39. package/src/records/ordinal.ts +243 -0
  40. package/src/records/validate.ts +28 -0
  41. package/src/tasks/answers.ts +10 -1
  42. package/src/teach/fonts.ts +9 -11
  43. package/src/transcripts/fetch.ts +31 -1
  44. package/standards/architecture.md +1 -0
  45. package/standards/figures.md +51 -0
  46. package/standards/groundwork.md +2 -1
  47. package/standards/index.md +1 -0
  48. package/standards/intake.md +2 -1
  49. package/standards/plan.md +1 -0
  50. package/standards/skill.md +1 -1
  51. package/tooling/base/configs/.husky/post-merge +27 -0
  52. package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +5 -4
@@ -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.73.0",
4
+ "version": "4.75.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: create-skill
3
3
  description: Creates a new `SKILL.md` in `.claude/skills/`. Use when asked to "create a skill", add a skill, or make a new skill.
4
- disable-model-invocation: true
5
4
  ---
6
5
 
7
6
  # Create skill
@@ -173,7 +173,7 @@ Grep the tree for the name that went, rather than for the paths the diff carries
173
173
 
174
174
  Report each hit as an ordinary rewrite.
175
175
 
176
- Do not create new entries automatically. Before treating a domain as new, confirm it holds no entry under either spelling, `.claude/context/<domain>.md` or `.claude/context/<domain>/index.md`, since a domain already split into a folder still passes a check that only looked for the flat file. New entries are a deliberate decision: the user invokes `docs-fold --new-context <domain>` (future flag) or hand-creates the file following `${CLAUDE_SKILL_DIR}/../../standards/context.md`. Auto-creation risks padding `.claude/context/` with low-signal entries.
176
+ Create a new entry only for a domain `.claude/context/index.md` already lists but carries no file for, following `${CLAUDE_SKILL_DIR}/../../standards/context.md` for its shape. Before treating a domain as carrying no file, confirm it holds no entry under either spelling, `.claude/context/<domain>.md` or `.claude/context/<domain>/index.md`, since a domain already split into a folder still passes a check that only looked for the flat file. A row in the catalog is the deliberate decision, taken by whoever added it. This step only fills in what that decision left open, and only until the next `canon indexes regen` pass, which rebuilds the catalog from each entry's own frontmatter plus every sibling's and drops a row whose file still does not exist. Create the file before that regen runs, or the row this bar exists to fill in is gone. A domain the catalog does not list at all is a different case: report it and stop, rather than creating an entry or a catalog row for it.
177
177
 
178
178
  Write each updated entry immediately. Output one line per file, naming the path this run actually wrote rather than always the flat template:
179
179
 
@@ -29,15 +29,19 @@ A missing tracking ref is no longer a guard. An open pull request proves the bra
29
29
 
30
30
  The lease is what stops the force from overwriting a commit this session never read. Run the ancestry test only where an upstream resolves, since it reads `@{u}`.
31
31
 
32
- 5. Check for existing review comments: `gh api 'repos/{owner}/{repo}/pulls/<number>/comments' --jq 'length'`, resolving `<number>` from `gh pr view --json number`.
33
- 6. Sync the body and title on every invocation, before the routing below decides on the reply.
32
+ 5. Post the evidence comparison, on every invocation including `reply-owned`, since this comment is not the reply step 8 owns. Run `canon pr evidence <number> --json`, resolving `<number>` from `gh pr view --json number`, and read `reason` on the record rather than the exit code.
33
+ - `no-evidence`: nothing changed under an `evidence/` segment this push. Say nothing and move on.
34
+ - `ok`: write `body` to `.canon/tmp/pr-evidence/body-<number>.md` at the main worktree root (resolved the way `session-worktree` does), then post the comment with `gh pr comment <number> --body-file <main-root>/.canon/tmp/pr-evidence/body-<number>.md` when the record carries no `commentId`, or edit the existing one in place with `gh api -X PATCH repos/{owner}/{repo}/issues/comments/<commentId> -f body=@<main-root>/.canon/tmp/pr-evidence/body-<number>.md` when it does. Clean up the tmp file only after the call reports success.
35
+ - Any other reason is one of the mirrored git refusals (`gh-missing`, `gh-failed`, `no-base`, `unreadable-tree`, `unreadable-changes`). Report it and continue without stopping the chain.
36
+ 6. Check for existing review comments: `gh api 'repos/{owner}/{repo}/pulls/<number>/comments' --jq 'length'`, resolving `<number>` from `gh pr view --json number`.
37
+ 7. Sync the body and title on every invocation, before the routing below decides on the reply.
34
38
  - Run `gh pr view --json url,title,body` and update the body with `gh pr edit --body` when the new commit changes scope, and the title with `gh pr edit --title` when the scope shifted enough to make it inaccurate.
35
39
  - A fix commit answering a review changes what shipped exactly as much as an ordinary followup does, so the sync cannot wait on the invocation or the comment count below.
36
40
  - A body a person edited by hand between rounds gets no special handling: judge it against the tree the same way regardless of who wrote it last, since a hand-edit the fix commit has made stale is the exact drift this sync exists to close.
37
- 7. Route on the invocation and the comment count for the reply alone.
41
+ 8. Route on the invocation and the comment count for the reply alone.
38
42
  - When invoked with `reply-owned`, skip this step: the caller posts its own reply.
39
- - Otherwise, if the count is above zero, the followup addresses review feedback: post a one-line summary of the fix with `gh pr comment --body`, first running the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against it, since the hook does not see an inline comment body. The `pull_request` check the git-pr surface carries triggers on a push or an open rather than a plain edit, so neither this comment nor the title and body step 6 synced reaches it.
40
- - If it is zero, nothing further runs. The sync in step 6 already did this branch's job.
43
+ - Otherwise, if the count is above zero, the followup addresses review feedback: post a one-line summary of the fix with `gh pr comment --body`, first running the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against it, since the hook does not see an inline comment body. The `pull_request` check the git-pr surface carries triggers on a push or an open rather than a plain edit, so neither this comment nor the title and body step 7 synced reaches it.
44
+ - If it is zero, nothing further runs. The sync in step 7 already did this branch's job.
41
45
 
42
46
  ## After completion
43
47
 
@@ -176,6 +176,27 @@ rmdir <main-root>/.canon/tmp/ui-checklist 2>/dev/null || true
176
176
 
177
177
  The `rmdir` is a no-op when another branch's pending checklist still sits in the folder, which keeps this step from deleting a handoff that is not its own.
178
178
 
179
+ ### Post the evidence comparison
180
+
181
+ Run `canon pr evidence <number> --json` against the number the pull request step above resolved. Read `reason` on the record rather than the exit code.
182
+
183
+ - `no-evidence`: nothing changed under an `evidence/` segment. Say nothing and move on.
184
+ - `ok`: write `body` to `.canon/tmp/pr-evidence/body-<number>.md` at the main worktree root (resolved the way `session-worktree` does), then post or update the comment:
185
+
186
+ ```bash
187
+ gh pr comment <number> --body-file <main-root>/.canon/tmp/pr-evidence/body-<number>.md
188
+ ```
189
+
190
+ When the record carries a `commentId`, edit that comment in place instead of posting a second one, reading the body field from the tmp file with `@`:
191
+
192
+ ```bash
193
+ gh api -X PATCH repos/{owner}/{repo}/issues/comments/<commentId> -f body=@<main-root>/.canon/tmp/pr-evidence/body-<number>.md
194
+ ```
195
+
196
+ Clean up the tmp file the way the UI-checklist step does, only after the call reports success.
197
+
198
+ Any other `reason` is one of the mirrored git refusals (`gh-missing`, `gh-failed`, `no-base`, `unreadable-tree`, `unreadable-changes`). Report it and move on without stopping the chain: a branch that carries no evidence images most of the time should not fail here on a transient git or `gh` read.
199
+
179
200
  ### Record the number on the task
180
201
 
181
202
  Write the `number` the final command printed onto the task the branch is closing. Do not resolve it again. `${CLAUDE_SKILL_DIR}/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
@@ -21,7 +21,7 @@ A second failure compounds the first. A claim copied across several files is cor
21
21
  - Grep the named surface for the concern before proposing, so a defect's site count is measured rather than assumed
22
22
  - Draft the replacement text, not only the finding
23
23
  - Carry three labelled variants on a change whose replacement was invented, and one on a change that corrects text to a recorded fact
24
- - Write one proposal file per source file under `.canon/proposals/<slug>/` and stop, leaving `You:` empty on every change
24
+ - Write one proposal file per source file under `.canon/proposals/<nn>-<slug>/` and stop, leaving `You:` empty on every change
25
25
  - Apply only a change carrying an answer, one file at a time, and re-grep its anchor before applying it
26
26
  - Report what each file leaves alone, so a proposal cannot be read as finding everything wanting
27
27
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: markdown-propose
3
- description: Reviews a named markdown surface against a named concern, drafts a per-file proposal under `.canon/proposals/<slug>/` carrying a diff and a reason for each change, and stops without editing a source file. Takes the concern and the surface as inputs, such as a claim stated stronger than the record, a fact gone stale, two files disagreeing, or a passage duplicated without derivation. A later invocation applies what the operator answered. Use when asked to "propose a change to CLAUDE.md", "draft a rewrite of this standard", "propose fixes to this doc", "draft alternatives for this passage", or "apply the answered proposals". Do NOT use to report without drafting a replacement (`standards-audit` or `canon markdown audit`), to review a diff already made (`review-branch`), or to file a raw brain dump as findings (`plan-intake`).
3
+ description: Reviews a named markdown surface against a named concern, drafts a per-file proposal under `.canon/proposals/<nn>-<slug>/` carrying a diff and a reason for each change, and stops without editing a source file. Takes the concern and the surface as inputs, such as a claim stated stronger than the record, a fact gone stale, two files disagreeing, or a passage duplicated without derivation. A later invocation applies what the operator answered. Use when asked to "propose a change to CLAUDE.md", "draft a rewrite of this standard", "propose fixes to this doc", "draft alternatives for this passage", or "apply the answered proposals". Do NOT use to report without drafting a replacement (`standards-audit` or `canon markdown audit`), to review a diff already made (`review-branch`), or to file a raw brain dump as findings (`plan-intake`).
4
4
  ---
5
5
 
6
6
  # Markdown propose
@@ -20,7 +20,9 @@ The value is the gate. A rewrite delivered in chat gets applied from memory acro
20
20
 
21
21
  Two phases share this body, picked by whether a proposal folder already exists for the request's slug.
22
22
 
23
- Derive `<slug>` from the concern and the surface, kebab-case, naming the subject rather than the activity. List `.canon/proposals/` at the main worktree root and match the topic against the folders already there before deriving a fresh one, the same way `plan-intake` matches its own folder. Never match against `.claude/` itself.
23
+ Derive `<slug>` from the concern and the surface, kebab-case, naming the subject rather than the activity. List `.canon/proposals/` at the main worktree root and match the topic against the slug half of each `<nn>-<slug>` folder already there before deriving a fresh one, the same way `plan-intake` matches its own folder. Never match against `.claude/` itself.
24
+
25
+ With no match, a fresh folder takes the next ordinal in `.canon/proposals/`'s own sequence: list the folders present, take the highest `<nn>`, and increment it, starting at `01` when none exist. That sequence is independent of the one `intake` and `groundwork` share, so a proposals folder never reads or claims from theirs.
24
26
 
25
27
  - No matching folder, or the operator names a concern and a surface: **Propose**.
26
28
  - A matching folder exists and the operator says apply, ship, or commit the answers: **Apply**.
@@ -29,7 +31,7 @@ All `.canon/proposals/` reads and writes resolve at the main worktree root, not
29
31
 
30
32
  ## Write scope
31
33
 
32
- Write only inside `.canon/proposals/<slug>/`. A source file, a standard, a rule, and a plan all live outside that folder, so this one rule forbids every one of them during the Propose phase. The Apply phase is the one exception, and only for a change carrying a `You:` answer.
34
+ Write only inside `.canon/proposals/<nn>-<slug>/`. A source file, a standard, a rule, and a plan all live outside that folder, so this one rule forbids every one of them during the Propose phase. The Apply phase is the one exception, and only for a change carrying a `You:` answer.
33
35
 
34
36
  ## Concerns
35
37
 
@@ -66,7 +68,7 @@ A matched string means different things in different registers. A word describin
66
68
 
67
69
  ### 4. Write the proposals
68
70
 
69
- One file per source file under `.canon/proposals/<slug>/`, per `${CLAUDE_SKILL_DIR}/references/format.md`.
71
+ One file per source file under `.canon/proposals/<nn>-<slug>/`, per `${CLAUDE_SKILL_DIR}/references/format.md`.
70
72
 
71
73
  Draft the replacement text. A proposal reporting a problem without a replacement hands the work back rather than doing it.
72
74
 
@@ -97,20 +99,20 @@ Report the counts once the pass stops: files committed, changes applied, changes
97
99
  Chat output is the report. This skill persists only the proposal folder itself.
98
100
 
99
101
  ```plaintext
100
- 📂 Opened .canon/proposals/<slug>/
102
+ 📂 Opened .canon/proposals/<nn>-<slug>/
101
103
 
102
104
  **Screened:** <N> files, <N> changes proposed, <N> carrying three variants
103
105
 
104
106
  **Highest value:** <the single strongest change, one line>
105
107
 
106
- Next: answer the `You:` slots in the files under .canon/proposals/<slug>/,
108
+ Next: answer the `You:` slots in the files under .canon/proposals/<nn>-<slug>/,
107
109
  then re-invoke this skill to apply what you answered.
108
110
  ```
109
111
 
110
112
  Use `📂 Resumed` in place of `📂 Opened` on a resume pass. The Apply phase reports instead:
111
113
 
112
114
  ```plaintext
113
- ✅ Applied .canon/proposals/<slug>/
115
+ ✅ Applied .canon/proposals/<nn>-<slug>/
114
116
 
115
117
  **Committed:** <N> files, <N> changes
116
118
 
@@ -4,11 +4,13 @@ Governs the proposal `markdown-propose` writes before it edits anything. The con
4
4
 
5
5
  ## Folder
6
6
 
7
- - One folder per screening pass at `.canon/proposals/<slug>/`, where the slug names the subject rather than the activity
7
+ - One folder per screening pass at `.canon/proposals/<nn>-<slug>/`, where the slug names the subject rather than the activity
8
8
  - One proposal file per source file, named `<nn>-<source filename, its own extension dropped>.md`, so a source already named `CLAUDE.md` becomes `01-CLAUDE.md` rather than `01-CLAUDE.md.md`
9
9
  - `00-overview.md` when the pass spans more than two source files, holding the cross-file pattern, the change counts, and the settle order
10
10
  - `applied.md` once the first change lands, holding every applied change with its reason and the answer it carried
11
11
 
12
+ The folder's own `<nn>` and a proposal file's own `<nn>` are two independent counters. The folder's ordinal records when the pass was opened, counted within `.canon/proposals/` alone rather than shared with `plan-groundwork` or `plan-intake`'s sequence, and a proposal file's ordinal inside it records settle order, the dependency order changes apply in. Reading one as the other gets a pass dated wrong or an apply ordered wrong.
13
+
12
14
  The name carries both structures the folder has. The basename pairs a proposal to its source so a reader opens the two side by side, and the number is the settle order, which is a dependency rather than a preference: a file other files quote is settled first. Do not group proposals by theme.
13
15
 
14
16
  A proposal opened after the initial pass takes the next free number, and the folder is never renumbered. Its number records when the site was found rather than where it belongs in the dependency order. Renumbering to restore the order would rewrite the names of files already carrying answers, which costs more than the ordering is worth once applying has started.
@@ -95,13 +97,13 @@ The folder is gitignored, and backed wherever a records remote is configured. It
95
97
  Count the unread changes per file:
96
98
 
97
99
  ```bash
98
- grep -c '^- \*\*You:\*\*$' .canon/proposals/<slug>/*.md
100
+ grep -c '^- \*\*You:\*\*$' .canon/proposals/<nn>-<slug>/*.md
99
101
  ```
100
102
 
101
103
  Report every answer given, against the change it sits under:
102
104
 
103
105
  ```bash
104
- awk '/^### /{h=FILENAME": "$0} /^- \*\*You:\*\*./{print h; print " "$0}' .canon/proposals/<slug>/*.md
106
+ awk '/^### /{h=FILENAME": "$0} /^- \*\*You:\*\*./{print h; print " "$0}' .canon/proposals/<nn>-<slug>/*.md
105
107
  ```
106
108
 
107
109
  Both walk `###` headings, which is the mechanical reason an answer typed anywhere else is lost.
@@ -31,7 +31,7 @@ Coding standards live in `.claude/rules/`. Claude Code loads them automatically.
31
31
 
32
32
  ## Step 2: scan relevant source files
33
33
 
34
- Based on the feature description, identify and read source files that are directly relevant. Do not read entire directories speculatively.
34
+ Based on the feature description, identify and read source files that are directly relevant. Do not read entire directories speculatively. Where a folder carries an `index.md`, read it first and load only the files it points at.
35
35
 
36
36
  Measure against the tree rather than recall. Grep for each construct the plan will name and count the sites, so the plan carries the count the tree holds today. Confirm any work the plan sequences behind is still open, so it does not lead with an item that already shipped.
37
37
 
@@ -52,8 +52,8 @@ What this skill adds on top of the standard:
52
52
 
53
53
  A plan sequencing its work into batches declares where one pull request ends, here rather than at ship time. A dependency chain cannot be split once it is built, so the choice exists only while the batches are still a plan.
54
54
 
55
- - One batch is one pull request. Carry one entry per batch naming what it targets, so a plan opening five says five before the first line is written.
56
- - A dependent batch stacks rather than merging into its neighbor. The batches are built in order and depend on each other in that order, which is what a stack expresses, so the dependency argues for stacking and never for collapsing two batches into one review.
55
+ - One batch is one pull request, and one pull request is one plan file, named for its own concern rather than a numeric suffix. `canon tasks plan-branch` derives one branch from one plan filename, so a `**Batch N**` sub-heading inside one file's `**Files to touch:**` shares that one branch across every batch, and the batch that merges first strands every batch behind it with nothing left to open a pull request against. Write a plan opening five batches as five files before the first line of any of them.
56
+ - State a dependent batch's dependency on the ones before it in its own `**Constraints:**`, naming the earlier batch's slug and stacking rather than merging into it. The batches are built in order and depend on each other in that order, which is what a stack expresses, so the dependency argues for stacking and never for collapsing two batches into one file or one review.
57
57
  - Mark each batch dependent or independent by comparing the file sets rather than the descriptions. A batch sharing no file with another is independent and earns its own branch even where both arrived in one request. One run measured at 68 files carried three such batches into a single review because nothing drew the boundary.
58
58
  - Sequence a sweep last. A batch deliberately rewriting files earlier batches touched is coherent as the final one and forces every batch behind it into one review anywhere else.
59
59
  - Keep each batch's commits contiguous once the work starts. A later fix to an earlier batch belongs on that batch's own commits, since a batch interrupted by another cannot be lifted onto its own branch afterward.
@@ -46,11 +46,12 @@ List `.canon/groundwork/` from the project root and match the topic against the
46
46
 
47
47
  Never match against `.claude/` itself. That directory holds every other workflow surface, so a topic matched there lands on a folder that was never a track.
48
48
 
49
- With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `ts-migration` over `migration-research`. Also list `.canon/intake/` and take `<nn>` as the highest ordinal present across both listings, incremented, per `${CLAUDE_SKILL_DIR}/../../standards/groundwork.md`. Then route on `.canon/groundwork/<nn>-<slug>/`:
49
+ With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `ts-migration` over `migration-research`. That is open mode outright: no folder can carry a slug nothing matched yet, so there is nothing to route on. `<nn>` for it is claimed in Open mode Step 1 rather than here, since the qualifying test below has to run before the folder exists.
50
50
 
51
- - Folder absent: open
52
- - Folder present without `06-decision.md`: resume
53
- - Present folder the user judges ready: close
51
+ A matched folder instead routes on its own `<nn>-<slug>/`:
52
+
53
+ - Present without `06-decision.md`: resume
54
+ - Present and the user judges it ready: close
54
55
 
55
56
  Detect the mode from the folder. Do not ask which one to run.
56
57
 
@@ -73,7 +74,7 @@ The standard sets the open question format and requires it inside a topic file a
73
74
 
74
75
  ## Open mode
75
76
 
76
- 1. Create `.canon/groundwork/<nn>-<slug>/`, with `<nn>` and `<slug>` as derived in Step 1.
77
+ 1. Claim the ordinal and create the folder in one act with `canon records ordinal groundwork <slug> --claim`, per `${CLAUDE_SKILL_DIR}/../../standards/groundwork.md`. Where the installed binary carries no such subcommand, fall back to listing both `.canon/groundwork/` and `.canon/intake/` and creating `.canon/groundwork/<nn>-<slug>/` at the highest ordinal present across the two, incremented.
77
78
  2. Write `README.md` first. Writing it first forces the question of what the track is for.
78
79
  3. Write `01-current-state.md` by measuring now. Never carry a figure from a previous session or from recall without re-measuring it. Measure only what an open question in the folder needs. A number with no question attached is how groundwork turns into the work.
79
80
  4. Write `00-scope.md` when the track is large enough to run away. Skip it on a small track.
@@ -42,7 +42,7 @@ List `.canon/intake/` from the project root and match the topic against the slug
42
42
 
43
43
  Never match against `.claude/` itself. That directory holds every other workflow surface, so a topic matched there lands on a folder that was never an intake.
44
44
 
45
- With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `toolkit-overview` over `august-triage`. Also list `.canon/groundwork/` and take `<nn>` as the highest ordinal present across both listings, incremented, per `${CLAUDE_SKILL_DIR}/../../standards/intake.md`. An absent folder opens, and a present one resumes by appending items and revising verdicts the tree has moved under.
45
+ With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `toolkit-overview` over `august-triage`. Claim the ordinal and create the folder in one act with `canon records ordinal intake <slug> --claim`, per `${CLAUDE_SKILL_DIR}/../../standards/intake.md`. Where the installed binary carries no such subcommand, fall back to listing both `.canon/intake/` and `.canon/groundwork/` and taking the highest ordinal present across the two, incremented. An absent folder opens, and a present one resumes by appending items and revising verdicts the tree has moved under.
46
46
 
47
47
  ## Step 2: orient
48
48
 
@@ -242,9 +242,9 @@ A minor the dispatched worker declines is what needs a surface that survives the
242
242
 
243
243
  Read the state off the most recent review comment rather than off the presence of a closed one. A close-out does not close the pull request, so a commit pushed after it gets its own pass, and that pass reopens the review under `## Review` when it raises a finding of any grade.
244
244
 
245
- Both of this skill's headings anchor as a section distinct from human threads, and neither invents beyond what the whole set already states. That set is five headings across two families, stated here once so `role-orchestrator`'s poll and every reply-posting skill cite it rather than carry a copy. The review family, `## Review` and `## Review closed`, belongs to this skill alone, and the reply family, `## Review response`, `## Rebase`, and `## Post-review findings`, belongs to `review-address`.
245
+ Both of this skill's headings anchor as a section distinct from human threads, and neither invents beyond what the whole set already states. That set is six headings across three families, stated here once so `role-orchestrator`'s poll and every reply-posting skill cite it rather than carry a copy. The review family, `## Review` and `## Review closed`, belongs to this skill alone. The reply family, `## Review response`, `## Rebase`, and `## Post-review findings`, belongs to `review-address`. The third family is a single heading, `## Evidence`, which `canon pr evidence` posts through `git-pr` and `git-followup` and which answers no comment already on the thread, so the poll excludes it from the unclassified count without reading it as a reply that owes a re-review.
246
246
 
247
- The first reply heading answers a finding this skill posted, the second reports a stale branch resolved without one, and the third carries a finding a worker produces after a close-out rather than in answer to one already on the thread, since a finding produced late is still a finding. A comment posted under a heading outside these five reaches the poll as unclassified rather than as silence, so an invented sixth heading is a gap the next run reports instead of one it repeats. Do not append the PR number, which GitHub already renders above the comment.
247
+ The first reply heading answers a finding this skill posted, the second reports a stale branch resolved without one, and the third carries a finding a worker produces after a close-out rather than in answer to one already on the thread, since a finding produced late is still a finding. A comment posted under a heading outside these six reaches the poll as unclassified rather than as silence, so an invented seventh heading is a gap the next run reports instead of one it repeats. Do not append the PR number, which GitHub already renders above the comment.
248
248
 
249
249
  Name the scope in every summary line after the first pass, since a reader cannot otherwise tell a narrow read from a full one. When the fallback in Step 2 fired, replace the commit count with `Re-reviewed the full change, the prior pass's commit is no longer on the branch`.
250
250
 
@@ -28,6 +28,7 @@ Run `canon tasks plan-answers <plan> --json` and read `launchable` off the recor
28
28
  - `launchable: false`: the row is not dispatchable. Report every entry in `open`, each carrying the question label and the reason its suggestion gave for needing a person, and hand the row to the human-launch line below. Never fill the slot on the operator's behalf, which is the one move the plan standard forbids outright.
29
29
  - `reason: archived`: the row's plan sits in `.canon/plans/archive/` and describes work that already shipped. Repoint the row at a live plan rather than dispatching, since `auto-ship` Step 1 refuses the same file and the worker would meet that refusal after the launch spent.
30
30
  - The command refuses for any other reason, or the record carries no `launchable` key: treat the row as unverified rather than clear, name what could not be read, and fall back to the human. A gate that reads nothing and proceeds is the gate not running.
31
+ - The same read also refuses a plan still staging its batches in one file with `**Batch N**` sub-headings. That reports as an entry in `open` labeled `Batch staging`, so a staged plan reads as `launchable: false` the same way an unanswered operator call does, and the row waits on a split rather than on the operator.
31
32
 
32
33
  Branch on `launchable` rather than on the exit code, which a shell function wrapping `canon` can flatten to zero and so read a held row as a clear one.
33
34
 
@@ -24,7 +24,7 @@ Take the board rows in board order and finish one before opening the next. Clear
24
24
  The blocker cell states what the row waits on, and each kind is tested differently. `canon tasks validate` already re-takes the first two and reports the rest as untested, so run it first and re-take by hand only what it names.
25
25
 
26
26
  - Collision with a track in flight: the validator tests the file the cell cites against the Touches column of every `## Run now` row. A track that merged since the row was parked is no longer in flight, whatever the sets still share. A cell naming the file in prose rather than in backticks cites nothing, so write the collision the way the board format spells it and the check picks the row up on the next run.
27
- - A dependency on another task: the validator opens the task a link in the cell names. One whose outcomes are all `[x]`, or one already archived, holds nothing. A cell naming the task in prose resolves to no file, so open it by hand and rewrite the cell as a link.
27
+ - A dependency on another task: the validator opens the task a link in the cell names. One whose outcomes are all `[x]`, one already archived, or one already declined, holds nothing. A cell naming the task in prose resolves to no file, so open it by hand and rewrite the cell as a link.
28
28
  - A condition about the tree, such as a count of some shape or the presence of a construct: measure it again, per Two ways a re-test goes wrong below.
29
29
  - Waiting on a plan: nothing external holds the row, so the pass writes the plan rather than testing anything. See The plan half below.
30
30
  - Waiting on an operator action, such as a run that happens from a shell: record it as untestable this pass and name what the operator has to do. A session cannot clear it, and re-measuring it every pass is waste.
@@ -72,6 +72,7 @@ A scoping defect can wear a blocker. A task whose file set collides with every o
72
72
  ```plaintext
73
73
  Re-tested: <row>, <blocker> → <what the measurement showed>
74
74
  Cleared: <row>, now <group>, plan at <path>
75
+ Declined: <row>, waited on <task> which was declined, now <group>
75
76
  Still parked: <row>, <blocker> re-confirmed against <what was measured>
76
77
  Untestable: <row>, waits on <operator action>
77
78
  Split: <task> into <tasks>, file set collided with everything by construction
@@ -32,19 +32,25 @@ if [ -z "$BASE_REF" ]; then
32
32
  fi
33
33
  BASE_BRANCH="${BASE_REF#origin/}"
34
34
 
35
- # These five strings are owned elsewhere and pinned here. `review-pr`
36
- # writes `## Review` and `## Review closed`, and states the full five-heading
35
+ # These six strings are owned elsewhere and pinned here. `review-pr`
36
+ # writes `## Review` and `## Review closed`, and states the full six-heading
37
37
  # set once, beside the threshold it already states once. `review-address`
38
38
  # writes `## Review response`, `## Rebase`, and `## Post-review findings`, the
39
39
  # last for a finding a worker produces after a close-out rather than in answer
40
- # to one already on the thread. All three surfaces ship separately, so a
41
- # heading added in either skill breaks a test here that no check reaches across.
40
+ # to one already on the thread. `canon pr evidence`, run through `git-pr` and
41
+ # `git-followup`, writes `## Evidence`. All these surfaces ship separately, so
42
+ # a heading added in any of them breaks a test here that no check reaches
43
+ # across.
42
44
  #
43
- # Both families match on the first line alone so the tests stay symmetric. The
44
- # reply family carries `## Rebase` and `## Post-review findings` beside
45
- # `## Review response` because neither answers a comment already on the thread,
46
- # which is why both were kept outside the `## Review` family rather than folded
47
- # into it.
45
+ # All three families match on the first line alone so the tests stay
46
+ # symmetric. The reply family carries `## Rebase` and `## Post-review
47
+ # findings` beside `## Review response` because neither answers a comment
48
+ # already on the thread, which is why both were kept outside the `## Review`
49
+ # family rather than folded into it. `## Evidence` answers no comment either,
50
+ # and it stays out of the reply family rather than joining it: an evidence
51
+ # comment is not a response to a review, so counting it as one would send the
52
+ # poll back for a re-review nothing asked for. It is excluded from the
53
+ # unmatched filter below instead, on its own.
48
54
  #
49
55
  # The review family reaches this file through `canon pr review-state` rather
50
56
  # than through a filter of its own, so the two headings are pinned here only in
@@ -81,7 +87,7 @@ JQ_UNMATCHED_STATE='
81
87
  | select(startswith("## "))
82
88
  | select(. != "## Review" and . != "## Review closed"
83
89
  and . != "## Review response" and . != "## Rebase"
84
- and . != "## Post-review findings")
90
+ and . != "## Post-review findings" and . != "## Evidence")
85
91
  ] as $unclassified
86
92
  | ($unclassified | length | tostring)
87
93
  + " "
@@ -14,7 +14,7 @@ Read `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` before writing any file. It
14
14
  - Resolve the board at the main worktree root, not `pwd`. Run `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`, falling back to `pwd` outside a git repo. Every read and write below resolves against that root. The board is gitignored scratch shared across worktrees, so a linked worktree writing to its own `pwd` creates a second board nothing else reads.
15
15
  - From a linked worktree the file-editing tools refuse that root, so a new task file goes out through `Bash` as a plain single command carrying a heredoc. Archiving already runs through `canon tasks archive`, which resolves the root in-process. Marking an outcome shipped is `docs-fold` and runs through `canon tasks outcome`. Resolve that root the way `session-worktree` does.
16
16
  - If `.canon/tasks/` does not exist at that root, stop: `❌ No .canon/tasks/ board. Run canon claude init to set it up.`
17
- - Route on the request rather than on a flag. Creating names work that does not exist yet, archiving names a task file already on the board. If the request fits neither, stop: `❌ Ambiguous. Say whether to create a task or archive one.`
17
+ - Route on the request rather than on a flag. Creating names work that does not exist yet, archiving names a task file already shipped, and declining names one decided against. If the request fits none of the three, stop: `❌ Ambiguous. Say whether to create a task, archive one, or decline one.`
18
18
  - Never hand-edit `.canon/tasks/index.md`. A hook regenerates it from sibling frontmatter after a write. Do not run the regen command directly, except after a shell write from a linked worktree: the hook matches `Write|Edit|MultiEdit` and nothing fires on `Bash`, so that one case regenerates explicitly with `canon indexes regen --no-stage --root <main-root> <main-root>/.canon/tasks/index.md`.
19
19
 
20
20
  ## Create
@@ -63,13 +63,13 @@ Scan for work that has been decided and would otherwise be forgotten. Three orig
63
63
 
64
64
  List `.canon/groundwork/` and run `gh issue list --state open` when a remote is configured, then grep the board for each track name and issue number. Report any with no task, one line each.
65
65
 
66
- Read the dumps through `canon intake list --json`, which reports items, open, unread, and malformed per folder and owns the parse of the answer contract `${CLAUDE_SKILL_DIR}/../../standards/intake.md` fixes. Then grep both `.canon/tasks/` and `.canon/tasks/archive/` for each folder slug. A dump with no live task is the ordinary shape of one already promoted and shipped, so a check reading the board by itself reports every finished folder as abandoned.
66
+ Read the dumps through `canon intake list --json`, which reports items, open, unread, and malformed per folder and owns the parse of the answer contract `${CLAUDE_SKILL_DIR}/../../standards/intake.md` fixes. Then grep `.canon/tasks/`, `.canon/tasks/archive/`, and `.canon/tasks/declined/` for each folder slug. A dump with no live task is the ordinary shape of one already promoted and either shipped or declined, so a check reading the board by itself reports every settled folder as abandoned.
67
67
 
68
68
  A dump is the stronger case for this scan rather than the weaker one. A track holds one question and stays visible, while a dump holds dozens of items whose verdicts were reached and then left with nothing carrying them forward.
69
69
 
70
70
  Those two reads give four states, and the first three earn a line each:
71
71
 
72
- - Every item answered, `malformed` at zero, and neither the board nor the archive cites the folder. Decided work nobody promoted, which is what this step exists to find.
72
+ - Every item answered, `malformed` at zero, and none of the board, the archive, or the declined folder cites it. Decided work nobody promoted, which is what this step exists to find.
73
73
  - Unread items. The folder is waiting on the operator rather than forgotten, so it takes its own wording and never lands in the block above.
74
74
  - `malformed` above zero. An item carrying no answer slot can be reached by no verb, so name the folder as a file to repair rather than as work in either state above.
75
75
  - The archive cites it. Promoted and shipped, so say nothing.
@@ -129,6 +129,40 @@ Leave `TASK-ARCHIVE.md` alone when it is present in the archive folder. It recor
129
129
 
130
130
  The command drops the task's row from `.canon/tasks/priority.md` and leaves prose alone. Remove any sentence that still names the archived task or counts the rows that changed, since a stale count reads as board state.
131
131
 
132
+ ## Decline
133
+
134
+ A task decided against carries no `post-merge` hook of its own, so every decline request arrives here directly rather than through work the hook already did.
135
+
136
+ Do not move the file, edit `priority.md` or `backlog.md`, or regenerate the index by hand. `canon tasks decline` owns all three as one unit.
137
+
138
+ ### Step 1: gather the reason
139
+
140
+ Ask for the reason when the request does not carry one, and stop rather than guessing: `❌ No reason. Say why the task is being declined.` The command takes it as `--reason <text>` and refuses without it, so gathering it here saves a round trip through that refusal.
141
+
142
+ ### Step 2: run the decline
143
+
144
+ Pass the task's filename stem:
145
+
146
+ ```bash
147
+ canon tasks decline <stem> --reason "<text>" [--by <name>] --json
148
+ ```
149
+
150
+ The command refuses rather than reports, and the refusal reaches this skill through the record rather than through the exit, the same wrapper hazard `canon tasks archive` carries. Branch on `ok`, then on `reason`.
151
+
152
+ On success the record carries `from`, `to`, `priorityRowRemoved`, `backlogRowRemoved`, and `indexRegenerated`, where `backlogRowRemoved` is decline's own field since a task can be declined straight off `backlog.md` and archive never checks that file. It also carries `plan` when the task was the last live citation of a live plan, holding the `from` and `to` of the plan moved alongside it, the same shape `canon tasks archive` uses for its own `plan` field.
153
+
154
+ ### Step 3: route on a refusal
155
+
156
+ - `no-match`: the stem does not name exactly one task. Either none matches, or exactly one starts with it and the full name is needed. Check the name against the listed stems.
157
+ - `ambiguous`: the stem is a prefix more than one task starts with, unlike archive's own `ambiguous`, which fires on a shared pull request. Decline takes no pull-request selector, so this is the only route to it. Pass the full stem.
158
+ - `bad-input`: the command line was wrong rather than the board. Read the message, fix the arguments, and run it again. Nothing on the board needs repair, which is what separates this from the two above.
159
+
160
+ Do not move a plan by hand from this skill. The command carries the plan with the task when no other live task cites it, and retargets the declined task's `Plan:` line at the new path. A second mover drifts into relocating the same file differently.
161
+
162
+ ### Step 4: clear prose naming the task
163
+
164
+ The command drops the task's row from `.canon/tasks/priority.md` or `.canon/tasks/backlog.md` and leaves prose alone. Remove any sentence that still names the declined task or counts the rows that changed, since a stale count reads as board state.
165
+
132
166
  ## Output
133
167
 
134
168
  Emit the full relative path from the project root for every file written or moved. A bare filename names a file the reader cannot open.
@@ -35,12 +35,7 @@ Every element competes with the material for attention.
35
35
 
36
36
  ## Diagrams
37
37
 
38
- A diagram earns its place when a relationship, a boundary, a path, a before-and-after, is the point of the passage rather than decoration for it. Most passages do not clear that bar, and a lesson reaching for a diagram on every section is the failure the neighboring `## Restraint` section already guards against. When prose or a list already carries that point, leave it there.
39
-
40
- - Wrap it in a `<figure>` holding an inline `<svg>` and a `<figcaption>` that names what to take from the diagram rather than what it shows. Let the figure run wider than the prose column, since a diagram cramped to the reading measure loses the labels it needs.
41
- - Draw it by hand, as plain shapes and lines authored directly in the markup, not exported from a diagramming tool.
42
- - Color every fill and stroke through a custom property the workspace's own stylesheet defines, never a literal hex value, which is the same rule that stylesheet's opening comment already states for every rule added under it. The diagram then re-colors itself on the same switch that re-colors the page.
43
- - Give the `<svg>` an accessible name: `role="img"` with `aria-label` for one short line, or `aria-labelledby` pointing at a `<title>` element inside it for a longer one.
38
+ Follow `${CLAUDE_SKILL_DIR}/../../standards/figures.md` for when a diagram earns its place, the render-first policy between Mermaid and freehand SVG, and the wrapping, color, and accessibility rules every figure carries. A lesson reaching for a diagram on every section is the failure the neighboring `## Restraint` section already guards against.
44
39
 
45
40
  ## Quiz construction
46
41
 
@@ -48,6 +48,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
48
48
  | `canon teach nav` | Rewrite the teach-root listing, a workspace's contents page, and each lesson's chrome from its four marker regions (`--json`) |
49
49
  | `canon records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
50
50
  | `canon records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
51
+ | `canon records ordinal` | Report or claim the next ordinal `intake` and `groundwork` share, creating the folder atomically (`--claim`, `--json`) |
51
52
  | `canon records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
52
53
  | `canon records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
53
54
  | `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
@@ -83,6 +84,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
83
84
  | `canon pr head` | Compare the head a pull request object reports against the branch tip the remote carries, naming both shas (`--root`, `--json`) |
84
85
  | `canon pr checks` | Report the check runs belonging to the branch tip, reading pending for a tip carrying none rather than clean (`--root`, `--json`) |
85
86
  | `canon pr review-state` | Report the commit and instant the last review pass covered, off the pass's own marker rather than the fields GitHub stamps at submission (`--root`, `--json`) |
87
+ | `canon pr evidence` | Render a before-and-after comment comparing changed `evidence/` images against the merge base, editing its own prior comment in place rather than posting a second (`--root`, `--json`) |
86
88
  | `canon repo metadata propose` | Compare a description, homepage, and topic set computed from the README and `package.json` against what the remote carries, writing nothing (`--root`, `--json`) |
87
89
  | `canon repo metadata apply` | Write an explicitly supplied description, homepage, or topic set to the remote through `gh repo edit` (`--description`, `--homepage`, `--topics`, `--root`, `--json`) |
88
90
  | `canon census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
@@ -28,8 +28,9 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
28
28
  - [Markdown audit](markdown-audit.md): Running the audit over any markdown path, where its bans and checkpoints are read from, what each check reports, and why the ban half gates while the structural half reports
29
29
  - [Output shape](output-shape.md): Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
30
30
  - [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
31
+ - [The pull request evidence comparison](pr-evidence.md): What canon pr evidence compares, the marker that lets it edit its own comment rather than duplicate it, the refusal reasons it names, and why the comparison anchors at the merge base rather than the previous push
31
32
  - [Head-sensitive pull request reads](pr-reads.md): Resolving a branch tip from the remote rather than from the pull request object, reading what a review pass covered off its own marker rather than off GitHub's submission stamps, why an empty run list is not a pass, the refusal reasons each verb names, and what the remote read costs
32
- - [Records](records.md): The two roots a record folder resolves at, validating the session records and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
33
+ - [Records](records.md): The two roots a record folder resolves at, validating the session records and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, claiming the ordinal intake and groundwork share, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
33
34
  - [Restated instructions](restated.md): Counting the instructions the always-loaded file and every path-scoped rule share with the seed, the shipped 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
34
35
  - [Review classification](review-classification.md): Deciding whether a changed set needs the review pass, the two tests it runs, why an empty set refuses rather than skipping, and the written fallback a target on an older binary falls back to
35
36
  - [Routing report](routing.md): 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
@@ -402,16 +402,19 @@ so this section reports and gates nothing.
402
402
 
403
403
  ## Bootstrap
404
404
 
405
- `canon init` installs up to four core domains and reports each one independently. A
405
+ `canon init` installs up to five core domains and reports each one independently. A
406
406
  domain that fails does not abort the run, so the command finishes the rest and
407
407
  exits 1 naming the failures. Passing any flag skips the confirmation prompt,
408
408
  which is what makes it scriptable.
409
409
 
410
410
  `--stack` defaults to `base`, and the default
411
411
  does not read as a passed flag, so a bare `canon init` installs governance and
412
- still prompts. `--skip` takes `wiki` and `governance`, and warns
413
- without aborting on any other value. There is no `--standards` and no
414
- `--snippets`, since no run writes either corpus into the target.
412
+ still prompts. `--skip` takes `wiki`, `governance`, and `records`, and warns
413
+ without aborting on any other value. Records has nothing to run non-interactively,
414
+ since the private backup repository does not exist yet at scaffold time, so its
415
+ step only prints the one-time setup reminder and `--skip records` silences it.
416
+ There is no `--standards` and no `--snippets`, since no run writes either corpus
417
+ into the target.
415
418
 
416
419
  ## Unguarded tooling primitives
417
420