typeclaw 0.20.0 → 0.22.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.
- package/package.json +2 -1
- package/src/agent/index.ts +55 -1
- package/src/agent/loop-guard.ts +180 -53
- package/src/agent/restart/index.ts +101 -0
- package/src/agent/tools/restart.ts +23 -52
- package/src/bundled-plugins/bun-hygiene/README.md +82 -0
- package/src/bundled-plugins/bun-hygiene/index.ts +11 -0
- package/src/bundled-plugins/bun-hygiene/policy.ts +318 -0
- package/src/bundled-plugins/github-cli-auth/gh-command.ts +98 -6
- package/src/bundled-plugins/github-cli-auth/graphql-auth-nudge.ts +80 -0
- package/src/bundled-plugins/github-cli-auth/index.ts +7 -0
- package/src/bundled-plugins/memory/memory-logger.ts +6 -2
- package/src/bundled-plugins/reviewer/skills/code-review.ts +8 -0
- package/src/channels/adapters/discord-bot-classify.ts +8 -2
- package/src/channels/adapters/discord-bot.ts +29 -2
- package/src/channels/adapters/github/decoy-reviewer.ts +43 -0
- package/src/channels/adapters/github/inbound.ts +92 -1
- package/src/channels/adapters/github/index.ts +12 -1
- package/src/channels/adapters/github/reactions.ts +138 -4
- package/src/channels/adapters/slack-bot-classify.ts +2 -2
- package/src/channels/adapters/slack-bot.ts +129 -7
- package/src/channels/engagement.ts +71 -31
- package/src/channels/manager.ts +8 -0
- package/src/channels/router.ts +180 -25
- package/src/channels/schema.ts +18 -0
- package/src/channels/types.ts +16 -1
- package/src/cli/builtins.ts +1 -0
- package/src/cli/dreams.ts +148 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/inspect.ts +2 -1
- package/src/cli/ui.ts +34 -0
- package/src/commands/index.ts +5 -2
- package/src/config/config.ts +89 -0
- package/src/dreams/git.ts +85 -0
- package/src/dreams/index.ts +134 -0
- package/src/dreams/parse.ts +224 -0
- package/src/dreams/render.ts +155 -0
- package/src/dreams/types.ts +50 -0
- package/src/mcp/catalog.ts +29 -0
- package/src/mcp/client.ts +236 -0
- package/src/mcp/index.ts +25 -0
- package/src/mcp/manager.ts +156 -0
- package/src/mcp/tools.ts +190 -0
- package/src/permissions/builtins.ts +9 -0
- package/src/reload/format.ts +14 -0
- package/src/reload/index.ts +1 -0
- package/src/run/bundled-plugins.ts +7 -0
- package/src/run/channel-session-factory.ts +3 -0
- package/src/run/index.ts +38 -1
- package/src/server/command-runner.ts +5 -0
- package/src/server/index.ts +53 -0
- package/src/shared/protocol.ts +2 -0
- package/src/skills/typeclaw-channel-github/SKILL.md +86 -22
- package/src/tui/index.ts +70 -18
- package/typeclaw.schema.json +82 -0
|
@@ -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 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.
|
|
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. When a review comment **you authored** gets addressed — the author pushed a fix or replied that resolves it — verify the fix at the PR's head SHA and then resolve the thread with the `resolveReviewThread` GraphQL mutation (see "Resolving review threads you authored" below); resolving is the close-out that tells the author the concern is settled. 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.
|
|
@@ -8,6 +8,7 @@ GitHub renders normal Markdown in issues, PRs, discussions, and review comments.
|
|
|
8
8
|
- Do not send attachments on GitHub chats; the adapter rejects them.
|
|
9
9
|
- There is no typing indicator.
|
|
10
10
|
- For PR review threads, keep `thread` set to reply in-place. Omit `thread` for a top-level PR/issue comment.
|
|
11
|
+
- When a review comment **you authored** has been addressed, resolve its thread — see "Resolving review threads you authored" below. The base principle is **whoever opened the thread closes it**: you resolve only the threads you started, never a human's.
|
|
11
12
|
|
|
12
13
|
## Mid-turn status replies need `continue: true`
|
|
13
14
|
|
|
@@ -17,15 +18,15 @@ A successful `channel_reply` ends your turn by default — the runtime stops the
|
|
|
17
18
|
|
|
18
19
|
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
20
|
|
|
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.**
|
|
21
|
+
| Inbound | Looks like | What to do |
|
|
22
|
+
| -------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| **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. |
|
|
24
|
+
| **Issue comment** (`issue:N`) | A comment on an issue. | Reply in the issue thread with `channel_reply`. |
|
|
25
|
+
| **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.** |
|
|
26
|
+
| **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. **If it addresses a comment you authored → verify and resolve the thread (below).** |
|
|
27
|
+
| **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`. |
|
|
28
|
+
| **New discussion / discussion comment** (`discussion:N`) | A discussion thread or a comment in one. | Reply with `channel_reply` on `discussion:N`. |
|
|
29
|
+
| **Review requested** (`pr:N`) | See "When you are being asked to review" below. | **PR review flow.** |
|
|
29
30
|
|
|
30
31
|
### When you are being asked to review
|
|
31
32
|
|
|
@@ -42,14 +43,28 @@ A `review_request_removed` inbound ("removed your review request on PR #N") is t
|
|
|
42
43
|
|
|
43
44
|
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.
|
|
44
45
|
|
|
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.
|
|
46
|
+
1. **Confirm the target, and check whether you already reviewed it.** Capture the PR number, the repo, and the head SHA — you may need the SHA to read files at the revision the reviewer analyzed.
|
|
46
47
|
|
|
47
48
|
```sh
|
|
48
49
|
gh pr view <N> --repo owner/repo --json title,body,baseRefName,headRefOid,files
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
Then check for a **prior review by you** — this is what makes the current request a _re-review_ (the author pushed fixes and re-requested you after you previously blocked the PR):
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
gh api --paginate --slurp /repos/owner/repo/pulls/<N>/reviews --jq 'add | [.[] | select(.user.login == "<your-login>" and (.state == "CHANGES_REQUESTED" or .state == "APPROVED"))] | last | .state'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If that prints `CHANGES_REQUESTED`, treat the current request as a **re-review** and carry that fact into the spawn in step 2; any other output (including empty) means no live block, so handle the request normally. (`<your-login>` is your GitHub App login, typically `name[bot]`.)
|
|
59
|
+
|
|
60
|
+
Two things make this query load-bearing — both are bugs if you simplify it:
|
|
61
|
+
- **Filter to _decision_ states, not the latest review row.** GitHub's sticky block is cleared only by a later `APPROVED` (or a dismissal) from the same reviewer — a later `COMMENTED` review does **not** clear it. So a history of `CHANGES_REQUESTED` → `COMMENTED` is _still blocked_, even though the latest row is `COMMENTED`. Selecting `last` over the raw review list would misread that as "not a re-review". Filtering to `{CHANGES_REQUESTED, APPROVED}` first, then taking `last`, asks the right question: "what is my latest _blocking decision_, ignoring non-deciding comments?" (Dismissed reviews surface as `state: "DISMISSED"`, so they're correctly excluded from the decision set too.)
|
|
62
|
+
- **`--paginate --slurp` is mandatory.** GitHub returns reviews 30 per page; a bot on a long-lived PR can have its blocking `CHANGES_REQUESTED` past the first page. Without paginating, that review is invisible and a genuine re-review silently falls back to the plain-comment path. `--slurp` collects every page into one array of arrays; the `add` concatenates them before filtering.
|
|
63
|
+
|
|
51
64
|
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"`.
|
|
52
65
|
|
|
66
|
+
**If step 1 found a prior `CHANGES_REQUESTED` review, say so in the spawn payload** — e.g. _"This is a re-review: you previously requested changes on this PR (the prior blockers were …). Verify they are resolved and return `approve` or `request-changes` — a re-review must re-decide the blocking state, not return `comment`."_ The reviewer's `code-review` skill enforces the same rule, but telling it the prior verdict is what lets it apply that rule; a fresh reviewer session has no memory of your earlier review.
|
|
67
|
+
|
|
53
68
|
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.
|
|
54
69
|
|
|
55
70
|
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:
|
|
@@ -80,6 +95,19 @@ The `reviewer` subagent is the analyst; you are the integration layer between it
|
|
|
80
95
|
| `request-changes` | `REQUEST_CHANGES` |
|
|
81
96
|
| `comment` | `COMMENT` |
|
|
82
97
|
|
|
98
|
+
**Operator approval policy.** If the inbound carries a note that PR approval is disabled (`channels.github.review.approve: false` — the adapter appends "Operator policy: PR approval is disabled for this agent" to the message), you must **not** submit an `APPROVE`. Map an `approve` verdict to `COMMENT` instead: post the same `<summary>` and all inline `comments[]` as a `COMMENT` review, just without the formal approval. `request-changes` and `comment` verdicts are unaffected (they never approve). Absent that note, approval is enabled and the table above applies unchanged.
|
|
99
|
+
|
|
100
|
+
**Re-review.** If step 1 established this is a re-review (your latest blocking decision was `CHANGES_REQUESTED`), the result MUST clear or re-assert that block — never a top-level PR comment. On GitHub, `CHANGES_REQUESTED` is sticky: **only** a fresh `APPROVE` from you, or a dismissal of your prior review, clears it. A plain issue comment does **not** clear it, and — critically — **neither does a `COMMENT` review.** So even if the reviewer returns zero actionable findings, do **not** take the `comment` → top-level-comment branch below for a re-review. The reviewer's skill is instructed not to return `comment` on a re-review; if it does anyway despite a reachable diff, prefer `approve` when the prior blockers are visibly resolved in the diff, otherwise `request-changes` — and say which in your reasoning. Resolve the re-review by verdict:
|
|
101
|
+
- **`request-changes`** — submit a fresh `REQUEST_CHANGES` review (re-asserts the block with the new findings). Straightforward.
|
|
102
|
+
- **`approve`, approval enabled** — submit `APPROVE`. This clears the block.
|
|
103
|
+
- **`approve`, approval disabled (`channels.github.review.approve: false`)** — you cannot `APPROVE`, and a `COMMENT` review will **not** clear the sticky block, so the PR would stay blocked by your stale review. Clear it explicitly by **dismissing your own prior `CHANGES_REQUESTED` review**. Grab that review's `id` by re-running the step-1 query with the trailing filter changed from `| .state` to `| {state, id}` (same `select`), take the entry whose `state` is `CHANGES_REQUESTED`, then:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
gh api -X PUT /repos/owner/repo/pulls/<N>/reviews/<review_id>/dismissals -f message="Blockers resolved; dismissing my prior changes request per operator approval-disabled policy." -f event=DISMISS
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This transitions your review to `DISMISSED` and unblocks the PR without an approval. It needs the bot's installation to have **write** access (or to be on the branch's "who can dismiss reviews" list); if the dismissal returns 403, the block cannot be cleared under this policy — post the `<summary>` as a `COMMENT` review and say plainly in the body that the prior changes-request stands until a human dismisses it, rather than implying the PR is unblocked.
|
|
110
|
+
|
|
83
111
|
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:
|
|
84
112
|
|
|
85
113
|
First write `/tmp/review.json` (via the `write` tool, not bash):
|
|
@@ -118,26 +146,62 @@ The `reviewer` subagent is the analyst; you are the integration layer between it
|
|
|
118
146
|
|
|
119
147
|
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.
|
|
120
148
|
|
|
121
|
-
6. **
|
|
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.
|
|
149
|
+
6. **The decoy reviewer is dropped for you — no action needed.** Under **GitHub App** auth, the adapter automatically removes the decoy reviewer from the PR's requested-reviewers list the moment your formal review lands (it reacts to your own `pull_request_review.submitted` webhook). Why this matters: GitHub auto-adds **you** (the App account) to the PR's reviewers when your review posts, but the **decoy** account would otherwise stay pinned as a perpetual "review requested", as if the review never happened. You do **not** need to issue a `DELETE /requested_reviewers` yourself — and you should not, since it would race the adapter's own cleanup. The removal is self-loop-safe: the adapter's `DELETE` is authenticated as the App, so the `review_request_removed` webhook carries your bot actor (`slug[bot]`) as `sender`, which the classifier drops (see "Self-loop safety" below). This is a no-op under **PAT** auth (no decoy) and for **plain-language**/**team** requests (no decoy user was placed). See [GitHub decoy reviewer](/docs/internals/github-decoy-reviewer).
|
|
128
150
|
|
|
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
|
|
151
|
+
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, 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
152
|
|
|
131
153
|
### Zero actionable findings
|
|
132
154
|
|
|
133
155
|
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
156
|
|
|
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.
|
|
157
|
+
- `approve` → post a plain `APPROVE` with the `<summary>` as the review body (no `comments[]` array). **If the operator approval policy above disabled approval, submit a `COMMENT` review instead — same `<summary>` as the review body, `event: "COMMENT"`, no `comments[]` array. Keep it a formal review, not a top-level issue comment, so the review metadata and flow are preserved.** (Re-review caveat: a `COMMENT` review does **not** clear a sticky `CHANGES_REQUESTED` block. If this is a re-review under approval-disabled policy, follow the step-4 re-review branch — dismiss your prior review — instead of relying on this `COMMENT`.)
|
|
158
|
+
- `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. **Exception — re-reviews:** if this is a re-review (your latest blocking decision was `CHANGES_REQUESTED`), a top-level comment does not clear the sticky block. Do not use this branch; resolve it via the step-4 re-review branch (`APPROVE` if resolved and approval is enabled, the dismissal endpoint if resolved but approval is disabled, `REQUEST_CHANGES` if not resolved).
|
|
137
159
|
- `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
160
|
|
|
139
161
|
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
162
|
|
|
163
|
+
## Resolving review threads you authored
|
|
164
|
+
|
|
165
|
+
A review you posted leaves inline comment threads open on the PR. When one of **your** threads is addressed — the author pushed a fix, or replied that they handled it — close it out by **resolving the thread**. Leaving it open after the concern is settled reads as if you never noticed; a resolved thread is the signal that the loop is closed.
|
|
166
|
+
|
|
167
|
+
**The base principle: whoever opened the thread closes it.** Resolve only threads whose root comment **you** authored. Never resolve a human reviewer's thread on your behalf — that erases their open question. The thread you can resolve is the one you started; the inbound that brings you here is a **review-thread reply on `pr:N` with `thread` set**, replying inside a thread you opened.
|
|
168
|
+
|
|
169
|
+
### When a thread counts as addressed
|
|
170
|
+
|
|
171
|
+
Do not resolve on a bare "done" claim. A reply that says "fixed" is a prompt to check, not proof. Before resolving, **verify the fix at the PR's current head SHA**:
|
|
172
|
+
|
|
173
|
+
1. Re-read the PR head: `gh pr view <N> --repo owner/repo --json headRefOid` gives you the SHA the author's latest push landed on.
|
|
174
|
+
2. Read the lines your comment anchored to, at that SHA: `gh api /repos/owner/repo/contents/<path>?ref=<headRefOid>` (or `gh pr diff <N>` to see what the new push changed). Confirm the change actually addresses the concern your comment raised — not a different line, not a partial fix.
|
|
175
|
+
3. Only when the code at head genuinely resolves the finding do you resolve the thread. If the fix is partial or misses the point, **reply in the thread** explaining what's still open and leave it unresolved.
|
|
176
|
+
|
|
177
|
+
If the author merely **replied** without pushing (e.g. "this is intentional because …") and their reasoning settles it, that is also "addressed" — **resolve first, then optionally leave a one-line acknowledgement.** Order matters: a bare `channel_reply` ends your turn (see "Mid-turn status replies need `continue: true`" above), so acknowledging _before_ you resolve would stop the turn and the `resolveReviewThread` mutation would never run, leaving the thread open. Resolve, then reply. If you genuinely want to acknowledge before resolving, the acknowledgement must use `channel_reply({ …, continue: true })` so the turn survives long enough to resolve. If their reasoning does **not** settle it, keep the thread open and answer.
|
|
178
|
+
|
|
179
|
+
### How to resolve — `resolveReviewThread` GraphQL mutation
|
|
180
|
+
|
|
181
|
+
There is no REST endpoint for this. Resolution is a GraphQL mutation that takes the thread's **node id** (`PRRT_…`), not the comment's numeric id. Two steps: find the thread id, then resolve it.
|
|
182
|
+
|
|
183
|
+
1. **Find the node id of the thread you authored.** Query the PR's review threads and pick the one whose root comment is yours and matches the `thread` you're replying in:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
gh api graphql -f query='query($owner:String!,$name:String!,$number:Int!,$after:String){repository(owner:$owner,name:$name){pullRequest(number:$number){reviewThreads(first:100,after:$after){pageInfo{hasNextPage endCursor}nodes{id isResolved comments(first:1){nodes{databaseId author{login}}}}}}}}' -F owner=OWNER -F name=REPO -F number=N
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Match on the root comment: its `comments.nodes[0].databaseId` equals the root comment id (the `thread` value the inbound carried), and `author.login` is you. Skip threads already `isResolved: true`.
|
|
190
|
+
|
|
191
|
+
**Paginate until you find the match — `first:100` is one page, not all threads.** A busy PR can carry more than 100 review threads, and yours may sit past the first page; stopping at page one would silently miss it and leave your thread open. Omit `-F after=…` on the first call, then while `pageInfo.hasNextPage` is true and you have not yet matched the `databaseId`, re-run the same query with `-F after=<endCursor>` from the previous page. Stop the moment the target thread is found (no need to walk the rest) or when `hasNextPage` is false (the thread is genuinely absent — don't fabricate a node id).
|
|
192
|
+
|
|
193
|
+
2. **Resolve it** with the node id from step 1:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
gh api graphql -f query='mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{id isResolved}}}' -F threadId=PRRT_xxx
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The returned `isResolved: true` is your proof it landed. As with every repo-targeting `gh` call, this is a **single bare `gh` invocation** — no pipes, `;`, `&&`, heredocs, or command substitution (the `github-cli-auth` plugin injects the App token into the command's environment; a pipeline would leak it). `-F` passes the id as a typed variable, so there is no shell-metacharacter hazard for the simple id/number values here.
|
|
200
|
+
|
|
201
|
+
### Self-loop safety — resolving never wakes you
|
|
202
|
+
|
|
203
|
+
Resolving your own thread is safe from the self-response loop. The `pull_request_review_thread.resolved` webhook that GitHub emits carries **you** as its `sender`, and the inbound classifier maps `pull_request_review_thread` events to their `sender` (not the PR opener) for the self-author drop — so the bot resolving a thread is recognized as self-authored and dropped, exactly like the decoy-reviewer cleanup in the PR review flow. You will not be re-woken by your own resolution. See "Self-loop safety" below.
|
|
204
|
+
|
|
141
205
|
## Opening new issues and PRs
|
|
142
206
|
|
|
143
207
|
The `gh` CLI is pre-authenticated via `GH_TOKEN` (injected by the adapter at startup). Use it to open new issues or PRs:
|
|
@@ -156,4 +220,4 @@ For App auth, `GH_TOKEN` is an installation access token that refreshes automati
|
|
|
156
220
|
|
|
157
221
|
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
222
|
|
|
159
|
-
The same guard covers **removing** a reviewer: when
|
|
223
|
+
The same guard covers **removing** a reviewer: when the adapter drops the decoy after your review lands (step 6 of the PR review flow), the `DELETE` is authenticated 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 made under your identity stays silent.
|
package/src/tui/index.ts
CHANGED
|
@@ -11,19 +11,25 @@ export type TerminalFactory = () => Terminal
|
|
|
11
11
|
|
|
12
12
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 30_000
|
|
13
13
|
|
|
14
|
-
// Bare slash-command names (no leading `/`) the TUI intercepts client-side
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
14
|
+
// Bare slash-command names (no leading `/`) the TUI intercepts client-side.
|
|
15
|
+
// The hatching ritual tells the agent to point users at `/quit` (see
|
|
16
|
+
// src/init/hatching.ts); without an intercept the literal text would be shipped
|
|
17
|
+
// to the LLM as a chat message. Grammar (case-insensitive, whitespace-tolerant,
|
|
18
|
+
// `//foo` escapes to a literal prompt) comes from `parseCommand` in
|
|
19
|
+
// src/commands so channel and TUI slash commands stay consistent. Arguments
|
|
20
|
+
// after the name disqualify the match: `/quit me a story` is a real prompt, not
|
|
21
|
+
// a command.
|
|
22
22
|
const QUIT_COMMAND_NAMES: ReadonlySet<string> = new Set(['quit', 'exit'])
|
|
23
|
+
const TUI_COMMAND_NAMES: ReadonlySet<TuiCommandName> = new Set(['quit', 'reload', 'restart'])
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
type TuiCommandName = 'quit' | 'reload' | 'restart'
|
|
26
|
+
|
|
27
|
+
function parseBareTuiCommand(text: string): TuiCommandName | null {
|
|
25
28
|
const parsed = parseCommand(text)
|
|
26
|
-
|
|
29
|
+
if (parsed === null || parsed.args.length > 0) return null
|
|
30
|
+
if (QUIT_COMMAND_NAMES.has(parsed.name)) return 'quit'
|
|
31
|
+
if (TUI_COMMAND_NAMES.has(parsed.name as TuiCommandName)) return parsed.name as TuiCommandName
|
|
32
|
+
return null
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export type VersionMismatch = { expected: string; actual: string }
|
|
@@ -203,6 +209,25 @@ export function createTui({
|
|
|
203
209
|
updateQueuePanel(msg.pending)
|
|
204
210
|
break
|
|
205
211
|
}
|
|
212
|
+
case 'reload_result': {
|
|
213
|
+
for (const result of msg.results) {
|
|
214
|
+
const text = result.ok
|
|
215
|
+
? `${colors.green('●')} ${colors.bold(`[${result.scope}]`)} ${result.summary}`
|
|
216
|
+
: `${colors.red('●')} ${colors.bold(`[${result.scope}]`)} ${result.reason}`
|
|
217
|
+
appendHistory(new Text(text, 0, 0))
|
|
218
|
+
}
|
|
219
|
+
tui.requestRender()
|
|
220
|
+
break
|
|
221
|
+
}
|
|
222
|
+
case 'restart_result': {
|
|
223
|
+
const text =
|
|
224
|
+
msg.status === 'accepted'
|
|
225
|
+
? colors.green(colors.dim(msg.message ?? 'restart scheduled; reconnecting when the new container is up'))
|
|
226
|
+
: colors.red(`restart failed: ${msg.error ?? 'unknown error'}`)
|
|
227
|
+
appendHistory(new Text(text, 0, 0))
|
|
228
|
+
tui.requestRender()
|
|
229
|
+
break
|
|
230
|
+
}
|
|
206
231
|
}
|
|
207
232
|
})
|
|
208
233
|
|
|
@@ -222,6 +247,25 @@ export function createTui({
|
|
|
222
247
|
})
|
|
223
248
|
}
|
|
224
249
|
|
|
250
|
+
function runTuiCommand(command: TuiCommandName): boolean {
|
|
251
|
+
if (command === 'quit') {
|
|
252
|
+
shutdown(0)
|
|
253
|
+
return true
|
|
254
|
+
}
|
|
255
|
+
if (command === 'reload') {
|
|
256
|
+
client.send({ type: 'reload' })
|
|
257
|
+
appendHistory(new Text(colors.dim('reloading...'), 0, 0))
|
|
258
|
+
tui.requestRender()
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
261
|
+
client.send({ type: 'restart' })
|
|
262
|
+
appendHistory(
|
|
263
|
+
new Text(colors.yellow(colors.dim('restart requested... reconnecting when the new container is up')), 0, 0),
|
|
264
|
+
)
|
|
265
|
+
tui.requestRender()
|
|
266
|
+
return true
|
|
267
|
+
}
|
|
268
|
+
|
|
225
269
|
// Esc aborts an in-flight reply. The Editor does not bind Esc, so a
|
|
226
270
|
// top-level input listener can intercept it without fighting the editor.
|
|
227
271
|
tui.addInputListener((data) => {
|
|
@@ -252,8 +296,13 @@ export function createTui({
|
|
|
252
296
|
|
|
253
297
|
editor.onSubmit = (text) => {
|
|
254
298
|
if (text.trim().length === 0) return
|
|
255
|
-
|
|
256
|
-
|
|
299
|
+
const command = parseBareTuiCommand(text)
|
|
300
|
+
if (command !== null) {
|
|
301
|
+
if (command !== 'quit') {
|
|
302
|
+
editor.setText('')
|
|
303
|
+
editor.addToHistory(text)
|
|
304
|
+
}
|
|
305
|
+
runTuiCommand(command)
|
|
257
306
|
return
|
|
258
307
|
}
|
|
259
308
|
editor.setText('')
|
|
@@ -275,13 +324,16 @@ export function createTui({
|
|
|
275
324
|
|
|
276
325
|
if (initialPrompt) {
|
|
277
326
|
// initialPrompt bypasses editor.onSubmit, so the quit intercept above
|
|
278
|
-
// would never run. Guard the same way so `typeclaw tui /quit` exits
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
327
|
+
// would never run. Guard the same way so `typeclaw tui /quit` exits —
|
|
328
|
+
// and `/reload` / `/restart` stay websocket control frames — instead of
|
|
329
|
+
// leaking the command into the agent's chat context.
|
|
330
|
+
const command = parseBareTuiCommand(initialPrompt)
|
|
331
|
+
if (command !== null) {
|
|
332
|
+
runTuiCommand(command)
|
|
333
|
+
if (command === 'quit') return { lostConnection: false }
|
|
334
|
+
} else {
|
|
335
|
+
await send(initialPrompt)
|
|
283
336
|
}
|
|
284
|
-
await send(initialPrompt)
|
|
285
337
|
}
|
|
286
338
|
|
|
287
339
|
const lostConnection = await closed
|
package/typeclaw.schema.json
CHANGED
|
@@ -104,6 +104,76 @@
|
|
|
104
104
|
]
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"type": "array",
|
|
109
|
+
"items": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"properties": {
|
|
112
|
+
"name": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"pattern": "^[a-z0-9][a-z0-9-_]*$"
|
|
115
|
+
},
|
|
116
|
+
"description": {
|
|
117
|
+
"type": "string"
|
|
118
|
+
},
|
|
119
|
+
"enabled": {
|
|
120
|
+
"default": true,
|
|
121
|
+
"type": "boolean"
|
|
122
|
+
},
|
|
123
|
+
"timeoutMs": {
|
|
124
|
+
"type": "integer",
|
|
125
|
+
"exclusiveMinimum": 0,
|
|
126
|
+
"maximum": 600000
|
|
127
|
+
},
|
|
128
|
+
"command": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"minLength": 1
|
|
131
|
+
},
|
|
132
|
+
"args": {
|
|
133
|
+
"default": [],
|
|
134
|
+
"type": "array",
|
|
135
|
+
"items": {
|
|
136
|
+
"type": "string"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"url": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"format": "uri"
|
|
142
|
+
},
|
|
143
|
+
"env": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"propertyNames": {
|
|
146
|
+
"type": "string",
|
|
147
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
|
|
148
|
+
},
|
|
149
|
+
"additionalProperties": {
|
|
150
|
+
"anyOf": [
|
|
151
|
+
{
|
|
152
|
+
"type": "string",
|
|
153
|
+
"minLength": 1
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"type": "object",
|
|
157
|
+
"properties": {
|
|
158
|
+
"value": {
|
|
159
|
+
"type": "string",
|
|
160
|
+
"minLength": 1
|
|
161
|
+
},
|
|
162
|
+
"env": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"minLength": 1
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"required": [
|
|
173
|
+
"name"
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
},
|
|
107
177
|
"plugins": {
|
|
108
178
|
"default": [],
|
|
109
179
|
"type": "array",
|
|
@@ -475,6 +545,18 @@
|
|
|
475
545
|
"items": {
|
|
476
546
|
"type": "string"
|
|
477
547
|
}
|
|
548
|
+
},
|
|
549
|
+
"review": {
|
|
550
|
+
"default": {
|
|
551
|
+
"approve": true
|
|
552
|
+
},
|
|
553
|
+
"type": "object",
|
|
554
|
+
"properties": {
|
|
555
|
+
"approve": {
|
|
556
|
+
"default": true,
|
|
557
|
+
"type": "boolean"
|
|
558
|
+
}
|
|
559
|
+
}
|
|
478
560
|
}
|
|
479
561
|
}
|
|
480
562
|
},
|