@erclx/canon 5.24.0 → 5.26.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 (185) hide show
  1. package/README.md +2 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
  4. package/claude/skills/backlog-triage/SKILL.md +19 -6
  5. package/claude/skills/canvas/REQUIREMENT.md +49 -0
  6. package/claude/skills/canvas/SKILL.md +106 -0
  7. package/claude/skills/context-fold/REQUIREMENT.md +6 -2
  8. package/claude/skills/context-fold/SKILL.md +8 -6
  9. package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
  10. package/claude/skills/design-extract/REQUIREMENT.md +1 -1
  11. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  12. package/claude/skills/design-taste/SKILL.md +4 -2
  13. package/claude/skills/design-taste/references/adopted.md +40 -0
  14. package/claude/skills/design-taste/references/kinds.md +1 -1
  15. package/claude/skills/design-taste/references/systems.md +1 -7
  16. package/claude/skills/design-taste/references/tells.md +2 -24
  17. package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
  18. package/claude/skills/document-health/REQUIREMENT.md +2 -0
  19. package/claude/skills/document-health/SKILL.md +21 -0
  20. package/claude/skills/document-health/references/axes.md +12 -0
  21. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  22. package/claude/skills/draft-doc/REQUIREMENT.md +1 -1
  23. package/claude/skills/draft-figure/REQUIREMENT.md +14 -3
  24. package/claude/skills/draft-figure/SKILL.md +24 -51
  25. package/claude/skills/draft-figure/references/architecture-set.md +35 -0
  26. package/claude/skills/draft-figure/references/hand-drawn.md +31 -0
  27. package/claude/skills/draft-figure/references/mermaid.md +55 -0
  28. package/claude/skills/draft-identity/SKILL.md +1 -1
  29. package/claude/skills/draft-screencast/SKILL.md +9 -7
  30. package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
  31. package/claude/skills/draft-slides/SKILL.md +31 -20
  32. package/claude/skills/git-followup/SKILL.md +17 -6
  33. package/claude/skills/git-issue/references/issue.md +10 -0
  34. package/claude/skills/git-pr/references/evidence.md +12 -2
  35. package/claude/skills/git-pr/references/labels.md +12 -0
  36. package/claude/skills/git-split/SKILL.md +19 -8
  37. package/claude/skills/index-lookup/SKILL.md +2 -2
  38. package/claude/skills/markdown-propose/references/format.md +9 -0
  39. package/claude/skills/memory-review/references/later-phases.md +7 -0
  40. package/claude/skills/review-address/SKILL.md +6 -13
  41. package/claude/skills/review-address/references/rest-reads.md +43 -0
  42. package/claude/skills/review-craft/references/ui.md +7 -1
  43. package/claude/skills/review-pr/REQUIREMENT.md +1 -0
  44. package/claude/skills/review-pr/SKILL.md +9 -3
  45. package/claude/skills/review-pr/references/close-out.md +2 -2
  46. package/claude/skills/review-pr/references/post.md +17 -5
  47. package/claude/skills/review-pr/references/scope.md +10 -8
  48. package/claude/skills/review-ui/SKILL.md +12 -4
  49. package/claude/skills/review-ui/references/post.md +4 -2
  50. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +38 -1
  51. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
  52. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +4 -0
  53. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +4 -4
  54. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +6 -0
  55. package/claude/skills/role-orchestrator/scripts/poll.ts +90 -27
  56. package/claude/skills/role-orchestrator/scripts/repo.ts +3 -5
  57. package/claude/skills/role-orchestrator/scripts/watch.ts +4 -7
  58. package/claude/skills/search-craft/REQUIREMENT.md +1 -1
  59. package/claude/skills/search-craft/SKILL.md +3 -1
  60. package/claude/skills/search-craft/references/adopted.md +30 -0
  61. package/claude/skills/target-setup/references/detect.md +8 -0
  62. package/claude/skills/target-setup/references/indexes.md +13 -0
  63. package/claude/skills/target-setup/references/verify.md +8 -0
  64. package/claude/skills/test-first/SKILL.md +2 -0
  65. package/claude/skills/test-first/references/adopted.md +36 -0
  66. package/claude/skills/write-human/SKILL.md +1 -1
  67. package/claude/skills/write-human/references/machine-tells.md +13 -0
  68. package/docs/agents/canvas.md +4 -2
  69. package/docs/agents/capture.md +1 -1
  70. package/docs/agents/commands.md +47 -47
  71. package/docs/agents/context-audit-checks.md +2 -2
  72. package/docs/agents/index.md +2 -3
  73. package/docs/agents/key-changes.md +3 -3
  74. package/docs/agents/pr-evidence.md +11 -2
  75. package/docs/agents/pr-reads.md +5 -4
  76. package/docs/agents/records-stale.md +22 -9
  77. package/docs/agents/records.md +1 -1
  78. package/docs/agents/sandbox.md +1 -1
  79. package/docs/agents/skills-audit.md +10 -3
  80. package/docs/agents/slides.md +15 -3
  81. package/docs/target/projects.md +4 -4
  82. package/docs/workflow/ai-workflow.md +1 -2
  83. package/docs/workflow/index.md +1 -0
  84. package/docs/workflow/parallel-features.md +1 -1
  85. package/docs/workflow/skill-map-on-request.md +42 -0
  86. package/docs/workflow/skill-map.md +3 -36
  87. package/docs/workflow/visual-design-workflow.md +1 -0
  88. package/governance/renames.toml +0 -4
  89. package/governance/rules/standards/markdown.md +1 -0
  90. package/package.json +1 -2
  91. package/scripts/core/install-check.sh +1 -1
  92. package/scripts/lib/sandbox-dispatch.sh +1 -1
  93. package/scripts/lib/sandbox-git.sh +1 -9
  94. package/scripts/lib/sandbox-path.sh +1 -1
  95. package/scripts/sandbox-hook.sh +99 -0
  96. package/src/audits/catalog.ts +2 -2
  97. package/src/canvas/capture.ts +1 -9
  98. package/src/canvas/client/inspector/color-field.tsx +368 -0
  99. package/src/canvas/client/inspector/field.tsx +200 -0
  100. package/src/canvas/client/inspector/fields.css +292 -0
  101. package/src/canvas/client/inspector/section.tsx +27 -0
  102. package/src/canvas/client/inspector/values.ts +183 -0
  103. package/src/canvas/client/inspector.tsx +176 -160
  104. package/src/canvas/client/shell.css +0 -101
  105. package/src/capture/render.ts +45 -7
  106. package/src/capture/sources.ts +43 -0
  107. package/src/census/count.ts +1 -1
  108. package/src/claude/cases/misc.ts +5 -0
  109. package/src/claude/cases/workflow.ts +6 -1
  110. package/src/claude/seeds.ts +1 -2
  111. package/src/claude/skills-audit.ts +68 -14
  112. package/src/client-commands.ts +1 -1
  113. package/src/commands/claude.ts +27 -2
  114. package/src/commands/design.ts +2 -64
  115. package/src/commands/feedback.ts +3 -3
  116. package/src/commands/pr.ts +219 -202
  117. package/src/commands/records.ts +88 -6
  118. package/src/commands/sandbox.ts +50 -10
  119. package/src/commands/slides.ts +31 -77
  120. package/src/context/audit.ts +8 -7
  121. package/src/design/tokens.ts +2 -18
  122. package/src/docs/read.ts +1 -1
  123. package/src/{github.ts → feedback/github.ts} +1 -1
  124. package/src/gate/measures.ts +6 -4
  125. package/src/gov/citations.ts +1 -1
  126. package/src/gov/payload.ts +1 -1
  127. package/src/gov/superseded.ts +2 -2
  128. package/src/help.ts +2 -2
  129. package/src/markdown/ceiling.ts +1 -1
  130. package/src/migrate/skill-names.ts +7 -1
  131. package/src/pr/evidence.ts +25 -5
  132. package/src/pr/rest.ts +137 -0
  133. package/src/record-root.ts +1 -0
  134. package/src/records/backup.ts +1 -0
  135. package/src/records/push-guard.ts +1 -1
  136. package/src/records/stale-canonical.ts +146 -0
  137. package/src/records/stale.ts +1 -1
  138. package/src/sandbox/hooks.ts +128 -0
  139. package/src/sandbox/provision.ts +596 -0
  140. package/src/sandbox/tree.ts +103 -4
  141. package/src/secrets/marker.ts +2 -2
  142. package/src/secrets/scan.ts +1 -1
  143. package/src/shipped/references.ts +2 -2
  144. package/src/slides/convert/export.ts +2 -16
  145. package/src/slides/decks.ts +110 -0
  146. package/src/standards/read.ts +1 -1
  147. package/src/sync/seeds-report.ts +1 -1
  148. package/src/tasks/answers.ts +1 -1
  149. package/src/tasks/archive.ts +1 -1
  150. package/src/teach/nav.ts +1 -1
  151. package/src/tooling/inject.ts +1 -1
  152. package/src/web/readme-citations.ts +1 -1
  153. package/standards/architecture.md +1 -0
  154. package/standards/canvas.md +112 -0
  155. package/standards/context.md +2 -1
  156. package/standards/figures.md +0 -1
  157. package/standards/index.md +1 -1
  158. package/standards/markdown.md +1 -0
  159. package/standards/mermaid.md +1 -7
  160. package/standards/requirements.md +2 -0
  161. package/standards/skill-practice.md +2 -1
  162. package/standards/skill.md +1 -0
  163. package/tooling/claude/reference.md +0 -5
  164. package/claude/skills/draft-diagram/REQUIREMENT.md +0 -45
  165. package/claude/skills/draft-diagram/SKILL.md +0 -177
  166. package/docs/agents/design-board.md +0 -33
  167. package/governance/rules/standards/diagrams.md +0 -14
  168. package/governance/rules/standards/mermaid.md +0 -15
  169. package/scripts/core/check-gallery-exclusion.sh +0 -50
  170. package/scripts/manage-sandbox.sh +0 -472
  171. package/src/design/board.ts +0 -576
  172. package/src/slides/layouts.ts +0 -478
  173. package/src/slides/parse.ts +0 -84
  174. package/src/slides/render.ts +0 -110
  175. package/src/slides/styles.ts +0 -96
  176. package/standards/diagrams.md +0 -132
  177. package/tooling/claude/seeds/.claude/diagrams/index.md +0 -8
  178. /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
  179. /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
  180. /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
  181. /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
  182. /package/src/{binary.ts → sweep/binary.ts} +0 -0
  183. /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
  184. /package/src/{paths.ts → tasks/paths.ts} +0 -0
  185. /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
@@ -26,15 +26,23 @@ rather than inside it, so the two passes finish on their own clocks.
26
26
 
27
27
  ## Step 1: resolve the pull request
28
28
 
29
- Resolve the number with `gh pr view --json number,headRefName,headRefOid`, or take the one the launch names. Take `<head>` off `canon pr head <number> --json`, reading its `tip`, and fall back to `headRefOid` when no record comes back. The first seven characters are `<short-sha>`. Record `<pass>` once, as the instant this pass starts, with `date -u +%Y%m%dT%H%M%SZ`. Every frame Step 5 pushes files under it, so a later pass at the same head never replaces an image this one's comment shows.
29
+ Resolve the number and the head through `canon pr head <number> --json` for the number the launch names, or `canon pr head --json` for the current branch when it names none. Take `<number>` off the record's `number` and `<head>` off its `tip`. The first seven characters are `<short-sha>`. Record `<pass>` once, as the instant this pass starts, with `date -u +%Y%m%dT%H%M%SZ`. Every frame Step 5 pushes files under it, so a later pass at the same head never replaces an image this one's comment shows.
30
30
 
31
- Read the newest UI verdict for the head guard:
31
+ Every read in this step runs on REST, since the `gh pr` lookups run on GraphQL and a cloud session's GitHub proxy refuses it. A target whose CLI predates the verb gets no record back. When the launch named no number, fall back there to the open pull requests on the current branch, encoding it so a `/` in its name reaches the query as `%2F`, and stop when that prints anything but one number. Two open pull requests on one head against different bases leave nothing to say which one the launch meant. A named number skips that lookup, since this checkout's branch need not be the pull request's. Either way, read the head off the pull request itself:
32
32
 
33
33
  ```bash
34
- gh pr view <number> --json reviews,comments --jq '([.reviews[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | startswith("## UI review"))] | last) as $v | [($v.body // ""), ($v.submittedAt // ""), ([.comments[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review response" or . == "## Post-review findings") | .createdAt] | max // "")]'
34
+ gh api "repos/{owner}/{repo}/pulls?head={owner}:$(git branch --show-current | jq -Rr @uri)&state=open" --jq '.[].number'
35
+ gh api repos/{owner}/{repo}/pulls/<number> --jq .head.sha
35
36
  ```
36
37
 
37
- The three values are the verdict's body, when it was posted, and when the newest reply was. Take the sha off the body's last-line `<!-- review-ui: head=<sha> -->` marker. A body carrying none covers no head this pass can trust, so drive.
38
+ Read the newest UI verdict for the head guard, and the newest reply:
39
+
40
+ ```bash
41
+ gh api --paginate 'repos/{owner}/{repo}/pulls/<number>/reviews?per_page=100' --jq '.[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | startswith("## UI review")) | {body, submitted_at} | @json' | tail -n 1
42
+ gh api --paginate 'repos/{owner}/{repo}/issues/<number>/comments?per_page=100' --jq '.[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review response" or . == "## Post-review findings") | .created_at' | tail -n 1
43
+ ```
44
+
45
+ The first read prints the verdict's body and when it was posted as one JSON line, and the second prints when the newest reply was. `--paginate` runs the filter once per page, so `tail` outside it keeps the newest row in the thread rather than the newest on each page. The verdict goes out as JSON because its body spans many lines, and `tail` over the raw body would keep only its last one. An empty first read is a pull request carrying no verdict yet. Take the sha off the body's last-line `<!-- review-ui: head=<sha> -->` marker. A body carrying none covers no head this pass can trust, so drive.
38
46
 
39
47
  Read nothing else about the change. The diff, the plan, the task, and the description's Summary and Technical Context all carry the author's argument for it, and a pass that has heard it drives towards what it expects.
40
48
 
@@ -68,7 +68,9 @@ Record the instant just before the post, in UTC, as `<posted-at>`. Step 8 keeps
68
68
 
69
69
  ```bash
70
70
  date -u +%Y-%m-%dT%H:%M:%SZ
71
- gh pr review <number> --comment --body-file .canon/tmp/pr/review-ui/body-<number>-<short-sha>.md
71
+ gh api -X POST 'repos/{owner}/{repo}/pulls/<number>/reviews' -f event=COMMENT -F body=@.canon/tmp/pr/review-ui/body-<number>-<short-sha>.md --jq .html_url
72
72
  ```
73
73
 
74
- Never post through `--approve` or `--request-changes`, which carry a state the poll does not read and a weight this pass has not earned.
74
+ The post goes through the REST reviews endpoint rather than the `gh pr` review subcommand, which runs on GraphQL and fails where a cloud session's GitHub proxy refuses it. `-F` reads the file into the body, where `-f` would post the path itself.
75
+
76
+ Never pass `event=APPROVE` or `event=REQUEST_CHANGES`, which carry a state the poll does not read and a weight this pass has not earned.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Orchestrator cloud launch
3
- description: The cloud build shape, being the launch prompt that carries the plan as text and announces over GitHub, the create under a pseudo-terminal with its trust keys, and the readout that fails loudly on a missing session id
3
+ description: The cloud build and review shapes, being the launch prompt that carries the plan as text and announces over GitHub, the reviewer prompt whose posted review is its announcement, the create under a pseudo-terminal with its trust keys, and the readout that fails loudly on a missing session id
4
4
  ---
5
5
 
6
6
  # Orchestrator cloud launch
@@ -9,6 +9,14 @@ Run this once `orchestrator-dispatch.md` has cleared a row and picked cloud for
9
9
 
10
10
  The shape needs a `SessionStart` hook in the repository that, on the VM alone, copies the plugin skills into `.claude/skills/` and puts `canon` on the path. A repository without one cannot take this shape, so fall back to a local worker there. Where the hook exists, the launch prompt calls `/auto-ship` with no `canon:` prefix, since the copied skills list as project skills.
11
11
 
12
+ ## Contents
13
+
14
+ - [Check the row is unclaimed](#check-the-row-is-unclaimed)
15
+ - [The prompt](#the-prompt)
16
+ - [The create](#the-create)
17
+ - [Recognize the pull request](#recognize-the-pull-request)
18
+ - [Dispatch a review to a cloud reviewer](#dispatch-a-review-to-a-cloud-reviewer)
19
+
12
20
  ## Check the row is unclaimed
13
21
 
14
22
  A cloud worker never appears in `canon sessions list` and never pushes the derived branch, since the cloud assigns it a `claude/` branch of its own. So the roster and the refs read clear on a row a cloud worker is already building. Run the local check in `orchestrator-dispatch.md` anyway, since a local worker on the same row still collides, then read the two places a cloud worker does show:
@@ -74,3 +82,32 @@ gh api 'repos/{owner}/{repo}/pulls/<number>/commits' --jq '.[].commit.message' |
74
82
  ```
75
83
 
76
84
  A count above zero matches the row. A count of zero is someone else's pull request, whatever its title says. Once it matches, record the number on the row's task yourself with `canon tasks pull-request <number> <task>`, since `git-pr` on the VM has no task file to write, then review it the way step 5 reviews any worker's pull request.
85
+
86
+ ## Dispatch a review to a cloud reviewer
87
+
88
+ Run this once `orchestrator-review-dispatch.md` has sent a code review to a dispatched reviewer and the operator picked cloud for it. The reviewer needs the same `SessionStart` hook as the build shape, and a `canon` on the VM whose `pr` reads run on REST, since the proxy refuses GraphQL. A UI review never takes this shape, because it drives a browser the VM does not have.
89
+
90
+ Confirm the VM's `canon` reads over REST before dispatching. In this repository the setup hook links the checkout's own CLI, so a pushed branch carries the reads. Any other repository's VM installs `canon` from npm, so its `pr` reads run on REST only once a release carrying them is published, and its copied skills carry the REST lookups only once that repository's skills are re-scaffolded from that release.
91
+
92
+ Read `npm view @erclx/canon version` against the first release whose changelog names the REST reads, and check the target's copied `review-pr` the same way, by running `grep -rnE 'gh pr (list|view|review|diff|comment)'` over it and expecting no output. A version short of that release, or a hit, dispatches a reviewer that fails on its first GraphQL call, so take the local shape instead.
93
+
94
+ Check this pass's launched record for a cloud reviewer already holding the number, and run `canon pr review-state <number> --json` for a pass already posted. A cloud reviewer never appears in `canon sessions list`, so the live-session check the local shape runs reads clear on a pull request one already holds.
95
+
96
+ The create targets whichever repository the checkout it runs from names as `origin`. For a pull request in this repository, write the prompt to `.canon/tmp/cloud-review-<number>/prompt.md` at the main worktree root and run the create there. For a pull request in another repository, run the create from a checkout of that repository and write the prompt under that checkout's own `.canon/tmp/cloud-review-<number>/`, which its `.gitignore` has to cover before the create runs. A fresh clone made for this is untrusted, so the create takes the trust-prompt key variant under `## The create`:
97
+
98
+ ```plaintext
99
+ /role-reviewer
100
+ Then run /review-pr <number> on <owner>/<repo>.
101
+ Your controller cannot receive a message from this session. The review you post is the announcement, so post nothing else for it. If you stop on a question before the review posts, comment on the pull request starting with QUESTION: review <number>.
102
+ <the cross-branch facts>
103
+ ```
104
+
105
+ The cross-branch facts are the ones `### What the brief may carry` in `orchestrator-launch.md` allows a reviewer brief, and nothing of this session's own read. Neither skill carries the invocation flag, so no position-zero expansion is at stake and the bare names reach the copied project skills.
106
+
107
+ Create the session with the wrapper under `## The create`, setting `dir` to that prompt's folder, and keep its readout rule: no `session_` id means nothing was dispatched. Record the session id beside the number in this pass's launched record, and name the number, the repository, and the session id in the dispatch report.
108
+
109
+ `poll.ts` lists every open pull request from this machine and reads each one's review state, so the posted `## Review` reaches the next poll like a local reviewer's. Start the poll when the dispatch goes out rather than at its fallback. A `QUESTION: review <number>` comment reaches the same poll as an unclassified reply, so answer it on the pull request.
110
+
111
+ The pull request is the record to act on, and the transcript is read back only to check for a refused call. Run `claude --teleport <session>` under `script` from a throwaway clone of the target outside this repository, answer its trust prompt with the same piped keys, and set the terminal to 500 rows first, since only the screen tail is captured otherwise. Search the capture for `HTTP 403` and `graphql`. A hit names a call still on GraphQL, so report it as a finding rather than reading the posted review as clean.
112
+
113
+ A cloud reviewer cannot be messaged after it posts. A re-review after a worker's address pass is a fresh dispatch, local or cloud by the same pick, rather than the message `orchestrator-handback.md` sends a live reviewer.
@@ -5,6 +5,19 @@ description: The plan-answer gate, the collision check before a self-dispatch, t
5
5
 
6
6
  Run this at loop step 4, for a `## Run now` row whose plan is verified, in place of handing the worktree to a human. The disjointness gate below is where that row's file set is tested against every track in flight.
7
7
 
8
+ ## Contents
9
+
10
+ - [Derive the candidate](#derive-the-candidate)
11
+ - [Check the plan waits on nobody](#check-the-plan-waits-on-nobody)
12
+ - [Check the branch is unclaimed](#check-the-branch-is-unclaimed)
13
+ - [Hold what this pass already launched](#hold-what-this-pass-already-launched)
14
+ - [Check the file sets are disjoint](#check-the-file-sets-are-disjoint)
15
+ - [Pick the model](#pick-the-model)
16
+ - [Pick local or cloud](#pick-local-or-cloud)
17
+ - [Dispatch](#dispatch)
18
+ - [Fall back to the human](#fall-back-to-the-human)
19
+ - [Stop the loop](#stop-the-loop)
20
+
8
21
  ## Derive the candidate
9
22
 
10
23
  Run `canon tasks plan-branch <plan> --json` against the row's plan file and read `branch`, `type`, `slug`, and `conforms` off the record.
@@ -103,6 +103,10 @@ claude --bg --model <model> -n "reviewer-<project>-<number>" "Run /canon:role-re
103
103
 
104
104
  Check `canon sessions list --json` for a live `reviewer-<project>-<number>` before launching, and message that session instead when one holds the pull request.
105
105
 
106
+ ## Dispatch a review to a cloud reviewer
107
+
108
+ Read `${CLAUDE_SKILL_DIR}/references/orchestrator-cloud-launch.md` when the operator picked cloud for a code review. Its prompt opens with `/role-reviewer`, carries the cross-branch facts, and makes the posted review the announcement.
109
+
106
110
  ## Dispatch to drive a pull request's checklist
107
111
 
108
112
  `review-ui` is the second pass `role-reviewer` may run, so this shape reaches the role first the way the code reviewer's does. `orchestrator-review-dispatch.md` decides whether a pull request takes it, and it runs beside the code reviewer rather than after it.
@@ -9,7 +9,7 @@ Run the orchestrator's review trigger. The poll reports pull request movement an
9
9
 
10
10
  ## When to run it
11
11
 
12
- Start the poll on an open pull request and stop it when the last one merges with nothing else out. That is the whole condition and it resolves from `gh pr list` without asking the operator. A release pull request alone does not qualify, since its sweep carries no findings.
12
+ Start the poll on an open pull request and stop it when the last one merges with nothing else out. That is the whole condition and it resolves from the open list without asking the operator. A release pull request alone does not qualify, since its sweep carries no findings.
13
13
 
14
14
  A dispatch no longer starts it. The script reads pull requests and a building worker has none, so every interval between the launch and the push is a fixed cost returning nothing, measured as five consecutive runs reporting no movement across roughly fifteen minutes while one worker built. What replaces it is the worker announcing its own pull request the moment it opens one, per the `role-worker` skill that session loads, which covers exactly the transition the poll cannot observe.
15
15
 
@@ -71,15 +71,15 @@ The state reaches every stalled dispatch, since one threshold governs the headin
71
71
 
72
72
  ### The count behind the review dispatch
73
73
 
74
- The report is also where the load count in `orchestrator-review-dispatch.md` is legible, and that count decides where a first pass runs, here or in a dispatched reviewer. That threshold trips on open pull requests awaiting a first pass, which is what `OPENED` and a pull request with no prior pass name here and what `SEEN` excludes, so read the count off these lines rather than off `gh pr list`, which counts a branch closed out and waiting on a merge the same as one nobody has read. It is a separate condition from the poll-start fallback above, which decides when this loop runs rather than where a review runs.
74
+ The report is also where the load count in `orchestrator-review-dispatch.md` is legible, and that count decides where a first pass runs, here or in a dispatched reviewer. That threshold trips on open pull requests awaiting a first pass, which is what `OPENED` and a pull request with no prior pass name here and what `SEEN` excludes, so read the count off these lines rather than off the open list, which counts a branch closed out and waiting on a merge the same as one nobody has read. It is a separate condition from the poll-start fallback above, which decides when this loop runs rather than where a review runs.
75
75
 
76
76
  The count used to read low, and it erred in the direction that breaks the trigger. A review's `commit.oid` is stamped with the head at submission rather than with the commit the reviewer read, so an author pushing between the diff read and the post left the pass recorded against a commit it never saw, and `SEEN` then fired on a head still awaiting its first look at that delta.
77
77
 
78
78
  The pass now carries its own record instead. `review-pr` writes the commit it read and the instant it read it as a marker on the last line of every body it posts, and both this script and that skill resolve the covered state through `canon pr review-state`, which is the one place the marker is parsed. A commit pushed inside a pass's compose window falls outside the marked range, so it reads as `MOVED` rather than as `SEEN`. A `SEEN` is a claim about what a session read rather than about what GitHub stamped, so it wants no second read to be believed.
79
79
 
80
- Read `source` on the record before trusting a `SEEN` on a thread whose newest pass is old. `marker` is the read-time record. `fallback` is a pass posted before the mechanism shipped, or a target whose CLI predates the verb, and it carries the defect above unchanged, so a `SEEN` under it is worth one `gh pr view --json reviews` before it is believed. `none` is a thread carrying no pass at all.
80
+ Read `source` on the record before trusting a `SEEN` on a thread whose newest pass is old. `marker` is the read-time record. `fallback` is a pass posted before the mechanism shipped, or a target whose CLI predates the verb, and it carries the defect above unchanged, so a `SEEN` under it is worth one read of the pull's reviews before it is believed. `none` is a thread carrying no pass at all.
81
81
 
82
- The other side of that comparison used to lag as well, which made the two errors compound rather than cancel. `gh pr view --json headRefOid` answers from the pull request object and that object trails the branch ref by up to a minute after a push, reporting nothing about the trail, so a pass stamped ahead of the commit it read was compared against a head stamped behind the commit that exists. The head now comes from `canon pr head`, which resolves the tip through `git ls-remote` and reports which commit each source names, and the object's head stays behind it as the fallback for a target whose CLI predates the verb. Both halves of the comparison now answer about a commit somebody named rather than one GitHub stamped, which is what lets a `SEEN` be read as covered.
82
+ The other side of that comparison used to lag as well, which made the two errors compound rather than cancel. The pull read's `head.sha` answers from the pull request object and that object trails the branch ref by up to a minute after a push, reporting nothing about the trail, so a pass stamped ahead of the commit it read was compared against a head stamped behind the commit that exists. The head now comes from `canon pr head`, which resolves the tip through `git ls-remote` and reports which commit each source names, and the object's head stays behind it as the fallback for a target whose CLI predates the verb. Both halves of the comparison now answer about a commit somebody named rather than one GitHub stamped, which is what lets a `SEEN` be read as covered.
83
83
 
84
84
  ## The watch beside it
85
85
 
@@ -33,3 +33,9 @@ Hand what it finds to each reviewer as facts in the brief, per `### What the bri
33
33
  ## Launching the reviewer
34
34
 
35
35
  Take the reviewer shape in `orchestrator-launch.md`, or its UI reviewer shape for the UI trigger. Check `canon sessions list --json` for a live `reviewer-<project>-<number>`, or `reviewer-ui-<project>-<number>`, first and message it instead of launching a second, since two reviewers on one pull request post two verdicts. The re-review after a worker's address pass goes back the same way, per `orchestrator-handback.md`.
36
+
37
+ ## Local or cloud
38
+
39
+ A code review goes to a cloud reviewer rather than `claude --bg` only on the operator's explicit pick for that dispatch, the same standing the build shape's pick holds in `orchestrator-dispatch.md`. The cloud session costs no local context and no machine slot, which matters when the load trigger fired. It also reads nothing this machine holds, so a review that needs the board, a local preview, or a sandbox stays local.
40
+
41
+ This session hears a cloud reviewer through `poll.ts` alone, which already lists every open pull request from this machine, so the posted review is the announcement and no message arrives. `orchestrator-cloud-launch.md` carries the shape under `## Dispatch a review to a cloud reviewer`. A UI review always stays local.
@@ -20,7 +20,6 @@ interface Review {
20
20
 
21
21
  export interface Payload {
22
22
  comments?: Comment[]
23
- headRefOid?: string
24
23
  reviews?: Review[]
25
24
  }
26
25
 
@@ -416,11 +415,80 @@ const parseJson = (text: string): unknown => {
416
415
  }
417
416
  }
418
417
 
418
+ /**
419
+ * One JSON object per line, which is what `.[] | <projection> | @json` prints
420
+ * across every page. The projection keeps stdout to the fields the mappers
421
+ * read, since a whole REST row carries user objects and links and a long thread
422
+ * could overrun the spawn buffer and read as unreadable. A failed read and a
423
+ * line that does not parse both return null, so neither reads as a thread with
424
+ * nothing on it.
425
+ */
426
+ const readRows = (
427
+ path: string,
428
+ fields: string,
429
+ ): null | Record<string, unknown>[] => {
430
+ const read = run('gh', [
431
+ 'api',
432
+ '--paginate',
433
+ `repos/{owner}/{repo}/${path}`,
434
+ '--jq',
435
+ `.[] | {${fields}} | @json`,
436
+ ])
437
+ if (!read.isOk) return null
438
+
439
+ const rows: Record<string, unknown>[] = []
440
+ for (const line of read.stdout.split('\n')) {
441
+ if (line.trim() === '') continue
442
+ const row = parseJson(line)
443
+ if (typeof row !== 'object' || row === null) return null
444
+ rows.push(row as Record<string, unknown>)
445
+ }
446
+
447
+ return rows
448
+ }
449
+
450
+ const text = (value: unknown): null | string =>
451
+ typeof value === 'string' ? value : null
452
+
453
+ /**
454
+ * The REST rows carry snake_case names, mapped onto the camelCase shape the
455
+ * classifiers read. `commit_id` becomes `commit.oid`. A pending review has no
456
+ * `submitted_at`, which maps to null.
457
+ */
458
+ export const toReview = (row: Record<string, unknown>): Review => ({
459
+ body: text(row.body),
460
+ commit: { oid: text(row.commit_id) },
461
+ submittedAt: text(row.submitted_at),
462
+ })
463
+
464
+ export const toComment = (row: Record<string, unknown>): Comment => ({
465
+ body: text(row.body),
466
+ createdAt: text(row.created_at),
467
+ })
468
+
469
+ /**
470
+ * Two reads feed one payload, so either failing makes the pull request
471
+ * unreadable rather than a thread missing half its rows.
472
+ */
473
+ const readPayload = (number: string): null | Payload => {
474
+ const reviews = readRows(
475
+ `pulls/${number}/reviews?per_page=100`,
476
+ 'body, commit_id, submitted_at',
477
+ )
478
+ const comments = readRows(
479
+ `issues/${number}/comments?per_page=100`,
480
+ 'body, created_at',
481
+ )
482
+ if (reviews === null || comments === null) return null
483
+
484
+ return { comments: comments.map(toComment), reviews: reviews.map(toReview) }
485
+ }
486
+
419
487
  type Snapshot = { baseline: Baseline } | { row: Row }
420
488
 
421
489
  const nowSeconds = (): number => Math.floor(Date.now() / 1000)
422
490
 
423
- const readHead = (number: string, payload: Payload): string => {
491
+ const readHead = (number: string): string => {
424
492
  // The tip is the authority for the head, not the pull request object, which
425
493
  // lags the ref by up to a minute after a push. The object's head is the
426
494
  // fallback for a target on an older binary with no `pr head` verb.
@@ -432,13 +500,20 @@ const readHead = (number: string, payload: Payload): string => {
432
500
  ? (record as Record<string, unknown>).tip
433
501
  : undefined
434
502
 
435
- return typeof tip === 'string' && tip !== ''
436
- ? tip
437
- : (payload.headRefOid ?? '')
503
+ if (typeof tip === 'string' && tip !== '') return tip
504
+
505
+ const pull = run('gh', [
506
+ 'api',
507
+ `repos/{owner}/{repo}/pulls/${number}`,
508
+ '--jq',
509
+ '.head.sha',
510
+ ])
511
+
512
+ return pull.isOk ? pull.stdout.trim() : ''
438
513
  }
439
514
 
440
515
  /**
441
- * `gh pr view --json mergeable` reports UNKNOWN until GitHub finishes
516
+ * The pull read's `mergeable_state` reports unknown until GitHub finishes
442
517
  * computing it, which is exactly when a poll asks. merge-tree answers locally
443
518
  * against the base this machine has. It exits non-zero on a ref it cannot
444
519
  * resolve as well as on a real conflict, so both sides are checked first.
@@ -530,12 +605,9 @@ const main = (): number => {
530
605
  // one as GONE. That is a louder wrong answer than the one this script was
531
606
  // fixed for, so the run aborts rather than classify on it.
532
607
  const listed = run('gh', [
533
- 'pr',
534
- 'list',
535
- '--state',
536
- 'open',
537
- '--json',
538
- 'number',
608
+ 'api',
609
+ '--paginate',
610
+ 'repos/{owner}/{repo}/pulls?state=open&per_page=100',
539
611
  '--jq',
540
612
  '.[].number',
541
613
  ])
@@ -550,19 +622,10 @@ const main = (): number => {
550
622
 
551
623
  const snapshots: Snapshot[] = []
552
624
  for (const number of listed.stdout.split(/\s+/).filter((n) => n !== '')) {
553
- // A query that failed and a pull request with no reviews both arrive as
554
- // an empty result, so one query per pull request gives the failure a
555
- // single place to surface.
556
- const view = run('gh', [
557
- 'pr',
558
- 'view',
559
- number,
560
- '--json',
561
- 'headRefOid,reviews,comments',
562
- ])
563
- const payload = view.isOk
564
- ? (parseJson(view.stdout) as Payload | null)
565
- : null
625
+ // A read that failed and a pull request with no reviews both arrive as an
626
+ // empty result, so each read returns null on failure and the payload gives
627
+ // the failure a single place to surface.
628
+ const payload = readPayload(number)
566
629
  const old = known.get(number)
567
630
  const unreadable = (reason: string): void => {
568
631
  if (old === undefined) {
@@ -578,12 +641,12 @@ const main = (): number => {
578
641
  snapshots.push({ baseline: carryForward(old) })
579
642
  }
580
643
 
581
- if (payload === null || typeof payload !== 'object') {
644
+ if (payload === null) {
582
645
  unreadable('could not be read')
583
646
  continue
584
647
  }
585
648
 
586
- const head = readHead(number, payload)
649
+ const head = readHead(number)
587
650
  if (head === '') {
588
651
  unreadable('returned no head')
589
652
  continue
@@ -53,12 +53,10 @@ export const baseRef = (): string => {
53
53
  if (symbolic.isOk && ref !== '') return ref
54
54
 
55
55
  const repo = run('gh', [
56
- 'repo',
57
- 'view',
58
- '--json',
59
- 'defaultBranchRef',
56
+ 'api',
57
+ 'repos/{owner}/{repo}',
60
58
  '--jq',
61
- '.defaultBranchRef.name',
59
+ '.default_branch',
62
60
  ])
63
61
  const name = repo.isOk ? repo.stdout.trim() : ''
64
62
 
@@ -175,14 +175,11 @@ export const pass = (
175
175
 
176
176
  const readPulls = (): Read<string[]> => {
177
177
  const listed = run('gh', [
178
- 'pr',
179
- 'list',
180
- '--state',
181
- 'open',
182
- '--json',
183
- 'number,headRefName,title',
178
+ 'api',
179
+ '--paginate',
180
+ 'repos/{owner}/{repo}/pulls?state=open&per_page=100',
184
181
  '--jq',
185
- '.[] | "PR-OPEN #\\(.number) \\(.headRefName) \\(.title)"',
182
+ '.[] | "PR-OPEN #\\(.number) \\(.head.ref) \\(.title)"',
186
183
  ])
187
184
  if (!listed.isOk) return { isOk: false }
188
185
 
@@ -18,7 +18,7 @@ The miss is measured. On 2026-10-02 two queries ran twice each with identical te
18
18
 
19
19
  The authorities in both pairs were named by the session unprompted once it was asked to, which is the argument for a method over a list. Two queries at one run each show the effect exists and do not size it.
20
20
 
21
- Access is a second gap a session cannot derive. Probed on 2026-10-02, Reddit refused its JSON search with a 403 and refused `WebFetch`, while its RSS feed answered 200. X redirected `curl` to a login page and returned 402 to `WebFetch`, so no free route reaches it.
21
+ Access is a second gap a session cannot derive. Probed on 2026-10-02, Reddit refused its JSON search with a 403 and refused `WebFetch`, while its RSS feed answered 200. X redirected `curl` to a login page and returned 402 to `WebFetch`, so no free route reaches it. Re-probed on 2026-10-04, Reddit's RSS feed, its search feed, and its JSON listing and search all answered 403, so the feed that answered two days earlier no longer does.
22
22
 
23
23
  ## Must
24
24
 
@@ -44,10 +44,12 @@ The procedure that loaded this skill owns what the result is for and where it is
44
44
 
45
45
  Each line is a claim measured once, and `${CLAUDE_SKILL_DIR}/REQUIREMENT.md` records the date under its Gap. Sites change their blocking without notice, so re-check a line before relying on it. Add a line only for a failure a session actually hit, and record the date it hit in the requirement beside it.
46
46
 
47
- - Reddit answers its RSS feed and refuses its JSON search with a 403. `WebFetch` is refused for the site.
47
+ - Reddit refuses its RSS feed and its JSON search alike with a 403. `WebFetch` is refused for the site, so no keyless route reaches it.
48
48
  - X answers nothing without paying or a login. `curl` redirects to a login page and `WebFetch` returns 402.
49
49
  - Stop and ask the operator before any route that pays, logs in, or works around a refusal. Never suggest scraping behind a login.
50
50
 
51
+ Read `${CLAUDE_SKILL_DIR}/references/adopted.md` only when extending this guidance or arguing against a rule in it. It records which comparable skills were read, what was adopted and declined, and why.
52
+
51
53
  ## Excuses and rebuttals
52
54
 
53
55
  | Excuse | Rebuttal |
@@ -0,0 +1,30 @@
1
+ ---
2
+ title: Adopted and declined
3
+ description: Which comparable research skills this skill was read against, what it took from them, what it declined, and which of its rules rest on measurement rather than a source
4
+ ---
5
+
6
+ # Adopted and declined
7
+
8
+ Three third-party research skills were read before this one was written. None of them was imported, and most of the body rests on paired searches measured in this skill's `REQUIREMENT.md` rather than on any of them. A later session extending the skill adds to this file rather than re-arguing an item settled here.
9
+
10
+ ## Adopted
11
+
12
+ **Recording each route's failure.** [aghori3004/claude-research-stack](https://github.com/aghori3004/claude-research-stack) keeps one `research-sources` skill that orders sources cheapest first and writes down how each one fails. Adopted as the access note, which records a refusal a session hit rather than leaving the next session to learn it by failing.
13
+
14
+ **Calling the tools directly.** The same skill ships no wrapper scripts, so it calls the command-line tools and servers it names. Adopted as the rule to reach a keyless API with a plain request rather than writing a wrapper for one search.
15
+
16
+ **Treating a paid route as the operator's call.** [rohunvora/x-research-skill](https://github.com/rohunvora/x-research-skill) reaches X through the official API at a cost per search page. Adopted as the stop before any route that pays or logs in, since that cost is one the operator decides rather than one a session discovers.
17
+
18
+ ## Declined
19
+
20
+ **A directory of sites per category.** None of the three is one, and each earns its keep on access and cost instead. A site list also rots with nobody noticing, so the body derives a field's authorities per question rather than reading them off a list.
21
+
22
+ **Reaching walled sites through a logged-in browser.** `claude-research-stack` reaches Reddit and X through a desktop Chrome session holding the operator's login. Declined because the body never suggests working around a login.
23
+
24
+ **A bundled engine.** [mvanhorn/last30days-skill](https://github.com/mvanhorn/last30days-skill) is a Python engine with its own test suite and paid aggregators behind it. Declined because a skill here ships no code, and access a session needs repeatedly becomes a `canon` verb.
25
+
26
+ ## Stated by no source
27
+
28
+ **Naming the field and deriving its authorities.** No comparable skill takes a thinking step before searching. The step rests on the paired open and scoped searches recorded in `REQUIREMENT.md` under `Gap`, where the scoped run returned primary sources and caught a wrong version number the open run carried.
29
+
30
+ **Preferring a public API over a fetched page.** It rests on the same probes, where the scholarly and reference APIs returned the dates, authors, and identifiers a citation needs and a fetched page returned a summary.
@@ -9,6 +9,14 @@ What the `install` and `gov` phases both do before they write anything. Stated
9
9
  once here rather than twice in the body, since the two phases differ in what
10
10
  they install rather than in how they read the project.
11
11
 
12
+ ## Contents
13
+
14
+ - [Read the catalogs](#read-the-catalogs)
15
+ - [Detect](#detect)
16
+ - [Resolve arguments](#resolve-arguments)
17
+ - [Gap handling](#gap-handling)
18
+ - [Preview](#preview)
19
+
12
20
  ## Read the catalogs
13
21
 
14
22
  Run in parallel. Never hardcode stack, rule, or standards names. The
@@ -12,6 +12,19 @@ user, and writes. The CLI handles validation and regeneration only.
12
12
  Read `canon docs indexes` from the toolkit if context on the system is needed
13
13
  before scanning.
14
14
 
15
+ ## Contents
16
+
17
+ - [Scope](#scope)
18
+ - [Scan](#scan)
19
+ - [Present candidates](#present-candidates)
20
+ - [Draft frontmatter](#draft-frontmatter)
21
+ - [Scaffold the folder index](#scaffold-the-folder-index)
22
+ - [Inject](#inject)
23
+ - [Validate](#validate)
24
+ - [Offer the convention seed](#offer-the-convention-seed)
25
+ - [Closeout](#closeout)
26
+ - [Reference](#reference)
27
+
15
28
  ## Scope
16
29
 
17
30
  - Bootstrap un-indexed folders only. Skip folders that already contain an `index.md`.
@@ -12,6 +12,14 @@ Two depths over one contract. Both read the `scripts` block from the project's
12
12
  own `package.json` and run what is declared there, so neither hardcodes a script
13
13
  name and a stack exposing a different set is covered without an edit here.
14
14
 
15
+ ## Contents
16
+
17
+ - [Guards](#guards)
18
+ - [Step 1: read scripts](#step-1-read-scripts)
19
+ - [Step 2: run the chain](#step-2-run-the-chain)
20
+ - [Step 3: report](#step-3-report)
21
+ - [Out of scope](#out-of-scope)
22
+
15
23
  ## Guards
16
24
 
17
25
  A monorepo syncs each language root into its own subfolder, so the project root
@@ -19,6 +19,8 @@ description: Write the failing test for a behavior before writing the code that
19
19
  - Changing what a function already does starts with changing what its test asserts. Run that changed test once before touching the implementation, and confirm it fails against the code as it stands today.
20
20
  - A test that already passes before any code changes proves nothing about the new behavior. Widen the assertion or add a case until the suite fails against the current implementation.
21
21
 
22
+ Read `${CLAUDE_SKILL_DIR}/references/adopted.md` only when extending this guidance or arguing against a rule in it. It records which external sources the loop matches, which parts were declined, and which rules no source states.
23
+
22
24
  ## What this skill does not cover
23
25
 
24
26
  - Which layer a test belongs at and what makes it a good one. `canon:test-craft` owns that judgment, and step 1 loads it.
@@ -0,0 +1,36 @@
1
+ ---
2
+ title: Adopted and declined
3
+ description: Which external sources the test-first loop draws on, which parts it declined, and which of its rules no source states, so a later session extends the position instead of re-deriving it
4
+ ---
5
+
6
+ # Adopted and declined
7
+
8
+ The loop predates this ledger, so this file records the sources it matches rather than the ones it was drafted from. Each was opened before it was cited. A later session extending the skill adds to this file rather than re-arguing an item settled here.
9
+
10
+ ## Adopted
11
+
12
+ **The loop itself.** Kent Beck's [Canon TDD](https://newsletter.kentbeck.com/p/canon-tdd) states it as turning one item into "an actual, concrete, runnable test", changing the code "to make the test (& all previous tests) pass", then refactoring. Adopted as steps 2, 4, 5, and 6, with the whole suite run after the pass rather than the new test alone.
13
+
14
+ **Making it pass without designing.** The same post warns against "mixing refactoring into making the test pass", and keeps implementation design decisions for the refactor. Adopted as step 4's smallest change and step 6's cleanup of the shortcuts that change left.
15
+
16
+ **Never faking the pass.** The same post lists deleting assertions "so the test pretends to pass" and "copying actual, computed values & pasting them into the expected values" as mistakes. Adopted as step 5's rule that a pass must be the one the test was written to prove.
17
+
18
+ **Refactoring is a step, not an option.** Martin Fowler's [TestDrivenDevelopment](https://martinfowler.com/bliki/TestDrivenDevelopment.html) names "neglecting the third step" as the most common way to get the practice wrong. Adopted as step 6 being mandatory where Canon TDD writes "optionally refactor".
19
+
20
+ ## Declined
21
+
22
+ **The test list.** Canon TDD opens by listing every expected variant of the new behavior before writing a test. Declined because a plan already names the behaviors a build covers, and `canon:build-in-slices` owns the order they land in, so a second list here would hold a second copy of the scope.
23
+
24
+ **Refactoring only as far as the session needs.** Canon TDD warns against refactoring further than necessary and calls duplication "a hint, not a command". Declined as a separate rule, since `canon:code-craft` owns when an abstraction earns its place and step 6 already limits refactoring to what step 4 left.
25
+
26
+ ## Checked and not a source
27
+
28
+ **Software Engineering at Google, [Testing Overview](https://abseil.io/resources/swe-book/html/ch11.html).** The default shelf's testing chapter covers why tests matter and how a suite is structured, and never addresses writing a test before its code or watching it fail. It informs `canon:test-craft`, which step 1 loads, rather than this loop.
29
+
30
+ **Kent Beck's _Test-Driven Development: By Example_.** The book is the loop's origin, and no openable copy was available to cite a version from. Canon TDD is Beck's own later restatement and stands in for it here.
31
+
32
+ ## Stated by no source
33
+
34
+ **Confirming the failure is the right one.** Step 3's rule, that a test failing for a typo or a wrong signature is a defect in the test, comes from the failure recorded in this skill's `REQUIREMENT.md` under `Gap` rather than from any source above.
35
+
36
+ **Extending existing behavior.** The rule that a changed assertion must fail against the code as it stands today is the same failure applied to an edit, and carries no external source.
@@ -67,4 +67,4 @@ Report each file written or updated by its full path from the project root.
67
67
 
68
68
  ## Source material
69
69
 
70
- The position on external writing guidance sits in `${CLAUDE_SKILL_DIR}/references/source-material.md`, which states what this skill adopted, what it declined, and the measurement behind each. Read it before importing a rule from outside this corpus. Do not re-derive that position, since a second answer will disagree with the recorded one.
70
+ The position on external writing guidance sits in `${CLAUDE_SKILL_DIR}/references/adopted.md`, which states what this skill adopted, what it declined, and the measurement behind each. Read it before importing a rule from outside this corpus. Do not re-derive that position, since a second answer will disagree with the recorded one.
@@ -9,6 +9,19 @@ Each pattern below is a shape rather than a token, so no closed word set matches
9
9
 
10
10
  Work top down. The first three account for most of what a reader calls machine-written.
11
11
 
12
+ ## Contents
13
+
14
+ - [Rule of three](#rule-of-three)
15
+ - [Synonym cycling](#synonym-cycling)
16
+ - [False ranges](#false-ranges)
17
+ - [Inline-header lists](#inline-header-lists)
18
+ - [Adverb propping](#adverb-propping)
19
+ - [Passive voice with an unnamed actor](#passive-voice-with-an-unnamed-actor)
20
+ - [Closing restatement](#closing-restatement)
21
+ - [Question as transition](#question-as-transition)
22
+ - [Escalating parallel clauses](#escalating-parallel-clauses)
23
+ - [Empty superlative frames](#empty-superlative-frames)
24
+
12
25
  ## Rule of three
13
26
 
14
27
  A triad wherever a list appears, whatever the subject actually holds. Three adjectives, three clauses, three examples, over and over, until the count is the only thing a reader can predict.
@@ -5,7 +5,7 @@ description: The canvas server, its content format, and the verbs that list, arr
5
5
 
6
6
  # Canvas
7
7
 
8
- `canon canvas` serves a local canvas of pages and HTML frames and gives an agent the verbs the operator's mouse has. A page is a folder under `.canon/canvas/`, a frame is one HTML file in it, and `layout.json` beside them holds each frame's box.
8
+ `canon canvas` serves a local canvas of pages and HTML frames and gives an agent the verbs the operator's mouse has. A page is a folder under `.canon/canvas/`, a frame is one HTML file in it, and `layout.json` beside them holds each frame's box. `standards/canvas.md` fixes that content format, and the `canvas` skill carries the procedure a session follows to drive the canvas.
9
9
 
10
10
  ## Serve
11
11
 
@@ -39,7 +39,9 @@ A pick is refused as `address-mismatch` when the browser builds elements the fil
39
39
 
40
40
  `text` replaces the text of an element holding text alone and refuses one holding other elements as `not-text-only`. Every byte outside the element stays as it was. `--json` emits the page, frame, file, path, and the file's new `hash`.
41
41
 
42
- The operator edits the same set from the inspector, which posts to the same writer with the hash the frame was served with. An edit made against a version of the file Claude has since rewritten is refused as `stale-address` and the shell reloads the frame, so nothing lands on a shifted element. A color picked from the project's tokens is written as `var(--<name>)`. Both writers take the frame file's lock, and an edit that keeps the selected element where it was keeps the selection fresh.
42
+ The operator edits the same set from the inspector, which posts to the same writer with the hash the frame was served with. An edit made against a version of the file Claude has since rewritten is refused as `stale-address` and the shell reloads the frame, so nothing lands on a shifted element. Both writers take the frame file's lock, and an edit that keeps the selected element where it was keeps the selection fresh.
43
+
44
+ A color picked from the project's tokens is written as `var(--<name>)`, and below full opacity as a `color-mix()` of that `var()` with `transparent` at the opacity percent, so it keeps following the theme. A browser older than 2023 drops a `color-mix()` value, which leaves the property unset there. A hex is written as six digits, or eight below full opacity.
43
45
 
44
46
  Inline style beats a class, so an operator's edit masks a class change Claude makes later. Read a frame's inline styles before restyling it, and remove one the change should replace. The inspector marks a color or background set inline with the word `raw` when its value names no `var()` and is not `currentColor` or a CSS-wide keyword, since that value stays fixed when the theme changes. A property outside the set, or a value carrying `;`, `{`, `}`, `<`, `>`, or a line break, is refused as `invalid-edit`.
45
47
 
@@ -44,7 +44,7 @@ Neither digest is ever written by hand. A digest is what the gate compares, so a
44
44
 
45
45
  Each source renders at `deviceScaleFactor` 2 with a transparent background, and the success line reports the pixel dimensions the element wrapped to. Size is reported and never asserted. The height of a terminal frame is whatever its text wrapped to at a fixed width, so pinning that number would harden an accident.
46
46
 
47
- What is asserted is the font. The command reads the first family the captured element declares and fails when the browser did not resolve it, because a fallback face rewraps the block and silently changes the output. Sources therefore name a real font rather than relying on `monospace`. A source that cannot render reports its own line and exits 1 without dropping the rest of the batch.
47
+ What is asserted is the font. The command reads the first family of each rendered element holding text inside the captured element, and fails when the browser did not resolve one of them, because a fallback face rewraps the block and silently changes the output. A generic keyword as the first family, such as `monospace` or `system-ui`, also fails with its own reason, since it resolves to a different font on each machine, though a quoted `"monospace"` is a family name and goes through the probe. An element holding no text skips the check, since there is nothing to rewrap. Sources therefore name a real font rather than relying on `monospace`. A source that cannot render reports its own line and exits 1 without dropping the rest of the batch.
48
48
 
49
49
  A run refuses before it reads anything else when `--selector` is absent, naming the flag and pointing at `--help`. Ordering it first is what keeps the message about the invocation: the source defaults to `assets`, so checking that first would answer `assets not found` from whatever directory the caller happened to be in and say nothing about the flag that was actually missing. That source default is a deliberate choice rather than one inherited from some general convention: a missing folder still refuses loud, naming the argument, once the flag check passes, where the selector's dropped default cropped the wrong region in silence.
50
50