@erclx/canon 4.87.0 → 4.88.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 (34) 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/git-followup/SKILL.md +1 -1
  5. package/claude/skills/git-pr/SKILL.md +8 -8
  6. package/claude/skills/git-split/SKILL.md +19 -19
  7. package/claude/skills/memory-capture/SKILL.md +2 -2
  8. package/claude/skills/memory-review/SKILL.md +2 -2
  9. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  10. package/claude/skills/review-address/SKILL.md +11 -11
  11. package/claude/skills/review-pr/SKILL.md +2 -2
  12. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -1
  13. package/claude/skills/role-orchestrator/scripts/poll.sh +1 -1
  14. package/claude/skills/teach-workspace/SKILL.md +2 -2
  15. package/claude/skills/ui-test/REQUIREMENT.md +1 -1
  16. package/claude/skills/ui-test/SKILL.md +2 -2
  17. package/docs/agents/commands.md +2 -1
  18. package/docs/agents/records.md +27 -0
  19. package/docs/agents/sandbox.md +1 -1
  20. package/docs/workflow/ai-workflow.md +4 -2
  21. package/governance/rules/core/055-scratch.md +1 -0
  22. package/package.json +1 -1
  23. package/scripts/tooling/verify.sh +2 -2
  24. package/src/claude/skills-headings.ts +1 -1
  25. package/src/commands/migrate.ts +1 -1
  26. package/src/commands/records.ts +159 -0
  27. package/src/migrate/record-layout.ts +2 -2
  28. package/src/migrate/scratch-evidence.ts +3 -5
  29. package/src/records/prune.ts +488 -0
  30. package/src/records/size.ts +24 -1
  31. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +2 -2
  32. package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +2 -2
  33. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +2 -2
  34. 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.0",
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
 
@@ -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
 
@@ -63,7 +63,7 @@ Work in Claude Code directly. It reads `CLAUDE.md` automatically and has full fi
63
63
 
64
64
  The routing test is whether the repository can answer an item today. A session grepping handles the yes, and a groundwork track handles the no.
65
65
 
66
- A groundwork track may run experiments to settle a question, writing a fixture it reads itself under `.canon/tmp/groundwork-fixtures/<slug>/` and spawning up to three billed headless runs before it asks. A fixture a headless run is pointed at sits outside the repository, since a session started under the project root inherits that project's `CLAUDE.md` and rules and would measure them instead of the arm.
66
+ A groundwork track may run experiments to settle a question, writing a fixture it reads itself under `.canon/tmp/runs/groundwork-fixtures/<slug>/` and spawning up to three billed headless runs before it asks. A fixture a headless run is pointed at sits outside the repository, since a session started under the project root inherits that project's `CLAUDE.md` and rules and would measure them instead of the arm.
67
67
 
68
68
  What a spike produces goes somewhere else again. An input the run reads is re-runnable and cited by nothing, so the scratch path above is the right lifetime for it, while a recording or a render the track cites as evidence for a finding is what a later reader opens to check the claim. Evidence therefore lives in `evidence/` inside the track beside the file citing it, since the scratch tree holds only what can be deleted without loss.
69
69
 
@@ -81,7 +81,7 @@ When features are independent, run them in parallel instead of sequentially. Use
81
81
 
82
82
  - Create a worktree per feature, then start a Claude Code session in each
83
83
  - Invoke `canon:plan-feature` in each session. Plans land at the main worktree root as `.canon/plans/feature-<slug>.md`, one per feature, no collisions. Small features stay in chat and skip the file.
84
- - Implement, verify, and review each feature independently. `review-branch` writes a per-branch report at the main worktree root (`review/branch-<slug>.md`), and `ui-test` writes a per-branch checklist handoff there too (`tmp/ui-checklist/<slug>.md`) that `git-pr` posts to the pull request and removes, so parallel sessions do not overwrite each other. The slug is the branch name with any leading type segment dropped, so `feat/jwt-expiration` and the plan at `feature-jwt-expiration.md` meet on one name
84
+ - Implement, verify, and review each feature independently. `review-branch` writes a per-branch report at the main worktree root (`review/branch-<slug>.md`), and `ui-test` writes a per-branch checklist handoff there too (`tmp/handoff/ui-checklist/<slug>.md`) that `git-pr` posts to the pull request and removes, so parallel sessions do not overwrite each other. The slug is the branch name with any leading type segment dropped, so `feat/jwt-expiration` and the plan at `feature-jwt-expiration.md` meet on one name
85
85
  - Ship each worktree separately with `canon:git-ship`
86
86
  - For full autonomy per worktree, invoke `canon:auto-ship` instead of the manual chain. Approve the plan, walk away, come back to a pull request the chain marked as a draft and then read the flag back on. The mark says the work has had no review yet, and it holds no window, since readying a pull request to merge lifts it directly, an act reserved to the operator or to the controlling session that closed the review.
87
87
 
@@ -111,6 +111,8 @@ A person points it at a private repository once and both verbs refuse until they
111
111
 
112
112
  `canon records size` reports what each of these folders holds, heaviest first, along with `.canon/tmp`. Each row carries the file count, the bytes, how many files were written in the last 7 and 30 days, and the dates of the least and most recently written one. Nothing fails on a number, because a record folder has no correct size. What the verb replaces is a reading somebody had to remember to take: the memory pen went from 44 entries to 236 between two counts made by hand a fortnight apart, and nothing reported the rate in between.
113
113
 
114
+ `canon records prune-tmp` reports scratch under `.canon/tmp` nobody has touched inside an age window, and deletes it only with `--write`. A `tmp/handoff/` folder and the live `tmp/pr/poll/` baseline are never offered, since a reader deletes a handoff once it is read and a poll baseline is state a session still needs. See [records](../agents/records.md) for the candidate shapes and the exit codes.
115
+
114
116
  A plan that ships is archived, never deleted. `canon tasks archive` moves it to `.canon/plans/archive/` alongside the task it belonged to and retargets that task's `Plan:` line at the new location, so a completed task still leads to the reasoning behind it. An archive sits inside the record folder it archives rather than beside it, so one ignore entry and one backed-folder entry cover a record and everything it has retired. The folder is gitignored, which is why a deleted plan had no recovery path. A plan cited by more than one task stays put until the last of them closes, since moving it early would strand every other pointer.
115
117
 
116
118
  A branch review report takes the other route and is swept rather than archived. `review-branch` writes it flat into `.canon/review/` as `branch-<slug>.md`, the session addressing it reads it once, and the durable record of what a review found is the comment `review-pr` posts on the pull request, so `docs-fold` deletes any report whose branch is gone. The body that writes a report owns how long it lives, which leaves the shipping branch's own report on disk through the run that cites it and collects it a branch later. What that loses is a local-only review on a branch that never opened a pull request, which is why the report says so where a reader meets it.
@@ -9,3 +9,4 @@ description: Write temporary files to the scratch folder structure
9
9
  - Write temporary files to `.canon/tmp/<slug>/<file>.md` in the project root, a nested `<slug>/` folder with a kebab-slug tied to the topic, not a flat `<slug>-<file>.md`. The scratch-guard hook enforces the location.
10
10
  - Write to `.claude/.tmp/<slug>/` instead in a project that carries no `.canon/` root, which is one the record move has not reached. The hook accepts either, but the ignore file is the source: `.gitignore` and its manifest counterpart are what keep the fallback spelling out of `git status`, and the hook and this rule both defer to it. `canon migrate records` is what moves a project from this second spelling to the first. <!-- canon-keep-record-root -->
11
11
  - "The project root" above means the current worktree by default. `085-worktrees.md` states the same mixed default for `.canon/tmp/` and defers to each skill's own body for which write resolves at the main root instead.
12
+ - `runs/`, `hooks/`, `handoff/`, `pr/`, and `render/` are reserved under `tmp/` for build and command output that can be regenerated, hook state, skill-to-skill handoff, pull request scratch, and render output. A session slug must not take one of these five names.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.87.0",
4
+ "version": "4.88.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -16,7 +16,7 @@ show_help() {
16
16
  echo -e "${GREY}│${NC} --keep ${GREY}# Keep tmp dir after run for inspection${NC}"
17
17
  echo -e "${GREY}│${NC} -h, --help ${GREY}# Show this help message${NC}"
18
18
  echo -e "${GREY}│${NC}"
19
- echo -e "${GREY}│${NC} Scaffolds <stack> into .canon/tmp/verify-<stack>/, syncs the full"
19
+ echo -e "${GREY}│${NC} Scaffolds <stack> into .canon/tmp/runs/verify-<stack>/, syncs the full"
20
20
  echo -e "${GREY}│${NC} layer chain, and runs check + test:e2e + screenshot."
21
21
  echo -e "${GREY}└${NC}"
22
22
  exit 0
@@ -102,7 +102,7 @@ main() {
102
102
  open_timeline "canon tooling verify $stack"
103
103
  trap close_timeline EXIT
104
104
 
105
- local tmp_root="$PROJECT_ROOT/.canon/tmp"
105
+ local tmp_root="$PROJECT_ROOT/.canon/tmp/runs"
106
106
  local tmp_dir="$tmp_root/verify-$stack"
107
107
 
108
108
  mkdir -p "$tmp_root"
@@ -21,7 +21,7 @@
21
21
  * `markdown.md` has a body backtick a named thing, so the natural spelling of
22
22
  * the citation is `` See the `Parallel sessions` heading in `CLAUDE.md` `` and
23
23
  * a class stopping at the first backtick never reaches the file. That heading
24
- * is real in this repository's own root file. Both classes returned the same
24
+ * was real in this repository's own root file when the check was written. Both classes returned the same
25
25
  * twenty lines over `claude/skills/` at the stamp, so the backtick exclusion
26
26
  * was carrying nothing and hiding the spelling an author would reach for.
27
27
  *