From ed4b66c2299e082bb798e5df6c4898caba92539e Mon Sep 17 00:00:00 2001 From: Radu Topala Date: Mon, 27 Jul 2026 20:01:43 +0300 Subject: [PATCH] fix(review): deliver code-review findings to the Review panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A PR review run reported its findings and the panel stayed empty. Three defects stacked up between the agent's tool call and the ingest path. 1. collectOutput picked the streaming reader off a hand-listed subset of the callbacks (onTurn / onThinking / onToolResult). A review request sets none of those, so it fell through to the batch read, which re-scans the logs with an empty streamCallbacks and drops every callback. The findings were on the stream with nothing listening. Replaced with an exhaustive streamCallbacks.any(), which also restores stream-derived activity events for requests that set only OnActivity. 2. Tool inputs reached callers only as summarizeToolInput's chat-facing summary, which is empty for any tool it has no case for — including ReportFindings. Added ToolUse.Raw / onToolUseRaw / OnToolUseRaw so a caller that needs to decode arguments gets the verbatim JSON, and pointed the review runner at it. 3. Review runs forked the built-in /code-review command into a silent subagent instead of running it inline. Inline requires CLAUDE_CODE_REPORT_FINDINGS plus an available ReportFindings tool, so review mode now drops that tool from the batch disallow list and passes the env via --settings. Container env is applied *under* the user's settings file, so a host CLAUDE_CODE_SUBAGENT_MODEL would otherwise overwrite ours; --settings lands in a scope that wins. Tests cover the raw channel, the streaming gate per callback, and a review-shaped request end to end, plus the real findings payload. Docs updated to describe ReportFindings as the default reporting channel with the MCP tool as the override path. --- .gitignore | 3 + docs/configuration.md | 4 +- docs/events.md | 2 +- docs/review.md | 36 +++++--- internal/agent/types.go | 21 +++++ internal/api/review_handler.go | 28 ++++--- internal/api/review_handler_test.go | 45 +++++++++- internal/api/review_service.go | 23 ++++-- internal/config/config.go | 8 +- internal/container/claude_cmd.go | 41 ++++++++- internal/container/runner.go | 3 +- internal/container/runner_mounts_test.go | 31 +++++++ internal/container/runner_retry_test.go | 16 ++-- internal/container/runner_shell_test.go | 101 +++++++++++++++++++++++ internal/container/runner_stream_test.go | 34 ++++++++ internal/container/stream.go | 34 ++++++-- internal/review/parser.go | 47 +++++++++++ internal/review/parser_test.go | 57 +++++++++++++ internal/review/runner.go | 30 +++++-- internal/review/runner_test.go | 61 +++++++++++++- 20 files changed, 562 insertions(+), 63 deletions(-) diff --git a/.gitignore b/.gitignore index 67b95736..423d8ce1 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,9 @@ memory/ tickets/ .worktrees/ +# Throwaway working files (binary dumps, captured payloads) — never committed +.scratch/ + # Component test artifacts test/component/screenshots/ diff --git a/docs/configuration.md b/docs/configuration.md index 19b411f5..eace2d1e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -48,7 +48,7 @@ Exactly one of these should be set. OAuth takes precedence if both are provided. | `claude_effort` | `string` | `""` | Reasoning-effort level passed to the Claude CLI as `--effort`. Empty leaves it unset (model default). Options: `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`. | Both `claude_model` and `claude_effort` can additionally be **overridden per channel/thread/worktree on demand** from the chat composer's model pill (persisted on the channel row, applied from the next run) — see [Chat: Model / Effort Override](chat.md#model--effort-override). The per-channel override beats the merged config value. -| `claude_batch_disallowed_tools` | `[]string` | `["ScheduleWakeup", "CronCreate", "CronDelete", "CronList", "Monitor", "ReportFindings"]` | Claude Code tool names passed to `--disallowedTools` for **batch** (`--print`) agent runs only — the interactive terminal is unaffected. The defaults rely on a persistent harness that one-shot containers lack: `ScheduleWakeup`/`Cron*` schedule re-invocations that never fire, and `Monitor` arms a background watcher whose cross-turn events are dropped when the container exits at end of turn — so they silently park work that never resumes. `ReportFindings` reports code-review findings into a harness UI channel batch runs don't render — Loop's review flow uses the `report_review_findings` MCP tool instead. Override to tune the list (e.g. to re-enable a tool). | +| `claude_batch_disallowed_tools` | `[]string` | `["ScheduleWakeup", "CronCreate", "CronDelete", "CronList", "Monitor", "ReportFindings"]` | Claude Code tool names passed to `--disallowedTools` for **batch** (`--print`) agent runs only — the interactive terminal is unaffected. The defaults rely on a persistent harness that one-shot containers lack: `ScheduleWakeup`/`Cron*` schedule re-invocations that never fire, and `Monitor` arms a background watcher whose cross-turn events are dropped when the container exits at end of turn — so they silently park work that never resumes. `ReportFindings` reports code-review findings into a harness UI channel batch runs don't render. Review runs are the exception: they drop `ReportFindings` from this list, because the built-in `/code-review` command only runs inline (rather than forking a silent subagent) when it can call that tool, and the daemon reads the findings off the stream. Override to tune the list (e.g. to re-enable a tool). | | `claude_retry` | `object` | see below | Automatic recovery from Claude API limits on batch agent runs. Keys: `max_attempts` (`5`) extra attempts after the first failure (`0` disables), `backoff_base_sec` (`5`) first-retry delay that doubles each attempt, `backoff_max_sec` (`120`) per-attempt delay ceiling — these govern in-process exponential backoff for **transient** errors (rate limiting, overload, 5xx). `session_limit_auto_continue` (`true`) handles a **session limit** error (`You've hit your session limit · resets