@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
@@ -14,7 +14,16 @@ its own reply, so skip the comment in step 7. The push and body sync still run.
14
14
 
15
15
  - If `git branch --show-current` returns `main`, stop: `❌ On main. Switch to a PR branch first.`
16
16
  - If `git status --porcelain` is empty, stop: `❌ No changes to ship.`
17
- - If `gh pr view --json state -q '.state' 2>/dev/null` is not `OPEN`, stop: `❌ No open PR for this branch. Use git-ship to open one.`
17
+ - Resolve `<number>` once, here, through `canon pr head --json`, reading the record's `number`. Every later step names this `<number>` rather than resolving it again. A record refusing with `ambiguous-pull` stops: `❌ Two open PRs on this branch. Name the one to update.`
18
+ - When no record comes back, or it carries no `number`, read the open pull requests on the branch over REST instead. The branch is encoded so a `/` in its name reaches the query as `%2F`:
19
+
20
+ ```bash
21
+ gh api "repos/{owner}/{repo}/pulls?head={owner}:$(git branch --show-current | jq -Rr @uri)&state=open" --jq '.[].number'
22
+ ```
23
+
24
+ Nothing printed stops: `❌ No open PR for this branch. Use git-ship to open one.` More than one number takes the `ambiguous-pull` stop above rather than the first, since two open pull requests on one head against different bases leave nothing to say which one this push updates.
25
+
26
+ Every read and write in this skill runs on REST, since the `gh pr` subcommands run on GraphQL and a cloud session's GitHub proxy refuses it. A cloud worker answering a review reaches this skill through `review-address`, so one GraphQL call here breaks that whole leg.
18
27
 
19
28
  A missing tracking ref is no longer a guard. An open pull request proves the branch reached the remote, and a branch created by worktree entry has been measured carrying an open pull request with no tracking ref, where the old guard reported that the branch still needed pushing. That message describes a state the branch is not in, so the run stopped on a diagnosis nobody could act on. Step 4 sets the ref instead.
20
29
 
@@ -29,21 +38,21 @@ A missing tracking ref is no longer a guard. An open pull request proves the bra
29
38
 
30
39
  The lease is what stops the force from overwriting a commit this session never read. Run the ancestry test only where an upstream resolves, since it reads `@{u}`.
31
40
 
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.
41
+ 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` and read `reason` on the record rather than the exit code.
33
42
  - `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.
43
+ - `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 api -X POST repos/{owner}/{repo}/issues/<number>/comments -F body=@<main-root>/.canon/tmp/pr/evidence/body-<number>.md --silent` 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
44
  - Any other reason is one of the mirrored git refusals (`gh-missing`, `gh-failed`, `no-base`, `unreadable-changes`, `would-empty`). Report it and continue without stopping the chain.
36
45
 
37
46
  After an `ok` post, read what the pull request is still owed with `canon pr evidence <number> --check --json`, branching on `reason` and `owed` rather than the exit. When `owed` lists `preview`, mint it through the preview section of `canon:git-pr`'s `references/evidence.md`, then re-read once. Report anything but `settled` after that re-read and continue, since a refused deploy would otherwise loop. A binary lacking `--check` reports the thread unread rather than settled. A preview already on the comment counts as settled whatever head built it, so a later push does not re-mint one.
38
47
 
39
- 6. Check for existing review comments: `gh api 'repos/{owner}/{repo}/pulls/<number>/comments' --jq 'length'`, resolving `<number>` from `gh pr view --json number`.
48
+ 6. Check for existing review comments: `gh api 'repos/{owner}/{repo}/pulls/<number>/comments' --jq 'length'`.
40
49
  7. Sync the body and title on every invocation, before the routing below decides on the reply.
41
- - Run `gh pr view --json url,title,body` and update the body with `gh pr edit --body` when the new commit changes scope, and the title with `gh pr edit --title` when the scope shifted enough to make it inaccurate.
50
+ - Read the live title and body with `gh api repos/{owner}/{repo}/pulls/<number> --jq '{html_url, title, body}'`. Update the body with `gh api -X PATCH repos/{owner}/{repo}/pulls/<number> -F body=@<file> --silent` when the new commit changes scope, writing the new body to `<file>` first, and the title with `gh api -X PATCH repos/{owner}/{repo}/pulls/<number> -f title="<title>" --silent` when the scope shifted enough to make it inaccurate.
42
51
  - A fix commit answering a review changes what shipped exactly as much as an ordinary followup does, so the sync cannot wait on the invocation or the comment count below.
43
52
  - A body a person edited by hand between rounds gets no special handling: judge it against the tree the same way regardless of who wrote it last, since a hand-edit the fix commit has made stale is the exact drift this sync exists to close.
44
53
  8. Route on the invocation and the comment count for the reply alone.
45
54
  - When invoked with `reply-owned`, skip this step: the caller posts its own reply.
46
- - Otherwise, if the count is above zero, the followup addresses review feedback: post a one-line summary of the fix with `gh pr comment --body`, first running the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against it, since the hook does not see an inline comment body. The `pull_request` check the git-pr surface carries triggers on a push or an open rather than a plain edit, so neither this comment nor the title and body step 7 synced reaches it.
55
+ - Otherwise, if the count is above zero, the followup addresses review feedback: post a one-line summary of the fix with `gh api -X POST repos/{owner}/{repo}/issues/<number>/comments -f body="<summary>" --silent`, first running the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md` against it, since the hook does not see an inline comment body. The `pull_request` check the git-pr surface carries triggers on a push or an open rather than a plain edit, so neither this comment nor the title and body step 7 synced reaches it.
47
56
  - If it is zero, nothing further runs. The sync in step 7 already did this branch's job.
48
57
 
49
58
  ## After completion
@@ -53,3 +62,5 @@ Output one line:
53
62
  ```plaintext
54
63
  ✅ Followup shipped: <pr-url>
55
64
  ```
65
+
66
+ Take `<pr-url>` off the `html_url` step 7 read.
@@ -1,5 +1,15 @@
1
1
  # Issue reference
2
2
 
3
+ ## Contents
4
+
5
+ - [Scope](#scope)
6
+ - [Title](#title)
7
+ - [Labels](#labels)
8
+ - [Content](#content)
9
+ - [Sections](#sections)
10
+ - [Formatting](#formatting)
11
+ - [Examples](#examples)
12
+
3
13
  ## Scope
4
14
 
5
15
  Governs a tracker issue: its title, its labels, and the sections its body carries.
@@ -7,6 +7,14 @@ description: How git-pr finds the UI checklist and the local server, posts the e
7
7
 
8
8
  The rendered-surface step of `git-pr`, reached after the final command printed the pull request number. The step runs on every pull request, with no test on the diff ahead of it. `no-evidence` is the silent branch, so a pull request that changes no rendered surface posts nothing.
9
9
 
10
+ ## Contents
11
+
12
+ - [Find the UI checklist](#find-the-ui-checklist)
13
+ - [Find the local server](#find-the-local-server)
14
+ - [Post the evidence comparison](#post-the-evidence-comparison)
15
+ - [Post the UI checklist alone](#post-the-ui-checklist-alone)
16
+ - [Post the preview address](#post-the-preview-address)
17
+
10
18
  ## Find the UI checklist
11
19
 
12
20
  `ui-checklist` writes a visual 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 skill 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 the two steps below each skip their checklist half.
@@ -53,9 +61,11 @@ Read `reason` on the record rather than the exit code.
53
61
  - `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:
54
62
 
55
63
  ```bash
56
- gh pr comment <number> --body-file <main-root>/.canon/tmp/pr/evidence/body-<number>.md
64
+ gh api -X POST repos/{owner}/{repo}/issues/<number>/comments -F body=@<main-root>/.canon/tmp/pr/evidence/body-<number>.md --silent
57
65
  ```
58
66
 
67
+ Both posts in this file go through the REST issue-comments endpoint, since the `gh pr` subcommands run on GraphQL and a cloud session's GitHub proxy refuses it. `git-followup` mints a preview through this file, so a GraphQL post here would break the cloud review-address leg.
68
+
59
69
  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 `@`, which needs the typed-field flag `-F` because the raw-string flag `-f` posts the path itself as the body:
60
70
 
61
71
  ```bash
@@ -71,7 +81,7 @@ Any other `reason` is one of the mirrored git refusals (`gh-missing`, `gh-failed
71
81
  Run this step only as the fallback, when the evidence step above reported one of the git refusals and a checklist file exists, since every other branch with a checklist rendered `ok` and carried it. The comment this posts carries no marker, so a later `canon pr evidence` call reports no `checklist` for it. Post it as its own comment on `<number>`:
72
82
 
73
83
  ```bash
74
- gh pr comment <number> --body-file <main-root>/.canon/tmp/handoff/ui-checklist/<slug>.md
84
+ gh api -X POST repos/{owner}/{repo}/issues/<number>/comments -F body=@<main-root>/.canon/tmp/handoff/ui-checklist/<slug>.md --silent
75
85
  ```
76
86
 
77
87
  Run the cleanup below only once the call that carried the checklist reports success, whichever of the two steps that was. 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.
@@ -5,6 +5,18 @@ description: Path-to-label map format, prefix matching against the changed set,
5
5
 
6
6
  # Label reference
7
7
 
8
+ ## Contents
9
+
10
+ - [Scope](#scope)
11
+ - [Map format](#map-format)
12
+ - [Paths a map declines to label](#paths-a-map-declines-to-label)
13
+ - [Reporting a surface no row reaches](#reporting-a-surface-no-row-reaches)
14
+ - [Matching](#matching)
15
+ - [Applying](#applying)
16
+ - [A label the remote does not carry](#a-label-the-remote-does-not-carry)
17
+ - [Release pull requests](#release-pull-requests)
18
+ - [Labels at run time](#labels-at-run-time)
19
+
8
20
  ## Scope
9
21
 
10
22
  Governs the labels a pull request carries: the map a project declares, how a changed path resolves to a label, and what a label missing from the remote costs.
@@ -86,7 +86,8 @@ git switch -c <branch> main && git cherry-pick <sha> <sha> \
86
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 \
89
+ ) && gh api -X POST 'repos/{owner}/{repo}/pulls' -f title="<title>" -f head=<branch> -f base=main -F body=@.canon/tmp/pr/split/<branch>.md --jq .html_url \
90
+ && printf 'opened=true\n' \
90
91
  && rm .canon/tmp/pr/split/<branch>.md
91
92
 
92
93
  # Return to primary branch, push, and open its PR
@@ -94,13 +95,18 @@ git checkout <new_name> && git push -u origin <new_name> \
94
95
  && (cat <<'BODY' > .canon/tmp/pr/split/<new_name>.md
95
96
  <body following pr.md template, written from the primary's commits>
96
97
  BODY
97
- ) && gh pr create --title "<title>" --body-file .canon/tmp/pr/split/<new_name>.md \
98
+ ) && gh api -X POST 'repos/{owner}/{repo}/pulls' -f title="<title>" -f head=<new_name> -f base=main -F body=@.canon/tmp/pr/split/<new_name>.md --jq .html_url \
99
+ && printf 'opened=true\n' \
98
100
  && rm .canon/tmp/pr/split/<new_name>.md
99
101
 
100
102
  # Clean up the body-file dir if all PRs succeeded (no-op when non-empty)
101
103
  rmdir .canon/tmp/pr/split 2>/dev/null || true
102
104
  ```
103
105
 
106
+ Every pull request opens through the REST pulls endpoint rather than the `gh pr` create subcommand, which runs on GraphQL and fails where a cloud session's GitHub proxy refuses it. The endpoint requires `base`, so each call names the branch its layer sits on. `-F` reads the body file, where `-f` would post the path itself.
107
+
108
+ A create the endpoint refuses, such as one for a head that already has an open pull request, exits non-zero and stops the chain before `opened=true` prints and before the body file is removed. The `opened=true` line is what a pull request creation log reads to count the opening, since the REST call prints a URL and never names itself.
109
+
104
110
  For stacked mode, base each branch on the previous and cherry-pick only that group's commits:
105
111
 
106
112
  ```bash
@@ -115,7 +121,8 @@ git switch -c <branch-1> main && git cherry-pick <g1-sha> <g1-sha> \
115
121
  && (cat <<'BODY' > .canon/tmp/pr/split/<branch-1>.md
116
122
  <body following pr.md template, written from the cherry-picked commits>
117
123
  BODY
118
- ) && gh pr create --title "<title>" --body-file .canon/tmp/pr/split/<branch-1>.md \
124
+ ) && gh api -X POST 'repos/{owner}/{repo}/pulls' -f title="<title>" -f head=<branch-1> -f base=main -F body=@.canon/tmp/pr/split/<branch-1>.md --jq .html_url \
125
+ && printf 'opened=true\n' \
119
126
  && rm .canon/tmp/pr/split/<branch-1>.md
120
127
 
121
128
  # Group 2: based on <branch-1>, this group's commits only
@@ -124,15 +131,17 @@ git checkout -b <branch-2> && git cherry-pick <g2-sha> <g2-sha> \
124
131
  && (cat <<'BODY' > .canon/tmp/pr/split/<branch-2>.md
125
132
  <body following pr.md template, written from the cherry-picked commits>
126
133
  BODY
127
- ) && gh pr create --title "<title>" --body-file .canon/tmp/pr/split/<branch-2>.md \
134
+ ) && gh api -X POST 'repos/{owner}/{repo}/pulls' -f title="<title>" -f head=<branch-2> -f base=<branch-1> -F body=@.canon/tmp/pr/split/<branch-2>.md --jq .html_url \
135
+ && printf 'opened=true\n' \
128
136
  && rm .canon/tmp/pr/split/<branch-2>.md
129
137
 
130
- # Return to primary branch, push, and open its PR
138
+ # Return to primary branch, push, and open its PR on the last group's branch
131
139
  git checkout <new_name> && git push -u origin <new_name> \
132
140
  && (cat <<'BODY' > .canon/tmp/pr/split/<new_name>.md
133
141
  <body following pr.md template, written from the primary's commits>
134
142
  BODY
135
- ) && gh pr create --title "<title>" --body-file .canon/tmp/pr/split/<new_name>.md \
143
+ ) && gh api -X POST 'repos/{owner}/{repo}/pulls' -f title="<title>" -f head=<new_name> -f base=<branch-2> -F body=@.canon/tmp/pr/split/<new_name>.md --jq .html_url \
144
+ && printf 'opened=true\n' \
136
145
  && rm .canon/tmp/pr/split/<new_name>.md
137
146
 
138
147
  # Clean up the body-file dir if all PRs succeeded (no-op when non-empty)
@@ -145,6 +154,8 @@ Respond with exactly one line:
145
154
 
146
155
  `✅ Renamed: <old> → <new> | PRs: <primary-url>, <url1>, <url2>`
147
156
 
157
+ Read each URL off the line its create printed through `--jq .html_url`.
158
+
148
159
  Do not add any other text.
149
160
 
150
161
  ## Stacked merge loop
@@ -153,10 +164,10 @@ When the user signals the previous stacked PR has merged, restack the next one.
153
164
 
154
165
  1. Rebase and push. The own-commit-count comes from the original split table.
155
166
  `git fetch origin main && git checkout <branch> && git rebase --onto origin/main HEAD~<own-commit-count> && git push --force-with-lease`
156
- 2. Verify the PR's base auto-retargeted to main with `gh pr view <num> --json baseRefName`. If not, `gh pr edit <num> --base main`.
167
+ 2. Verify the PR's base auto-retargeted to main with `gh api repos/{owner}/{repo}/pulls/<num> --jq .base.ref`, a REST read a cloud session's GitHub proxy serves where it refuses GraphQL. If not, retarget it with `gh api -X PATCH repos/{owner}/{repo}/pulls/<num> -f base=main --silent`.
157
168
  3. Reply: `✅ <branch> rebased onto main. Ready for squash-merge.`
158
169
 
159
170
  Edge cases:
160
171
 
161
172
  - If this branch's commits modify a file added by an unmerged upstream PR, wait for that PR to merge before rebasing.
162
- - If the PR was orphaned by base-branch deletion, recreate it with `gh pr create --base main --head <branch>` and a regenerated body.
173
+ - If the PR was orphaned by base-branch deletion, recreate it with the same create the final commands run, passing `-f head=<branch> -f base=main` and a regenerated body, and print `opened=true` after it.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: index-lookup
3
- description: Answers a topic search over every tracked index.md catalog in the project by running `canon indexes list --json` and matching the topic against each entry's title, description, and path, then reports the hits or names why there are none. Names the gitignored task, memory, and diagram catalogs and the groundwork and intake record folders as pointers outside its walked corpus, so it never claims a topic is undocumented when the topic only sits there. Use when asked "where is X documented", "is there a page about X", "search the docs for X", "find the index entry for X", or "what covers X". Do NOT use to grep source code, browse a file whose path is already known, or regenerate an index, which is `canon indexes regen`.
3
+ description: Answers a topic search over every tracked index.md catalog in the project by running `canon indexes list --json` and matching the topic against each entry's title, description, and path, then reports the hits or names why there are none. Names the gitignored task and memory catalogs and the groundwork and intake record folders as pointers outside its walked corpus, so it never claims a topic is undocumented when the topic only sits there. Use when asked "where is X documented", "is there a page about X", "search the docs for X", "find the index entry for X", or "what covers X". Do NOT use to grep source code, browse a file whose path is already known, or regenerate an index, which is `canon indexes regen`.
4
4
  ---
5
5
 
6
6
  # Index lookup
@@ -13,7 +13,7 @@ Answers a topic search over the project's tracked `index.md` catalogs, not a tex
13
13
  2. Run `canon indexes list --json` from that root.
14
14
  3. Read `errors` first. A folder failing frontmatter validation drops out of `entries` and lands its message in `errors` instead, per the verb's own per-folder isolation, so report an error alongside the match rather than reading it as a reason to stop.
15
15
  4. Match the topic case-insensitively as a substring against each entry's `title`, `description`, and `path`. Report every match. A lookup with no ranking is honest about what it found, and a single best guess is not.
16
- 5. Check whether the project carries a gitignored indexed folder or a README-based record catalog outside `list`'s walk: `.canon/tasks/`, `.canon/memory/`, `.canon/diagrams/`, `.canon/groundwork/`, `.canon/intake/`. Test each for existence and at least one file inside it before naming it. A folder absent from this project names nothing.
16
+ 5. Check whether the project carries a gitignored indexed folder or a README-based record catalog outside `list`'s walk: `.canon/tasks/`, `.canon/memory/`, `.canon/groundwork/`, `.canon/intake/`. Test each for existence and at least one file inside it before naming it. A folder absent from this project names nothing.
17
17
  6. Report the hits, plus a pointer to any outside-the-walk folder found present, per Output below.
18
18
 
19
19
  ## Rules
@@ -2,6 +2,15 @@
2
2
 
3
3
  Governs the proposal `markdown-propose` writes before it edits anything. The concern being screened is an input and does not change the format.
4
4
 
5
+ ## Contents
6
+
7
+ - [Folder](#folder)
8
+ - [Change format](#change-format)
9
+ - [The answer contract](#the-answer-contract)
10
+ - [Applying](#applying)
11
+ - [After](#after)
12
+ - [Retrieval](#retrieval)
13
+
5
14
  ## Folder
6
15
 
7
16
  - One folder per screening pass at `.canon/proposals/<nn>-<slug>/`, where the slug names the subject rather than the activity
@@ -7,6 +7,13 @@ description: The Challenge, Discuss, Apply, and Cleanup phases of memory-review,
7
7
 
8
8
  The phases of `memory-review` after Propose. The session reads this file when the phase table in the skill picks Challenge, Discuss, Apply, or Cleanup, and reads only the section for that phase. The skill's Guards still hold for every phase here.
9
9
 
10
+ ## Contents
11
+
12
+ - [Challenge phase](#challenge-phase)
13
+ - [Discuss phase](#discuss-phase)
14
+ - [Apply phase](#apply-phase)
15
+ - [Cleanup phase](#cleanup-phase)
16
+
10
17
  ## Challenge phase
11
18
 
12
19
  Trigger: user says "challenge the promotes", "challenge before apply", or asks for a high-bar pass. Run before Apply. No mutations to memory files or promotion targets. Review file only.
@@ -10,19 +10,13 @@ the PR from an independent session. This skill consumes them: fix, reply, push.
10
10
 
11
11
  ## Guards
12
12
 
13
- - If no open PR resolves for the current branch via `gh pr view`, stop: `❌ No open PR. Nothing to address.`
13
+ - Resolve `<number>` and `<tip>` off `canon pr head --json`. An `ambiguous-pull` refusal stops: `❌ Two open PRs on this branch. Name the one to address.` A record with no `number` takes the lookup in `${CLAUDE_SKILL_DIR}/references/rest-reads.md`. Nothing resolving stops: `❌ No open PR. Nothing to address.`
14
14
  - If the PR has no review comments or threads, run step 5's staleness test before deciding. A branch that still merges stops here: `✅ No review findings to address.` One that does not skips steps 1 through 4 and runs step 5 onward, since a branch goes stale from `main` moving and a closed review says nothing about whether it still merges.
15
15
  - Fix findings. Do not merge.
16
16
 
17
17
  ## Step 1: pull the review findings and CI status
18
18
 
19
- Read the review comments and threads on the PR:
20
-
21
- ```bash
22
- gh pr view --json number,reviews,comments
23
- ```
24
-
25
- For inline review comments, read them via `gh api` on the PR's review comments. Collect each finding with its file, location, and body.
19
+ Read the reviews, the thread's comments, and the inline review comments through the three reads in `${CLAUDE_SKILL_DIR}/references/rest-reads.md`. Collect each finding with its file, location, and body.
26
20
 
27
21
  Also read the CI check status so the fixes cover failing checks, not only review comments:
28
22
 
@@ -30,7 +24,7 @@ Also read the CI check status so the fixes cover failing checks, not only review
30
24
  canon pr checks <number> --json
31
25
  ```
32
26
 
33
- Read the verdict off the record's `state` rather than off the exit. Treat `failing` as a finding to resolve alongside the review comments. A `pending` covers a tip whose runs have yet to conclude and a tip carrying no run at all, which the record separates on `matched`, and neither is a green to continue on. A record carrying `conflicted: true` is a third case: the branch conflicts with its base, so no run will start and the wait never ends. Stop and report it, since a rebase is the repair. Fall back to `gh pr checks <number>` when no record comes back at all, which is a target whose CLI predates the verb.
27
+ Read the verdict off the record's `state` rather than off the exit. Treat `failing` as a finding to resolve alongside the review comments. A `pending` covers a tip whose runs have yet to conclude and a tip carrying no run at all, which the record separates on `matched`, and neither is a green to continue on. A record carrying `conflicted: true` is a third case: the branch conflicts with its base, so no run will start and the wait never ends. Stop and report it, since a rebase is the repair. When no record comes back at all, read the check runs on the tip through `${CLAUDE_SKILL_DIR}/references/rest-reads.md`.
34
28
 
35
29
  ## Step 2: address each finding
36
30
 
@@ -160,12 +154,11 @@ canon labels scan --body-file .canon/tmp/pr/reply/reply-<number>.md
160
154
 
161
155
  The hook skips `.canon/tmp/`, so this scan is the only gate on the published
162
156
  reply. Fix any hit the standard names, then post the reply to the PR and
163
- capture the posted comment's id, since Step 7 edits this exact comment rather
157
+ keep the `id` the POST answers with, since Step 7 edits this exact comment rather
164
158
  than trusting whichever one `gh` considers last:
165
159
 
166
160
  ```bash
167
- comment_url=$(gh pr comment <number> --body-file .canon/tmp/pr/reply/reply-<number>.md)
168
- echo "${comment_url##*issuecomment-}" > .canon/tmp/pr/reply/reply-<number>.id
161
+ gh api -X POST 'repos/{owner}/{repo}/issues/<number>/comments' -F body=@.canon/tmp/pr/reply/reply-<number>.md --jq .id > .canon/tmp/pr/reply/reply-<number>.id
169
162
  ```
170
163
 
171
164
  ## Step 7: confirm resolution
@@ -252,5 +245,5 @@ Not everything worth reaching the reviewing session surfaces inside the numbered
252
245
  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.
253
246
 
254
247
  ```bash
255
- gh pr comment <number> --body-file .canon/tmp/pr/reply/reply-<number>.md
248
+ gh api -X POST 'repos/{owner}/{repo}/issues/<number>/comments' -F body=@.canon/tmp/pr/reply/reply-<number>.md --silent
256
249
  ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: REST reads
3
+ description: The reads review-address runs over REST, being the findings on the pull request, plus the pull request lookup and the check runs it falls back to when the installed canon predates pr head or pr checks
4
+ ---
5
+
6
+ # REST reads
7
+
8
+ The guard and Step 1 of `review-address`. The session reads this file on reaching Step 1, and from the guard when `canon pr head --json` hands back no `number`. Every read runs on REST, since the `gh pr` subcommands run on GraphQL and a cloud session's GitHub proxy refuses it. A cloud worker answering a review runs this skill, so one GraphQL call breaks the whole leg.
9
+
10
+ ## The findings
11
+
12
+ Read the reviews, the thread's comments, and the inline review comments:
13
+
14
+ ```bash
15
+ gh api --paginate 'repos/{owner}/{repo}/pulls/<number>/reviews?per_page=100' --jq '.[] | {id, user: .user.login, state, commit_id, submitted_at, body} | @json'
16
+ gh api --paginate 'repos/{owner}/{repo}/issues/<number>/comments?per_page=100' --jq '.[] | {id, user: .user.login, created_at, body} | @json'
17
+ gh api --paginate 'repos/{owner}/{repo}/pulls/<number>/comments?per_page=100' --jq '.[] | {id, user: .user.login, path, line, body} | @json'
18
+ ```
19
+
20
+ Each read prints one JSON line per row, oldest first across every page. A multi-line body stays inside its own row, and the newest review is the last line of the first read rather than the last row of its first page. `--paginate` runs the filter once per page, so never reduce inside it with `last` or `max`.
21
+
22
+ ## The pull request on the branch
23
+
24
+ The guard's fallback, for a target whose CLI predates `canon pr head`. Read the open pull requests on the branch and the head of the one it finds:
25
+
26
+ ```bash
27
+ gh api "repos/{owner}/{repo}/pulls?head={owner}:$(git branch --show-current | jq -Rr @uri)&state=open" --jq '.[].number'
28
+ gh api repos/{owner}/{repo}/pulls/<number> --jq .head.sha
29
+ ```
30
+
31
+ The branch is encoded so a `/` in its name reaches the query as `%2F`. Written bare, it resolves nothing and reads as no open pull request.
32
+
33
+ Nothing printed takes the guard's no-PR stop. More than one number takes its `ambiguous-pull` stop rather than the first, since two open pull requests on one head against different bases leave nothing to say which one the review is on.
34
+
35
+ ## The check runs on the tip
36
+
37
+ Step 1's fallback, for a target whose CLI predates `canon pr checks`:
38
+
39
+ ```bash
40
+ gh api --paginate 'repos/{owner}/{repo}/commits/<tip>/check-runs?per_page=100' --jq '.check_runs[] | {name, status, conclusion} | @json'
41
+ ```
42
+
43
+ A run whose `status` is not `completed` is pending. A tip listing no run at all is pending too rather than green, the same reading `canon pr checks` gives it.
@@ -12,7 +12,13 @@ The diff touches a file that paints: markup, a stylesheet, a component or templa
12
12
 
13
13
  Run this before any other section, since a pass that finds nothing to open otherwise reads the stylesheet instead and reports as if it had looked. Read its answer before composing any report or body, never in the same batch as the write, since what it decides is a finding the report has to carry.
14
14
 
15
- Read `gh pr view <number> --json comments,headRefOid` and take the comment whose body carries a `<!-- pr-evidence: head=<sha> -->` marker. Match the marker, never the first line, since the body opens with a `**Preview:**` line when the project has a preview address.
15
+ Read the thread over REST, which a cloud session's GitHub proxy serves where it refuses GraphQL, and take the head off `canon pr head <number> --json`, from the record's `tip`:
16
+
17
+ ```bash
18
+ gh api --paginate 'repos/{owner}/{repo}/issues/<number>/comments?per_page=100' --jq '.[] | select((.body // "") | contains("<!-- pr-evidence: head=")) | .body'
19
+ ```
20
+
21
+ Take the comment whose body carries a `<!-- pr-evidence: head=<sha> -->` marker. Match the marker, never the first line, since the body opens with a `**Preview:**` line when the project has a preview address.
16
22
 
17
23
  Flag the change as shipping no screenshot when it paints and the pull request carries no marked comment, carries one with no `## Evidence` section, or carries only a checklist posted alone. Then decide how to raise it by whether the project keeps captures, which is the same test `canon pr evidence` applies:
18
24
 
@@ -27,6 +27,7 @@ Scoping a later pass off GitHub's review stamp skipped real work. On 2026-09-07
27
27
  - Post the closing pass even with nothing to report, since a review left unanswered reads as one nobody closed
28
28
  - State a withdrawal or a regrade on the thread with the fact that settled it, since a finding dropped in silence reads the same as one nobody answered
29
29
  - Key the body file on the pull request number and the head commit, and on the response it answers once the head repeats, so no two passes overwrite each other
30
+ - Link a file-bound finding to its line at the reviewed head, since a bare file block leaves a reader hunting the diff for the spot
30
31
  - Scan the comment for banned characters and internal phase labels before posting, since a finding phrased against a phase label reaches a reader with no task board
31
32
  - Answer a `## For the reviewer` bullet in the body, bounded to the section itself rather than the Summary or the Technical Context around it, so the independent vantage stays clear of the author's framing
32
33
  - Confirm each `**Review focus:**` item the plan names on a first pass, and turn one the pass cannot confirm into a finding, since the planning session named the breaking inputs before the diff existed and no other reader holds that list
@@ -30,7 +30,7 @@ reader scanning the thread finds the current verdict where the last one sat.
30
30
 
31
31
  ## Guards
32
32
 
33
- - If no open PR resolves for the target branch via `gh pr view`, stop: `❌ No open PR to review. Open one first, or use /review-branch for local changes.`
33
+ - If no open PR resolves for the target branch via `canon pr head --json`, stop: `❌ No open PR to review. Open one first, or use /review-branch for local changes.`
34
34
  - Review and post. Do not merge. Merging is the human's gate.
35
35
 
36
36
  ## Step 1: resolve the PR and read context
@@ -43,9 +43,15 @@ date -u +%Y-%m-%dT%H:%M:%SZ
43
43
 
44
44
  Everything from that line to the post is the compose window, and a commit pushed inside it is one this pass never saw. Stamping the body with the instant the window opened is what leaves that commit outside the covered range, so the next pass reads it rather than assuming it covered. Taking the stamp later, at the head resolution below or at Step 4 where the body is composed, claims a stretch this pass had already stopped reading through.
45
45
 
46
- Then resolve the PR: `gh pr view --json number,headRefName,headRefOid,title,body` for the current branch, or use a PR number the user names. Take `<headRefOid>` from `canon pr head <number> --json`, off that record's `tip`, and fall back to the `headRefOid` field above when no record comes back, which is a target whose CLI predates the verb. The first seven characters are `<short-sha>`, which names the body file in Step 4.
46
+ Then resolve the PR through `canon pr head --json` for the current branch, or `canon pr head <number> --json` for a number the user names. Take `<number>` off the record's `number` and `<headRefOid>` off its `tip`. The first seven characters are `<short-sha>`, which names the body file in Step 4. Read the title and body over REST:
47
47
 
48
- `<headRefOid>` and `<read-at>` travel together into Step 4's marker, and neither is re-derived after this point. Re-reading the head later in the pass would name a commit this pass did not review, which is the defect the marker exists against, reached from the inside.
48
+ ```bash
49
+ gh api repos/{owner}/{repo}/pulls/<number> --jq '{title, body, head: .head.sha, repo: .base.repo.html_url}'
50
+ ```
51
+
52
+ Every read in this skill 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. Fall back there to the `head` field above for `<headRefOid>`, and to `gh api "repos/{owner}/{repo}/pulls?head={owner}:<branch>&state=open" --jq '.[].number'` for a branch with no number named, stopping on the guard above when that prints anything but one number.
53
+
54
+ `<headRefOid>` and `<read-at>` travel into Step 4's marker, and `<headRefOid>` with the `repo` field as `<repo-url>` into its finding links. None is re-derived after this point. Re-reading the head later in the pass would name a commit this pass did not review, which is the defect the marker exists against, reached from the inside.
49
55
 
50
56
  Read these in parallel from the project root, skipping any that do not exist:
51
57
 
@@ -9,10 +9,10 @@ Part of Step 4 of `review-pr`, reached only on the branch `post.md` names. Every
9
9
 
10
10
  When `<prior-heading>` from Step 2 reads `## Review closed` and this pass carries nothing owed, the thread already holds this verdict. Replace the standing close-out rather than posting a second one beside it.
11
11
 
12
- Resolve its numeric id. `gh pr view --json reviews` carries a GraphQL node id under `id`, which no REST route accepts, so read the id off the REST listing instead:
12
+ Resolve its numeric id off the REST listing, keeping the newest match across every page:
13
13
 
14
14
  ```bash
15
- gh api repos/{owner}/{repo}/pulls/<number>/reviews --jq '[.[] | select((.body // "") | split("\n")[0] | rtrimstr("\r") == "## Review closed")] | last | .id'
15
+ gh api --paginate 'repos/{owner}/{repo}/pulls/<number>/reviews?per_page=100' --jq '.[] | select((.body // "") | split("\n")[0] | rtrimstr("\r") == "## Review closed") | .id' | tail -n 1
16
16
  ```
17
17
 
18
18
  Write the replacement body to `<body-file>`, the name `post.md` already derived at the top of Step 4, keeping the heading and the footer and naming what this pass covered on the scope line in place of what the old one covered. Derive that name the same way whichever path reached here, since the guard reads `<prior-heading>` alone and a repeated head resolves the third segment as usual. The folder then gains a record of every covered head rather than losing the one the standing comment named. Then replace the comment:
@@ -13,6 +13,16 @@ Derive both segments from Step 1. Never pick a suffix by hand, and never reuse a
13
13
 
14
14
  When `<prior-oid>` from Step 2 equals `headRefOid`, the head repeats and the folder already holds `body-<number>-<short-sha>.md`. Add a third segment taking the id of the reply Step 2 resolved, giving `body-<number>-<short-sha>-r<comment-id>.md`, which is `<body-file>` on that path. That satisfies both prohibitions above rather than carving an exception into either. Step 2 already stopped the pass when that resolution came back empty, so reaching this line means the comment id is in hand.
15
15
 
16
+ A finding bound to a line links to it, so a reader skips the hunt:
17
+
18
+ ```markdown
19
+ - **should-fix** ([line 12](<repo-url>/blob/<headRefOid>/<path>#L12)): what breaks and the fix
20
+ ```
21
+
22
+ A finding that spans lines anchors to the span as `#L12-L14`, with no spaces and no en dash, and a single-line finding anchors as `#L12`. Take the line number from the file read at `<headRefOid>`, `git show <headRefOid>:<path>`, never from another checkout. The URL carries the full 40-character `<headRefOid>`, never `<short-sha>`, `HEAD`, or the branch name, so the link survives a push. A later pass restating an open finding links it again at its own `<headRefOid>` rather than copying the old bullet.
23
+
24
+ Four findings take the plain bullet with no link: a `**PR body**` entry, a finding on a file the head deleted, a finding on a line the diff removed from a file that survives, since the head holds no number for it, and a finding about a whole file with no one line. A path holding a character a URL needs encoded, such as a space or `#`, is percent-encoded in the link or keeps the plain bullet. Keep the file block header a bare backticked path, so the section-marker reading in `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` does not change.
25
+
16
26
  The comment is a rendered-for-human GitHub surface, so load the `write-human` skill for voice and word choice and follow `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` for punctuation: cut editorializing, and keep every sentence load-bearing. Match this shape on a first pass:
17
27
 
18
28
  ```markdown
@@ -22,8 +32,8 @@ X critical, Y should-fix, Z minor. Reviewed against project docs and the board.
22
32
 
23
33
  **`path/to/file.ext`**
24
34
 
25
- - **should-fix**: what breaks and the fix, in two or three sentences.
26
- - **minor**: finding.
35
+ - **should-fix** ([line 12](<repo-url>/blob/<headRefOid>/path/to/file.ext#L12)): what breaks and the fix, in two or three sentences.
36
+ - **minor** ([lines 30-34](<repo-url>/blob/<headRefOid>/path/to/file.ext#L30-L34)): finding.
27
37
 
28
38
  **What is right**
29
39
 
@@ -47,7 +57,7 @@ Re-reviewed `<short-sha>`, N commits since the prior pass. X critical, Y should-
47
57
 
48
58
  **`path/to/file.ext`**
49
59
 
50
- - **should-fix**: what breaks and the fix, in two or three sentences.
60
+ - **should-fix** ([line 12](<repo-url>/blob/<headRefOid>/path/to/file.ext#L12)): what breaks and the fix, in two or three sentences.
51
61
 
52
62
  🤖 Reviewed by Claude Code
53
63
 
@@ -117,9 +127,11 @@ Before posting, run the scan in `${CLAUDE_SKILL_DIR}/../../standards/publish.md`
117
127
  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, so read `${CLAUDE_SKILL_DIR}/references/close-out.md` instead of posting. 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.
118
128
 
119
129
  ```bash
120
- gh pr review <number> --comment --body-file .canon/tmp/pr/review/body-<number>-<short-sha>.md
130
+ gh api -X POST 'repos/{owner}/{repo}/pulls/<number>/reviews' -f event=COMMENT -F body=@.canon/tmp/pr/review/body-<number>-<short-sha>.md --jq .html_url
121
131
  ```
122
132
 
133
+ 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. `event=COMMENT` submits the review as a comment, the same state the old `--comment` flag produced.
134
+
123
135
  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.`
124
136
 
125
137
  A pass carrying only minors is an ordinary finding-carrying pass, so it takes the open heading and the full shape rather than either short line, since the minors have to be readable and neither line reports them. A pass carrying only Testing questions, or only an unanswered reviewer request, takes the same route for the same reason. Keep whichever scope sentence the pass owes on the summary line:
@@ -131,7 +143,7 @@ A pass carrying only minors is an ordinary finding-carrying pass, so it takes th
131
143
 
132
144
  **`path/to/file.ext`**
133
145
 
134
- - **minor**: finding, and the fix it wants.
146
+ - **minor** ([line 12](<repo-url>/blob/<headRefOid>/path/to/file.ext#L12)): finding, and the fix it wants.
135
147
 
136
148
  🤖 Reviewed by Claude Code
137
149
 
@@ -17,28 +17,30 @@ The fields are `<prior-oid>` off `commit`, `<prior-heading>` off `heading`, and
17
17
 
18
18
  `source` says which stamp answered. `marker` is the pass's own read-time record and is the authority. `fallback` is a pass posted before this mechanism shipped, so its commit is whatever the head was when GitHub recorded the review rather than what that session read, and a push inside its compose window is invisible. `none` is a thread carrying no pass at all.
19
19
 
20
- Do not read `commit.oid` or `submittedAt` off `gh pr view --json reviews` here. Both are stamped at submission, so a push landing between a pass's read and its post moves them onto a commit that pass never saw, and this step then scopes the delta past it and reports it covered.
20
+ Do not read `commit_id` or `submitted_at` off the reviews listing here. Both are stamped at submission, so a push landing between a pass's read and its post moves them onto a commit that pass never saw, and this step then scopes the delta past it and reports it covered.
21
21
 
22
- A target whose CLI predates the verb meets a missing subcommand rather than a record. Fall back there to the jq below, which reads the stamps and carries the defect above, and say the fallback answered so a reader can tell a marker read from a stamped one:
22
+ A target whose CLI predates the verb meets a missing subcommand rather than a record. Fall back there to the REST read below, which reads the stamps and carries the defect above, and say the fallback answered so a reader can tell a marker read from a stamped one:
23
23
 
24
24
  ```bash
25
- gh pr view <number> --json reviews --jq '[.reviews[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review" or . == "## Review closed")] | last | select(. != null) | ((.commit.oid // "") + "\t" + (.body | split("\n")[0] | rtrimstr("\r")) + "\t" + (.submittedAt // ""))'
25
+ gh api --paginate 'repos/{owner}/{repo}/pulls/<number>/reviews?per_page=100' --jq '.[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review" or . == "## Review closed") | ((.commit_id // "") + "\t" + (.body | split("\n")[0] | rtrimstr("\r")) + "\t" + (.submitted_at // ""))' | tail -n 1
26
26
  ```
27
27
 
28
- The three fields are `<prior-oid>`, `<prior-heading>`, and `<prior-at>`. Keep the `select(. != null)` guard, since the string concatenation aborts jq on the null an empty selection returns, and an aborted command reaches the session as an error rather than as the empty result the first-pass branch reads.
28
+ The three fields are `<prior-oid>`, `<prior-heading>`, and `<prior-at>`. The filter emits one line per matching review and `tail` keeps the newest, since `--paginate` runs the filter once per page and a `last` inside it would answer for each page rather than for the thread.
29
29
 
30
30
  Match the first line for equality against the two headings this skill posts. A prefix test also matches `## Review response` and any heading merely starting with those words, which would scope the pass to whatever commit that comment carried. The `\r` trim covers a body composed in the GitHub web editor, which stores CRLF.
31
31
 
32
32
  A `source` of `none`, or an empty result from the fallback, is a first pass. Read the whole change:
33
33
 
34
34
  ```bash
35
- gh pr diff <number>
35
+ gh api 'repos/{owner}/{repo}/pulls/<number>' -H 'Accept: application/vnd.github.diff'
36
36
  ```
37
37
 
38
38
  ```bash
39
- gh pr diff <number> --name-only
39
+ gh api --paginate 'repos/{owner}/{repo}/pulls/<number>/files?per_page=100' --jq '.[].filename'
40
40
  ```
41
41
 
42
+ Both reads go through REST, since the `gh pr` diff subcommand runs on GraphQL and a cloud session's GitHub proxy refuses it. The diff media type returns the same unified diff, and the paginated files listing returns every path rather than the first page.
43
+
42
44
  A commit is a later pass. Fetch the pull request head so both commits are local:
43
45
 
44
46
  ```bash
@@ -58,14 +60,14 @@ On exit zero, review `<prior-oid>..<headRefOid>` and nothing else. `git diff` an
58
60
  A commit is its own ancestor, so an unchanged head passes that test too, with an empty range. When `<prior-oid>` equals `<headRefOid>`, decide whether this pass has anything to add before reading anything else, since the empty range itself cannot answer that:
59
61
 
60
62
  ```bash
61
- gh pr view <number> --json comments --jq '[.comments[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review response" or . == "## Rebase" or . == "## Post-review findings") | select(.createdAt > "<prior-at>")] | last | .url // empty | split("-") | last'
63
+ gh api --paginate 'repos/{owner}/{repo}/issues/<number>/comments?per_page=100' --jq '.[] | select(.body // "" | split("\n")[0] | rtrimstr("\r") | . == "## Review response" or . == "## Rebase" or . == "## Post-review findings") | select(.created_at > "<prior-at>") | .id' | tail -n 1
62
64
  ```
63
65
 
64
66
  `<prior-at>` is the instant Step 2 resolved above, which is the prior pass's `readAt` where it wrote one. Reading `submittedAt` off the thread here instead is the same submission-time defect on the time axis: a reply posted inside that pass's compose window sorts before the stamp and reads as already answered, when in fact the pass had stopped reading before it landed.
65
67
 
66
68
  Scope the replies to those newer than the prior pass, never to every reply the thread carries. A pass answering the newest reply and a pass answering an older one derive the same third segment (Step 4), so an unscoped read hands a re-run after a close-out the name its own prior pass already wrote. That is the collision this case exists to prevent, reached without a rebase or an error.
67
69
 
68
- Read the number off `.url`. The `id` field carries a GraphQL node id, which the thread never displays. Keep the `// empty` guard, since `split` aborts jq on the null an empty selection returns, and an aborted command reaches the session as an error rather than as the empty result the stop below reads.
70
+ The REST `id` is the number the thread displays in each comment's anchor. Keep `tail` outside the filter for the same per-page reason as the reviews read above.
69
71
 
70
72
  An empty result means no reply arrived since the prior pass, so the head is unchanged and this pass has nothing new to add. Stop here, before Step 3 or Step 4 run: `❌ The head is unchanged since the prior pass on <short-sha>. Nothing new to review.` This is the earliest point every path crosses, which is why the check sits here rather than inside Step 4's filename derivation. A path that decides there is nothing to add never reaches a step reached only when composing a body, so a stop written there is a stop a shortcut path can route around.
71
73