* feat(llm): support custom retry status codes via retry_codes config
Add a retry_codes configuration field that allows users to specify
additional HTTP status codes (4xx) that should trigger exponential
backoff retry. This targets self-hosted LLM clusters that misuse
status codes like 403 or 400 for rate limiting.
Implementation uses SDK middleware to inject the x-should-retry: true
response header when a configured status code is encountered, leveraging
the existing SDK retry mechanism (up to 5 retries with exponential
backoff) without any SDK modifications.
Configuration is supported via:
- Provider config: providers.<name>.retry_codes / custom_providers.<name>.retry_codes
- Legacy llm config: llm.retry_codes
- CLI: ocr config set providers.<name>.retry_codes 403,400
Also fixes cloneProviderEntry to copy previously missing fields
(TimeoutSec, ExtraHeaders, RetryCodes) for TUI rollback correctness.
* test(llm): add unit tests for retry_codes feature
Cover ParseRetryCodes validation, retryCodesMiddleware behavior,
resolver integration (provider and legacy config paths, invalid codes),
and end-to-end retry via httptest for both OpenAI and Anthropic clients.
* feat(llm): silently filter redundant retry_codes (408/409/429) instead of erroring
Replace validateRetryCodes with sanitizeRetryCodes that filters out
SDK-default codes and returns warnings. The resolve phase silently
drops redundant codes without interrupting startup. The config set
command prints warnings to stderr so users are informed.
This makes the tool friendlier for users migrating from other tools
who habitually configure 429 and similar codes.
* test(tui): add coverage for cloneProviderEntry deep-copy of TimeoutSec/RetryCodes/ExtraHeaders
Verify that the clone correctly copies these fields and that
mutations to the clone do not affect the original. Also pin the
nil-stays-nil behavior for RetryCodes and ExtraHeaders.
* docs(llm): fix contradictory ParseRetryCodes comment
* test(config): cover retry_codes warning output in config set path
Verify that redundant codes (429, 408) emit a WARNING to stderr while
still writing the valid codes to config. Also verify that valid-only
codes produce no warning output.
runPreview, runScanPreview, and delegate's preview each built a throwaway
agent via agent.New / scan.NewAgent purely to reach Preview. Both
constructors auto-create a session, and session.New opens persistence and
buffers a session_start record, so every preview created a JSONL file
under the OCR home. Preview never runs or finalizes a review, so that
file was left unfinalized and usually empty.
Make the exported entry point a package-level Preview(ctx, args) in both
internal/agent and internal/scan. It builds only what file selection
needs, so there is no session, manifest, or runner to leak. The existing
bodies stay as unexported methods, keeping the in-package tests
(including scan's regression test that Preview must not mutate a.items).
Deleting the file afterwards was rejected: it would still leak on crash
and would keep the wrong abstraction.
Tests assert at the CLI boundary, with a temporary OCR home, that no
session store is created by any of the three preview commands. They also
neutralize global git config, which git resolves via XDG_CONFIG_HOME
independently of HOME.
Both commands advertise `--format text|json` and parse `-f json -p`
without error, but the preview paths called outputPreviewText
unconditionally and never consulted the requested format, so automation
asking for JSON got the human view instead.
Route runPreview and runScanPreview through a small dispatcher so
`--format json` emits the existing model.Preview DTO as a single JSON
value on stdout. Only "json" opts out of the human view, so text output
is unchanged.
Agent.Preview also left Entries nil for an empty diff, which marshals as
`"files":null`. Scan preview already preallocated the slice; do the same
for review so both emit `"files":[]` and consumers can iterate
unconditionally.
Note the new behavior on both `--preview` rows of the CLI reference, in
all four locales.
* test: raise statement coverage to 90% and enforce it in CI
Add unit tests across the cmd and internal packages to bring total
statement coverage above 90%, and gate future regressions.
- Cover CLI helpers, provider TUI handlers, resume/manifest paths, and
error branches in config, llm, llmloop, scan, session, agent, viewer,
mcp, pathutil, and telemetry.
- Raise the coverage threshold from 80% to 90% in the Makefile
(COVERAGE_THRESHOLD) and in the CI "Check coverage threshold" step.
- Ignore generated coverage.out and coverage.html artifacts.
Total statement coverage is now 90.5%, measured consistently by both
`make coverage` and the CI `go test ./...` scope.
* test: widen statement coverage margin with environment-independent unit tests
Add table-driven unit tests for pure, environment-independent functions to
raise the statement-coverage safety margin above the 90% threshold:
- session.ResumeState.ValidateScanOptions (70% -> 100%)
- rules.SystemRule.UnmarshalJSON error branches (71% -> 82%)
- llmloop.stripMarkdownFences no-newline branch (82% -> 100%)
- diff.firstLine empty/blank-input branch
- diff.extractCodeBlock missing-newline and no-closing-fence branches
- main.truncate n<=1 and normalization branches
- agent.Agent nil-receiver accessor guards
* chore: add SPDX license headers to all source files
Add Apache-2.0 SPDX license identifiers and copyright notices to all
tracked .go, .sh, .js, .mjs, .ts, and .tsx source files.
Introduce scripts/verify-license.sh and scripts/add-license.sh for
automated verification and bulk addition of license headers. Integrate
the check into CI (ci.yml) and the Makefile (license-check target as
a prerequisite of the existing check target).
This satisfies the OpenSSF Best Practices Badge requirements for
copyright_per_file and license_per_file.
* fix: restore execute permissions on scripts
* docs: add license header instructions to CONTRIBUTING guides
* docs: add license header instructions to pages contributing guides
* fix(pages): strip unclosed HTML comment markers to satisfy CodeQL
* fix: apply code review suggestions for license scripts
- Fix portability: detect macOS vs Linux stat for permission copy
- Fix has_header: check both SPDX and copyright (match verify logic)
- Fix is_ignored: match on path boundaries to avoid false positives
- Fix year extraction: use consistent pipeline across both scripts
- Fix Bash 3.2 compat: quote array length expansion for set -u
* fix(pages): use loop-until-clean for HTML comment stripping (CodeQL)
* fix(pages): use split/join instead of replace to avoid CodeQL false positive
CodeQL's js/incomplete-multi-character-sanitization rule flags any
.replace() that removes multi-character sequences like '<!--...-->',
regardless of context. The data here comes from readFileSync on the
project's own index.html (no untrusted input), making this a false
positive. Using split(regex).join('') achieves the same result without
triggering the taint-tracking rule.
Parent commands (session, config, delegate, llm, rules) were defined
without a RunE function. In Cobra, when a parent command has no RunE
and receives an unrecognized subcommand, it falls back to displaying
help and returns nil (exit 0). This is inconsistent with the root
command and leaf commands, which correctly return exit code 1 with an
error message.
Add RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }
to all five parent commands so Cobra properly reports "unknown command"
errors instead of silently succeeding.
Includes tests verifying:
- unknown subcommands produce a non-nil error containing "unknown command"
- known subcommands still route correctly
- bare parent commands (no args) still print help and return nil
Closes#641
Adds a new 'ocr session comments <session-id>' subcommand that prints the
review comments persisted in a session, rendered in the same style as
'ocr review' terminal output (path, line range, severity badge, suggestion
diff). Supports --json for machine-readable output and --severity/--category
comma-separated filters.
Comments are read from the review_item_done / review_item_reused checkpoint
records via a new session.LoadComments, mirroring resume replay semantics:
a later checkpoint for the same fingerprint supersedes the earlier one and a
subsequent failure drops it.
Also adds shell tab completion for session ids (session show, session
comments, and review --resume) and for the --severity/--category values.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(session): add run manifest coverage data model and builder
First slice of issue #367 (run manifest coverage contract): the data
model and state machine only. Not yet wired into the agent or CLI, so
existing review/scan output is unchanged.
Introduce the versioned, immutable RunManifest (schema ocr.run-manifest/v1)
and a concurrency-safe ManifestBuilder that tracks per-file coverage
(selected/completed/reused/failed/waived) and freezes into a terminal
state.
- terminal state derived solely from coverage sets, never comments/warnings
(complete/partial/failed/skipped)
- Finalize sweeps any undecided selected item to failed/unknown so no item
is silently dropped
- single-mutex builder: first terminal state wins, frozen after Finalize,
nil-receiver safe
- fixed failure classification enum with an unknown catch-all
- redaction floor on failure/waive reasons (strip secrets, cap length) as a
single write entry so callers cannot bypass it
- 22 unit tests, race-clean
Refs: issue #367
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(session): harden run manifest per adversarial review
Address findings from the concurrency / JSON-contract / PR#306-coupling
adversarial review of the manifest data model (still slice 1; not wired to
agent or CLI).
- SetSweepClass: Finalize can classify undispatched items as cancelled/budget
instead of a blanket unknown (the one real model gap the review found)
- ItemID(fingerprint)=SHA-256 canonical mint helper; an item_id is never a raw
fingerprint, keeping the resume cross-reference explicit and mix-ups caught
- sanitizeReason: strip control/ANSI chars, coerce valid UTF-8, redact quoted
secret values, guarantee single line
- Finalize returns deep-copied coverage slices so the frozen snapshot is never
aliased across the two outlets
- RegisterSelected: nil-safe (lazy-init map) + documents that only the
post-deletion/post-filter dispatchable set may be registered
+7 unit tests (29 total), race-clean.
Refs: issue #367
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(manifest): wire input identity, config hashes and run-level failure (shard ②d)
- Freeze per-mode input identity (mode + resolved_base/head + exact_range +
source_artifact_sha256) via diff.ResolveInput/commitParents, and repository
identity via RemoteIdentity/canonicalRemote (credential-free).
- Add rule_config_sha256 and runtime_config_sha256 over an allowlist of
non-secret fields using a length-prefixed SHA-256 framework (no tokens/URLs).
- Replace SetRunLevelFailure(bool) with structured SetRunFailure(class, reason)
and set ManifestInput.mode; fill execution.* (ocr version, provider, model,
concurrency, config hashes).
- Thread error returns through Finalize/WriteSessionEnd (main review path
surfaces them; skip/all-failed/scan paths hardened in follow-up).
- Tests: manifest_hash, canonical_config, git_resolve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(manifest): propagate persistence errors and harden remote/error classification
Merged review themes A/B/E from the 07-22 consolidated assessment.
Theme A — Finalize / session_end delivery errors no longer swallowed:
- agent.go no-files path returns the Finalize error instead of nil (A1)
- agent.go loadDiffs failure joins the Finalize error via errors.Join (A2)
- session.Finalize uses sync.Once + cached finalizeErr: written exactly
once, concurrency-safe, and every caller replays the same result so a
retry cannot falsely report success (A3)
- scan/agent.go wires both Finalize call sites to surface the error (A4)
Theme B — canonicalRemote rewritten (internal/diff/git.go):
- keep the port (u.Host, not u.Hostname) so endpoints differing only by
port stay distinct (B1)
- split scp syntax on the first ':' so an '@' inside the path survives (B2)
- recognize local/file/Windows/UNC remotes and omit identity rather than
misparsing a path as a host (B3; local-remote policy still open)
Theme E — main_task-empty is now a sentinel (errMainTaskEmpty) classified
via errors.Is instead of matching error text.
Theme D (TOCTOU) deferred to shard 4 per issue #367 open-issues OI-12.
Tests: go build ./... + go vet + go test ./... all green (23 pkgs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(manifest): report both dispatch and persistence errors on the normal path
The success-path Finalize wiring used `ferr != nil && err == nil`, so when the
review (or scan) failed AND session_end also failed to persist, the persistence
error was dropped and only the dispatch error surfaced — the caller never
learned the session/manifest was not saved.
Join both with errors.Join when both occur (matching the loadDiffs path), so a
persistence failure is always reported even alongside a dispatch failure. This
closes the last gap in the OI-10 contract.
- internal/agent/agent.go: review normal path
- internal/scan/agent.go: scan normal path (+ errors import)
Tests: go build ./... + go vet + go test ./... all green (23 pkgs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(manifest): 接入 CLI 与 viewer 并补齐验收用例
- 使用冻结 manifest 统一 review JSON、文本与退出状态\n- session CLI 和 viewer 展示五集合覆盖并兼容 legacy/aborted\n- 补充本地 mock、跨出口一致性及安全验收用例
* test(manifest): 补齐验收矩阵缺口并修复审核发现的缺陷
验收用例:configuration 分类(run 级 sweep + item 级映射)、budget/timeout/panic 混合 partial 隔离、跨出口一致性改为规范化原始字节比对、flag 校验失败无产物断言。
代码修复:sanitizeReason 先剥控制字符再脱敏(堵控制字节绕过)、失败项异分类二次标记报冲突错误、source_artifact_sha256 按 item_id 去重并稳定排序、sortItems 改 SliceStable 对齐设计用词。
全仓 go test 23 包通过。
* test(manifest): 补充 provider transition resume 测试用例
覆盖 issue #367 验收标准 provider transition:resume 时 provider/model 改变后,子 manifest 记录当前值而非继承父运行,并经 parent_run_id 链接父会话以支持审计。用 mock client,不依赖真实 provider key。
* fix(manifest): 对齐预算终态与持久化语义
统一聚合预算停止时的 coverage、status 与退出码。传播 session writer 初始化错误,并补齐 merge first-parent 输入身份及回归测试。移除代码注释中的外部设计文档引用。
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: kite <254839944+lizhengfeng101@users.noreply.github.com>
The supported top-level config keys lived both in the switch/case in setConfigValue and, hardcoded again, in the unknown-key error message, so adding a key meant updating two places. Move them into a supportedConfigKeys slice and generate the error message from it. The message content is unchanged.
Closes#637
runConfig manually dispatched config subcommands and parseConfigArgs did manual arg slicing, duplicating the routing the production config command already defines with cobra. Rebuild runConfig as a fresh cobra tree mirroring production (matching the runSession pattern), and drop parseConfigArgs, configAction, and configParseError, which are no longer used. Dispatch behavior is unchanged and stays covered by the existing runConfig/config_dispatch tests.
Closes#638
* refactor(cli): migrate to Cobra framework for shell completion support
Replace the hand-rolled ocrFlagSet + switch dispatch with spf13/cobra,
enabling native bash/zsh/fish/powershell completion via `ocr completion`.
Key changes:
- Add root.go (rootCmd definition, version flag with -V shorthand)
- Add completion.go (ocr completion [bash|zsh|fish|powershell])
- Add shared_flags.go (reusable flag registration helpers + validation)
- Rewrite all *_cmd.go to use cobra.Command with RunE
- Delete flags.go (ocrFlagSet, expandShortFlags, parseXxxFlags)
- Add compat_test.go (test compatibility wrappers for existing tests)
- Promote github.com/spf13/cobra from indirect to direct dependency
Behavioral improvements over the previous implementation:
- Shell completion for all commands, flags, and enum values
- "Did you mean?" suggestions for misspelled commands
- cobra.NoArgs on review/scan prevents silent positional arg ignoring
- Cleaner error messages on unknown flags (no full flag dump)
- Consistent help output format across all subcommands
Closes#576
* fix(cli): add Args: cobra.NoArgs to viewerCmd
Prevents `ocr viewer localhost:3000` from silently ignoring the
positional argument and starting on the default address.
Consistent with reviewCmd and scanCmd.
* feat(cli): add "Did you mean?" suggestions for misspelled flags
Cobra only suggests corrections for unknown subcommands, not flags.
Add a levenshtein-distance based suggestion that fires when cobra
returns an "unknown flag" error, matching the same UX pattern.
Examples:
--hel → Did you mean this? --help
--audienc → Did you mean this? --audience
--comit → Did you mean this? --commit
* fix(deps): promote spf13/pflag to direct dependency
After the Cobra migration, pflag is directly imported but was still
marked as indirect in go.mod, causing CI's go-mod-tidy check to fail.
* refactor(cli): use idiomatic cobra patterns for args validation and flag errors
Replace hand-rolled argument validation in configSetCmd/configUnsetCmd
with cobra.ExactArgs, and move flag typo suggestions from post-hoc error
string parsing into SetFlagErrorFunc where cobra provides the command
context directly.
Treat FAILED and missing completion as review failures, and retry
invalid task_done states instead of accepting them as success.
Keep partial scan output and session history aligned with those states.
Co-authored-by: 4-1-1 <4401981+4-1-1@users.noreply.github.com>
Co-authored-by: kite <254839944+lizhengfeng101@users.noreply.github.com>
Add a runtime token-budget guardrail to the diff-review path so a large MR
stops itself before runaway cost (issue #409: ~90.4M tokens in one failed
attempt), instead of timing out and losing all structured output.
New `ocr review` flag (mirror scan's proven --max-tokens-budget):
- --max-tokens-budget N : cap aggregate token usage; dispatch stops once the
running total + a per-file look-ahead would exceed it (0 = unlimited).
Mechanism mirrors scan/agent.go's dispatchBatch gate exactly: read the
existing atomic Runner counters (no duplicate counter — a second one would be
a drift bug) before acquiring the semaphore, and break the dispatch loop on
exceed. In-flight workers are allowed to finish (overrun bounded by the
in-flight count, <= concurrency), matching scan's documented contract.
Budget exhaustion returns the partial comments already produced with a nil
error (not a Go error — the failure path suppresses output), and signals
budget-exceeded out-of-band so the output layer sets a typed `budget_exceeded`
status distinct from success / completed_with_warnings / completed_with_errors.
Additional changes tied to the invariants:
- Pre-review scale warning (files, diff tokens, configured budget) printed
before any model spend; non-blocking, warn-only.
- Structured usage emitted on the failure path (stderr) so the cost of a
failed attempt is never lost. Carries only token/tool tallies — no
credentials or prompts. Reports the agent's actual BudgetExceeded() state so
the residual budget-trip + all-dispatched-fail edge can never contradict the
typed status.
- summary.budget_exceeded JSON field (additive, omitempty) so old parsers are
unaffected; default 0/unlimited restores prior behavior.
Internal/scan gains only the BudgetExceeded() accessor (returns false) so the
shared ResultProvider interface compiles; scan keeps its own token budget and
its JSON output is unchanged.
Tests mirror internal/scan/budget_test.go: token-budget gate stops dispatch
early and sets BudgetExceeded(); unlimited default runs all files; estimate
helpers project sane values (with a humanTokens parity table so the two copies
cannot diverge); JSON output asserts the typed status and the failure-path
usage record. Full `make test` (-race) green.
- implement the documented `ocr v` alias for viewer in dispatch
- list the implemented --exclude flag in `ocr review -h`
- mention mcp_servers.<name> in the `config unset` usage error
- add missing delegate/viewer -h hints to top-level usage
- print localhost instead of wildcard hosts in viewer banner URLs
* feat(mcp): add remote MCP server support via Streamable HTTP transport
Support connecting to remote MCP servers using the Streamable HTTP
transport from go-sdk. Users configure remote servers with type=remote,
a URL, and optional headers (with environment variable expansion for
secrets). Authentication stays outside OCR — users provide tokens via
headers, matching CLI tool conventions.
Closes#335
* docs(mcp): document remote MCP server support and shell expansion warning
Add remote transport (Streamable HTTP) documentation to MCP Server
sections in all READMEs and CLI help text. Include type/url/headers
field descriptions, remote server examples (CLI and JSON config),
and a tip about using single quotes to prevent shell expansion.
* fix(mcp): harden remote MCP client validation and error handling
- Reject URLs with missing host (e.g. "http://") in config validation
- Make empty-after-expansion header values a fatal error instead of a warning
- Detect HTTP 401/403 responses and surface clear authentication error messages
- Drain response body before closing to preserve connection pool reuse
* test(mcp): add tests for header env var expansion failure in NewRemoteClient
Verify that NewRemoteClient returns a clear error when header values
expand to empty strings, covering both explicitly empty env vars and
completely unset env vars.
The module path was github.com/open-code-review/open-code-review but the
repo lives at github.com/alibaba/open-code-review. This mismatch prevents
pkg.go.dev indexing and breaks Go Report Card resolution.
The LLM resolver honors providers.<name>.timeout_sec and llm.timeout_sec
(and the docs tell users to hand-edit them into config.json), but the
cmd-side ProviderEntry/LlmConfig structs lacked the field. Every
loadOrCreateConfig + saveConfig cycle — any `ocr config set`,
`ocr config model`, or interactive provider setup — rewrites the whole
file from those structs, silently dropping the key and reverting the
per-request timeout to the 300s default.
Add the field to both structs so the value survives round-trips. This
intentionally does not add `ocr config set` write support for the key,
matching the documented behavior.
* feat(delegate): add delegation mode for host-agent driven code review
Add `ocr delegate` subcommand that provides deterministic file selection
and rule resolution without calling any LLM. This enables AI coding agents
to perform reviews themselves using OCR only for engineering scaffolding
(preview which files to review, resolve grouped rules by path).
Includes:
- `ocr delegate preview` — outputs reviewable file list with mode/ref metadata
- `ocr delegate rule <path...>` — outputs review rules grouped by content
- Claude Code plugin command (delegate-review.md)
- Skill definitions for Claude Code, Codex, and Cursor
- Unit tests for internal/delegate package
- README documentation synced across all 5 locales
* fix(delegate): group rules by source, pattern and text
GroupRules keyed groups on rule text alone, so files sharing identical
rule text but resolved from different sources or matched by different
patterns were merged into one group that kept only the first file's
Source/Pattern metadata. Use a composite (source, pattern, text) key so
each group's provenance is accurate for every file it contains.
* feat(llm): add OpenAI Responses API support
Add `openai-responses` as a third LLM protocol alongside `anthropic` and
`openai-chat-completions`, enabling code review via the OpenAI Responses
API (/v1/responses) for GPT-5.x / o-series models.
Protocol naming refactor (backward-compatible):
- Canonicalize "openai" -> "openai-chat-completions" (alias still accepted)
- Add NormalizeProtocol / ValidateProtocol / IsAnthropicProtocol helpers
- Registry uses canonical constants; resolver normalizes everywhere
New OpenAIResponsesClient (stateless replay, per DESIGN_STATE_CACHE_PHASE.md):
- system messages -> Instructions; tool calls -> function_call items keyed
by CallID so the agent loop pairs results correctly
- store=false (privacy); prompt_cache_key = sha256(instructions)[:32]
- Phase fields (commentary/final_answer) dropped with TODO for gpt-5.3-codex+
Config plumbing:
- llm.protocol field + OCR_LLM_PROTOCOL env (priority over use_anthropic /
OCR_USE_ANTHROPIC); TUI exposes all three protocols in Custom & Manual
- anthropic-vertex rejected with friendly "not yet implemented" message
Docs: protocol reference, config examples, env var table, and Responses API
notes (store=false caching caveat, cache key derivation, Phase TODO) updated
across en/zh-CN/ko-KR/ja-JP/ru-RU READMEs.
* refactor(llm): switch PromptCacheKey to precomputed scheme via ChatRequest.CacheKey
Replace per-turn sha256 computation inside buildResponsesParams with a
precomputed cache key that callers compute once per session and pass
through ChatRequest.CacheKey (json:"-"). The key now incorporates the
first user message alongside instructions, so different files under
review land in distinct cache buckets — the previous instructions-only
key was identical across all files.
Changes:
- ChatRequest gains CacheKey string field (json:"-", zero impact on
Chat Completions / Anthropic clients which never read it)
- New llm.ComputeCacheKey helper: sha256(instructions + "\x00" +
firstUser)[:32]
- responses_client.go: reads req.CacheKey directly, removes promptCacheKey
function and first-user-message scanning
- loop.go: RunPerFile computes cacheKey once before the loop, reuses
every turn
- All 8 remaining call sites (agent, scan, relocation, compression,
llm_cmd) compute once at request construction
- Update PLAN_RESPONSES_SUPPORT.md and DESIGN_STATE_CACHE_PHASE.md to
reflect the precomputed scheme
- Update tests: passthrough tests for client, dedicated TestComputeCacheKey
* refactor(llm): use canonical protocol name "openai" and UUID-based session ID for cache key
Two changes to maximize backward compatibility and simplify the design:
1. Protocol naming: revert ProtocolOpenAIChatCompletions value from
"openai-chat-completions" back to "openai". Old config files with
protocol: "openai" are now identical to what new configs write —
zero behavioral difference. The alias direction in NormalizeProtocol
is reversed: "openai-chat-completions" -> "openai" (for configs
written during this branch's testing phase only).
2. Cache key: replace content-based sha256 hash (ComputeCacheKey) with a
random UUID session ID. The agent loop generates one UUID per file in
RunPerFile and passes it via ChatRequest.SessionID; the Responses
client uses it as prompt_cache_key. Single-turn call sites no longer
set a cache key (no multi-turn caching benefit). This removes the
need to scan messages or compute hashes, and eliminates collision
risk between files with similar content.
ChatRequest.CacheKey is renamed to SessionID to reflect its actual
semantic — a per-session identifier that the Responses client
repurposes as prompt_cache_key.
Also updates PLAN_RESPONSES_SUPPORT.md, all 5 README translations,
test expectations, and promotes google/uuid to a direct dependency.
* refactor(llm): remove IsAnthropicProtocol helper and anthropic-vertex special case
* refactor(llm): remove openai-chat-completions branch-internal alias
* docs: remove DESIGN_STATE_CACHE_PHASE and PLAN_RESPONSES_SUPPORT design notes
* fix(llm): address code review findings on Responses API support
- provider_cmd: clear stale use_anthropic when switching to openai-responses
- resolver: validate preset protocol with ValidateProtocol for consistency
- responses_client: swap usage mapping to resolveUsage-first (matches OpenAIClient)
- responses_client: map failed/cancelled statuses to 'error' finish reason
- usage_resolver: add Responses API field paths (input_tokens, output_tokens,
input_tokens_details.cached_tokens)
- add tests for all four fixes
* fix(llm): mirror use_anthropic when setting llm.protocol
- config_cmd: 'ocr config set llm.protocol' now mirrors use_anthropic
(anthropic -> true, OpenAI family -> false) for backward compat with
older binaries that predate llm.protocol
- provider_cmd: openai-responses now sets use_anthropic=false instead of
nil, so older binaries fall back to the OpenAI family rather than
wrongly defaulting to anthropic
- update tests for both write paths
* fix(llm): mirror protocol when setting llm.use_anthropic
- config_cmd: 'ocr config set llm.use_anthropic' now mirrors protocol
(true -> anthropic, false -> openai) so the two fields never disagree,
matching the reverse llm.protocol mirroring added previously
- without this, setting use_anthropic=true while protocol=openai-responses
left a contradictory config that misled older binaries into using the
anthropic protocol against an OpenAI endpoint
- extend tests to cover both values and stale-protocol overwrite
* docs(llm): fix NormalizeProtocol comment to match lowercasing behavior
The comment claimed unknown values are 'returned unchanged', but the
default branch lowercases and trims them (corroborated by the
'gRPC -> grpc' test). Update the wording to describe the actual
behavior so callers aren't misled about round-trip fidelity.
* docs(llm): drop OpenAI Responses API implementation notes from READMEs
* fix(llm): address code review findings on Responses API support
- config: preserve openai-responses when setting llm.use_anthropic=false
(only mirror to openai when protocol is unset or a legacy anthropic/openai)
- config: add Protocol values guidance to unknown-key error message
- protocol: extract normalized local var in NormalizeProtocol
- responses_client: align SDK base URL trimming with NewOpenAIClient
- responses_client: drop unused test-only sdkBaseURL method
* fix(llm): use protocol constants consistently
- providers: edenai now uses ProtocolOpenAIChatCompletions like the rest
of the registry instead of the "openai" string literal
- provider_cmd: print the normalized protocol variable (what is actually
saved) instead of the raw TUI value
* fix(llm): drop stream key and surface non-completed status in Responses client
Address two PR review comments on OpenAI Responses API support:
1. extra_body.stream=true was forwarded to Responses.New, making the API
return SSE while the SDK expects JSON and breaking every call. Skip the
'stream' key (like OpenAIClient treats it as a non-forwarded key) while
still forwarding other extra_body entries.
2. The Responses API returns HTTP 200 even for failed/cancelled (terminal)
and queued/in_progress (background) states, so the SDK reports nil error.
Surface these as real errors so callers branching on err != nil (ocr llm
test, review loop) fail instead of treating a dead response as success.
Add table-driven tests covering both fixes.
- Add ContextWithTraceParentFromEnv to extract TRACEPARENT env var and
inject upstream span context via OTel TextMapPropagator.
- Register TraceContext+Baggage composite propagator in Init().
- Wire trace parent propagation into review and scan entry points.
- Add tests for valid, absent, disabled, and malformed TRACEPARENT.
* feat(telemetry): add OTLP HTTP exporter and print TraceID
- Add HTTP/protobuf exporter support alongside existing gRPC exporter
- Route based on OTEL_EXPORTER_OTLP_PROTOCOL config (http/protobuf vs grpc)
- Print TraceID to stderr when telemetry is enabled for easier correlation
- Add corresponding unit tests
* feat(telemetry): add span coverage for LLM calls, tool execution, plan/filter phases
- Add StartLLMSpan / RecordLLMResult helpers (span.go), symmetric with
existing StartToolSpan / RecordToolResult
- Wrap LLM completion calls in llmloop.RunPerFile with llm.request spans
- Wrap all three tool execution paths in executeToolCall with
tool.execute.* spans (dynamic tools, code_comment sync/async, other tools)
- Add plan.execute span around executePlanPhase
- Add main.loop span around RunPerFile call in executeSubtask
- Add review_filter.execute span around executeReviewFilter, with
comments.before / comments.filtered attributes
- Record llm.error attribute on LLM failures for diagnosability
- Record review.repo / review.from / review.to / review.model on the
top-level review.run span
- Metrics (RecordLLMRequest / RecordToolCall) are preserved alongside
the new spans — they serve different purposes (aggregate dashboards
vs per-run diagnosis)
Verified end-to-end against Sunfire (OTLP HTTP gateway): full span tree
observed for review.run -> subtask.execute -> plan.execute/main.loop/
review_filter.execute -> llm.request/tool.execute.*
* fix(telemetry): address CR findings — span error handling, async span lifecycle, protocol robustness
- Add span.RecordError(err) to RecordLLMResult and RecordToolResult for
consistency with EndSpan
- Use OTel standard pattern (span.SetStatus + span.RecordError) in error
paths of review.run, plan.execute, main.loop, review_filter.execute
- Move async code_comment span end into pool.Submit callback so span
duration reflects actual execution time
- Unify time.Since(startTime) in code_comment error path to a single dur
- Remove http/json from supported OTLP protocols (not actually implemented)
- Add stderr warning when unknown OTLP protocol falls back to gRPC
* feat(telemetry): include trace_id in JSON output, restrict stderr to text format
- Add trace_id as top-level field in jsonOutput struct (omitempty)
- JSON format: trace_id in structured response for programmatic extraction
- Text format: TraceID printed to stderr for human debugging
- Telemetry disabled: trace_id field omitted entirely
* fix: address PR review findings
- loop.go: wrap async span lifecycle in defer to prevent leak on panic
- exporter.go: update parseOTLPEndpoint comment to reflect gRPC+HTTP usage
- scan_cmd.go: align traceID extraction and OTel error handling with review_cmd
- output.go/shared.go: propagate traceID to outputJSONNoFiles for consistency
- agent.go: move comments.filtered attribute before early return so 0 is
distinguishable from not-executed
* feat(telemetry): address PR review — http/json routing, LLM span coverage, trace_id tests
- Route http/json to HTTP exporter (Go OTel SDK HTTP transport only
supports protobuf serialization; users need HTTP transport, not JSON encoding)
- Add llm.request spans to executePlanPhase, executeReviewFilter, and
ReLocateComment with Usage nil-safety consistent with loop.go
- Add trace_id assertions to output helper tests and emitRunResult
end-to-end tests using real TracerProvider
* docs: add OTLP protocol selection and endpoint format to telemetry section
Sync across all 5 README language versions (en, zh-CN, ja-JP, ko-KR, ru-RU).
* fix: unify time.Since in async code_comment defer to single dur variable
* fix(tool): resolve file_read paths against git top-level in monorepos
ocr review from a monorepo subdirectory failed with "file not found" (#287):
git reports diff and `git show HEAD:<path>` paths relative to the repo root,
but RepoDir was scoped to the invocation subdirectory, producing a double
prefix. resolveWorkingDir now anchors RepoDir at `git rev-parse
--show-toplevel` on the review path (requireGit=true); scan keeps the CWD so
its `git ls-files` walk stays scoped.
The top-level lookup uses a stdout-only git helper so stderr notices can't
pollute the path, and fails loudly if --show-toplevel errors or is empty
(e.g. a bare repo) instead of silently reusing the subdirectory. Adds
regression tests for the subdir hoist, the scan-path scoping, git-show
resolution of root-relative paths, and the bare-repo failure.
* docs(rules): document repo-root rule.json resolution in monorepos
Since #287 anchored RepoDir at the git top-level, ocr review from a
monorepo subdirectory loads the repo-root .opencodereview/rule.json
rather than a subdir-local one. Call out this user-visible behavior at
loadProjectRule so the scope change isn't a surprise (review feedback).
Add two structured fields, category and severity, to every review finding
so CI integrations can sort, group, filter, or gate builds without
re-parsing natural-language comment text.
- Tool schema (tools.json): add category/severity as enum-constrained,
required properties of code_comment. severity is limited to
critical/high/medium/low (info dropped, since LLMs struggle to
distinguish low from info).
- System prompt (task_template.json) is intentionally left untouched to
avoid the review-quality regression observed on the benchmark suite;
the tool schema alone drives field population.
- JSON output: category/severity are flat siblings of content/start_line,
omitted entirely when empty (backward compatible).
- CLI output: render an inline [category - severity] badge before the
comment, colored by severity.
- Sync docs across all five README locales.
* fix(tui): persist official-tab models and refine saved secret hint
Persist user-added models to providers.<name>.models on the official tab.
When an API key or auth token is already saved, show a replace hint with a
prefix/suffix fingerprint (skipped for short keys), use a fixed mask placeholder,
and ensure typing or paste replaces the saved value instead of re-saving it.
* fix(tui): model add/delete UX and config wizard hardening
- Add model add/delete in config provider and config model (official + custom)
- Show d Delete only on model rows; green highlight when selected
- Improve Esc cancel text; track savedInSession to avoid misleading messages
- Reload config on save failure; read registry models fresh after reload
- Export llm.ModelListContains; fix config model persist using registry-only check
* fix(tui): defer provider config until confirm and harden API key UX
Only persist provider/model on wizard confirm; keep in-session picks via
sessionModelPick. Validate API key before quit, clear saved keys when emptied,
and improve official env-var hints and custom edit clear behavior.
* feat(tui): show active model suffix on official provider list
Align Official tab with Custom: display (model) next to the active preset
when cfg.Provider matches and a global model is configured.
* feat(mcp): add Model Context Protocol server support
Add MCP client and provider packages that allow integrating external
MCP tool servers into the review loop. Includes config commands for
managing MCP servers, stdio subprocess integration tests, and
comprehensive test coverage.
* refactor(mcp): rename loop variable in contentToText to avoid shadowing Client receiver
* fix(mcp): use platform-specific shell for setup command
The MCP server setup command was hardcoded to use `sh -c`, which
fails on Windows. Extract a `shellCommand` helper behind build tags
to use `cmd /c` on Windows and `sh -c` elsewhere.
* docs(mcp): add MCP server documentation to all README locales
The testdiff CLI was an early-stage tool for manually testing the
internal/diff package. It has no external references and its role is
fully covered by the existing unit tests in internal/diff/.
* feat: improve config provider TUI interaction
- Support wrap-around navigation in provider, custom provider, and model lists
(up at first item jumps to last, down at last item jumps to first)
- Add d key to delete custom models in model selection step with confirmation prompt
- Add protocol selection (anthropic/openai) to manual configuration flow
and respect UseAnthropic setting in saved config
- Preserve manual protocol selection when re-entering the form
- Fix manual form input fields losing focus after returning from next step
- Use safe slice removal in removeFromSlice to avoid backing array corruption
* fix: address code review feedback
- Restore Blur() calls in handleManualFormEnter to prevent visual focus artifacts
when transitioning between manual form steps
- Add empty-providers guard in handleUp/handleDown to avoid negative index
- Consolidate redundant protocolIdx checks into single if/else in viewManualTab
and viewCustomProviderForm for clarity
* fix: persist custom model deletions and support masked auth token editing
- Track deleted custom models in TUI and apply them on exit
(both confirm and cancel paths) to keep config.json in sync
- Clear custom provider's active Model field when that model is deleted,
preventing stale "model" reference in the provider list label
- Add masked display for manual config Auth Token to allow easy re-entry,
matching the official provider flow (any key clears and starts fresh)
* test: update manual form tests for masked auth token behavior
Update TestProviderTUI_ManualFormPrefilledValues,
TestProviderTUI_ManualFormEscRestoresOriginalValues, and
TestProviderTUI_ManualFormPrefilledWhenProviderSet to assert
the new masked display state (manualTokenMasked + manualTokenOriginal)
instead of the raw token value.
* feat: refine config provider TUI flows and session persistence
Custom provider interactions:
- Simplify create/edit form to Name → Protocol → URL → API Key → Auth Header
- After create or edit save, jump straight into the model list for that provider
- Support comma-separated model names in the custom model input
- Show masked API key in edit form; empty Auth Header defaults to (Authorization)
- Populate edit form from existingCfg to avoid stale list data after in-session saves
Model selection and highlight:
- Green highlight follows the persisted active model, not the cursor position
- Prefer provider entry.model over global cfg.model when resolving active model
- Deleting a non-active model keeps the current green highlight unchanged
Delete and navigation:
- Delete custom providers (d) and custom models (d) with confirmation prompts
- Persist create/edit/model-select/add/delete changes to disk during the session
- Fix custom provider deletion not surviving Esc exe-entry
Tests:
- Add coverage for model highlight, delete-model behavior, and create→model-list flow
- Update manual/custom form tests for masked token and session save behavior
* refactor: address provider TUI code review feedback
- Remove dead helper removeFromSlice (superseded by removeModels)
- Move misplaced doc comment to applyEditCustomProviderSave
- Drop redundant applyProviderDeletions post-TUI call so provider
deletions rely solely on the in-session save
- Fix brace/indent drift in updateDeleteModelConfirm and cache the
model list instead of recomputing m.models() twice
* feat: refine provider TUI flows and manual config form
Custom provider flow:
- Default protocol to anthropic on the new-provider form
- Single-name model input; reject duplicates with inline error and
preserve the typed value so the user can edit instead of re-typing
- Drop the global green highlight; cursor/blue is the only selection cue
Manual configuration form:
- Add Auth Header step (URL → Protocol → Model → Auth Token → Auth Header)
and persist it to Llm.AuthHeader on confirm
- Reorder so Auth Header is always entered last
- Make Auth Token required; empty Enter stays on the field
- Show every field's label on every render, even when empty, matching
the custom-provider form style
Tests:
- Cover custom model input add / duplicate paths
- Cover manual form prefill of Llm.AuthHeader
- Cover that deleting a non-active model keeps the active model intact
* Improve provider TUI validation, persistence, and code clarity.
Address code review feedback: align custom Auth Header validation with manual mode; fix savedInSession after model deletion; refactor applyEditCustomProviderSave to return error; remove dead applyModelDeletions/deletedModels; document ExtraBody shallow-copy limit. Also fix manual/custom form UX (token skip on edit, k key input, formError scoping, switch indentation).
* Fix provider/model TUI list ordering and add test coverage.
Address review and UX feedback: remove model list sorting in provider and config model TUIs; preserve Models list order when selecting active model (ensureModelInList); add test for duplicate rename on custom provider edit. Includes prior review fixes for savedInSession, applyEditCustomProviderSave error return, and dead code removal.
* Normalize AuthHeader at apply layer and simplify UseAnthropic assignment.
Call NormalizeAuthHeader in applyManualConfig and applyCustomProviderConfig before save; simplify UseAnthropic assignment in manual config; add unit tests.
* Address review: lowercase error strings and newProviderTUI signature.
Use lowercase "failed to save" errors; replace variadic configPath with string and remove configPathFromArgs; pass "" in tests when no path; gofmt provider_tui.go.
* feat: add tool_calls field to JSON output for tool usage statistics
Track per-tool invocation counts in llmloop.Runner and expose them
through the ResultProvider interface so both review and scan modes
report tool call statistics in --format json output.
* fix: correct tool_calls counting and ensure stable JSON schema
Move recordToolCall after lookupTool nil check so only actually-executed
tool calls are counted. Always emit tool_calls field in JSON output for
a consistent schema, initializing by_tool to empty map when nil.
* fix: remove omitempty from tool_calls to ensure stable JSON schema
Drop omitempty from the tool_calls struct tag and initialize ToolCalls
in outputJSONNoFiles so the field is always present in JSON output
regardless of execution path.
* feat: add ocr scan for full-file code review
Introduce a new top-level subcommand `ocr scan` (alias `s`) that reviews
whole files instead of git diffs. Use cases include reviewing unfamiliar
codebases, pre-migration audits, and ad-hoc per-directory reviews.
Architecture splits scan and diff review at the package level so the two
pipelines can evolve independently:
- internal/scan/ new package: file enumeration via `git ls-files`,
full-scan agent, FULL_SCAN_TASK rendering, preview
- internal/llmloop/ new package: shared LLM tool-use loop, three-zone
memory compression, CommentWorkerPool, AgentWarning.
Both internal/agent and internal/scan delegate to
llmloop.Runner; agent and scan never import each other
- internal/agent/ slimmed: LLM loop / compression / token aggregation
moved to llmloop; review-only orchestration remains
- internal/model/ new ScanItem (full-file payload) + Preview /
PreviewEntry / ExcludeReason shared by both modes
- internal/diff/ new gitignore.go exporting helpers reused by scan
- cmd/opencodereview/ new scan_cmd.go; shared.go consolidates startup
(loadCommonContext / loadLLMRuntime), output
(emitRunResult, ResultProvider) and stdout silencing
(quietHandle); review_cmd.go follows the same shape
Template additions:
- FULL_SCAN_TASK: dedicated prompt with Tool-call discipline guidance to
reduce gratuitous tool calls per file
- FULL_SCAN_MAX_TOOL_REQUEST_TIMES (default 60): scan-only per-file budget,
raised over diff's 30 to fit multi-finding files; --max-tools still
composes (only raise, never lower)
In scan mode, file_read_diff is filtered out of MainToolDefs since it has
no useful semantics without a diff.
Tests cover provider enumeration (with temp git repo), template rendering,
filter passes, dependency budget, flag validation, and excludeToolDef.
* feat(scan): v2 — exclude / non-git / split template / plan / batch / dedup / project-summary
Address design-review feedback by evolving `ocr scan` along seven axes
while keeping `ocr review` behavior unchanged:
1. File size cap is now configurable (ScanTemplate.MaxFileSizeBytes,
default 2 MiB; previously a hard-coded 5 MiB). The cap exists only to
bound memory reading; the real review-feasibility gate is the per-file
token budget downstream.
2. Drop the `--all` flag. Bare `ocr scan` now scans the whole repo;
`--path` narrows. Less ceremony, fewer redundant flags.
3. New `--exclude` flag on both review and scan. Comma-separated
gitignore-style patterns; merged with rule.json's exclude layer via
the new shared.applyCLIExcludes helper.
4. Scan supports non-git directories. internal/scan.Provider chooses
between `git ls-files` (full .gitignore semantics) and a
filepath.WalkDir fallback (root .gitignore + ExcludedDirs blocklist)
per isGitRepo probe. loadCommonContext takes a requireGit bool; review
keeps the hard requirement, scan relaxes it.
5. Scan configuration lives in its own file. internal/config/template:
- new ScanTemplate type with LoadScanDefault/ApplyLanguage/Validate
- new embedded scan_template.json
- Template loses the FULL_SCAN_* fields (review template unaffected)
scan.Agent.Args.Template now holds a ScanTemplate; toLoopTemplate
adapts it for llmloop.Runner.
6. New scan phases — each nil-able in the template and toggleable via a
CLI flag, so users can revert to v1 behavior trivially:
* PLAN_TASK (--no-plan): per-file pre-pass that outputs a JSON
summary + checkpoints, embedded into MAIN_TASK as {{plan_guidance}}.
formatPlanGuidance renders to markdown; malformed JSON falls back
to raw text. PLAN_TASK failure never blocks the main loop.
* BATCH_STRATEGY (--batch): files are grouped before dispatch.
"none" preserves v1, "by-language" (default) groups by extension,
"by-directory" groups by first-level subdir. BatchSize caps natural
groups so a single language with 500 files doesn't form one giant
batch. Batches are processed sequentially; files within a batch
remain concurrent up to MaxConcurrency.
* DEDUP_TASK (--no-dedup): per-batch postprocess that asks the LLM
to cluster near-duplicate comments. Output is a `groups` JSON;
every input id must appear exactly once or the result is rejected
and originals are kept (safety: never silently lose comments).
CommentCollector grows Snapshot/Since/ReplaceSince for this.
* PROJECT_SUMMARY_TASK (--no-summary): once-per-run cross-file
summary appended to text output and surfaced as `project_summary`
in JSON output. ResultProvider grows ProjectSummary(); agent.Agent
returns "" (review mode has no project summary).
All four new LLM steps record token usage via runner.RecordUsage so
aggregate counters stay accurate.
7. Tests cover the new pure code paths:
- batch_test.go: 3 strategies, BatchSize cap, language-key edge cases
- dedup_test.go: groups parser, malformed shapes, fence stripping,
payload field selection
- agent_test.go: formatPlanGuidance variants, buildSummaryCommentsList
truncation, maybeRunPlan skip paths
- provider_test.go: non-git directory walker fallback
- template_test.go: ScanTemplate loads / ApplyLanguage / review
template no longer contains scan fields
The seven phases can be reverted independently by toggling flags or
clearing the corresponding optional template fields; nothing forces the
new behavior on existing review users.
* fix(scan): three real bugs surfaced by SCAN_PLAN_TASK self-review
A v2 end-to-end test (ocr scan --path internal/scan/preview.go) had the
PLAN_TASK phase flag three concrete bugs in the scan package itself.
This commit fixes them and adds regression tests.
1. Preview() mutated a.items as a side-effect.
Both Preview and Run wrote to a.items. Calling Preview before Run
silently primed Run with the preview's enumeration instead of
triggering a fresh listFiles. Preview is documented as a read-only
dry-run; uphold that. Local variable now; a.items stays nil after
Preview returns.
2. Preview.result.Entries was nil when there were no items.
With no items at all the loop never ran, so Entries remained nil and
JSON marshalling produced "files":null. Pre-allocate to a non-nil
empty slice so the JSON contract stays "files":[] regardless.
3. Provider.Enumerate and listFilesViaWalk never checked ctx.Done().
On a large repo a cancelled context would still complete the full
walk before the caller saw an error (every iteration costs a stat or
ReadFile syscall). Add the check at the top of each iteration in
both the git-ls-files path and the walker fallback path; the walker
returns ctx.Err() so filepath.WalkDir propagates the cancellation.
Three new regression tests pin the contracts:
- TestPreview_DoesNotMutateAgentItems
- TestPreview_EmptyResultEntriesIsNonNilSlice
- TestProvider_Enumerate_RespectsContextCancellation
* feat(scan): cost estimate + token budget cap; fix file_find on non-git dirs
Two cost-control features and one robustness fix, all surfaced by running
the scanner against a real ~870K-token repository.
Cost estimate (internal/scan/estimate.go):
- Before dispatch, Run prints an order-of-magnitude projection of token
usage (input/output/total), derived from per-file content size × an
assumed round count, plus the optional plan/dedup/summary phases.
- Deliberately reports tokens only, not dollars — pricing varies per
provider/model and a precise figure would mislead. Actual usage is still
reported from the API after the run.
Token budget cap (--max-tokens-budget / ScanTemplate.MaxTokensBudget):
- Caps total token usage for one scan. The gate is checked per file inside
dispatchBatch, right before acquiring a concurrency slot: if tokens
already spent plus a look-ahead estimate of the next file would exceed
the budget, that file and all remaining files are skipped and a
token_budget_reached warning is recorded.
- An earlier batch-level gate was too coarse: with the default by-language
batching, a Go-heavy repo puts most files in one batch, so the gate only
fired between batches and overran the budget ~2.4×. The per-file gate
bounds overrun to roughly one in-flight file per worker (~1.3× at
concurrency=1 in testing).
- 0 = unlimited (unchanged default behavior).
Phase-gate helpers (planEnabled/dedupEnabled/summaryEnabled) consolidate
the "template defines it AND --no-* flag not set" checks so the cost
estimate and the dispatch path agree on which phases will actually run.
file_find non-git fallback (internal/tool/file_find.go):
- `git ls-files` exits 128 in a non-git directory, which spammed failures
when scanning plain directories (scan already supports non-git repos via
the provider's walker, but the file_find tool did not). Now falls back
to filepath.WalkDir honoring the root .gitignore and the default
excluded-dir blocklist when git fails and no specific ref is requested.
Tests:
- estimate_test.go: humanTokens formatting, per-file vs aggregate estimate
consistency, phase scaling, phase-gate tri-state.
- budget_test.go: fake LLM client drives the gate deterministically —
verifies dispatch stops before exceeding budget and that 0 = unlimited.
- file_find_test.go: non-git directory fallback finds files, honors
.gitignore / blocklist, and returns the not-found sentinel correctly.
* docs(readme): document ocr scan subcommand and flags
ocr scan existed but was undiscoverable from the README. Add it to the
intro blurb, Quick Start, the Commands table, Examples, and a dedicated
flags table (path / exclude / preview / max-tokens-budget / no-plan /
no-dedup / no-summary / batch / format / concurrency / rule / repo).
Note non-git support and the pre-run cost estimate. Also backfill the
--exclude flag in the ocr review flags table (added during the v1.3 merge
but never documented).
Flag names and defaults verified against `ocr scan -h`.
* fix(scan): code_search works in non-git directories via git grep --no-index
code_search relied on `git grep`, which exits 128 in a non-git directory —
so `ocr scan` on a plain directory (already supported by file enumeration and
file_find) silently returned errors instead of search results. Detect that
failure and retry with `git grep --no-index --exclude-standard`, which searches
the working tree directly while still honoring .gitignore. Reuses all existing
grep flag/parsing logic; ref-based search still requires a real repo.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(scan): preserve context on compression failure; fix NUL parsing in gitLs
Address three real bugs from the PR #93 automated review that regressed when
compression moved into internal/llmloop:
- Sync compression failure / empty summary now return the original messages
instead of truncating to the frozen zone, which discarded the whole
per-file conversation context.
- Async compression now abandons the job on error instead of applying a
truncated snapshot, and re-applies messages appended while it ran
(snapshotLen), so concurrent tool results are no longer lost.
- scan Provider.gitLs uses cmd.Output() instead of CombinedOutput() so
stderr can't corrupt the NUL-delimited (-z) filename parsing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add ability to delete custom providers from configuration
Implements GitHub issue #136.
CLI: add 'ocr config unset custom_providers.<name>' command to delete
a custom provider from config. If the deleted provider is the active
one, clears 'provider' and 'model' fields and prompts the user.
TUI: press 'd' on Custom tab to delete a provider with y/n confirmation.
Shows warning when deleting the active provider. Deletions are persisted
even if the user cancels provider selection afterward.
- Add runConfigUnset() with key validation and active-provider handling
- Add 'unset' case to parseConfigArgs() and printConfigUsage()
- Add delete confirmation state machine to providerTUIModel
- Add applyProviderDeletions() helper in provider_cmd.go
- Add 10 new tests covering CLI parsing, deletion logic, and TUI flows
- Update README with unset command documentation
* fix: address PR review feedback
- Add defensive bounds check for deleteTargetIdx before deletion
- Use explicit slice copy to avoid retaining references (memory leak)
- Update subCmd comment to reflect 'set' and 'unset' values
* refactor: extract shared deleteCustomProvider and improve test coverage
Address PR review feedback from lizhengfeng101:
- Extract deleteCustomProvider() as a pure function shared by both
runConfigUnset (CLI) and applyProviderDeletions (TUI)
- Extract unsetCustomProvider() accepting configPath for testability
- Add existence check in applyProviderDeletions (skip non-existent providers)
- Rewrite 3 tests to call actual functions instead of inlining logic
- Remove unused strings import from config_cmd_test.go
* style: use [ocr] WARNING prefix for active provider deletion messages
Align with project convention (output.go, flags.go): warnings use
'[ocr] WARNING' prefix and write to stderr instead of stdout.
* fix: address second round of PR review feedback
- Fix test state pollution in TestUnsetInvalidKey: use t.Run sub-tests
with independent config files per case (#7)
- Log warning instead of silently swallowing errors in
applyProviderDeletions (#9)
- Add comment explaining existingCfg snapshot assumption in
viewCustomTab (#10)
- Simplify runConfigUnset error message to single line for consistency
with project style (#11)
ApplyLanguage was only called when the config file existed, so without
~/.opencodereview/config.json no language instruction was injected and
the LLM picked its own language. Now ApplyLanguage is called
unconditionally, defaulting to English via resolveLang("").
Update all README translations to reflect the corrected default
(English) and clarify that any language name is accepted.