Skip to content

Commit c13a9c7

Browse files
feat(review): remove --post, drop JSON contract, let Claude do posting (#70)
Third attempt at --post exposed that parser-side heuristics are the wrong abstraction. v1.0.9 hit the undici body-timeout bug. v1.0.10 added a stderr handoff trailer to shift posting to Claude Code. v1.0.11 tolerated unescaped inner quotes. Then a real run produced *valid* JSON of a *different shape* (`{findings: [{summary, findings: [...]}]}` with no top-level `verdict`), rejected by the schema-aware parser, falling through to raw-JSON-in-chat and raw-JSON-in-the-PR-comment again. Chasing model-invented schemas is a losing game. Fix: remove the whole flag, drop the JSON output contract entirely, and let Claude Code handle posting via its own Bash tool when the user asks. Changes: * opencode-companion.mjs: remove --post/--confidence-threshold flags from both review handlers, strip the tryParseReview + renderReview path so review output is now plain prose passed through unchanged (with the existing model header), delete parseConfidenceThreshold and emitPostTrailer. * Delete plugins/opencode/scripts/lib/pr-comments.mjs and plugins/opencode/scripts/lib/review-parser.mjs entirely. * Delete tests/pr-comments.test.mjs and tests/review-parser.test.mjs. * render.mjs: drop renderReview (no callers left). render.test.mjs: drop its test suite. * prompts/adversarial-review.md: replace structured_output_contract with an output_format section that asks for plain markdown prose with a per-finding heading/file/confidence shape. * lib/prompts.mjs: same treatment for the standard review prompt. * commands/review.md and adversarial-review.md: remove --post / --confidence-threshold from arg-hint and flag list, delete the "Post-review publishing" trailer-parsing section, add an "Optional: post the review to GitHub" section that fires ONLY when the user explicitly asked in natural language. Claude composes its own summary body, writes the gh review payload to a temp file via the Write tool, POSTs via `gh api ... --input`, and reports html_url. Never REQUEST_CHANGES — always COMMENT. Net effect: the companion is simpler, the chat output is prose the user can actually read, and posting is Claude's job (where it belongs, because Claude has full editorial discretion and full access to gh). Tests: 220/220 pass (down from 268 because 48 cases covered the removed post-plumbing). No regressions in remaining suites.
1 parent 814ba16 commit c13a9c7

11 files changed

Lines changed: 89 additions & 1834 deletions

File tree

plugins/opencode/commands/adversarial-review.md

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: Run a steerable adversarial OpenCode review that challenges implementation and design decisions
3-
argument-hint: '[--wait|--background] [--base <ref>] [--model <id> | --free] [--pr <number>] [--post] [--confidence-threshold <n>] [--path <path>] [--path <path2>] [focus area or custom review instructions]'
3+
argument-hint: '[--wait|--background] [--base <ref>] [--model <id> | --free] [--pr <number>] [--path <path>] [--path <path2>] [focus area or custom review instructions]'
44
disable-model-invocation: true
55
allowed-tools: Read, Glob, Grep, Bash(node:*), Bash(git:*), Bash(gh:*), AskUserQuestion
66
---
@@ -40,8 +40,6 @@ Argument handling:
4040
- `--model <id>` overrides the saved setup default model and OpenCode's own default model for this single review (e.g. `--model openrouter/anthropic/claude-opus-4-6`). Pass it through verbatim if the user supplied it.
4141
- `--free` tells the companion script to shell out to `opencode models`, filter for first-party `opencode/*` free-tier models (those ending in `:free` or `-free`), and pick one at random for this review. Restricted to the `opencode/*` provider because OpenRouter free-tier models have inconsistent tool-use support, and the review agent needs `read`/`grep`/`glob`/`list`. Pass it through verbatim if the user supplied it. `--free` and `--model` are mutually exclusive — the companion will error if both are given.
4242
- `--pr <number>` reviews a GitHub pull request via `gh pr diff` instead of the local working tree. The cwd must be a git repo whose remote points at the PR's repository, and `gh` must be installed and authenticated.
43-
- `--post` (opt-in, requires `--pr`) publishes the adversarial review back to the PR as a GitHub review comment. The summary (verdict + findings table + collapsible full findings) is posted as the review body, and any finding with confidence at or above the threshold whose line is part of the PR diff is posted as an inline review comment. Findings below the threshold or pointing at lines outside the diff stay in the summary only. The review is always published with `event: "COMMENT"` — never `REQUEST_CHANGES` — because this tool is advisory. Pass `--post` through verbatim if the user supplied it.
44-
- `--confidence-threshold <n>` (optional, default `0.8`) sets the confidence bar for inline comments when `--post` is set. Accepts `0..1` floats or percentages (`80`, `80%`). Pass through verbatim.
4543
- `--path <path>` reviews a specific file or directory instead of git diff. Can be specified multiple times (`--path src --path lib`). When `--path` is set, the review is assembled from the actual file contents at those paths rather than from `git diff`. This is useful for reviewing specific directories, fixed sets of files, or large untracked/imported code drops. Mutually exclusive with `--pr` (paths take precedence over PR mode).
4644

4745
PR reference extraction (REQUIRED — read this carefully):
@@ -60,15 +58,15 @@ Focus text quoting (REQUIRED):
6058
- `/opencode:adversarial-review --background look for race conditions in $RUNTIME``node ... adversarial-review --background 'look for race conditions in $RUNTIME'`
6159

6260
Foreground flow:
63-
- First, transform `$ARGUMENTS` using the **PR reference extraction** and **Focus text quoting** rules above. Pass through `--wait`, `--background`, `--base`, `--scope`, `--model`, `--pr`, `--post`, and `--confidence-threshold` flags as-is; convert any `PR #N` reference in the user's text to `--pr N`; single-quote whatever free-form focus text remains.
61+
- First, transform `$ARGUMENTS` using the **PR reference extraction** and **Focus text quoting** rules above. Pass through `--wait`, `--background`, `--base`, `--scope`, `--model`, and `--pr` flags as-is; convert any `PR #N` reference in the user's text to `--pr N`; single-quote whatever free-form focus text remains.
6462
- Then run the resulting command (illustrative shape — substitute the actual transformed args):
6563
```bash
6664
node "${CLAUDE_PLUGIN_ROOT}/scripts/opencode-companion.mjs" adversarial-review <flags> [--pr N] ['<quoted focus text>']
6765
```
6866
- Return the command stdout verbatim, exactly as-is.
6967
- Do not paraphrase, summarize, or add commentary before or after it.
7068
- Do not fix any issues mentioned in the review output.
71-
- If `--post` was set, ALSO follow the "Post-review publishing" steps below before your final turn output.
69+
- If the user's original request asked for the review to be posted/commented/published to the PR, follow the "Optional: post the review to GitHub" section below after returning the review output.
7270

7371
Background flow:
7472
- Apply the same `$ARGUMENTS` transformation as the foreground flow above (PR ref extraction + focus text single-quoting).
@@ -82,25 +80,19 @@ Bash({
8280
```
8381
- Do not call `BashOutput` or wait for completion in this turn.
8482
- After launching the command, tell the user: "OpenCode adversarial review started in the background. Check `/opencode:status` for progress."
85-
- When the user later asks to collect the result and `--post` was part of the original arguments, read the completed `BashOutput` and then follow the "Post-review publishing" steps below on the captured stderr.
83+
- When the user later asks to collect the result and the original request asked for a PR post, read the completed `BashOutput`, return the review output, then follow the "Optional: post the review to GitHub" section below.
8684

87-
Post-review publishing (only when `--post` is set):
88-
- After the companion script exits, scan its **stderr** (not stdout) for a block shaped like:
89-
```
90-
<opencode_post_instructions>
91-
<pr>412</pr>
92-
<inline_count>3</inline_count>
93-
<summary_only_count>2</summary_only_count>
94-
<payload_path>/tmp/opencode-plugin-cc/post-pr-412-…json</payload_path>
95-
<command>gh api -X POST "repos/{owner}/{repo}/pulls/412/reviews" --input '/tmp/…json'</command>
96-
<cleanup>rm -f '/tmp/…json'</cleanup>
97-
</opencode_post_instructions>
98-
```
99-
- If the block is present:
100-
1. Run the exact string inside `<command>` via a single `Bash` tool call. Do not edit the command, do not re-serialize the JSON, do not invent a different endpoint — the companion already quoted the path and picked the correct endpoint.
101-
2. Parse the response body; GitHub returns a JSON object with an `html_url` field pointing at the newly-created review.
102-
3. After the verbatim stdout from step "Return the command stdout verbatim" above, append a single-line status like: `Review posted to PR #<pr>: <html_url>`. Include "(N inline comments)" only when `<inline_count>` is greater than zero.
103-
4. Run the exact string inside `<cleanup>` via a second `Bash` tool call to delete the temp payload file. If this fails, mention it once; do not retry or escalate.
104-
5. If step 1 fails (non-zero exit, or `gh` returns an error), append `Failed to post review to PR #<pr>: <stderr snippet>` instead of the success line and still run `<cleanup>`. Do not retry.
105-
- If the block is **not** present (either `--post` wasn't requested or the companion's preparation step failed — in which case stderr will contain a `[opencode-companion] Failed to prepare PR post …` line instead), do nothing extra.
106-
- Never POST, comment, or otherwise mutate the PR unless you found an `<opencode_post_instructions>` block in stderr on THIS run. Do not "help" by reposting or retrying a previous run's block.
85+
Optional: post the review to GitHub (only when the user explicitly asked):
86+
- Triggers: the user's slash-command invocation said something like "and post it to the PR", "comment it back", "publish the review", "review on GitHub". A bare `/opencode:adversarial-review --pr 412` is NOT a request to post. If in doubt, do not post.
87+
- Preconditions: `--pr <N>` was in the arguments AND the user's request explicitly asked for publishing. Never post a review for a `--path` run (paths have no PR to post to).
88+
- What to post:
89+
- A single GitHub PR review (`event: COMMENT`) containing a clean markdown summary body written in your own voice based on the review output. Do not paste the raw review output verbatim into the body; rewrite it as a digest with a short ship/no-ship line, the handful of material findings, and a closing note.
90+
- Optionally, for findings where you are confident about the exact file and line, include inline review comments anchored to those lines. Only include an inline comment if the line is part of the PR's unified diff (`gh api repos/{owner}/{repo}/pulls/<N>/files``patch` fields). Skip inline anchoring when the review is prose-only or the lines are unclear.
91+
- How to post:
92+
1. Compose the payload as a single JSON object: `{ "commit_id": "<head SHA>", "event": "COMMENT", "body": "<markdown summary>", "comments": [ { "path": "...", "line": N, "side": "RIGHT", "body": "..." }, ... ] }`. Get the head SHA via `gh pr view <N> --json headRefOid --jq .headRefOid`.
93+
2. Write the payload to a temp file (`/tmp/opencode-post-<pr>-<random>.json`) using the `Write` tool so you control escaping — do NOT try to build a heredoc inside a Bash call.
94+
3. Run `gh api -X POST "repos/{owner}/{repo}/pulls/<N>/reviews" --input /tmp/...json` via a single `Bash` call.
95+
4. Extract `html_url` from the response and append a one-line status to your final turn output: `Review posted to PR #<N>: <url>`.
96+
5. Delete the temp file with `rm -f /tmp/...json`.
97+
6. On failure, say `Failed to post review to PR #<N>: <error>` and do not retry.
98+
- The review is always published with `event: "COMMENT"` — never `REQUEST_CHANGES`. This tool is advisory, not gatekeeping.

plugins/opencode/commands/review.md

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: Run an OpenCode code review against local git state or a GitHub PR
3-
argument-hint: '[--wait|--background] [--base <ref>] [--scope auto|working-tree|branch] [--model <id> | --free] [--pr <number>] [--post] [--confidence-threshold <n>] [--path <path>] [--path <path2>]'
3+
argument-hint: '[--wait|--background] [--base <ref>] [--scope auto|working-tree|branch] [--model <id> | --free] [--pr <number>] [--path <path>] [--path <path2>]'
44
disable-model-invocation: true
55
allowed-tools: Read, Glob, Grep, Bash(node:*), Bash(git:*), Bash(gh:*), AskUserQuestion
66
---
@@ -42,8 +42,6 @@ Argument handling:
4242
- `--model <id>` overrides the saved setup default model and OpenCode's own default model for this single review (e.g. `--model openrouter/anthropic/claude-opus-4-6`). Pass it through verbatim if the user supplied it.
4343
- `--free` tells the companion script to shell out to `opencode models`, filter for first-party `opencode/*` free-tier models (those ending in `:free` or `-free`), and pick one at random for this review. Restricted to the `opencode/*` provider because OpenRouter free-tier models have inconsistent tool-use support, and the review agent needs `read`/`grep`/`glob`/`list`. Pass it through verbatim if the user supplied it. `--free` and `--model` are mutually exclusive — the companion will error if both are given.
4444
- `--pr <number>` reviews a GitHub pull request via `gh pr diff` instead of the local working tree. The cwd must be a git repo whose remote points at the PR's repository, and `gh` must be installed and authenticated. Pass it through verbatim if the user supplied it.
45-
- `--post` (opt-in, requires `--pr`) publishes the review as a PR review comment on GitHub. The summary (verdict + findings table) is posted as the review body, and any finding with confidence at or above the threshold whose line is part of the PR diff is posted as an inline review comment on that line. Findings below the threshold or pointing at lines outside the diff stay in the summary only. The review is always published with `event: "COMMENT"` — never `REQUEST_CHANGES` — because this tool is advisory. Pass `--post` through verbatim if the user supplied it.
46-
- `--confidence-threshold <n>` (optional, default `0.8`) controls which findings become inline comments when `--post` is set. Accepts `0..1` floats or percentages (`80`, `80%`). Pass through verbatim if the user supplied it.
4745
- `--path <path>` reviews a specific file or directory instead of git diff. Can be specified multiple times (`--path src --path lib`). When `--path` is set, the review is assembled from the actual file contents at those paths rather than from `git diff`. This is useful for reviewing specific directories, fixed sets of files, or large untracked/imported code drops. Mutually exclusive with `--pr` (paths take precedence over PR mode).
4846
- **PR reference extraction (REQUIRED)**: if the user's input contains a PR reference like `PR #390`, `pr #390`, `PR 390`, or `pr 390` (e.g. `/opencode:review on PR #390`), you MUST extract the number yourself and pass it as `--pr 390`. Do not pass `PR #390` literally to bash — bash strips unquoted `#NNN` tokens as comments before they reach the companion script. Example: `node ... review --pr 390`, NOT `node ... review on PR #390`.
4947

@@ -55,7 +53,7 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/opencode-companion.mjs" review $ARGUMENTS
5553
- Return the command stdout verbatim, exactly as-is.
5654
- Do not paraphrase, summarize, or add commentary before or after it.
5755
- Do not fix any issues mentioned in the review output.
58-
- If `--post` was set, ALSO follow the "Post-review publishing" steps below before your final turn output.
56+
- If the user's original request asked for the review to be posted/commented/published to the PR, follow the "Optional: post the review to GitHub" section below after returning the review output.
5957

6058
Background flow:
6159
- Launch the review with `Bash` in the background:
@@ -68,25 +66,19 @@ Bash({
6866
```
6967
- Do not call `BashOutput` or wait for completion in this turn.
7068
- After launching the command, tell the user: "OpenCode review started in the background. Check `/opencode:status` for progress."
71-
- When the user later asks to collect the result and `--post` was part of the original arguments, read the completed `BashOutput` and then follow the "Post-review publishing" steps below on the captured stderr.
69+
- When the user later asks to collect the result and the original request asked for a PR post, read the completed `BashOutput`, return the review output, then follow the "Optional: post the review to GitHub" section below.
7270

73-
Post-review publishing (only when `--post` is set):
74-
- After the companion script exits, scan its **stderr** (not stdout) for a block shaped like:
75-
```
76-
<opencode_post_instructions>
77-
<pr>412</pr>
78-
<inline_count>3</inline_count>
79-
<summary_only_count>2</summary_only_count>
80-
<payload_path>/tmp/opencode-plugin-cc/post-pr-412-…json</payload_path>
81-
<command>gh api -X POST "repos/{owner}/{repo}/pulls/412/reviews" --input '/tmp/…json'</command>
82-
<cleanup>rm -f '/tmp/…json'</cleanup>
83-
</opencode_post_instructions>
84-
```
85-
- If the block is present:
86-
1. Run the exact string inside `<command>` via a single `Bash` tool call. Do not edit the command, do not re-serialize the JSON, do not invent a different endpoint — the companion already quoted the path and picked the correct endpoint.
87-
2. Parse the response body; GitHub returns a JSON object with an `html_url` field pointing at the newly-created review.
88-
3. After the verbatim stdout from step "Return the command stdout verbatim" above, append a single-line status like: `Review posted to PR #<pr>: <html_url>`. Include "(N inline comments)" only when `<inline_count>` is greater than zero.
89-
4. Run the exact string inside `<cleanup>` via a second `Bash` tool call to delete the temp payload file. If this fails, mention it once; do not retry or escalate.
90-
5. If step 1 fails (non-zero exit, or `gh` returns an error), append `Failed to post review to PR #<pr>: <stderr snippet>` instead of the success line and still run `<cleanup>`. Do not retry.
91-
- If the block is **not** present (either `--post` wasn't requested or the companion's preparation step failed — in which case stderr will contain a `[opencode-companion] Failed to prepare PR post …` line instead), do nothing extra.
92-
- Never POST, comment, or otherwise mutate the PR unless you found an `<opencode_post_instructions>` block in stderr on THIS run. Do not "help" by reposting or retrying a previous run's block.
71+
Optional: post the review to GitHub (only when the user explicitly asked):
72+
- Triggers: the user's slash-command invocation said something like "and post it to the PR", "comment it back", "publish the review", "review on GitHub". A bare `/opencode:review --pr 412` is NOT a request to post. If in doubt, do not post.
73+
- Preconditions: `--pr <N>` was in the arguments AND the user's request explicitly asked for publishing. Never post a review for a `--path` run (paths have no PR to post to).
74+
- What to post:
75+
- A single GitHub PR review (`event: COMMENT`) containing a clean markdown summary body written in your own voice based on the review output. Do not paste the raw review output verbatim into the body; rewrite it as a digest with a short ship/no-ship line, the handful of material findings, and a closing note.
76+
- Optionally, for findings where you are confident about the exact file and line, include inline review comments anchored to those lines. Only include an inline comment if the line is part of the PR's unified diff (`gh api repos/{owner}/{repo}/pulls/<N>/files``patch` fields). Skip inline anchoring when the review is prose-only or the lines are unclear.
77+
- How to post:
78+
1. Compose the payload as a single JSON object: `{ "commit_id": "<head SHA>", "event": "COMMENT", "body": "<markdown summary>", "comments": [ { "path": "...", "line": N, "side": "RIGHT", "body": "..." }, ... ] }`. Get the head SHA via `gh pr view <N> --json headRefOid --jq .headRefOid`.
79+
2. Write the payload to a temp file (`/tmp/opencode-post-<pr>-<random>.json`) using the `Write` tool so you control escaping — do NOT try to build a heredoc inside a Bash call.
80+
3. Run `gh api -X POST "repos/{owner}/{repo}/pulls/<N>/reviews" --input /tmp/...json` via a single `Bash` call.
81+
4. Extract `html_url` from the response and append a one-line status to your final turn output: `Review posted to PR #<N>: <url>`.
82+
5. Delete the temp file with `rm -f /tmp/...json`.
83+
6. On failure, say `Failed to post review to PR #<N>: <error>` and do not retry.
84+
- The review is always published with `event: "COMMENT"` — never `REQUEST_CHANGES`. This tool is advisory, not gatekeeping.

plugins/opencode/prompts/adversarial-review.md

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,29 @@ A finding should answer:
4444
4. What concrete change would reduce the risk?
4545
</finding_bar>
4646

47-
<structured_output_contract>
48-
Return only valid JSON matching the provided schema.
49-
Keep the output compact and specific.
50-
Use `needs-attention` if there is any material risk worth blocking on.
51-
Use `approve` only if you cannot support any substantive adversarial finding from the provided context.
52-
Every finding must include:
53-
- the affected file
54-
- `line_start` and `line_end`
55-
- a confidence score from 0 to 1
56-
- a concrete recommendation
57-
Write the summary like a terse ship/no-ship assessment, not a neutral recap.
58-
</structured_output_contract>
47+
<output_format>
48+
Respond in plain markdown prose. Do not wrap the response in JSON, do
49+
not emit a code-fenced schema, and do not prefix the review with a
50+
labelled verdict token.
51+
52+
Open with a one-line ship/no-ship assessment in your own words.
53+
54+
Then, for every material finding, use the shape below (literal
55+
headings, in order):
56+
57+
### <SEVERITY> — <title>
58+
- **File:** `<path>`:<line_start>-<line_end>
59+
- **Confidence:** <low | medium | high>
60+
61+
<one or two paragraphs of adversarial analysis>
62+
63+
**Recommendation:** <concrete change>
64+
65+
Keep severity one of `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`. Keep the file
66+
path real — do not invent paths or lines. If you have no material
67+
findings, say so directly after the opening line and stop; do not pad
68+
with stylistic nits.
69+
</output_format>
5970

6071
<grounding_rules>
6172
Be aggressive, but stay grounded.

0 commit comments

Comments
 (0)