feat(respond): conversational replies on review-comment threads#333
feat(respond): conversational replies on review-comment threads#333piekstra wants to merge 2 commits into
Conversation
Add a `cr respond <PR>` command that closes the conversational loop on cr's own open review-comment threads. It lists the PR's inline review threads, selects the ones cr authored (its own findings, matched by posting identity plus a codereview marker) that have a newer unaddressed human reply, and asks a small-tier LLM classifier whether to acknowledge and resolve, reply only, or skip. Replies post in-thread via the existing ReplyToThread plumbing and the thread is resolved via ResolveThread when the finding is addressed. The engine lives in a new internal/threadreply package modeled on internal/approvaloverride: a strict structured-output schema, a deterministic prompt builder, and a pure thread-selection step that keeps repeated runs idempotent without persisted per-thread state. The command supports --dry-run/--no-post, --json, and --no-resolve-threads, and respects the profile resolve_threads policy. This is the engine + CLI-trigger slice of the feature. The menu-bar watcher trigger (polling for new thread replies and invoking cr) is a separate tool and is intentionally out of scope. Closes #326
Automated PR ReviewReviewed commit: Summary
go:implementation-tests (1 finding)Minor -
|
| Field | Value |
|---|---|
| Model | claude-sonnet-4-6 |
| Reviewers | documentation:docs, go:implementation-tests, policies:conventions, structure:repo-health |
| Engine | claude_cli · claude-sonnet-4-6 |
| Reviewed by | cr · piekstra-dev |
| Duration | 5m 28s wall · 5m 20s compute |
| Cost | ~$0.35 (est.) |
| Tokens | 21 in / 6.8k out |
Per-workstream usage
| Workstream | Model | In | Out | Cache read | Cache create | Cost | Duration |
|---|---|---|---|---|---|---|---|
| orchestrator-selection | claude-sonnet-4-6 | 3 | 124 | 3.3k | 4.8k | ~$0.02 (est.) | 26s |
| documentation:docs | claude-sonnet-4-6 | 4 | 1.2k | 7.2k | 12.4k | ~$0.07 (est.) | 26s |
| go:implementation-tests | claude-sonnet-4-6 | 3 | 124 | 3.3k | 4.8k | ~$0.02 (est.) | 1m 54s |
| policies:conventions | claude-sonnet-4-6 | 3 | 124 | 3.3k | 4.8k | ~$0.02 (est.) | 50s |
| structure:repo-health | claude-sonnet-4-6 | 4 | 3.9k | 7.7k | 23.2k | ~$0.15 (est.) | 1m 13s |
| orchestrator-rollup | claude-sonnet-4-6 | 4 | 1.3k | 18.0k | 13.3k | ~$0.08 (est.) | 29s |
piekstra-dev
left a comment
There was a problem hiding this comment.
Automated PR review completed with outcome: approved.
- threadreply: strip cr markers before sanitizing in buildRequest so the first comment's Body and OriginalFinding derive from one consistent representation and no live/escaped marker leaks to the model - threadreply: drop dead duplicate adapter.Queue in the empty-reply test - respond: hide --no-post (keep it as a working legacy alias for --dry-run) and stop advertising both flags side-by-side in the long help - llm: extract shared NewAdapterFromConfig and have reviewcmd/respondcmd delegate to it, removing the duplicated adapter dispatch
Automated PR ReviewReviewed commit: Summary
go:implementation-tests (1 finding)Minor -
|
| Field | Value |
|---|---|
| Model | claude-sonnet-4-6 |
| Reviewers | documentation:docs, go:implementation-tests, policies:conventions, structure:repo-health |
| Engine | claude_cli · claude-sonnet-4-6 |
| Reviewed by | cr · piekstra-dev |
| Duration | 5m 27s wall · 5m 19s compute |
| Cost | ~$0.75 (est.) |
| Tokens | 23 in / 12.9k out |
Per-workstream usage
| Workstream | Model | In | Out | Cache read | Cache create | Cost | Duration |
|---|---|---|---|---|---|---|---|
| orchestrator-selection | claude-sonnet-4-6 | 4 | 3.0k | 8.3k | 38.5k | ~$0.19 (est.) | 57s |
| documentation:docs | claude-sonnet-4-6 | 4 | 1.3k | 7.2k | 12.4k | ~$0.07 (est.) | 28s |
| go:implementation-tests | claude-sonnet-4-6 | 5 | 5.7k | 41.3k | 36.0k | ~$0.23 (est.) | 1m 51s |
| policies:conventions | claude-sonnet-4-6 | 3 | 130 | 3.3k | 4.8k | ~$0.02 (est.) | 42s |
| structure:repo-health | claude-sonnet-4-6 | 4 | 2.5k | 7.2k | 24.4k | ~$0.13 (est.) | 51s |
| orchestrator-rollup | claude-sonnet-4-6 | 3 | 123 | 3.9k | 26.9k | ~$0.10 (est.) | 28s |
piekstra-dev
left a comment
There was a problem hiding this comment.
Automated PR review completed with outcome: approved.
Closes #326
Summary
Adds a new
cr respond <PR>command that closes the conversational loop oncr's own open review-comment threads, so a finding can be acknowledged and resolved (or answered) without forcing a full re-review.What it does:
codereviewmarker) that are still open and whose latest comment is a new human reply cr has not already answered. Threads where cr already had the last word are skipped, so repeated runs are idempotent without persisted per-thread state.acknowledge_and_resolve- post a brief acknowledgement and resolve the thread (finding addressed/conceded),reply_only- post a contextual answer, keep the thread open (question / pushback),skip- take no action.ReplyToThreadplumbing and resolves viaResolveThread, both already on the gitprovider seam.Why it matters
Reviews feel interactive, stale open threads get closed, and authors get answers without a full re-review cycle - exactly the loop described in #326.
Design notes
internal/threadreplyengine package modeled directly oninternal/approvaloverride: strict structured-output schema (schema_version=1), deterministic prompt builder, and a pureSelectCandidatesthread-selection step. The package has full unit coverage (schema decode, prompt content, classifier behavior, candidate selection, marker stripping).internal/cmd/respondcmdcommand following thereviewcmd/mecmdidioms: injectableRuntimeFactory, profile/provider/identity resolution, domain error mapping, and a lazy LLM classifier so runs with no candidate threads never touch the LLM provider.view.RespondResulttext/JSON renderer.--dry-run/--no-post,--json,--no-resolve-threads. Respects profileresolve_threads: never. The classifier uses the profilesmallmodel tier (falls back tomediumwith a warning; rejects a profile with neither).cr respondreference section and acmd/crwiring smoke test added.Scope / remaining work
This PR implements the engine + CLI-trigger slice (part 1 of the issue, plus a manual trigger). The menu-bar watcher trigger (part 2 - polling for new thread replies and invoking
cr, e.g. cr-daemon) is a separate tool and is intentionally out of scope here;cr respondis the engine it would invoke. The classifier is stateless per run and relies on thread shape (cr did not post the latest comment) rather than a ledger, which keeps this self-contained; wiring it into the durable ledger/outbox path (likecr review) is a possible follow-up if posts need the same retry/idempotency guarantees.Testing
All commands run from the repo root:
go build ./...- passgolangci-lint run- pass (0 issues)go test ./...- pass (entire suite green)go mod tidy+git diff --exit-code go.mod go.sum- cleango run ./cmd/cr respond --helpandgo run ./cmd/cr --helprender the new command correctly.New tests:
internal/threadreply- schema decode, decision parsing, prompt content, classifier decision/reply + skip-clears-reply + empty-reply rejection, candidate selection (open/resolved/no-reply/cr-had-last-word/human-only), same-login-without-marker exclusion, marker stripping.internal/cmd/respondcmd- live post+resolve, dry-run no-post,--no-resolve-threads, skip decision,--jsonoutput, no-candidates skips classifier, missing-PR usage error.internal/view- respond renderer covered by the command JSON test.Note:
gofmt -lflags the pre-existinginternal/cmd/configcmd/configcmd.go(unchanged by this PR); all files this PR touches are gofmt-clean andgolangci-lintreports 0 issues.