@erclx/canon 5.46.0 → 5.48.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 (55) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/git-followup/SKILL.md +1 -0
  3. package/claude/skills/git-pr/REQUIREMENT.md +2 -2
  4. package/claude/skills/git-pr/SKILL.md +2 -1
  5. package/claude/skills/git-pr/references/closing-issues.md +2 -0
  6. package/claude/skills/git-pr/references/pr.md +58 -7
  7. package/claude/skills/review-address/SKILL.md +18 -6
  8. package/claude/skills/review-address/references/post-review-findings.md +4 -1
  9. package/claude/skills/review-address/references/rebase-only.md +14 -2
  10. package/claude/skills/review-pr/SKILL.md +9 -2
  11. package/claude/skills/review-pr/references/body-variants.md +29 -8
  12. package/claude/skills/review-pr/references/post.md +29 -10
  13. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  14. package/claude/skills/review-ui/SKILL.md +4 -3
  15. package/claude/skills/review-ui/references/post.md +16 -2
  16. package/claude/skills/review-ui/references/tick.md +7 -1
  17. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
  18. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +4 -2
  19. package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
  20. package/claude/skills/role-reviewer/SKILL.md +4 -1
  21. package/claude/skills/role-worker/REQUIREMENT.md +1 -1
  22. package/claude/skills/role-worker/SKILL.md +1 -0
  23. package/claude/skills/task-board/references/decline.md +2 -2
  24. package/claude/skills/task-board/references/tasks.md +4 -2
  25. package/claude/skills/task-complete/SKILL.md +1 -0
  26. package/claude/skills/task-complete/references/archive.md +5 -3
  27. package/claude/skills/ui-checklist/SKILL.md +3 -3
  28. package/docs/agents/migrate.md +2 -2
  29. package/docs/target/migrations.md +4 -2
  30. package/docs/workflow/operating-model.md +2 -2
  31. package/docs/workflow/zshrc-aliases.md +3 -0
  32. package/package.json +1 -1
  33. package/scripts/core/bootstrap.sh +1 -0
  34. package/src/commands/claude.ts +4 -0
  35. package/src/commands/labels.ts +66 -1
  36. package/src/commands/migrate.ts +5 -4
  37. package/src/commands/pr.ts +405 -30
  38. package/src/commands/sessions.ts +25 -11
  39. package/src/commands/tasks.ts +11 -1
  40. package/src/labels/density.ts +363 -0
  41. package/src/migrate/session-defaults.ts +20 -5
  42. package/src/pr/evidence.ts +1 -1
  43. package/src/pr/lift.ts +19 -0
  44. package/src/pr/merge.ts +175 -0
  45. package/src/pr/rest.ts +3 -0
  46. package/src/pr/review-scope.ts +40 -1
  47. package/src/pr/tick.ts +109 -0
  48. package/src/sessions/live.ts +26 -0
  49. package/src/sessions/resolve.ts +62 -11
  50. package/src/tasks/archive.ts +32 -8
  51. package/src/tasks/closes.ts +9 -5
  52. package/src/tasks/validate.ts +4 -2
  53. package/tooling/base/seeds/.github/pull_request_template.md +1 -1
  54. package/tooling/claude/reference.md +3 -1
  55. package/tooling/claude/seeds/.claude/settings.json +1 -1
@@ -21,11 +21,12 @@ The third trigger reaches a window the first two cannot. A session with three wo
21
21
  - A finding that changes a rule goes to the standard or rule that states it
22
22
  - One that changes another task goes to that task's Findings, and one that overturns a groundwork lean to that folder marked answered
23
23
  3. Archive what closed.
24
- - A task whose outcomes are all `[x]` and whose pull request merged is archived by the `post-merge` hook through `canon tasks archive`, plans included
24
+ - A task whose outcomes are all `[x]`, or all `[x]` beside `Operator check:` lines, and whose pull request merged is archived by the `post-merge` hook through `canon tasks archive`, plans included
25
25
  - Run `task-complete` to archive one the hook left in place
26
26
  - A task whose outcomes are standing policy never closes, so hand it to a worker to encode the policy where it is enforced, then cut the outcomes with the reason and archive once that branch merges
27
27
  - Encoding it from this session would write a tracked file, which `## Boundaries` in the skill body forbids
28
- - A merged task whose only open outcomes need the operator or another repository never archives, since `canon tasks archive` refuses `open-outcomes`. Strike each one on the task file as `- ~~<outcome>~~ <reason>`, with the reason on the same line, then archive. The task files are gitignored scratch, so this session may edit them.
28
+ - An outcome marked `- [ ] Operator check: <outcome>` never blocks the archive. The hook prints no record, so read the archived file's `Operator check:` lines into the report's `Operator checks:` line and leave them unticked.
29
+ - A merged task whose only open outcomes another repository owes, or that an operator owns and nobody marked, never archives, since `canon tasks archive` refuses `open-outcomes`. Strike each one on the task file as `- ~~<outcome>~~ <reason>`, with the reason on the same line, then archive. The task files are gitignored scratch, so this session may edit them.
29
30
  - Strike only an outcome the operator or another repository owes. An open outcome this repository owes stays on the board, and the task stays with it.
30
31
  - Send a struck outcome that another repository owes to that repository's orchestrator through `session-relay`, which resolves the address, and let the receiver file it with `task-board` on its own board. `task-board` here would file it on this board. Name it in the report for the operator when no such session is live.
31
32
  - List every struck outcome in the report's `Struck:` line, so the work is carried by a named owner rather than lost to the strike
@@ -45,6 +46,7 @@ The third trigger reaches a window the first two cannot. A session with three wo
45
46
  Findings placed: <finding> → <destination>
46
47
  Archived: <task>
47
48
  Struck: <outcome>, owed by <operator or repository>, <reason>
49
+ Operator checks: <outcome>, left unticked on <task>
48
50
  Promoted: <task>, touches <surfaces>, parallel with <task>, sharing <paths or none>
49
51
  Serialized: <task> behind <task>, held on <dependency, contract, relocation, sweep, or reason>
50
52
  Backlogged: <task>, because <what stopped being near-term>
@@ -14,7 +14,7 @@ Each launch also restates the reviewer's bounds, though they belong to the role.
14
14
  ## Must
15
15
 
16
16
  - The role is asserted, with writes bounded to the comment and body file the pass posts, and the tracked file, branch, worktree, board, and commit named as outside them
17
- - Every file read a finding rests on goes through `git show <head>:<path>` at the resolved head, and launch-carried cross-branch facts are inputs
17
+ - Every file read a finding rests on goes through `git show <head>:<path>` at the resolved head, and launch-carried cross-branch facts are inputs. A task list, if the host has one, tracks each step.
18
18
  - The fix, a draft lift by any route but `canon pr lift`, and the merge are refused whoever asks, naming each owner, and the lift runs after a close is posted
19
19
  - An open pass sends the worker the `review-address` dispatch and this session's id, else the controller the cause, and a close sends the lift's reason unless the other review is open or missing
20
20
  - A re-review stays with the reviewer that took the first pass while it is live, and a block is messaged before it becomes an interactive prompt
@@ -31,6 +31,8 @@ The session writes its comment and nothing tracked. Each kind of reviewer writes
31
31
  - Resolve `.canon/plans/`, `.canon/tasks/`, and `.canon/tmp/` at the main worktree root, and send the body file as a heredoc, the main-root route `session-worktree` states
32
32
  - Report a plan or task that fails to resolve there as unreadable, naming the path, rather than reviewing as though the branch had none
33
33
 
34
+ Keep a task list from launch, one item per step of the pass, and tick each as it completes. Skip it on a host that offers no list tool, since the list is for the operator watching and not a gate.
35
+
34
36
  Review the pull request the launch named. Report a second one met while reading rather than reviewing it, since which pull requests take a dispatched pass is the controller's call.
35
37
 
36
38
  ## What to read and what to leave
@@ -54,11 +56,12 @@ Nobody watches this pass, so the messages below are owed and nothing else. The w
54
56
 
55
57
  ### The address-review dispatch
56
58
 
57
- Send it to the worker holding the branch as `review-pr` Step 5 returns an open `## Review`, or as `review-ui` reports an open `## UI review`.
59
+ Send it to the worker holding the branch as `review-pr` Step 5 returns an open `## Review`, or as `review-ui` reports an open `## UI review` that drove a box. A `## UI review` that drove no box goes to the controller with its cause instead, per `${CLAUDE_SKILL_DIR}/../review-ui/references/post.md`.
58
60
 
59
61
  - Carry the pull request number, the heading, that step's count line verbatim, and this session's own sessionId for the worker's reply
60
62
  - Resolve the worker with `canon sessions list --branch <branch> --json`, where `<branch>` is the pull request's head branch
61
63
  - Send to it only when exactly one session comes back with its confidence reading `confirmed`
64
+ - Count a row reading `via: "listener"` as confirmed, since its messaging socket accepted a connection. Never read it as `via: "socket"`, which rests on a socket file existing alone.
62
65
  - On zero, on any other count or confidence, or on a refusal, send the same message to the controller, naming the cause the roster read gives
63
66
  - Say the roster rests on sockets from another pid namespace, so the worker may be live and unconfirmed, when a row reads `via: "socket"` or an empty roster carries a `foreignDead` count above zero
64
67
  - Say the roster rests on a pid answering alone, since start times could not be read, when the confidence reads `unverified` and no row reads `via: "socket"`
@@ -13,7 +13,7 @@ The channel fails on the worker's side. Blocks open prompts nobody is attached t
13
13
 
14
14
  ## Must
15
15
 
16
- - Session scratch resolves at the main worktree root, and a plan that fails to resolve is reported unreadable from there rather than absent
16
+ - Session scratch resolves at the main worktree root, and a plan that fails to resolve is reported unreadable from there rather than absent. A task list, where the host has one, tracks each slice.
17
17
  - The announcement carries the number, branch, task, reach, any uncleared spend, and any row the build turned up, and a painted diff has its preview server running before the pull request opens
18
18
  - A block goes out as a message before it becomes a prompt, a fan-in member's done message carries its branch and head commit, and a review wait that times out reaches the controller
19
19
  - The plan's scope freezes at dispatch, and a late ask is sorted into a defect, a follow-up row, or an addition on the operator's word
@@ -23,6 +23,7 @@ This body states the role, the boundaries, and the channel, and starts no step o
23
23
  - Resolve `.canon/plans/`, `.canon/tasks/`, `.canon/review/`, and `.canon/ready/` at the main worktree root, never the linked worktree. Being gitignored, they are absent there rather than empty.
24
24
  - Report a plan that fails to resolve as unreadable from here, naming the main-root path. Reporting the task as having no plan is wrong about the world, and a reader cannot tell the two apart.
25
25
  - Build the plan the launch named. Never write a second one when the path fails to resolve, since a row citing a plan already has one.
26
+ - Keep a task list from launch, one item per slice, and tick each as its slice lands. Skip it on a host that offers no list tool, since the list is for the operator watching and not a gate.
26
27
 
27
28
  ### Ready folders and the draft mark
28
29
 
@@ -33,11 +33,11 @@ A successful decline returns one record whose fields match `canon tasks archive`
33
33
  | -------------------- | ------------------------------------------------------------------------ |
34
34
  | `from`, `to` | The task's old and new path |
35
35
  | `priorityRowRemoved` | Whether a `priority.md` row was dropped |
36
- | `backlogRowRemoved` | Whether a `backlog.md` row was dropped, a field archive lacks |
36
+ | `backlogRowRemoved` | Whether a `backlog.md` row was dropped |
37
37
  | `indexRegenerated` | Whether the index was rewritten |
38
38
  | `plans` | A `from` and `to` per live plan the task last cited, empty if none moved |
39
39
 
40
- `backlogRowRemoved` is decline's own, since a task can be declined straight off `backlog.md` and archive never checks that file.
40
+ A task can be declined straight off `backlog.md`, so decline checks that file as well as `priority.md`. Archive does the same and reports the same two fields.
41
41
 
42
42
  ## Step 3: route on a refusal
43
43
 
@@ -198,7 +198,7 @@ Session history and ordering both have homes off the task file.
198
198
 
199
199
  ## Archiving
200
200
 
201
- Never delete a task file. A shipped task moves to `.canon/tasks/archive/` under its own name through `canon tasks archive`, and the live index regenerates without it. What the verb does and refuses on is at `canon tasks archive --help`, and a flat `.claude/task-archive/` names an older binary rather than a second archive.
201
+ Never delete a task file. A shipped task moves to `.canon/tasks/archive/` under its own name through `canon tasks archive`, and the live index regenerates without it. What the verb does and refuses on is at `canon tasks archive --help`.
202
202
 
203
203
  ### The plan and ready folder
204
204
 
@@ -209,4 +209,6 @@ Archiving a task archives its plan alongside it when the task is that plan's las
209
209
 
210
210
  ### Open outcomes
211
211
 
212
- A task with an open outcome stays on the board, and so does its plan. Close it, or cut an abandoned outcome by striking its body, as `- ~~<outcome>~~ <why>`, which reads as cut whatever its checkbox holds, so a task of cut outcomes still archives.
212
+ A task with an open outcome stays on the board with its plan. Close it, or cut an abandoned one by striking its body, as `- ~~<outcome>~~ <why>`, which reads as cut.
213
+
214
+ An operator-only check outside this repository takes `- [ ] Operator check: <outcome>`, written before the pull request opens. It closes unticked.
@@ -43,6 +43,7 @@ For each unchecked outcome, decide whether the diff shipped the behavior it name
43
43
 
44
44
  - Mark only outcomes already written on the board. Never infer a new task from the diff.
45
45
  - Match on the behavior an outcome describes, not on filenames or commit subjects. The path match above only narrowed which task files to open.
46
+ - Never tick an outcome marked `Operator check:`, since a diff cannot prove a check only the operator can run.
46
47
  - Leave an outcome `[ ]` when the diff is ambiguous. An unmarked shipped outcome costs one manual edit, while a wrongly marked one hides work that never happened.
47
48
  - Do not touch task files the diff does not relate to.
48
49
 
@@ -8,7 +8,7 @@ description: How task-complete confirms a task's work reached main, runs canon t
8
8
  The Archive mode of `task-complete`. The session reads this file when the request names a task file already shipped.
9
9
 
10
10
  - The `post-merge` git hook archives the task a merge closed, so a request here is usually one the hook could not resolve. Run the steps against whatever it left in place.
11
- - `canon tasks archive` moves the file, edits `priority.md`, and regenerates the index as one unit. The hook calls the same command, so a hand-rolled move drifts from the unattended path.
11
+ - `canon tasks archive` moves the file, edits `priority.md` and `backlog.md`, and regenerates the index as one unit. The hook calls it too, so a hand-rolled move drifts from the unattended path.
12
12
 
13
13
  ## Step 1: confirm the work reached main
14
14
 
@@ -45,6 +45,7 @@ A successful archive returns one record naming what moved.
45
45
  | -------------------- | --------------------------------------------------------------------- |
46
46
  | `from`, `to` | The task's old and new path |
47
47
  | `priorityRowRemoved` | Whether a `priority.md` row was dropped |
48
+ | `backlogRowRemoved` | Whether a `backlog.md` bullet was dropped |
48
49
  | `indexRegenerated` | Whether the index changed |
49
50
  | `plans` | A `from` and `to` per live plan the task last cited, in `Plan:` order |
50
51
 
@@ -55,7 +56,8 @@ An empty `plans` means the task cited no live plan, or a sibling still holds eve
55
56
  Each reason has one resolution, and none of them is to archive around it.
56
57
 
57
58
  - `open-outcomes`: the named outcomes are unmarked or genuinely open. Run `task-complete` when the work shipped unmarked, and leave the task on the board when the outcome is real.
58
- - An outcome only the operator or another repository can close is the exception. The orchestrator's merge sweep strikes it with its reason and lists it in the report, per `${CLAUDE_SKILL_DIR}/../role-orchestrator/references/orchestrator-refill.md`, and the archive then runs. An outcome this repository owes is never struck for that.
59
+ - An outcome only the operator can check is marked `- [ ] Operator check: <outcome>` rather than struck, which the archive passes and the record lists under `operatorChecks`. The mark has to land before the pull request opens.
60
+ - Abandoned work, an outcome another repository owes, and an operator outcome nobody marked in time take the strike. The orchestrator's merge sweep strikes it with its reason and lists it in the report, per `${CLAUDE_SKILL_DIR}/../role-orchestrator/references/orchestrator-refill.md`, and the archive then runs. An outcome this repository owes is never struck for that.
59
61
  - `ambiguous`: two tasks name one pull request, the misfile `${CLAUDE_SKILL_DIR}/references/tasks.md` rules out. Resolve the citation by hand, since no sweep repairs it.
60
62
  - `earlier-slice`: the number is listed on the task but is not the last. Leave the task on the board, and its last slice's merge archives it.
61
63
 
@@ -80,4 +82,4 @@ Leave `TASK-ARCHIVE.md` alone when it is present in the archive folder. It recor
80
82
 
81
83
  ## Step 4: clear prose naming the task
82
84
 
83
- The command drops the row from `.canon/tasks/priority.md` and leaves prose alone. Remove any sentence that still names the archived task or counts the changed rows, since a stale count reads as board state.
85
+ The command drops the row from `.canon/tasks/priority.md` and the bullet from `.canon/tasks/backlog.md`, and leaves prose alone. Remove any sentence that still names the archived task or counts the changed rows, since a stale count reads as board state.
@@ -23,11 +23,11 @@ An automatable change already covered by a test written during implementation is
23
23
 
24
24
  ## The checklist
25
25
 
26
- Group visual items by feature area. Use `- [ ]` checkbox syntax, and write each item as the action and the result a person is looking for, so a reviewer who did not build the change can run it.
26
+ Group visual items under a bold label per feature area, at most five boxes a group, and split a larger area into two labels. Use `- [ ]` checkbox syntax, and write each item as the action and the result a person is looking for, so a reviewer who did not build the change can run it.
27
27
 
28
- ```markdown
29
- **What to verify visually:**
28
+ The evidence comment already heads the list with `## What to look at`, so the file opens on its first feature label with no heading line of its own.
30
29
 
30
+ ```markdown
31
31
  **<Feature area>**
32
32
 
33
33
  - [ ] <route>[, <width>]: <verb> <target> → <expected visual result>
@@ -18,13 +18,13 @@ kind: reference
18
18
  | `rule-layout` | A flat installed-rule layout onto the `canon/`-wrapped one |
19
19
  | `scratch-evidence` | A folder a durable record cites out of `.tmp/`, with its citations |
20
20
  | `record-layout` | Review receipts, memory archives, feedback, renders, picks, and evidence into their current folders |
21
- | `session-defaults` | The seeded session defaults into an existing `.claude/settings.json`: `Agent` and `Task` denied, auto memory off |
21
+ | `session-defaults` | The seeded session defaults into an existing `.claude/settings.json`: `Agent`, `Task`, and both merge commands denied, auto memory off |
22
22
 
23
23
  - `skill-names` takes no `--scope`, and skips the changelog, eval transcripts, and the pointer each renamed `draft-` skill leaves at its old name
24
24
  - `surface-roots` leaves `rules/`, `skills/`, `hooks/`, and `settings.json` where Claude Code reads them
25
25
  - `rule-layout` classifies each flat file against the stamp as clean, edited, or unclaimed. Clean and edited files move untouched, and an unclaimed name stays.
26
26
  - `record-layout` labels each evidence folder `(pick)` or `(evidence)` in the dry run
27
- - `session-defaults` adds a key only when absent. It never removes a deny entry or changes a set `autoMemoryEnabled`, and a missing settings file is nothing to do.
27
+ - `session-defaults` adds a key only when absent, merge entries included, and each older project runs it once. `--write` restores a removed `Agent` or `Task` and never removes a deny entry.
28
28
 
29
29
  ## Output
30
30
 
@@ -150,9 +150,9 @@ The wireframes record is retired, and nothing deletes what a project holds.
150
150
 
151
151
  The toolkit no longer seeds `canon/ARCHITECTURE.md`, and a held file and its `CLAUDE.md` import stay. Move a trap a session would hit into that domain's `## Gotchas`, then delete the file and its `@canon/ARCHITECTURE.md` line.
152
152
 
153
- ## Deny subagents and turn auto memory off, once
153
+ ## Deny subagents and merges, and turn auto memory off, once
154
154
 
155
- The seeded `.claude/settings.json` now denies the `Agent` and `Task` tools and sets `autoMemoryEnabled` to `false`. `canon claude sync` never rewrites a settings file you hold, so it warns while the file holds neither default. Removing the deny later to allow subagents draws no warning.
155
+ The seeded `.claude/settings.json` now denies the `Agent` and `Task` tools and both merge commands, and sets `autoMemoryEnabled` to `false`. `canon claude sync` never rewrites a settings file you hold, so it warns while the file holds no default or lacks a merge entry. Removing `Agent` and `Task` later draws no warning.
156
156
 
157
157
  ```bash
158
158
  canon migrate session-defaults --json
@@ -161,6 +161,8 @@ canon migrate session-defaults --write --json
161
161
 
162
162
  The verb only adds a key that is absent. A person who wants auto memory back sets it in their own `.claude/settings.local.json`.
163
163
 
164
+ The verb adds `Bash(canon pr merge:*)` and `Bash(gh pr merge:*)` so a session cannot merge. A project migrated before them runs `--write` once more, which also restores `Agent` and `Task` for a person who removed them to allow subagents, so remove them again after.
165
+
164
166
  ## Move the capture cases out of the harness, once
165
167
 
166
168
  A web project's `e2e/screenshot.ts` is a config, so sync overwrites it. It now loads `CASES` from a seeded `e2e/capture/cases.ts` that sync copies once and never overwrites. Cases edited in the old harness leave the working tree on the first sync and stay in git history.
@@ -32,9 +32,9 @@ One warm session holds the cross-feature call and reviews. Cold worker sessions
32
32
  5. The reviewer tells the session holding the branch to run `review-address`, found with `canon sessions list --branch`, once a pass posts a finding at any severity.
33
33
  6. The worker addresses the findings, rebases onto `origin/main` if a sibling landed on its files, pushes, and asks that reviewer for a re-review of the added commits.
34
34
  7. A pass posts `## Review` while anything is open and `## Review closed` when nothing is. The reviewer then runs `canon pr lift`.
35
- 8. The human merges. The orchestrator tells any trailing worker whose files the merge touched to run `review-address`, which rebases.
35
+ 8. The human merges with `canon pr merge <number>`. The orchestrator tells any trailing worker whose files the merge touched to run `review-address`, which rebases.
36
36
 
37
- Each worker halts at the PR, and the merge stays a human gate.
37
+ Each worker halts at the PR, and the merge stays a human gate. `canon pr merge` squashes with the body's Summary, Key changes, and Technical context as the message, and the seeded deny rule covers the forms an agent writes by default. The merge button stays for a bot's pull request, such as a dependency bump or a release.
38
38
 
39
39
  ## The review layer
40
40
 
@@ -17,6 +17,7 @@ A hand-written block without markers is detected through its `alias clp=` line,
17
17
  ```zsh
18
18
  # >>> canon aliases >>>
19
19
  TOOLKIT="/path/to/toolkit"
20
+ export CLAUDE_CODE_ENABLE_TODO_TOOLS=1
20
21
 
21
22
  alias cl='claude'
22
23
  alias clr='cl -r'
@@ -32,6 +33,8 @@ alias clpac='clpa --cwd .'
32
33
  # <<< canon aliases <<<
33
34
  ```
34
35
 
36
+ The export gives a session started from your terminal the task tools, which Claude Code reads before it starts, so workers and reviewers can show their checklist on `Ctrl+T`. A background session has them without it.
37
+
35
38
  To opt out, delete the block and run `bun install` and `bun link` yourself. Bash takes the block unchanged in `~/.bashrc`, pasted by hand. Bootstrap writes `~/.zshrc` alone.
36
39
 
37
40
  ## What each one does
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.46.0",
4
+ "version": "5.48.0",
5
5
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -26,6 +26,7 @@ alias_block() {
26
26
  cat <<EOF
27
27
  $MARKER_OPEN
28
28
  TOOLKIT="$PROJECT_ROOT"
29
+ export CLAUDE_CODE_ENABLE_TODO_TOOLS=1
29
30
 
30
31
  alias cl='claude'
31
32
  alias clr='cl -r'
@@ -673,6 +673,10 @@ async function runSync(target: string): Promise<number> {
673
673
  logWarn(
674
674
  'settings.json lacks the session defaults. Run `canon migrate session-defaults --write`',
675
675
  )
676
+ } else if (defaults.kind === 'planned' && defaults.plan.lacksMergeDeny) {
677
+ logWarn(
678
+ 'settings.json lacks the merge deny entries. Run `canon migrate session-defaults --write`',
679
+ )
676
680
  }
677
681
 
678
682
  logStep('Scanning .gitignore')
@@ -6,6 +6,12 @@ import { gitEnv } from '@/git/env'
6
6
  import { diffLabels } from '@/labels/apply'
7
7
  import { type LabelAuditRefusal, auditLabels } from '@/labels/audit'
8
8
  import { checkClosing } from '@/labels/closing'
9
+ import {
10
+ bodyDensity,
11
+ isPost,
12
+ POST_HEADINGS,
13
+ postDensity,
14
+ } from '@/labels/density'
9
15
  import { resolveScanInput } from '@/labels/event'
10
16
  import { checkTitleFormat, type TitleFormatIssue } from '@/labels/format'
11
17
  import { MAP_REL } from '@/labels/map'
@@ -241,7 +247,27 @@ export function register(program: Command): void {
241
247
  'It grades the body of an ordinary pull request against the closed',
242
248
  'section list: a level-two heading outside it, or a missing',
243
249
  '## Summary or ## Key changes, fails. A review, a release-please body,',
244
- 'and a scan carrying no title skip this check. It also fails a banned',
250
+ 'and a scan carrying no title skip this check.',
251
+ '',
252
+ 'It reports the density of that same body per section under `density`,',
253
+ 'without failing on it: a bullet past 200 characters, more than 5 plain',
254
+ 'bullets or 2 paragraphs in a row, a paragraph past 350 characters, any',
255
+ 'H3 or deeper heading, and Technical context mixing bullets with',
256
+ 'paragraphs. A bold label alone on a line splits a Key changes run, a',
257
+ 'bold-led paragraph starts a new Technical context run, and Testing',
258
+ 'takes the heading limit alone. Each finding carries its section, kind,',
259
+ 'line in the whole body, value, and limit.',
260
+ '',
261
+ 'A body the section check skips is graded as a post under the same',
262
+ '`density` key when its first level-two heading is one of',
263
+ `${POST_HEADINGS.map((heading) => `## ${heading}`).join(', ')}.`,
264
+ 'A post takes the same length and run limits, allows an H3, and',
265
+ 'measures a nested item as a bullet of its own. The footer, the',
266
+ 'marker, and a table never report, a block inside <details> takes the',
267
+ 'length limits alone, and a checklist box under ## What to look at',
268
+ 'takes no length limit. Post density never fails the scan.',
269
+ '',
270
+ 'It also fails a banned',
245
271
  'character, an em dash or a semicolon, in the title or body of a pull',
246
272
  'request or a review, outside a code span or a link.',
247
273
  '',
@@ -692,6 +718,44 @@ async function runScan(opts: ScanOptions): Promise<number> {
692
718
  }
693
719
  }
694
720
 
721
+ // Reported and never gated yet. New bodies come back clean under the
722
+ // shape in pr.md first, so an older open pull request fails no edit event.
723
+ // A post is never gated, since CI reads one only after it is published.
724
+ const post = !sectionsChecked && isPost(resolved.body)
725
+ const density = sectionsChecked
726
+ ? bodyDensity(resolved.body)
727
+ : post
728
+ ? postDensity(resolved.body)
729
+ : []
730
+
731
+ logStep(
732
+ !sectionsChecked && !post
733
+ ? 'Density not checked'
734
+ : density.length === 0
735
+ ? 'Clean'
736
+ : 'Density finding found',
737
+ )
738
+ if (!sectionsChecked && !post) {
739
+ logInfo(
740
+ 'only an ordinary pull request body or a post opening on a post heading is graded for density',
741
+ )
742
+ } else if (density.length === 0) {
743
+ logInfo('every section is within its length, run, and heading limits')
744
+ } else {
745
+ logWarn(
746
+ `${plural(density.length, 'density finding')} in the ${post ? 'post' : 'body'}. Regroup the section under bold labels or split the long item rather than cutting a path.`,
747
+ )
748
+ for (const finding of density) {
749
+ const measured =
750
+ finding.value === null
751
+ ? ''
752
+ : ` (${finding.value} over ${finding.limit})`
753
+ logWarn(
754
+ `line ${finding.line} in ${finding.section ?? 'the opening'}: ${finding.kind}${measured}`,
755
+ )
756
+ }
757
+ }
758
+
695
759
  const banFindings = [
696
760
  ...scanBans(
697
761
  [{ number: 1, text: resolved.title, fenced: false }],
@@ -781,6 +845,7 @@ async function runScan(opts: ScanOptions): Promise<number> {
781
845
  sessionLinks: result.sessionLinks,
782
846
  titleFormatIssues,
783
847
  sections: { checked: sectionsChecked, ...sections },
848
+ density,
784
849
  bannedCharacters,
785
850
  missingClosures: closing.missing,
786
851
  strayClosures: closing.stray,
@@ -1421,10 +1421,11 @@ export function register(program: Command): void {
1421
1421
  'after',
1422
1422
  [
1423
1423
  '',
1424
- 'Adds Agent and Task to permissions.deny in .claude/settings.json,',
1425
- 'appending to the list without reordering it, and sets',
1426
- 'autoMemoryEnabled to false only when the key is absent. A deny entry',
1427
- 'is never removed and an existing autoMemoryEnabled is never changed.',
1424
+ 'Adds Agent, Task, Bash(canon pr merge:*), and Bash(gh pr merge:*) to',
1425
+ 'permissions.deny in .claude/settings.json, appending only the ones',
1426
+ 'the list lacks without reordering it, and sets autoMemoryEnabled to',
1427
+ 'false only when the key is absent. A deny entry is never removed and',
1428
+ 'an existing autoMemoryEnabled is never changed.',
1428
1429
  '',
1429
1430
  'A missing settings file is nothing to do. A file that is not valid',
1430
1431
  'JSON, or whose permissions or deny has an unexpected shape, is',