@erclx/canon 4.87.0 → 4.88.1

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 (36) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/SKILL.md +1 -1
  3. package/claude/skills/docs-fold/SKILL.md +4 -4
  4. package/claude/skills/draft-and-pick/REQUIREMENT.md +10 -1
  5. package/claude/skills/draft-and-pick/SKILL.md +7 -3
  6. package/claude/skills/git-followup/SKILL.md +1 -1
  7. package/claude/skills/git-pr/SKILL.md +8 -8
  8. package/claude/skills/git-split/SKILL.md +19 -19
  9. package/claude/skills/memory-capture/SKILL.md +2 -2
  10. package/claude/skills/memory-review/SKILL.md +2 -2
  11. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  12. package/claude/skills/review-address/SKILL.md +11 -11
  13. package/claude/skills/review-pr/SKILL.md +2 -2
  14. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
  15. package/claude/skills/role-orchestrator/scripts/poll.sh +1 -1
  16. package/claude/skills/teach-workspace/SKILL.md +2 -2
  17. package/claude/skills/ui-test/REQUIREMENT.md +1 -1
  18. package/claude/skills/ui-test/SKILL.md +2 -2
  19. package/docs/agents/commands.md +2 -1
  20. package/docs/agents/records.md +27 -0
  21. package/docs/agents/sandbox.md +1 -1
  22. package/docs/workflow/ai-workflow.md +4 -2
  23. package/governance/rules/core/055-scratch.md +1 -0
  24. package/package.json +1 -1
  25. package/scripts/tooling/verify.sh +2 -2
  26. package/src/claude/skills-headings.ts +1 -1
  27. package/src/commands/migrate.ts +1 -1
  28. package/src/commands/records.ts +159 -0
  29. package/src/migrate/record-layout.ts +2 -2
  30. package/src/migrate/scratch-evidence.ts +3 -5
  31. package/src/records/prune.ts +488 -0
  32. package/src/records/size.ts +24 -1
  33. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +2 -2
  34. package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +2 -2
  35. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +2 -2
  36. package/tooling/claude/seeds/CLAUDE.md +2 -6
@@ -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.87.0",
4
+ "version": "4.88.1",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -121,7 +121,7 @@ The verb ships with the CLI and this body ships with the plugin, matching Step 6
121
121
 
122
122
  If the diff touches UI files (JSX, TSX, Vue, Svelte, HTML, or CSS under `src/`), invoke `canon:ui-test`.
123
123
 
124
- If `ui-test` produces a manual checklist, stop: `❌ UI requires visual verification. Checklist at .canon/tmp/ui-checklist/<slug>.md, which reaches the pull request once /git-ship runs. Verify manually, then run /git-ship.`
124
+ If `ui-test` produces a manual checklist, stop: `❌ UI requires visual verification. Checklist at .canon/tmp/handoff/ui-checklist/<slug>.md, which reaches the pull request once /git-ship runs. Verify manually, then run /git-ship.`
125
125
 
126
126
  If all UI changes are covered by e2e tests, continue.
127
127
 
@@ -149,7 +149,7 @@ Read `canon/context/index.md` at `pwd` to see which domain entries exist. Skip t
149
149
 
150
150
  Two sources feed this step, the same split Step 2 runs on. The diff carries what the repository changed. The routed facts carry what the session learned, which a diff cannot show.
151
151
 
152
- **Routed facts.** Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`, falling back to `latest` on an empty result, and read `.canon/tmp/memory-routing/<slug>.md` at the main worktree root. `memory-capture` writes it, one H2 per target entry naming the path, with the fact underneath. Fold each fact into the entry its heading names, which for a nested `canon/context/<domain>/index.md` heading is the sibling file the fact belongs under rather than the generated index itself. Then delete the handoff file so a later run does not fold it twice.
152
+ **Routed facts.** Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`, falling back to `latest` on an empty result, and read `.canon/tmp/handoff/memory-routing/<slug>.md` at the main worktree root. `memory-capture` writes it, one H2 per target entry naming the path, with the fact underneath. Fold each fact into the entry its heading names, which for a nested `canon/context/<domain>/index.md` heading is the sibling file the fact belongs under rather than the generated index itself. Then delete the handoff file so a later run does not fold it twice.
153
153
 
154
154
  This half is not diff-scoped and must not be. A gotcha a session hit while working is exactly the fact the diff never shows, and scoping it to changed files would drop the entries worth keeping. The handoff is a named input rather than a scan, so the reach stays bounded to what capture decided.
155
155
 
@@ -183,13 +183,13 @@ Write each updated entry immediately. Output one line per file, naming the path
183
183
 
184
184
  Add a line naming the handoff when one was consumed:
185
185
 
186
- `🧹 Folded: .canon/tmp/memory-routing/<slug>.md`
186
+ `🧹 Folded: .canon/tmp/handoff/memory-routing/<slug>.md`
187
187
 
188
188
  The base lint-staged config runs `canon indexes regen` on every committed `*.md`, so `canon/context/index.md` refreshes automatically on commit. No manual step needed.
189
189
 
190
190
  ## Step 8: fold promoted pages
191
191
 
192
- Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`, falling back to `latest` on an empty result, and read `.canon/tmp/teach-promotion/<slug>.md` at the main worktree root. `teach-workspace` writes it, one H2 per destination naming the path, with a source line under the heading and the page body in a fenced block below that. Read the body out of the fence rather than off the heading level, since a reference page carries headings of its own and only the fence separates them from the next destination. Skip this step silently when the file is absent, which is every run where nothing was promoted.
192
+ Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`, falling back to `latest` on an empty result, and read `.canon/tmp/handoff/teach-promotion/<slug>.md` at the main worktree root. `teach-workspace` writes it, one H2 per destination naming the path, with a source line under the heading and the page body in a fenced block below that. Read the body out of the fence rather than off the heading level, since a reference page carries headings of its own and only the fence separates them from the next destination. Skip this step silently when the file is absent, which is every run where nothing was promoted.
193
193
 
194
194
  Each block is a page an operator already confirmed a destination for, so this step lands it rather than judging it again. Write to the destination the heading names, at `pwd` rather than at the main root, since every destination here is a tracked file that commits with the branch:
195
195
 
@@ -204,7 +204,7 @@ Output one line per page landed:
204
204
 
205
205
  Add a line naming the handoff when one was consumed:
206
206
 
207
- `🧹 Folded: .canon/tmp/teach-promotion/<slug>.md`
207
+ `🧹 Folded: .canon/tmp/handoff/teach-promotion/<slug>.md`
208
208
 
209
209
  Report a block left unfolded rather than dropping it:
210
210
 
@@ -20,12 +20,21 @@ Without this skill, a session facing a decision nobody can settle from a diff:
20
20
  - Reads a color without compositing its alpha, samples a round control at the corner of its bounding box, or confirms a panel's dimensions while it sits above the viewport. Each reads as a passing measurement about something no reader sees.
21
21
  - Meets a machine with no browser binary and reports on the arms anyway, since nothing in the default flow separates a render that failed from one that was never attempted.
22
22
  - Hand-writes a mockup of a surface a running app already serves, which drifts from what ships by a few pixels and a few words the moment the app's own markup or stylesheet moves on.
23
+ - Varies a property the decision is not about, which satisfies the one-property rule and produces a set differing in the layer a reader notices least. Five palettes over one identical page read as five skins of one design, and the operator rejected the whole set on sight.
24
+ - Renders each arm in whatever theme it happens to carry, so a set spanning both themes cannot be compared without the operator toggling every arm and holding the earlier ones in memory.
25
+ - Looks at a render to confirm it came out rather than to judge whether it is worth showing, which satisfies the rule against reporting an unseen result and still hands over weak work.
26
+ - Writes the run's renders to a scratch path outside the record that cites them, leaving the folder holding source markup and none of the images, so the judgment is unreachable to everyone except the session that made it.
27
+ - Narrows the page onto the picked arm in place on each iteration, so every earlier round is overwritten and a later pass cannot see what was already rejected.
23
28
 
24
29
  ## Must
25
30
 
26
31
  - Produce three to five arms with the shipping state among them, each carrying an id, a label, and what the arm costs.
27
32
  - Author the whole candidate set as one self-contained page and render it once, so the comparison arrives as one image, except where the surface under decision is a running app: there the page links a copy of the built stylesheet rather than inlining it, per `references/live-arms.md`.
28
- - Vary one property across the arms, so the answer names a difference.
33
+ - Name the layer the decision lives at before drafting, and vary the arms at that layer alone, so the answer names the difference that decided it rather than the cheapest one to change.
34
+ - Render every arm in one theme at a time, with a control that sets the whole set, so the comparison holds still while the operator reads it.
35
+ - Judge each render against a stated bar before handing it over, name the weakest thing on the page, and fix it where that sentence would embarrass the work.
36
+ - Write each iteration to its own folder rather than narrowing the previous one in place, so every earlier round stays openable.
37
+ - Write the run's renders inside the record that cites them wherever one exists, and reserve the scratch path for inputs that are re-runnable and cited by nothing.
29
38
  - Render before reporting on any arm, and report a missing browser binary as a refusal naming the repair rather than describing an arm nobody has seen.
30
39
  - Take the pick through the structured question surface, with the recommendation ranked first and marked, and every option carrying its cost.
31
40
  - Keep every write inside the run's scratch folder until the pick is final.
@@ -15,7 +15,7 @@ Some decisions are settled by looking rather than by reasoning, and no draft is
15
15
 
16
16
  ## Step 1: name the decision and the arms
17
17
 
18
- 1. State the decision in one sentence, naming what changes between arms and what stays fixed.
18
+ 1. State the decision in one sentence, naming what changes between arms and what stays fixed. Name the layer that sentence puts the decision at, and check that the arms will differ there rather than somewhere cheaper to change.
19
19
  2. Derive a kebab slug from that sentence. Call the folder every file this run writes to `<dest>` below. `<dest>` is `.canon/tmp/<slug>/`, a nested `<slug>/` folder rather than a flat `<slug>-<file>.md`, which is the shape every temporary write in this project takes. Running inside a live `plan-groundwork` track is the one exception: `<dest>` is the track's own `evidence/<slug>/` instead, since a candidate render is evidence the track's decision file cites rather than spike input.
20
20
  3. Write one arm per candidate, each carrying an id, a label, and what the arm costs. An arm with no stated cost is not an option.
21
21
  4. Make the current state arm `0`, so the baseline is a candidate rather than an absence. A decision with nothing shipped yet says so and starts at arm `1`.
@@ -28,10 +28,12 @@ Write every arm side by side on one self-contained HTML page at `<dest>/candidat
28
28
  - One page for the pick, never a set of separate images handed to the operator to compare from memory. The comparison they judge is `candidates.html`, which Step 3 renders and Step 4 asks about. The per-arm files exist only for Step 6's archival capture, once the pick is made, and only the last pass through this step is what Step 6 finds there.
29
29
  - Wrap each arm's markup in the same class on both files, chosen once per run and reused everywhere, so one selector addresses an arm on the combined page and on its own standalone file alike.
30
30
  - Label each arm on the page with its id and its cost, so the render carries what the question will ask about.
31
+ - Give the combined page one control that sets every arm's theme at once, beside whatever per-arm control the arms carry. A set spanning both themes cannot be compared, since the operator has to toggle each arm and hold the earlier ones in memory, which is the failure the single-page rule exists to prevent.
31
32
  - Take the live-app branch instead when the surface under decision is a running app: lift the rendered markup and link a copy of the built stylesheet rather than inlining, per `${CLAUDE_SKILL_DIR}/references/live-arms.md`.
32
33
  - On the default path, inline every style, script, and asset the page needs. The render reads the file off disk, so a page reaching for a build step or a network font renders without it and the arms differ by something nobody chose.
33
34
  - On the default path, declare a font stack the machine resolves, such as `system-ui` behind a generic fallback. The render refuses a page that would rewrap against a substitute rather than shipping a false comparison, so a page naming no font at all is refused on whatever the default resolves to.
34
35
  - Vary one property across the arms. A page whose arms differ in three ways answers no question, since the pick cannot say which difference decided it.
36
+ - Vary the property the decision is about, which the rule above is satisfiable without. Holding composition fixed and varying color obeys it exactly and produces five skins of one design, because a set differing in the layer a reader notices least answers nothing. A palette is chosen to serve a composition, so it cannot be picked ahead of one.
35
37
 
36
38
  ## Step 3: render and hand off
37
39
 
@@ -44,8 +46,10 @@ canon capture <dest>/candidates.html --selector <element>
44
46
  - `--selector` has no default and the command refuses without it. Name the element wrapping the arms rather than `body`, which crops to whatever the page's own margins leave.
45
47
  - `canon capture` and `canon drive` both need a browser binary the toolkit does not install. When either refuses for that reason, report the refusal and name `bunx playwright install chromium` as the repair, then stop rather than describing an arm nobody has seen.
46
48
  - Serve the page instead of capturing it when the operator has to drive the decision, such as a hover response, a scroll-linked position, or a pace. Start `canon serve <dest> --entry candidates.html` in the background and read the link off its record, since the printed link opens `index.html` without that flag and `<dest>` holds no such file. A still answers how a thing looks and answers none of those.
49
+ - Write the render inside the record that cites it wherever one exists, by pointing `--out` at `<dest>/renders/` rather than at a session scratch path. A pick taken from an image the record does not hold is a judgment nobody but this session can check, and the archival capture in Step 6 covers the final round alone.
47
50
  - Hand over the address rather than a description. Emit the PNG path on its own line, and the link beside it where the page is served.
48
51
  - Never report a visual result you have not looked at. A claim about appearance with no render behind it is a guess.
52
+ - Look to judge rather than to confirm. Reading the image back to check it rendered satisfies the rule above and still hands over weak work, so name the weakest thing on the page in a sentence. Where that sentence would embarrass the work, fix it and hand over the second version. Say the remaining weakness out loud either way, so the operator is not hunting for what you already know.
49
53
 
50
54
  ## Step 4: take the pick
51
55
 
@@ -58,11 +62,11 @@ Put the choice to the operator through the structured question surface, since a
58
62
 
59
63
  ## Step 5: loop on the pick
60
64
 
61
- 1. Narrow the page to the picked arm, plus whatever the operator asked to change about it.
65
+ 1. Write each iteration to its own `<dest>` rather than narrowing the previous one in place, suffixing the slug so the folders sort. An iteration overwritten is one a later pass cannot open, and the losing round is what stops a correction re-proposing something already rejected.
62
66
  2. Write fresh arms off the pick and return to Step 2 where the correction opens a new question. Revise the one arm where it does not.
63
67
  3. Re-render, hand off again, and take the next answer.
64
68
  4. Repeat until the operator says it is right. The loop stops on their word and on nothing else, so a run stopping because the arms stopped differing has stopped early.
65
- 5. Hold the real surface untouched across every iteration. Nothing outside `<dest>` changes until the pick is final.
69
+ 5. Hold the real surface untouched across every iteration. Nothing outside the run's own folders changes until the pick is final.
66
70
 
67
71
  ## Step 6: close
68
72
 
@@ -31,7 +31,7 @@ A missing tracking ref is no longer a guard. An open pull request proves the bra
31
31
 
32
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
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.
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
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
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
37
  7. Sync the body and title on every invocation, before the routing below decides on the reply.
@@ -148,18 +148,18 @@ if [ -n "$pr_labels" ]; then
148
148
  gh pr edit "$pr_number" --add-label "$pr_labels" >/dev/null ||
149
149
  printf 'Label apply failed. Create a missing label with: gh label create <name>\n' >&2
150
150
  fi
151
- rm -rf .canon/tmp/pr
151
+ rm -rf .canon/tmp/pr/body
152
152
  printf 'number=%s\nurl=%s\n' "$pr_number" "$pr_url"
153
153
  ```
154
154
 
155
155
  ### Post the UI checklist
156
156
 
157
- `ui-test` writes a manual checklist to `.canon/tmp/ui-checklist/<slug>.md` at the main worktree root when a change needs visual verification, with `<slug>` derived per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. This step is the file's sole consumer. Resolve the main root the way `session-worktree` does (`git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`, falling back to `pwd`) and check for the file there. A missing file means no checklist was produced, and there is nothing to post.
157
+ `ui-test` writes a manual checklist to `.canon/tmp/handoff/ui-checklist/<slug>.md` at the main worktree root when a change needs visual verification, with `<slug>` derived per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. This step is the file's sole consumer. Resolve the main root the way `session-worktree` does (`git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`, falling back to `pwd`) and check for the file there. A missing file means no checklist was produced, and there is nothing to post.
158
158
 
159
159
  When it exists, scan it against `${CLAUDE_SKILL_DIR}/../../standards/publish.md` before posting, the same as the pull request body above. Post it as its own comment on `<number>`, the number the final command above resolved, rather than folding it into the body, since a later push editing the body would overwrite checkboxes a reviewer already ticked:
160
160
 
161
161
  ```bash
162
- gh pr comment <number> --body-file <main-root>/.canon/tmp/ui-checklist/<slug>.md
162
+ gh pr comment <number> --body-file <main-root>/.canon/tmp/handoff/ui-checklist/<slug>.md
163
163
  ```
164
164
 
165
165
  Run the cleanup below only once that call reports success. On a failure, stop and leave the file in place: a retry needs the checklist to still be there, and deleting it on a failed post loses the only copy with nothing landed on the pull request.
@@ -167,11 +167,11 @@ Run the cleanup below only once that call reports success. On a failure, stop an
167
167
  From a linked worktree the file-editing tools refuse a main-root path, so the cleanup goes out through `Bash` as two plain commands, the file and then the folder, rather than joined by `&&`, which is refused as compound:
168
168
 
169
169
  ```bash
170
- rm <main-root>/.canon/tmp/ui-checklist/<slug>.md
170
+ rm <main-root>/.canon/tmp/handoff/ui-checklist/<slug>.md
171
171
  ```
172
172
 
173
173
  ```bash
174
- rmdir <main-root>/.canon/tmp/ui-checklist 2>/dev/null || true
174
+ rmdir <main-root>/.canon/tmp/handoff/ui-checklist 2>/dev/null || true
175
175
  ```
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.
@@ -181,16 +181,16 @@ The `rmdir` is a no-op when another branch's pending checklist still sits in the
181
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
182
 
183
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:
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
185
 
186
186
  ```bash
187
- gh pr comment <number> --body-file <main-root>/.canon/tmp/pr-evidence/body-<number>.md
187
+ gh pr comment <number> --body-file <main-root>/.canon/tmp/pr/evidence/body-<number>.md
188
188
  ```
189
189
 
190
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
191
 
192
192
  ```bash
193
- gh api -X PATCH repos/{owner}/{repo}/issues/comments/<commentId> -f body=@<main-root>/.canon/tmp/pr-evidence/body-<number>.md
193
+ gh api -X PATCH repos/{owner}/{repo}/issues/comments/<commentId> -f body=@<main-root>/.canon/tmp/pr/evidence/body-<number>.md
194
194
  ```
195
195
 
196
196
  Clean up the tmp file the way the UI-checklist step does, only after the call reports success.
@@ -80,25 +80,25 @@ For independent mode, base every branch on `main`:
80
80
  git branch -m <current_branch> <new_name>
81
81
 
82
82
  # Create, cherry-pick, push, and open PR for each secondary branch
83
- mkdir -p .canon/tmp/pr-split
83
+ mkdir -p .canon/tmp/pr/split
84
84
  git switch -c <branch> main && git cherry-pick <sha> <sha> \
85
85
  && git push -u origin <branch> \
86
- && (cat <<'BODY' > .canon/tmp/pr-split/<branch>.md
86
+ && (cat <<'BODY' > .canon/tmp/pr/split/<branch>.md
87
87
  <body following pr.md template, written from the cherry-picked commits>
88
88
  BODY
89
- ) && gh pr create --title "<title>" --body-file .canon/tmp/pr-split/<branch>.md \
90
- && rm .canon/tmp/pr-split/<branch>.md
89
+ ) && gh pr create --title "<title>" --body-file .canon/tmp/pr/split/<branch>.md \
90
+ && rm .canon/tmp/pr/split/<branch>.md
91
91
 
92
92
  # Return to primary branch, push, and open its PR
93
93
  git checkout <new_name> && git push -u origin <new_name> \
94
- && (cat <<'BODY' > .canon/tmp/pr-split/<new_name>.md
94
+ && (cat <<'BODY' > .canon/tmp/pr/split/<new_name>.md
95
95
  <body following pr.md template, written from the primary's commits>
96
96
  BODY
97
- ) && gh pr create --title "<title>" --body-file .canon/tmp/pr-split/<new_name>.md \
98
- && rm .canon/tmp/pr-split/<new_name>.md
97
+ ) && gh pr create --title "<title>" --body-file .canon/tmp/pr/split/<new_name>.md \
98
+ && rm .canon/tmp/pr/split/<new_name>.md
99
99
 
100
100
  # Clean up the body-file dir if all PRs succeeded (no-op when non-empty)
101
- rmdir .canon/tmp/pr-split 2>/dev/null || true
101
+ rmdir .canon/tmp/pr/split 2>/dev/null || true
102
102
  ```
103
103
 
104
104
  For stacked mode, base each branch on the previous and cherry-pick only that group's commits:
@@ -107,36 +107,36 @@ For stacked mode, base each branch on the previous and cherry-pick only that gro
107
107
  # Rename current branch to reflect primary concern
108
108
  git branch -m <current_branch> <new_name>
109
109
 
110
- mkdir -p .canon/tmp/pr-split
110
+ mkdir -p .canon/tmp/pr/split
111
111
 
112
112
  # Group 1: based on main
113
113
  git switch -c <branch-1> main && git cherry-pick <g1-sha> <g1-sha> \
114
114
  && git push -u origin <branch-1> \
115
- && (cat <<'BODY' > .canon/tmp/pr-split/<branch-1>.md
115
+ && (cat <<'BODY' > .canon/tmp/pr/split/<branch-1>.md
116
116
  <body following pr.md template, written from the cherry-picked commits>
117
117
  BODY
118
- ) && gh pr create --title "<title>" --body-file .canon/tmp/pr-split/<branch-1>.md \
119
- && rm .canon/tmp/pr-split/<branch-1>.md
118
+ ) && gh pr create --title "<title>" --body-file .canon/tmp/pr/split/<branch-1>.md \
119
+ && rm .canon/tmp/pr/split/<branch-1>.md
120
120
 
121
121
  # Group 2: based on <branch-1>, this group's commits only
122
122
  git checkout -b <branch-2> && git cherry-pick <g2-sha> <g2-sha> \
123
123
  && git push -u origin <branch-2> \
124
- && (cat <<'BODY' > .canon/tmp/pr-split/<branch-2>.md
124
+ && (cat <<'BODY' > .canon/tmp/pr/split/<branch-2>.md
125
125
  <body following pr.md template, written from the cherry-picked commits>
126
126
  BODY
127
- ) && gh pr create --title "<title>" --body-file .canon/tmp/pr-split/<branch-2>.md \
128
- && rm .canon/tmp/pr-split/<branch-2>.md
127
+ ) && gh pr create --title "<title>" --body-file .canon/tmp/pr/split/<branch-2>.md \
128
+ && rm .canon/tmp/pr/split/<branch-2>.md
129
129
 
130
130
  # Return to primary branch, push, and open its PR
131
131
  git checkout <new_name> && git push -u origin <new_name> \
132
- && (cat <<'BODY' > .canon/tmp/pr-split/<new_name>.md
132
+ && (cat <<'BODY' > .canon/tmp/pr/split/<new_name>.md
133
133
  <body following pr.md template, written from the primary's commits>
134
134
  BODY
135
- ) && gh pr create --title "<title>" --body-file .canon/tmp/pr-split/<new_name>.md \
136
- && rm .canon/tmp/pr-split/<new_name>.md
135
+ ) && gh pr create --title "<title>" --body-file .canon/tmp/pr/split/<new_name>.md \
136
+ && rm .canon/tmp/pr/split/<new_name>.md
137
137
 
138
138
  # Clean up the body-file dir if all PRs succeeded (no-op when non-empty)
139
- rmdir .canon/tmp/pr-split 2>/dev/null || true
139
+ rmdir .canon/tmp/pr/split 2>/dev/null || true
140
140
  ```
141
141
 
142
142
  ## After execution
@@ -44,7 +44,7 @@ For each project candidate, match its subject against `canon/context/index.md`.
44
44
 
45
45
  Fail closed. A project candidate matching no entry stays a memory file, and so does one matching two entries where neither is clearly the owner. The residue is what the folder is for, and a fact filed under the wrong entry is worse than one in memory because a context entry is a surface sessions trust.
46
46
 
47
- Do not edit a context entry here. `docs-fold` owns those edits and folds the routed facts in on its own pass, or two skills write one file at the same step. Write each routed fact to `.canon/tmp/memory-routing/<slug>.md` at the main worktree root instead, appending when the file exists. Name the heading with the entry's own path from `canon/context/index.md`, flat or the nested `index.md`, since that heading is what tells `docs-fold`'s routed-facts fold which file to open. An append is a whole-file operation the shell does directly, so send it as a plain single `Bash` command carrying a heredoc:
47
+ Do not edit a context entry here. `docs-fold` owns those edits and folds the routed facts in on its own pass, or two skills write one file at the same step. Write each routed fact to `.canon/tmp/handoff/memory-routing/<slug>.md` at the main worktree root instead, appending when the file exists. Name the heading with the entry's own path from `canon/context/index.md`, flat or the nested `index.md`, since that heading is what tells `docs-fold`'s routed-facts fold which file to open. An append is a whole-file operation the shell does directly, so send it as a plain single `Bash` command carrying a heredoc:
48
48
 
49
49
  A flat domain takes:
50
50
 
@@ -96,7 +96,7 @@ Respond with one line per fact routed, written, or updated:
96
96
 
97
97
  When anything routed, add a line naming the handoff so the caller knows a `docs-fold` pass is owed:
98
98
 
99
- `→ Routed facts wait at .canon/tmp/memory-routing/<slug>.md. Run /docs-fold to fold them in.`
99
+ `→ Routed facts wait at .canon/tmp/handoff/memory-routing/<slug>.md. Run /docs-fold to fold them in.`
100
100
 
101
101
  Omit that line when the caller runs `docs-fold` itself later in its own chain.
102
102
 
@@ -68,7 +68,7 @@ For each in-scope entry (see Scope), pick one action:
68
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.
69
69
  - **Promote to a skill body**: the rule fires only when editing a specific path-scoped domain. Name the target skill.
70
70
  - **Promote to a standards file**: the rule is an authoring reference that belongs in the project's own standards folder as `<domain>.md`.
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.
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/handoff/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
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).
73
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/`.
74
74
  - In a target project, point the user at the `create-rule` skill, which scaffolds a project-local rule under `.claude/rules/`.
@@ -153,7 +153,7 @@ Free-form text after the verb is a reason. Capture it in the receipt but do not
153
153
  Action by action type:
154
154
 
155
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.
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.
156
+ - **Promote to a context entry**: append the fact to `.canon/tmp/handoff/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.
157
157
  - **Hand off**: do not edit governance. Archive the memory file only if the user confirmed the handoff explicitly. Otherwise leave it in place.
158
158
  - **Retire**: archive the memory file.
159
159
 
@@ -21,7 +21,7 @@ Read `${CLAUDE_SKILL_DIR}/../../standards/groundwork.md` before writing any file
21
21
 
22
22
  - Write only inside `.canon/groundwork/<nn>-<slug>/`. A feature plan, source changes, a standard, a rule, and a reference doc all live outside that folder, so this one rule forbids every one of them.
23
23
  - One exception, at close only: write one task file recording what the track concluded.
24
- - A second exception, for what a spike reads: write an input under `.canon/tmp/groundwork-fixtures/<slug>/`. Keep it out of `.canon/groundwork/` so mode detection never matches a fixture as a track. A fixture a headless run is pointed at goes outside the repository instead, per the rule in `## Running a spike`.
24
+ - A second exception, for what a spike reads: write an input under `.canon/tmp/runs/groundwork-fixtures/<slug>/`. Keep it out of `.canon/groundwork/` so mode detection never matches a fixture as a track. A fixture a headless run is pointed at goes outside the repository instead, per the rule in `## Running a spike`.
25
25
  - What a spike produces stays inside the track rather than joining the two exceptions above: write evidence a spike file cites under `.canon/groundwork/<nn>-<slug>/evidence/`, which the first rule already permits. Mode detection matches entries at the top level of the tracks directory, so the sibling the fixtures rule guards against cannot be a folder nested inside a track, and the scratch path holds only what can be deleted without loss, which a recording a finding rests on is not.
26
26
  - A `draft-and-pick` run invoked from inside a live track follows its own branch for this, writing to `evidence/` here rather than `.canon/tmp/<slug>/`.
27
27
  - Reading is not restricted. External research is in scope, so read documentation, comparable projects, and papers whenever a live question needs them.
@@ -99,7 +99,7 @@ moved, which the earlier test could not see. Rebase again under
99
99
  continue. The second push costs one extra force-push in a
100
100
  case that needs the fix and the sibling to touch the same lines.
101
101
 
102
- Then write a summary reply to `.canon/tmp/address-review/reply-<number>.md`
102
+ Then write a summary reply to `.canon/tmp/pr/reply/reply-<number>.md`
103
103
  mapping each finding to what changed, or to a one-line reason when it is a
104
104
  conscious-accept rather than a defect. Key the filename on the PR number so two
105
105
  sessions addressing different pull requests never overwrite each other between
@@ -154,7 +154,7 @@ Before posting, follow `${CLAUDE_SKILL_DIR}/../../standards/publish.md`
154
154
  and run its scan against the reply:
155
155
 
156
156
  ```bash
157
- canon labels scan --body-file .canon/tmp/address-review/reply-<number>.md
157
+ canon labels scan --body-file .canon/tmp/pr/reply/reply-<number>.md
158
158
  ```
159
159
 
160
160
  The hook skips `.canon/tmp/`, so this scan is the only gate on the published
@@ -163,8 +163,8 @@ capture the posted comment's id, since Step 7 edits this exact comment rather
163
163
  than trusting whichever one `gh` considers last:
164
164
 
165
165
  ```bash
166
- comment_url=$(gh pr comment <number> --body-file .canon/tmp/address-review/reply-<number>.md)
167
- echo "${comment_url##*issuecomment-}" > .canon/tmp/address-review/reply-<number>.id
166
+ comment_url=$(gh pr comment <number> --body-file .canon/tmp/pr/reply/reply-<number>.md)
167
+ echo "${comment_url##*issuecomment-}" > .canon/tmp/pr/reply/reply-<number>.id
168
168
  ```
169
169
 
170
170
  ## Step 7: confirm resolution
@@ -176,7 +176,7 @@ the closing confirmation to the reply file Step 6 already posted, so the thread
176
176
  carries one terminal state rather than a second comment under no heading:
177
177
 
178
178
  ```bash
179
- printf '\n✅ All review findings addressed, CI green.\n' >> .canon/tmp/address-review/reply-<number>.md
179
+ printf '\n✅ All review findings addressed, CI green.\n' >> .canon/tmp/pr/reply/reply-<number>.md
180
180
  ```
181
181
 
182
182
  A rebase-only run addressed no finding, so it appends its own confirmation
@@ -184,14 +184,14 @@ instead of that one. Claiming findings were addressed on a pull request that
184
184
  carries none is false on a surface nothing else checks:
185
185
 
186
186
  ```bash
187
- printf '\n✅ Rebased onto origin/main, CI green. No review findings were open.\n' >> .canon/tmp/address-review/reply-<number>.md
187
+ printf '\n✅ Rebased onto origin/main, CI green. No review findings were open.\n' >> .canon/tmp/pr/reply/reply-<number>.md
188
188
  ```
189
189
 
190
190
  Re-run the scan against the updated file, since the appended line is new
191
191
  content the Step 6 scan never saw:
192
192
 
193
193
  ```bash
194
- canon labels scan --body-file .canon/tmp/address-review/reply-<number>.md
194
+ canon labels scan --body-file .canon/tmp/pr/reply/reply-<number>.md
195
195
  ```
196
196
 
197
197
  Then edit the exact comment Step 6 posted, read back from the id it saved,
@@ -201,12 +201,12 @@ so read the id as its own plain command and write the printed value as a
201
201
  literal in the `gh api` call rather than a substitution or a variable:
202
202
 
203
203
  ```bash
204
- cat .canon/tmp/address-review/reply-<number>.id
204
+ cat .canon/tmp/pr/reply/reply-<number>.id
205
205
  ```
206
206
 
207
207
  ```bash
208
208
  gh api -X PATCH "repos/{owner}/{repo}/issues/comments/<id printed above>" \
209
- -F body=@.canon/tmp/address-review/reply-<number>.md
209
+ -F body=@.canon/tmp/pr/reply/reply-<number>.md
210
210
  ```
211
211
 
212
212
  `--edit-last` was the first shape and it targets the wrong object here.
@@ -243,10 +243,10 @@ Do not merge. Hand back to the orchestrator for re-review.
243
243
 
244
244
  ## Post-review findings
245
245
 
246
- Not everything worth reaching the reviewing session surfaces inside the numbered flow above. A worker that settled a risk, filed a follow-up, or found something else worth reporting after Step 7 already closed the review posts it directly rather than waiting on a review pass that has nothing left to trigger it. Write the body the way Step 6 writes a reply: load `write-human` for voice, follow `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` for the banned words, and run the `${CLAUDE_SKILL_DIR}/../../standards/publish.md` scan before posting with `canon labels scan --body-file .canon/tmp/address-review/reply-<number>.md`.
246
+ Not everything worth reaching the reviewing session surfaces inside the numbered flow above. A worker that settled a risk, filed a follow-up, or found something else worth reporting after Step 7 already closed the review posts it directly rather than waiting on a review pass that has nothing left to trigger it. Write the body the way Step 6 writes a reply: load `write-human` for voice, follow `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` for the banned words, and run the `${CLAUDE_SKILL_DIR}/../../standards/publish.md` scan before posting with `canon labels scan --body-file .canon/tmp/pr/reply/reply-<number>.md`.
247
247
 
248
248
  Open with `## Post-review findings` rather than `## Review response`, since nothing on the thread is being answered. `review-pr` states the full heading set this belongs to and routes it the same as a response: `role-orchestrator`'s poll picks it up and sends the reviewing session back for a pass. Close the body with `🤖 Addressed by Claude Code` on its own line, matching the reply's footer.
249
249
 
250
250
  ```bash
251
- gh pr comment <number> --body-file .canon/tmp/address-review/reply-<number>.md
251
+ gh pr comment <number> --body-file .canon/tmp/pr/reply/reply-<number>.md
252
252
  ```
@@ -165,7 +165,7 @@ Use severity: `critical` (blocks merge), `should-fix` (fix before merge), `minor
165
165
 
166
166
  ## Step 4: post to the PR
167
167
 
168
- Write the comment to `.canon/tmp/pr-review/body-<number>-<short-sha>.md` at the main worktree root, not the current worktree, which the rest of this step calls `<body-file>`. Resolve that root the way `session-worktree` does, and send the write as a plain single `Bash` command carrying a heredoc from a linked worktree, since `Edit` and `Write` refuse a main-root path there. The PR number stops two sessions reviewing different pull requests from overwriting each other between the write and the post, and the head commit stops a second pass overwriting the first one's body, leaving the folder a record of which commit each review covered.
168
+ Write the comment to `.canon/tmp/pr/review/body-<number>-<short-sha>.md` at the main worktree root, not the current worktree, which the rest of this step calls `<body-file>`. Resolve that root the way `session-worktree` does, and send the write as a plain single `Bash` command carrying a heredoc from a linked worktree, since `Edit` and `Write` refuse a main-root path there. The PR number stops two sessions reviewing different pull requests from overwriting each other between the write and the post, and the head commit stops a second pass overwriting the first one's body.
169
169
 
170
170
  Derive both segments from Step 1. Never pick a suffix by hand, and never reuse a name the folder already holds.
171
171
 
@@ -279,7 +279,7 @@ Before posting, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md`
279
279
  Do not run the command below when `<prior-heading>` from Step 2 reads `## Review closed` and this pass carries nothing owed. That pass replaces the standing comment rather than adding one, under `### A close-out that repeats the standing one` at the end of this step. Posting first and reaching that section afterward leaves two close-outs both naming the new head, which is worse than the pair the guard exists against.
280
280
 
281
281
  ```bash
282
- gh pr review <number> --comment --body-file .canon/tmp/pr-review/body-<number>-<short-sha>.md
282
+ gh pr review <number> --comment --body-file .canon/tmp/pr/review/body-<number>-<short-sha>.md
283
283
  ```
284
284
 
285
285
  A pass carrying nothing at all takes `## Review closed` and a short body, with the footer line included either way. On a first pass, post `✅ No findings. Reviewed against project docs and the board.` On a later pass, post `✅ Prior findings addressed. Re-reviewed <short-sha>, N commits since the prior pass.`
@@ -54,7 +54,7 @@ Every classification line names a pull request and a state. A line starting `pol
54
54
 
55
55
  The script exits non-zero and classifies nothing when the open pull request list itself fails to load. That case would otherwise report every tracked pull request as merged, so the baseline is left untouched and the run says so.
56
56
 
57
- The baseline lives at `.canon/tmp/pr-poll/baseline.txt` under the main worktree root and is per-machine. A first run against a board already in flight reports each open pull request once before it settles.
57
+ The baseline lives at `.canon/tmp/pr/poll/baseline.txt` under the main worktree root and is per-machine. A first run against a board already in flight reports each open pull request once before it settles.
58
58
 
59
59
  The five review headings the script matches are written by `review-pr` and `review-address`, and the whole set is stated once in the first. The reply family is matched by jq filters in the script, so a project posting its replies under different headings edits those to match. The review family is matched inside `canon pr review-state` instead, which the script and `review-pr` both read through, so a project renaming either review heading changes the verb rather than the script. Either way, a heading nothing matches reads as a pull request nobody has reviewed.
60
60
 
@@ -18,7 +18,7 @@ if [ -z "$MAIN_ROOT" ]; then
18
18
  echo "poll: not a git repository, so nothing is classified" >&2
19
19
  exit 1
20
20
  fi
21
- STATE_DIR="$MAIN_ROOT/.canon/tmp/pr-poll"
21
+ STATE_DIR="$MAIN_ROOT/.canon/tmp/pr/poll"
22
22
  mkdir -p "$STATE_DIR"
23
23
  STATE="$STATE_DIR/baseline.txt"
24
24
  touch "$STATE"
@@ -217,7 +217,7 @@ Still owed: <what the destination expects that the page does not carry yet>
217
217
 
218
218
  Then stop and let the operator strike, redirect, or confirm each block.
219
219
 
220
- Write nothing to a destination here. One skill owns the durable writes, and two skills editing one file at one step is the failure that rule exists against. Record each confirmed block in `.canon/tmp/teach-promotion/<slug>.md` at the main worktree root instead, appending when the file exists, with one H2 per destination naming its path, the source page beneath it, and the page body fenced:
220
+ Write nothing to a destination here. One skill owns the durable writes, and two skills editing one file at one step is the failure that rule exists against. Record each confirmed block in `.canon/tmp/handoff/teach-promotion/<slug>.md` at the main worktree root instead, appending when the file exists, with one H2 per destination naming its path, the source page beneath it, and the page body fenced:
221
221
 
222
222
  ````markdown
223
223
  ## <destination path>
@@ -258,7 +258,7 @@ A promotion pass reports its own shape instead, one line per page the operator c
258
258
 
259
259
  ```plaintext
260
260
  ➡️ Promoting: .canon/teach/<nn>-<topic>/reference/<slug>.md → <destination path>
261
- → Confirmed pages wait at .canon/tmp/teach-promotion/<slug>.md. Run /docs-fold from a branch to fold them in.
261
+ → Confirmed pages wait at .canon/tmp/handoff/teach-promotion/<slug>.md. Run /docs-fold from a branch to fold them in.
262
262
  ```
263
263
 
264
264
  A pass where the operator confirmed nothing writes no handoff file and reports that alone.
@@ -19,7 +19,7 @@ Writing the checklist correctly to disk does not close the gap either. The file
19
19
  - Read the project's existing test config and patterns before writing a test against them
20
20
  - Assert a user action and its outcome per test, covering the happy path and the key edge cases
21
21
  - Run the tests after writing them and fix what fails
22
- - Write a produced checklist to the `.canon/tmp/ui-checklist/<slug>.md` handoff at the main worktree root, overwriting
22
+ - Write a produced checklist to the `.canon/tmp/handoff/ui-checklist/<slug>.md` handoff at the main worktree root, overwriting
23
23
  - Report that everything is covered rather than manufacturing a checklist to show work
24
24
 
25
25
  ## Must not
@@ -59,7 +59,7 @@ If all changes are automatable, skip the manual checklist:
59
59
 
60
60
  Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. Fall back to `latest` on an empty result.
61
61
 
62
- When a manual checklist is produced, write it directly to `.canon/tmp/ui-checklist/<slug>.md` at the main worktree root, not the current worktree. Resolve that root the way `session-worktree` does. Create the directory if it does not exist. Always overwrite. This is a handoff file rather than a deliverable: `git-pr` posts it as a pull request comment once one opens, then removes it, and nothing here talks to `gh` directly.
62
+ When a manual checklist is produced, write it directly to `.canon/tmp/handoff/ui-checklist/<slug>.md` at the main worktree root, not the current worktree. Resolve that root the way `session-worktree` does. Create the directory if it does not exist. Always overwrite. This is a handoff file rather than a deliverable: `git-pr` posts it as a pull request comment once one opens, then removes it, and nothing here talks to `gh` directly.
63
63
 
64
64
  From a linked worktree the file-editing tools refuse that path, so the checklist goes out through `Bash`. Send the `mkdir -p` and the heredoc as two plain commands rather than joining them with `&&`, which is refused as compound.
65
65
 
@@ -71,7 +71,7 @@ The `.canon/tmp/` directory is gitignored. Do not stage or commit the file.
71
71
 
72
72
  1. Write and run e2e tests (report pass/fail)
73
73
  2. If a manual checklist was produced, write it to file, then output only the file path in chat:
74
- `📝 Wrote .canon/tmp/ui-checklist/<slug>.md`
74
+ `📝 Wrote .canon/tmp/handoff/ui-checklist/<slug>.md`
75
75
  3. If no checklist was needed: `✅ All changes covered by e2e tests. No manual verification needed.`
76
76
 
77
77
  Do not repeat the full checklist in chat.
@@ -53,6 +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 prune-tmp` | Report scratch nobody has touched inside an age window and delete it (`--write`, `--older-than`, `--json`) |
56
57
  | `canon records push` | Commit every record folder less the exclusion set and push it to a private records remote (`--json`) |
57
58
  | `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
58
59
  | `canon migrate rename` | Rewrite every unprotected `aitk` token to `canon` and move the paths that carry the name, reporting the plan without `--write` (`--scope`, `--json`) |
@@ -190,7 +191,7 @@ A destination that already holds the same bytes marks the flat file a duplicate,
190
191
 
191
192
  `migrate record-layout` folds two record folders under the memory pen: review receipts move from `.canon/review/memory/` to `.canon/memory/review/`, and retired entries move from `.canon/tmp/memory-archive/` to `.canon/memory/archive/`, backed for the first time since `memory/` is a folder `canon records push` carries and `tmp/` is not. Each move is refused independently when its destination already exists, and the citation repoint sweeps every folder `canon records push` carries, archives included, since an archived receipt still cites the row it retired. The same `canon-keep-record-root` marker and second-run idempotence check apply. A receipt sitting at the flat `review/` root, from before `.canon/review/memory/` existed, matches neither mapped move and is reported by name rather than moved.
192
193
 
193
- The same verb leaves `.canon/review/` holding only reviews. Feedback moves to `.canon/feedback/`, and the design, board, slides, and diagram renders move to `.canon/tmp/render/<kind>/`, off the records remote since the commands regenerate them. Reference images move to `.canon/picks/references/`. A branch report flattens from `review/branch/review-<slug>.md` to `review/branch-<slug>.md`, and a flat `review/ui-checklist-<slug>.md` moves into `.canon/tmp/ui-checklist/`. Each folder under `review/evidence/` is decided from what it holds: one directly carrying an `arm-<id>` capture or a `design-handoff.md` is a pick and moves to `.canon/picks/<slug>/`, and any other is evidence and moves to `.canon/evidence/<nn>-<slug>/`. Evidence folders are numbered by the oldest file each holds, continuing past any ordinal `evidence/` already carries. The dry run labels each derived destination `(pick)` or `(evidence)`, so a misfiled folder shows before `--write`, and one filed wrong moves back by hand. A bare mention of an emptied folder, such as `review/evidence/` with no slug or `review/branch/` with no report name, matches no row and stays as written.
194
+ The same verb leaves `.canon/review/` holding only reviews. Feedback moves to `.canon/feedback/`, and the design, board, slides, and diagram renders move to `.canon/tmp/render/<kind>/`, off the records remote since the commands regenerate them. Reference images move to `.canon/picks/references/`. A branch report flattens from `review/branch/review-<slug>.md` to `review/branch-<slug>.md`, and a flat `review/ui-checklist-<slug>.md` moves into `.canon/tmp/handoff/ui-checklist/`. Each folder under `review/evidence/` is decided from what it holds: one directly carrying an `arm-<id>` capture or a `design-handoff.md` is a pick and moves to `.canon/picks/<slug>/`, and any other is evidence and moves to `.canon/evidence/<nn>-<slug>/`. Evidence folders are numbered by the oldest file each holds, continuing past any ordinal `evidence/` already carries. The dry run labels each derived destination `(pick)` or `(evidence)`, so a misfiled folder shows before `--write`, and one filed wrong moves back by hand. A bare mention of an emptied folder, such as `review/evidence/` with no slug or `review/branch/` with no report name, matches no row and stays as written.
194
195
 
195
196
  ## Version skew
196
197
 
@@ -152,6 +152,33 @@ The window counts read `mtime`, so what they report is a file written inside the
152
152
 
153
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
+ ## Prune
156
+
157
+ `canon records prune-tmp` reports scratch nobody has touched inside an age window, and deletes it only with `--write`. Nothing removed an abandoned scratch folder before this, so a spike folder from months ago sat beside the ones a session still needs.
158
+
159
+ ```bash
160
+ canon records prune-tmp
161
+ canon records prune-tmp --write
162
+ canon records prune-tmp --older-than 30 --json
163
+ ```
164
+
165
+ | Option | Behavior |
166
+ | --------------------- | -------------------------------------------------------------- |
167
+ | `--json` | Add a machine-readable record on stdout |
168
+ | `--write` | Delete every candidate the report lists |
169
+ | `--older-than <days>` | Age a unit's newest file must clear to be offered (default 14) |
170
+ | `--root <path>` | Project root, defaulting to the main worktree |
171
+
172
+ A candidate is a unit whose newest file is older than the threshold: a `tmp/<slug>/` folder, a folder one level inside `runs/`, `render/`, or `pr/`, or a single marker file inside `hooks/<hook>/`. `pr/review/` groups its body files by pull request number rather than reporting as one folder, since a review pass leaves one file per pass and a folder holding thousands of them would otherwise report as a single row nobody can prune apart. A unit holding no files, empty subfolders included, is offered whatever its age.
173
+
174
+ `tmp/handoff/` and `tmp/pr/poll/` are never offered. A reader deletes a handoff themselves once it has been read, and a poll baseline is live state rather than scratch. The pre-split names the reserved split replaced, `memory-routing/`, `teach-promotion/`, `ui-checklist/`, and `pr-poll/` at the scratch root, are skipped the same way on a project the rename never reached, naming the folder they moved to, rather than being offered as ordinary slugs. An unread handoff is the one thing a wrong delete here loses for good.
175
+
176
+ A scratch-root name `canon migrate record-layout` or `canon migrate scratch-evidence` moves out of scratch for good, such as `memory-archive`, is skipped the same way on a project that has not yet run that migration, naming the verb to run. The retired-entry archive `memory-archive` holds is never deleted, per the memory standard, so it stays skipped rather than aging into an offer.
177
+
178
+ It writes nothing until `--write` is passed, matching `canon records migrate`: a session record has no history to undo a wrong delete from. It reads `mtime` the way `canon records size` does, so a machine restored by `canon records pull` reads its whole tree as new and fails safe by offering nothing.
179
+
180
+ Exit codes: `0` nothing to prune, or `--write` deleted every candidate. `1` refused, sharing `no-folder` with `size`, or a delete failed. `2` candidates exist and `--write` was not passed.
181
+
155
182
  ## Push and pull
156
183
 
157
184
  `canon records push` commits the backed record folders to a private remote and pushes them. `canon records pull` fetches the other direction and writes them back. Both take `--json` and `--root` the way `validate` does, and both exit `0` on agreement and `1` on a refusal.
@@ -69,7 +69,7 @@ A skill pairs to a scenario by filename, `<category>-<command>` first and bare `
69
69
 
70
70
  `exempt` means no arm should be written and holds only with a reason, declared in `scripts/sandbox/exempt.toml` and limited to a harness limit the checker cannot reach past or a skill that writes no artifact. An armed arm outranks an exemption. An exemption naming no shipped skill, or naming one an arm now asserts, exits 1 without `--strict`. Each armed arm reports as `<category>:<command>/<arm>`, so two same-named arms under different scenarios stay distinct.
71
71
 
72
- `scripts/sandbox/run.sh` calls this after a headless run and merges the verdict into the envelope it prints. It also writes that merged record to `.canon/tmp/sandbox-runs/<target>-<arm>-<timestamp>.json` with a `writes` array appended, and logs the path on stderr. Both fields are what a later re-score needs, since `--envelope` and `--writes` read files the run deletes on exit.
72
+ `scripts/sandbox/run.sh` calls this after a headless run and merges the verdict into the envelope it prints. It also writes that merged record to `.canon/tmp/runs/sandbox/<target>-<arm>-<timestamp>.json` with a `writes` array appended, and logs the path on stderr. Both fields are what a later re-score needs, since `--envelope` and `--writes` read files the run deletes on exit.
73
73
 
74
74
  Two more fields ride alongside the verdict rather than inside it. `escapes` lists what the run wrote under a watched toolkit root, which the verdict cannot assert over because those files sit outside the sandbox tree. `sessions` reports the nested-dispatch bound, carrying `watched` for whether the client's session registry was there to read, `new` for the records that appeared while the run was in flight, `concurrent` for the records present both before and after, and `reap` for what the run found in the session's process group afterwards. Neither field fails a run on its own.
75
75