typeclaw 0.18.0 → 0.20.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 (46) hide show
  1. package/package.json +1 -1
  2. package/src/agent/index.ts +9 -1
  3. package/src/agent/live-subagents.ts +4 -0
  4. package/src/agent/model-overrides.ts +77 -0
  5. package/src/agent/plugin-tools.ts +53 -4
  6. package/src/agent/session-origin.ts +32 -10
  7. package/src/agent/tools/channel-react.ts +79 -0
  8. package/src/agent/tools/grant-role.ts +102 -8
  9. package/src/agent/tools/spawn-subagent.ts +1 -0
  10. package/src/agent/tools/subagent-access.ts +67 -0
  11. package/src/agent/tools/subagent-cancel.ts +11 -6
  12. package/src/agent/tools/subagent-output.ts +10 -2
  13. package/src/bundled-plugins/github-cli-auth/gh-command.ts +372 -0
  14. package/src/bundled-plugins/github-cli-auth/index.ts +42 -0
  15. package/src/bundled-plugins/github-cli-auth/token-class.ts +11 -0
  16. package/src/bundled-plugins/reviewer/skills/code-review.ts +18 -1
  17. package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +9 -2
  18. package/src/channels/adapters/discord-bot.ts +242 -7
  19. package/src/channels/adapters/github/inbound.ts +40 -55
  20. package/src/channels/adapters/github/index.ts +89 -18
  21. package/src/channels/adapters/github/membership.ts +4 -0
  22. package/src/channels/adapters/github/permission-guidance.ts +20 -1
  23. package/src/channels/adapters/github/reactions.ts +142 -0
  24. package/src/channels/adapters/slack-bot-slash-commands.ts +3 -1
  25. package/src/channels/adapters/slack-bot.ts +4 -4
  26. package/src/channels/commands.ts +10 -0
  27. package/src/channels/engagement.ts +30 -2
  28. package/src/channels/github-token-bridge.ts +42 -0
  29. package/src/channels/index.ts +6 -0
  30. package/src/channels/manager.ts +6 -0
  31. package/src/channels/membership.ts +9 -0
  32. package/src/channels/router.ts +295 -42
  33. package/src/channels/types.ts +42 -0
  34. package/src/cli/inspect.ts +3 -0
  35. package/src/cli/ui.ts +6 -0
  36. package/src/commands/index.ts +54 -4
  37. package/src/init/dockerfile.ts +60 -0
  38. package/src/init/validate-api-key.ts +15 -1
  39. package/src/inspect/loop.ts +12 -1
  40. package/src/permissions/permissions.ts +24 -0
  41. package/src/plugin/context.ts +8 -0
  42. package/src/plugin/manager.ts +3 -0
  43. package/src/plugin/types.ts +6 -0
  44. package/src/run/bundled-plugins.ts +9 -0
  45. package/src/run/index.ts +4 -0
  46. package/src/skills/typeclaw-channel-github/SKILL.md +80 -43
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: typeclaw-channel-github
3
- description: Use this skill BEFORE every `channel_reply` or `channel_send` call whose adapter is `github`, AND before composing replies to GitHub-originated inbounds, AND before opening new issues or PRs with `gh`, AND ALWAYS when an inbound says "requested your review on PR #N" or "requested a review from team @… on PR #N" (the agent has been assigned as a reviewer and must delegate the analysis to the `reviewer` subagent, then translate its findings into line-by-line comments via `gh api`). GitHub renders **real markdown** — `**bold**`, `## headings`, `| tables |`, fenced code blocks, and `inline code` all render natively. Use rich markdown freely. GitHub cannot send file attachments via API — do not call `channel_send` with attachments on github chats. GitHub has no typing indicator. PR review threads use `thread` keyed on the root comment id; reply to a thread to stay in it, or omit `thread` to post a top-level issue/PR comment. To open new issues or PRs use the `gh` CLI — `GH_TOKEN` is pre-set by the adapter. Read this skill before composing anything on GitHub.
3
+ description: Use this skill BEFORE every `channel_reply` or `channel_send` call whose adapter is `github`, AND before composing replies to GitHub-originated inbounds, AND before opening new issues or PRs with `gh`, AND ALWAYS when you are asked to review a PR — whether the inbound says "requested your review on PR #N" / "requested a review from team @… on PR #N", or a human asks for a review in plain language in an issue/PR body or comment ("@bot review this", "can you take a look at #123"). On a review request you delegate the analysis to the `reviewer` subagent, which produces line-anchored findings, then you post them as an inline review via `gh api`. GitHub renders **real markdown** — `**bold**`, `## headings`, `| tables |`, fenced code blocks, and `inline code` all render natively. Use rich markdown freely. GitHub cannot send file attachments via API — do not call `channel_send` with attachments on github chats. GitHub has no typing indicator. PR review threads use `thread` keyed on the root comment id; reply to a thread to stay in it, or omit `thread` to post a top-level issue/PR comment. To open new issues or PRs use the `gh` CLI — `GH_TOKEN` is pre-set by the adapter. Read this skill before composing anything on GitHub.
4
4
  ---
5
5
 
6
6
  GitHub renders normal Markdown in issues, PRs, discussions, and review comments. Use headings, lists, tables, fenced code blocks, links, and inline code when they improve clarity.
@@ -13,39 +13,44 @@ GitHub renders normal Markdown in issues, PRs, discussions, and review comments.
13
13
 
14
14
  A successful `channel_reply` ends your turn by default — the runtime stops the model right after the reply lands. That is correct for a final answer, but it will **silently truncate** a turn that still has work to do. If you post a status line like "Reviewing now, I'll be back with findings" and then expect to keep working (fetch the diff, spawn the reviewer, post the review) in the **same** turn, you must call `channel_reply({ text: "…", continue: true })`. Without `continue: true`, the turn ends at that status reply and the review never runs. Reserve `continue: true` for genuine multi-step turns; the final reply that wraps up the turn omits it.
15
15
 
16
- ## Opening new issues and PRs
16
+ ## What to do, by inbound type
17
17
 
18
- The `gh` CLI is pre-authenticated via `GH_TOKEN` (injected by the adapter at startup). Use it to open new issues or PRs:
18
+ Every GitHub inbound lands on a `chat` keyed by its subject: `issue:N`, `pr:N`, or `discussion:N`. Pick your action from the kind of thing that arrived. The default action for anything addressed to you is a normal `channel_reply` in that thread; the **PR review flow** below is the one exception that requires delegation.
19
19
 
20
- ```sh
21
- # Open a new issue
22
- gh issue create --repo owner/repo --title "Bug: ..." --body "..."
20
+ | Inbound | Looks like | What to do |
21
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
22
+ | **New issue** (`issue:N`) | A freshly opened issue body. | Triage or answer it. `channel_reply` on `issue:N`. Open follow-up issues/PRs with `gh` if needed. |
23
+ | **Issue comment** (`issue:N`) | A comment on an issue. | Reply in the issue thread with `channel_reply`. |
24
+ | **PR conversation comment** (`pr:N`, no `thread`) | A comment on a PR's main conversation (GitHub models PR comments as issue comments). | Reply on the PR with `channel_reply`. **If the text asks you to review → go to the PR review flow.** |
25
+ | **PR review-thread reply** (`pr:N`, `thread` set) | A reply on an existing inline review comment thread. | Stay in the thread: `channel_reply` with `thread` kept as-is. |
26
+ | **A submitted review** (`pr:N`) | Someone submitted a formal review (approve / changes / comment) on a PR. | React if a response is warranted (answer a question, acknowledge changes). `channel_reply` on `pr:N`. |
27
+ | **New discussion / discussion comment** (`discussion:N`) | A discussion thread or a comment in one. | Reply with `channel_reply` on `discussion:N`. |
28
+ | **Review requested** (`pr:N`) | See "When you are being asked to review" below. | **PR review flow.** |
23
29
 
24
- # Open a new PR
25
- gh pr create --repo owner/repo --title "Fix: ..." --head my-branch --base main --body "..."
26
- ```
30
+ ### When you are being asked to review
27
31
 
28
- For App auth, `GH_TOKEN` is an installation access token that refreshes automaticallyit stays current as long as the adapter is running.
32
+ You are being asked to review a PR in **either** of these cases treat them identically:
33
+
34
+ - **(A) An explicit review-request inbound.** The message text says **"requested your review on PR #N"** or **"requested a review from team @… on PR #N"**. (You do not need to know how it was triggered — the adapter synthesizes this same text whether a human requested you as a reviewer directly or requested a decoy user account that impersonates you as a GitHub App. From your side it reads the same. See [GitHub decoy reviewer](/docs/internals/github-decoy-reviewer).)
35
+ - **(B) A human asks you to review in plain language** in a PR/issue body or any comment — "@bot review this PR", "can you take a look at #123", "review the changes when you get a chance". There is no synthetic request text here; you recognize the intent from the message.
29
36
 
30
- ## Reviewing pull requests
37
+ Both run the **PR review flow**. Do not review inline yourself and do not just reply with prose impressions: delegate to the `reviewer` subagent so the analysis runs on the `deep` model, then post its findings as an inline review.
31
38
 
32
- When an incoming message says **"requested your review on PR #N"** (or "requested a review from team @… on PR #N"), you have been assigned as a reviewer. Do **not** review inline yourself and do **not** just reply in the channel — delegate the analysis to the bundled `reviewer` subagent, then translate its findings into line-by-line comments via `gh api`.
39
+ A `review_request_removed` inbound ("removed your review request on PR #N") is the inverse: the requester un-assigned you. Cancel any in-flight reviewer subagent (`subagent_cancel`) and do not post a partial review.
33
40
 
34
- Why delegate: the `reviewer` subagent runs on the `deep` model profile, loads a curated `code-review` skill on demand, and produces a structured `<review>` block with severity-tagged findings. You are the integration layer between that output and GitHub's review API.
41
+ ## PR review flow
35
42
 
36
- ### Workflow
43
+ The `reviewer` subagent is the analyst; you are the integration layer between its output and GitHub's review API. It loads the `code-review` skill on demand and returns line-anchored findings inside a `<review>` block. Your job is mechanics: spawn, wait, translate, post.
37
44
 
38
- 1. **Confirm the target.** Capture the PR number, the repo, and the head SHA — you'll need the SHA to read files at the revision the reviewer analyzed.
45
+ 1. **Confirm the target.** Capture the PR number, the repo, and the head SHA — you may need the SHA to read files at the revision the reviewer analyzed.
39
46
 
40
47
  ```sh
41
48
  gh pr view <N> --repo owner/repo --json title,body,baseRefName,headRefOid,files
42
49
  ```
43
50
 
44
- 2. **Spawn the `reviewer` subagent with the PR target.** Use `run_in_background: true` so you stay responsive while the deep model works. Pass the PR URL (or `owner/repo#N`) plus any context the requester gave you (focus areas, specific files, etc.) so the reviewer knows what the requester cares about.
51
+ 2. **Spawn the `reviewer` subagent with the PR target.** Use `run_in_background: true` so you stay responsive while the deep model works. Pass the PR URL (or `owner/repo#N`) plus any context the requester gave you (focus areas, specific files, etc.). The reviewer fetches the diff itself (`gh pr diff`, `gh api /repos/.../pulls/<n>`), loads the `code-review` skill, and returns a `<review>` block whose code findings carry `location="path:line"`.
45
52
 
46
- If you post an "on it" acknowledgement before fetching the diff or spawning the reviewer, it **must** be `channel_reply({ text: "…", continue: true })` a bare reply ends the turn and the review never starts (see "Mid-turn status replies need `continue: true`" above).
47
-
48
- The reviewer will fetch the diff itself (`gh pr diff`, `gh api /repos/.../pulls/<n>`), load the matching skill (`code-review` for a code PR; `general` for a mixed-format change), and return a `<review>` block.
53
+ Do **not** post an "on it" acknowledgement comment before spawning the reviewer the runtime already adds an :eyes: reaction to the PR the moment it engages, so a "looking into this" comment is redundant noise. Just spawn the reviewer with `run_in_background: true` and keep working; the formal review is your reply. If you want to acknowledge explicitly, use `channel_react({ emoji: "eyes" })`, which reacts without posting a comment.
49
54
 
50
55
  3. **Wait for the completion `<system-reminder>`,** then call `subagent_output({ task_id })` to read the reviewer's final assistant message. The structured payload looks like:
51
56
 
@@ -63,11 +68,11 @@ Why delegate: the `reviewer` subagent runs on the `deep` model profile, loads a
63
68
  </review>
64
69
  ```
65
70
 
66
- 4. **Translate findings into a `gh api` review payload.** Each `<finding>` with `severity` of `blocker`, `concern`, or `nit` and a `location="path:line"` becomes one entry in `comments[]`. Compose the inline `body` from the reviewer's `<issue>` + `<evidence>` + `<suggestion>` preserve the reviewer's wording, do not paraphrase. Findings whose `location` is `general` (no file:line anchor) go into the top-level review `body` instead. **Skip `praise` findings when building `comments[]`** — they are not actionable, and inline praise comments are exactly the noise the reviewer is supposed to filter out at the source; if you want to surface them, weave them into the top-level review `body` alongside the summary.
71
+ 4. **Translate findings into a `gh api` review payload.** Each `<finding>` with `severity` of `blocker`, `concern`, or `nit` and a `location="path:line"` becomes one entry in `comments[]`. Compose the inline `body` from the reviewer's `<issue>` + `<evidence>` + `<suggestion>` verbatim (modulo markdown). Findings whose `location` is `general` (no file:line anchor) go into the top-level review `body` instead. **Skip `praise` findings when building `comments[]`** — if you want to surface them, weave them into the top-level `body`.
67
72
 
68
- **The verdict and the inline comments are independent. The verdict sets only the `event` field; it never decides whether you post `comments[]`.** Whenever there is at least one actionable finding (`blocker`/`concern`/`nit`) with a `location="path:line"`, you MUST submit a formal review via `POST /pulls/<N>/reviews` carrying those findings in `comments[]` — including when the verdict is `approve`. An `approve` with three nits is still a formal `APPROVE` review with three inline comments, **not** a plain approval and **not** a flattened summary posted as a top-level comment. Collapsing inline findings into a single `channel_reply` or issue comment loses the line anchors the reviewer worked to produce — that is the exact failure mode this step exists to prevent.
73
+ **The verdict and the inline comments are independent. The verdict sets only the `event` field; it never decides whether you post `comments[]`.** Whenever there is at least one actionable finding (`blocker`/`concern`/`nit`) with a `location="path:line"`, you MUST submit a formal review via `POST /pulls/<N>/reviews` carrying those findings in `comments[]` — including when the verdict is `approve`. An `approve` with three nits is still a formal `APPROVE` review with three inline comments, **not** a plain approval and **not** a flattened summary. Collapsing inline findings into a single `channel_reply` or issue comment loses the line anchors the reviewer worked to produce.
69
74
 
70
- Map the reviewer's `<verdict>` to the GitHub `event`:
75
+ Map the reviewer's `<verdict>` to the GitHub `event`, and trust it — do not upgrade `comment` → `APPROVE` to seem agreeable, or downgrade `request-changes` → `COMMENT` to soften the tone:
71
76
 
72
77
  | Reviewer verdict | GitHub `event` |
73
78
  | ----------------- | ----------------- |
@@ -75,22 +80,35 @@ Why delegate: the `reviewer` subagent runs on the `deep` model profile, loads a
75
80
  | `request-changes` | `REQUEST_CHANGES` |
76
81
  | `comment` | `COMMENT` |
77
82
 
78
- Then submit the review in one API call:
83
+ Then submit the review. **Write the JSON payload to a file with the `write` tool, then run a single bare `gh api --input <file>`** — two steps:
79
84
 
80
- ```sh
81
- cat <<'JSON' | gh api -X POST /repos/owner/repo/pulls/<N>/reviews --input -
85
+ First write `/tmp/review.json` (via the `write` tool, not bash):
86
+
87
+ ```json
82
88
  {
83
89
  "event": "COMMENT",
84
90
  "body": "<reviewer's <summary> goes here>",
85
91
  "comments": [
86
- { "path": "src/foo.ts", "line": 42, "side": "RIGHT", "body": "<issue + evidence + suggestion from the reviewer's finding>" },
92
+ {
93
+ "path": "src/foo.ts",
94
+ "line": 42,
95
+ "side": "RIGHT",
96
+ "body": "<issue + evidence + suggestion from the reviewer's finding>"
97
+ },
87
98
  { "path": "src/bar.ts", "line": 10, "side": "RIGHT", "body": "..." }
88
99
  ]
89
100
  }
90
- JSON
91
101
  ```
92
102
 
93
- **Always use `--input -` with a quoted heredoc (`<<'JSON'`) for review bodies.** Do **not** use `-f body=...` or `-F 'comments[][body]=...'`: those go through shell argument parsing, so backticks (\`) trigger command substitution and have to be backslash-escaped, which leaks the literal `\` into the rendered comment. The quoted heredoc passes the JSON through untouched — backticks, newlines, and `${...}` all survive verbatim. The same applies to any other `gh api` POST whose body contains backticks, embedded newlines, or shell metacharacters.
103
+ Then post it:
104
+
105
+ ```sh
106
+ gh api -X POST /repos/owner/repo/pulls/<N>/reviews --input /tmp/review.json
107
+ ```
108
+
109
+ **A repo-targeting `gh` command must be a single bare `gh` invocation — no pipes, `;`, `&&`, heredocs, or command substitution.** The `github-cli-auth` plugin injects the GitHub App token into the command's environment, so any sibling/upstream stage in a pipeline would inherit a live token; the runtime blocks those shapes. That is why the old `cat <<'JSON' | gh api --input -` heredoc-pipe no longer works: write the JSON to a file and feed it with `--input <file>` instead. Do **not** use `-f body=...` or `-F 'comments[][body]=...'`: those go through shell argument parsing, so backticks trigger command substitution. The file passes the JSON through untouched — backticks, newlines, and `${...}` all survive verbatim. The same file-then-`--input` pattern applies to any `gh api` POST whose body contains backticks, embedded newlines, or shell metacharacters.
110
+
111
+ Anchor mechanics: `line` is a line number **in the file**, not a position in the diff. `side: RIGHT` is the new revision (default for additions); `side: LEFT` is the old revision (use for comments on removed lines). For multi-line comments, also set `start_line` and `start_side` (same semantics). If you need to read whole files at the PR's head SHA to validate an anchor before posting, use `gh api /repos/owner/repo/contents/<path>?ref=<headRefOid>`.
94
112
 
95
113
  5. **Verify the review actually landed before announcing it.** The `gh api` call can fail silently from the model's perspective — a permission denial, a bad `line` anchor, or a malformed payload returns an error you must not paper over. After submitting, confirm the review exists:
96
114
 
@@ -100,23 +118,42 @@ Why delegate: the `reviewer` subagent runs on the `deep` model profile, loads a
100
118
 
101
119
  The returned `id`/`state` is your proof the formal review posted. If the call errored or the review is absent, do **not** fall back to a top-level `channel_reply` that _claims_ a review was posted — fix the payload (most often a `line` that isn't part of the diff; re-anchor it or move that finding to the top-level `body`) and resubmit. A trace reply that says "Posted review" when no review exists is worse than silence.
102
120
 
103
- 6. **End the turn with `skip_response`, not a trace reply.** The formal review from step 4 already landed _in this PR_ it carries the summary, the verdict, and the inline comments. A `channel_reply` here does **not** go to a separate operator channel; on GitHub it posts another public comment on the same PR. A one-line "Posted review on PR #N: …" narrated into the PR thread is meta-commentary addressed to a phantom operator, and it reads absurdly next to the review it claims to point at. So once step 5 confirms the review exists, call `skip_response({ reason: "review posted via gh api" })` to close the turn silently. Only fall back to `channel_reply` when there was **no** formal review to post the zero-actionable-findings branches in Rules below already use `channel_reply`/issue comments _as_ the substantive reply.
121
+ 6. **Drop the decoy reviewer App auth only, request-path only.** When this review was triggered by an explicit review-request inbound (path A) **and** you are running under **GitHub App** auth, remove the decoy reviewer from the PR's requested-reviewers list once the review has landed (after step 5 confirms it). Why: GitHub auto-adds **you** (the App account) to the PR's reviewers the moment your formal review posts, so the "reviewed" state is already recorded under the App identity but the **decoy** account stays pinned in the requested-reviewers list as a perpetual "review requested", as if the review never happened. Removing it is the natural "I'm done, drop me" cleanup a human reviewer gets for free. The decoy login is the App slug `selfLogin` with the trailing `[bot]` removed (`my-app[bot]` `my-app`); see [GitHub decoy reviewer](/docs/internals/github-decoy-reviewer).
122
+
123
+ ```sh
124
+ gh api -X DELETE /repos/owner/repo/pulls/<N>/requested_reviewers -f 'reviewers[]=<decoy-login>'
125
+ ```
126
+
127
+ **Skip this entirely** when (a) you are under **PAT** auth — there is no decoy; the bot is a real user GitHub keeps listed as a reviewer, and removing yourself there is not wanted — or (b) the review came from a **plain-language** ask (path B) or a **team** request, where no decoy user was ever placed on the requested-reviewers list and the DELETE would be a no-op/404. The removal is safe under the adapter's self-loop guard: you make the `DELETE` authenticated as the App, so the `review_request_removed` webhook GitHub emits has your bot actor (`slug[bot]`) as `sender`, which the classifier drops, so it never wakes a fresh session (see "Self-loop safety" below). Treat a failure here as non-fatal — the review already landed; do not retry in a loop or surface it to the PR thread.
128
+
129
+ 7. **End the turn with `skip_response`, not a trace reply.** The formal review from step 4 already landed _in this PR_ — it carries the summary, the verdict, and the inline comments. A `channel_reply` here does **not** go to a separate operator channel; on GitHub it posts another public comment on the same PR. A one-line "Posted review on PR #N: …" narrated into the PR thread is meta-commentary addressed to a phantom operator, and it reads absurdly next to the review it claims to point at. So once step 5 confirms the review exists (and step 6 has dropped the decoy, if applicable), call `skip_response({ reason: "review posted via gh api" })` to close the turn silently. Only fall back to `channel_reply` when there was **no** formal review to post — the zero-actionable-findings branch below uses `channel_reply`/issue comments _as_ the substantive reply.
130
+
131
+ ### Zero actionable findings
132
+
133
+ A finding is "actionable" if its severity is `blocker`, `concern`, or `nit`. The inline-review post in step 4 applies whenever the actionable count is **at least one**. When the reviewer returns **exactly zero** actionable findings (only `praise`, or none), there is nothing to anchor inline — handle by verdict:
134
+
135
+ - `approve` → post a plain `APPROVE` with the `<summary>` as the review body (no `comments[]` array).
136
+ - `comment` → post the summary as a top-level PR comment via `gh api -X POST /repos/.../issues/<N>/comments` instead of submitting an empty review.
137
+ - `request-changes` → submit `REQUEST_CHANGES` with the `<summary>` as the review body and no `comments[]` array. This combination is rare (the reviewer's contract says `request-changes` requires at least one blocker or load-bearing concern); if it happens, faithfully encode the verdict and trust the reviewer's reasoning is in the summary.
138
+
139
+ The bundled `agent-browser` is **not** for PR reviews — `gh api` is faster and more reliable. Only use the browser when the API genuinely can't reach what you need.
140
+
141
+ ## Opening new issues and PRs
142
+
143
+ The `gh` CLI is pre-authenticated via `GH_TOKEN` (injected by the adapter at startup). Use it to open new issues or PRs:
144
+
145
+ ```sh
146
+ # Open a new issue
147
+ gh issue create --repo owner/repo --title "Bug: ..." --body "..."
104
148
 
105
- ### Rules
149
+ # Open a new PR
150
+ gh pr create --repo owner/repo --title "Fix: ..." --head my-branch --base main --body "..."
151
+ ```
106
152
 
107
- - **Always delegate to the `reviewer` subagent.** Do not perform the review craft yourself. The reviewer is the source of truth for severity, evidence quality, and what counts as a finding. Your job is mechanics: spawn, wait, translate, post.
108
- - **Trust the verdict.** Use the GitHub `event` mapped from the reviewer's `<verdict>`. Do not upgrade `comment` → `APPROVE` to seem agreeable, and do not downgrade `request-changes` → `COMMENT` to soften the tone. The reviewer chose deliberately.
109
- - **No actionable findings → no inline review post.** A finding is "actionable" if its severity is `blocker`, `concern`, or `nit`. This branch applies **only when the actionable count is exactly zero** — if there is even one actionable finding with a line anchor, follow step 4 and submit a formal review with `comments[]` regardless of verdict. When the reviewer returns zero actionable findings:
110
- - `approve` verdict → post a plain `APPROVE` with the `<summary>` as the review body (no `comments[]` array).
111
- - `comment` verdict → post the summary as a top-level PR comment via `gh api -X POST /repos/.../issues/<N>/comments` instead of submitting an empty review.
112
- - `request-changes` verdict → submit `REQUEST_CHANGES` with the `<summary>` as the review body and no `comments[]` array. This combination is rare (the reviewer's contract says `request-changes` requires at least one blocker or load-bearing concern), so if it happens, faithfully encode the verdict and trust the reviewer's reasoning is in the summary.
113
- - **Preserve the reviewer's wording.** Inline comment bodies should reflect the reviewer's `<issue>`, `<evidence>`, and `<suggestion>` verbatim (modulo markdown formatting). Paraphrasing dilutes the analysis — the deep-model reviewer chose those words on purpose.
114
- - `line` is a line number **in the file**, not a position in the diff. `side: RIGHT` is the new revision (default for additions); `side: LEFT` is the old revision (use for comments on removed lines).
115
- - For multi-line comments, also set `start_line` and `start_side` (same semantics).
116
- - If you need to read whole files at the PR's head SHA, use `gh api /repos/owner/repo/contents/<path>?ref=<headRefOid>`. The reviewer can do this itself, but you may need to as well — e.g., when validating a finding's `location` against the actual file before posting.
117
- - The bundled `agent-browser` is **not** for PR reviews — `gh api` is faster and more reliable. Only use the browser when the API genuinely can't reach what you need.
118
- - A `review_request_removed` event means the requester un-assigned you. Cancel any in-flight reviewer subagent (`subagent_cancel`) and do not post a partial review.
153
+ For App auth, `GH_TOKEN` is an installation access token that refreshes automatically it stays current as long as the adapter is running.
119
154
 
120
- ### Self-loop safety
155
+ ## Self-loop safety
121
156
 
122
157
  The adapter will **not** wake you when you assign yourself as a reviewer (e.g., via `gh pr edit --add-reviewer`). It will only wake you when someone else requests your review.
158
+
159
+ The same guard covers **removing** a reviewer: when you drop the decoy after a review (step 6 of the PR review flow), you act as the App, so the `review_request_removed` webhook GitHub emits carries your bot actor (`slug[bot]`) as its `sender`, which the classifier drops. So the cleanup never echoes back as a fresh wake. Both directions — add and remove — are matched on `sender.login` (against either the bot actor or its decoy), so any reviewer-list mutation you make yourself stays silent.