Skip to content

feat(code-review): add local reviewer agents#3

Merged
kdegeek merged 19 commits into
mainfrom
feature/local-review-agents-coderabbit-kilo
Jun 29, 2026
Merged

feat(code-review): add local reviewer agents#3
kdegeek merged 19 commits into
mainfrom
feature/local-review-agents-coderabbit-kilo

Conversation

@kdegeek

@kdegeek kdegeek commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • add spec-first local review agents doc for CodeRabbit and Kilo Code
  • add first-class local review service, CodeRabbit/Kilo CLI adapters, structured findings, gate decisions, and JSON artifact tracking
  • register list/check/run local review tools and include them in the code toolbox
  • add hook runner script for advisory/blocking pre-push or pre-PR usage

Validation

  • bun test src/server/services/local-review.test.ts src/server/services/toolboxes.test.ts src/server/tools/index.test.ts
  • bun test src/server/tools/i18n-tool-names.test.ts src/server/services/local-review.test.ts src/server/services/toolboxes.test.ts src/server/tools/index.test.ts
  • bun run typecheck
  • git diff --check
  • bun run build
  • pre-commit hook: bun run typecheck + bun run test passed during commit

Notes

  • CodeRabbit CLI is not installed on this host, so runtime CodeRabbit reviews will report CLI-missing until installed/authenticated.
  • Kilo CLI 7.3.44 is installed; no dedicated non-interactive local-review slash command was confirmed, so the MVP uses the documented prompt fallback via kilo run --format json --auto.

Summary by CodeRabbit

  • New Features
    • Added local code review agents for CodeRabbit and Kilo, including provider discovery, auth checks, and advisory vs blocking gating.
    • Introduced a local review execution flow that extracts findings and saves durable JSON artifacts.
    • Added a CLI to run local reviews with mode-based exit codes, plus built-in code tools to list providers, check auth, and run reviews.
  • Bug Fixes
    • Improved finding parsing from noisy, mixed JSON/markdown outputs (including Kilo fallback behavior).
  • Documentation
    • Added a full specification for the local review workflow, artifact schema, and exit-code semantics.
  • Localization / Configuration
    • Added localized labels for the new tools and configurable limits for timeouts and review output size.
  • Tests
    • Added coverage for parsing, gating decisions, artifact persistence, and missing/unavailable CLI handling.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a local review pipeline for CodeRabbit and Kilo Code, with docs, service logic, tool registration, a CLI entrypoint, config, locale strings, and tests.

Changes

Local Code Review Agents

Layer / File(s) Summary
Local review agent documentation
docs/local-review-agents.md
Adds the local review agent specification covering workflow, tools, provider auth behavior, artifact schema, remediation loop, and deferred enhancements.
Config and locale support
src/server/config.ts, src/test-helpers.ts, src/client/locales/en.json, src/client/locales/fr.json, src/server/services/toolboxes.ts
Adds config.codeReview, mirrors it in test config, updates toolbox allow-lists, and extends localized tool-name strings for the new local review tools.
Service contracts and CLI execution
src/server/services/local-review.ts
Defines review types and provider metadata, and implements CLI execution with timeout, truncation, and fallback command probing.
Provider discovery, parsing, and gating
src/server/services/local-review.ts
Implements local reviewer discovery, provider auth checks, JSON and JSON-lines parsing, finding normalization, and blocking-mode gate evaluation.
Review orchestration and artifact writing
src/server/services/local-review.ts
Implements the main local review run flow, per-provider execution, CodeRabbit and Kilo runners, artifact persistence, and the test hook export.
Tool registrations and CLI entrypoint
src/server/tools/code-review-tools.ts, src/server/tools/register.ts, scripts/hivekeep-local-review.ts
Adds the local review tool registrations, registers them in the tool system, and provides the Bun CLI wrapper for running reviews.
Tests for parsing, gating, and persistence
src/server/services/local-review.test.ts
Covers parsing cases, gate evaluation, artifact persistence, and missing-CLI behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hop through logs and JSON lines,
Two review pals join in neat designs.
With artifacts tucked safe and sound,
the code review trail spins round and round.
Blocking bites, advisory breezes glide —
this rabbit grins with plushy pride.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding local reviewer agents for code review.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces local code review capabilities to Hivekeep, integrating CodeRabbit and Kilo Code as first-class reviewer providers. It adds CLI scripts, tools, configuration options, translation keys, and comprehensive unit tests. The review feedback highlights several robust improvements to the core local-review service: wrapping Bun.spawn in a try-catch block to prevent unhandled synchronous errors when a CLI is missing, enhancing JSON parsing to support pretty-printed JSON objects in addition to JSON lines, safely parsing stringified line numbers, and explicitly checking for execution timeouts to provide clearer error messages.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/server/services/local-review.ts Outdated
Comment thread src/server/services/local-review.ts
Comment thread src/server/services/local-review.ts
Comment thread src/server/services/local-review.ts Outdated
Comment thread src/server/services/local-review.ts Outdated
Comment thread src/server/services/local-review.ts Outdated
Comment thread src/server/services/local-review.test.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jun 28, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • src/server/services/local-review.ts
  • src/server/services/local-review.test.ts
Previous Review Summaries (11 snapshots, latest commit 6b6d0d9)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 6b6d0d9)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (5 files)
  • src/client/App.tsx
  • src/client/components/layout/AppTopBar.tsx
  • src/client/pages/reviewer-agents/ReviewerAgentsPage.tsx
  • src/server/services/local-review.ts
  • src/server/services/local-review.test.ts

Previous review (commit cbfaaec)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • docs/local-review-agents.md
  • src/server/services/reviewer-agents.ts
  • src/server/services/reviewer-agents.test.ts

Previous review (commit a45e384)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 2

The incremental commits 8caf6135 (UI), cab25f03 (API hardening), and a45e3841 (gate summary) build out first-class reviewer agents with proper path-traversal guards, admin middleware, and a gate summary that now reflects failed runs. The cursor[bot] concerns that were resolved by this commit range include:

  • Gate summary ignores failed runs (cursor[bot, local-review.ts:runLocalCodeReview) - now computes runStatus and appends \nGate: FAILED. when any provider failed (local-review.ts:526-531).
  • Artifact run ID path traversal (cursor[bot, local-review.ts:artifactPathFor) - now rejects .., /, \, and \0 in the id and verifies the resolved path stays inside the artifact dir (local-review.ts:434-444).
  • Reviewer API lacks admin guard (cursor[bot, reviewer-agents routes) - requireAdmin middleware now applied via reviewerAgentRoutes.use('*', requireAdmin) (reviewer-agents.ts:28-41).
  • Unhandled errors in runOneProvider are now fail-open for blocking reviews (kilo-code-bot, local-review.ts) - catch block now sets blocked: input.mode === 'blocking' to fail closed (local-review.ts:562-565).
  • Hook ignores configured default mode (cursor[bot, scripts/hivekeep-local-review.ts) - script now reads config.codeReview.defaultMode as the fallback (scripts/hivekeep-local-review.ts:17) and the failed-mode exit check uses result.mode (scripts/hivekeep-local-review.ts:41).
  • Relative repo path ignores workspace (cursor[bot, code-review-tools.ts) - resolveRepoPath now resolves relative paths under the tool workspace (code-review-tools.ts:11).

New findings (changed-code)

Issue Details (click to expand)

WARNING

File Line Issue
src/client/pages/reviewer-agents/ReviewerAgentsPage.tsx 442 PageHeader action cluster (w-72 + w-40 + w-32 + Refresh) overflows on phone widths (<sm); violates design rule #8
src/client/components/layout/ActivityBar.tsx 32 New /reviewer-agents nav item not mirrored in mobile AppTopBar.tsx modeItems / sectionPrefixes, so the page is unreachable on mobile

SUGGESTION

File Line Issue
src/server/services/local-review.ts 482 updateReviewFindingState is read-modify-write on disk and loses concurrent updates
src/server/services/local-review.ts 119 readCappedStream head/tail slices can cut a multi-byte UTF-8 sequence, producing U+FFFD replacement characters in findings
Files Reviewed (13 files)
  • docs/local-review-agents.md - 0 issues
  • scripts/hivekeep-local-review.ts - 0 issues (pre-existing flagged issues resolved)
  • src/client/App.tsx - 0 issues
  • src/client/components/layout/ActivityBar.tsx - 1 issue (WARNING)
  • src/client/locales/en.json - 0 issues
  • src/client/pages/reviewer-agents/ReviewerAgentsPage.tsx - 1 issue (WARNING)
  • src/server/app.ts - 0 issues
  • src/server/routes/reviewer-agents.ts - 0 issues (admin guard properly added)
  • src/server/services/local-review.test.ts - 0 issues
  • src/server/services/local-review.ts - 2 issues (SUGGESTION)
  • src/server/services/reviewer-agents.test.ts - 0 issues
  • src/server/services/reviewer-agents.ts - 0 issues
  • src/server/tools/code-review-tools.ts - 0 issues (relative-path fix verified)

Fix these issues in Kilo Cloud

Previous review (commit 5746e97)

Status: No Issues Found | Recommendation: Merge

The incremental commit 02557f76 (fix: forward base ref to Kilo /local-review slash command) correctly forwards base/baseCommit to the Kilo /local-review slash command, resolves the pre-existing cursor[bot] comment Kilo slash ignores base ref (ID 3488952253), and adds matching unit-test expectations. No new issues introduced on changed lines.

What changed

  • src/server/services/local-review.ts:452-460 — two new pure helpers:
    • kiloSlashCommandBaseArg(input) returns input.base ?? input.baseCommit.
    • kiloSlashCommandToken(input) returns /local-review <base> when both kiloSlashCommand resolves to /local-review and a base arg is truthy; otherwise returns the bare command (so /local-review-uncommitted stays unchanged and missing-base inputs are not stringified).
  • src/server/services/local-review.ts:463kiloSlashCommandArgs now uses kiloSlashCommandToken(input). Args are still passed as one argv entry per element (Node spawn), so no shell injection from base values containing spaces/specials.
  • src/server/services/local-review.test.ts:72-74 — three updated expectations covering base, baseCommit, and head: 'working tree' (no base) cases. Test fakes updated to use $7 == /local-review* glob (line 83, 102, 122) so they match both /local-review and /local-review <base> argument variants. The end-to-end test at line 78-95 exercises base: 'origin/main' and confirms localReviewMode: 'slash-command' plus the minor finding round-trip.

Pre-existing concerns resolved by this commit

  • cursor[bot] local-review.ts:464 (id 3488952253) — Kilo slash ignores base ref — directly resolved. The primary Kilo path now forwards input.base ?? input.baseCommit to /local-review.

Observations (unchanged file, not in PR diff)

  • docs/local-review-agents.md:81-91 — the documented slash-command examples show kilo run --format json --auto --dir <repo> /local-review without the base arg, which no longer matches the implementation when a base/baseCommit is supplied. The doc was accurate when written and is now stale by this commit; per changed-code scope the docs file is out of PR diff so this is summary-only, not an inline comment. Worth updating in a follow-up so users running the documented command by hand still get the diff-scope they expect.

No other pre-existing comments target changed lines.

Files Reviewed (2 files)
  • src/server/services/local-review.test.ts - 0 issues
  • src/server/services/local-review.ts - 0 issues

Previous review (commit 02557f7)

Status: No Issues Found | Recommendation: Merge

The incremental commit 02557f76 (fix: forward base ref to Kilo /local-review slash command) correctly forwards base/baseCommit to the Kilo /local-review slash command, resolves the pre-existing cursor[bot] comment Kilo slash ignores base ref (ID 3488952253), and adds matching unit-test expectations. No new issues introduced on changed lines.

What changed

  • src/server/services/local-review.ts:452-460 — two new pure helpers:
    • kiloSlashCommandBaseArg(input) returns input.base ?? input.baseCommit.
    • kiloSlashCommandToken(input) returns /local-review <base> when both kiloSlashCommand resolves to /local-review and a base arg is truthy; otherwise returns the bare command (so /local-review-uncommitted stays unchanged and missing-base inputs are not stringified).
  • src/server/services/local-review.ts:463kiloSlashCommandArgs now uses kiloSlashCommandToken(input). Args are still passed as one argv entry per element (Node spawn), so no shell injection from base values containing spaces/specials.
  • src/server/services/local-review.test.ts:72-74 — three updated expectations covering base, baseCommit, and head: 'working tree' (no base) cases. Test fakes updated to use $7 == /local-review* glob (line 83, 102, 122) so they match both /local-review and /local-review <base> argument variants. The end-to-end test at line 78-95 exercises base: 'origin/main' and confirms localReviewMode: 'slash-command' plus the minor finding round-trip.

Pre-existing concerns resolved by this commit

  • cursor[bot] local-review.ts:464 (id 3488952253) — Kilo slash ignores base ref — directly resolved. The primary Kilo path now forwards input.base ?? input.baseCommit to /local-review.

Observations (unchanged file, not in PR diff)

  • docs/local-review-agents.md:81-91 — the documented slash-command examples show kilo run --format json --auto --dir <repo> /local-review without the base arg, which no longer matches the implementation when a base/baseCommit is supplied. The doc was accurate when written and is now stale by this commit; per changed-code scope the docs file is out of PR diff so this is summary-only, not an inline comment. Worth updating in a follow-up so users running the documented command by hand still get the diff-scope they expect.

No other pre-existing comments target changed lines.

Files Reviewed (2 files)
  • src/server/services/local-review.test.ts - 0 issues
  • src/server/services/local-review.ts - 0 issues

Previous review (commit 4f443b6)

Status: No Issues Found | Recommendation: Merge

The incremental commit 4f443b67 (fix: preserve Kilo slash review timeouts) is a single-line fix that adds slash.timedOut to the early-return condition in runKilo, with a matching regression test. No new issues introduced on changed lines.

What changed

  • src/server/services/local-review.ts:463 — adds slash.timedOut to the predicate that returns the slash-command ExecResult instead of falling through to the prompt fallback. After a kill by the timeout handler in execReviewCli (line 126-129: proc.kill() + timedOut = true), slash.exitCode is non-zero and any stdout is typically empty, so the previous slash.exitCode === 0 || parseReviewFindings(...) check would drop through to the fallback and double the wall-clock timeout budget. With || slash.timedOut short-circuiting before the parseReviewFindings call, a timed-out slash command now returns immediately with localReviewMode: 'slash-command'; the pre-existing failed = exec.timedOut || (exec.exitCode !== 0 && findings.length === 0) in runOneProvider:418 keeps the run classified as failed with error: '${provider} review timed out'.
  • src/server/services/local-review.test.ts:116-135 — new test does not fall back to prompt mode after a timed-out slash command builds a fake kilo whose slash arm does sleep 1; echo ...; exit 0 and whose fallback arm would emit a critical finding tagged 'should not run'. With timeoutMs: 50 the slash is killed during sleep (no JSON emitted), and the test asserts status: 'failed', localReviewMode: 'slash-command', error contains 'timed out', rawOutput does not contain 'should not run', and result.findings.length === 0. All four assertions are reachable against the current code path (auth + config + run all match the existing fake-bin arms) and the temp dir is cleaned via the same try/finally { rmSync(root, { recursive: true, force: true }) } pattern as the other tests in this file.

Pre-existing concerns resolved by this commit

Two open review comments anchored on the unchanged runKilo fallback call at local-review.ts:465 are now addressed by the line-463 change:

  • cursor[bot] (local-review.ts:465) — Kilo review doubles timeout budget — the fallback path is no longer entered after a slash-command timeout, so the wall-clock budget is no longer doubled.
  • coderabbitai[bot] (local-review.ts:465) — Do not mask a slash-command timeout with prompt fallback — directly resolved; the slash result is now preserved with localReviewMode: 'slash-command' and runOneProvider still classifies it as failed.

No other pre-existing comments target changed lines, so per the changed-lines-only scope nothing else is carried forward.

Files Reviewed (2 files)
  • src/server/services/local-review.test.ts - 0 issues
  • src/server/services/local-review.ts - 0 issues

Previous review (commit 5f44109)

Status: No Issues Found | Recommendation: Merge

The incremental commit 5f44109 (Use Kilo local-review slash command) correctly adds the documented /local-review slash-command path for Kilo with prompt-mode fallback and a new localReviewMode adapter tag:

  • src/server/services/local-review.ts:11,48,69,94LocalReviewAdapterMode union ('native' | 'slash-command' | 'prompt-fallback') added and threaded through ReviewProviderStatus, ReviewResult, and ExecResult without breaking the existing coderabbit paths.
  • src/server/services/local-review.ts:190checkKiloAuth now advertises localReviewMode: 'slash-command', matching the new primary path.
  • src/server/services/local-review.ts:285,293-301parseMarkdownFindingTable accepts an issue header in place of title and parses a combined file:line column (regex /^(.*):(\d+)$/), so reviewers emitting either column shape are now normalized correctly.
  • src/server/services/local-review.ts:426runOneProvider propagates exec.localReviewMode ?? status.localReviewMode, keeping the per-result tag accurate when a slash command returns its own mode and status only fills in when the exec didn't.
  • src/server/services/local-review.ts:441runCodeRabbit stamps localReviewMode: 'native' on its ExecResult.
  • src/server/services/local-review.ts:448-458 — new kiloSlashCommand, kiloSlashCommandArgs, and kiloPromptFallbackArgs helpers expose the arg shape for tests and keep the prompt text single-sourced via kiloReviewPrompt. /local-review-uncommitted is selected only when head === 'working tree' and no base/baseCommit is supplied.
  • src/server/services/local-review.ts:460-472runKilo runs the slash command first and falls back to the prompt only when the slash command exits non-zero AND yields zero parseable findings; the fallback's stderr prepends the slash stderr so the artifact still captures why the slash command failed. The pre-existing failed = exec.timedOut || (exec.exitCode !== 0 && findings.length === 0) at runOneProvider:418 still correctly classifies a slash-command timeout as failed even when the mode tag is slash-command.
  • src/server/services/local-review.test.ts:70-115 — two new tests cover the slash-command arg shape, the slash-success result tag, and the slash-failure prompt-fallback tag (with the slash stderr substring check). Both use the existing makeFakeBin harness; no production data directory pollution introduced.
  • docs/local-review-agents.md — the Kilo Code section now leads with the documented /local-review and /local-review-uncommitted slash commands, keeps the prompt fallback as a retry path, and records the locally observed behavior on Kilo 7.3.44 (and removes the now-resolved "native command when available" future-work bullet).

The pre-existing cursor bugbot comments (run_local_code_review workspace containment at code-review-tools.ts:53, the multi-line JSON / timeout-as-success / invalid-mode / unknown-provider bugbot summaries, the kilo auth regex and fail-open catch warnings, etc.) target lines that are not changed by this incremental commit and are therefore out of scope for this pass.

No new CRITICAL, WARNING, or SUGGESTION issues were introduced on changed lines. The mode-tag type narrowing is preserved (LocalReviewAdapterMode literal union on every new assignment), and the existing failed/blocked flow in runOneProvider keeps timeouts and non-zero-exit failures classified correctly regardless of which adapter mode produced the ExecResult.

Files Reviewed (3 files)
  • docs/local-review-agents.md - 0 issues
  • src/server/services/local-review.test.ts - 0 issues
  • src/server/services/local-review.ts - 0 issues

Previous review (commit 2655a06)

Status: No Issues Found | Recommendation: Merge

The incremental commit 2655a06 (fix: validate hook provider and tighten finding detection) correctly addresses the two prior bugbot findings on the changed lines:

  • scripts/hivekeep-local-review.ts:22-27 — new readProvider() rejects invalid --provider / HIVEKEEP_LOCAL_REVIEW_PROVIDER values with console.error + process.exit(1), complementing the existing readMode() validator at lines 15-20.
  • src/server/services/local-review.ts:302 — narrowed FINDING_ARRAY_KEYS set to ['findings', 'issues', 'comments', 'diagnostics'] (the broader results / text / output / summary / message / content list is gone).
  • src/server/services/local-review.ts:304-312isFindingCandidate() now requires a type / event substring of finding / issue or a location / file / path / fileName marker (operational log/error payloads with only severity no longer qualify); isFindingArrayItem() requires severity / title / message / body / description / codegenInstructions for items under finding-array keys.
  • src/server/services/local-review.ts:319-333visit() propagates fromFindingArray based on whether the iterated key is in FINDING_ARRAY_KEYS, so nested recurse only treats known finding-array containers as arrays-of-findings.

The two bugbot summaries confirm the fixes are correct:

  • Hook script skips provider validation (issue id bc4e8ec1-7915-44be-912c-25740b551279) — addressed by readProvider() matching the readMode() shape.
  • Non-finding severity triggers gate (issue id 0248bace-5a45-4183-84d6-09d7bb57be02) — addressed by tightening isFindingCandidate() so bare-severity log objects never reach the candidate set.

No new CRITICAL, WARNING, or SUGGESTION issues were introduced on changed lines. Type narrowing on the narrowed FINDING_ARRAY_KEYS (a Set<string> matched against Object.entries's typed string keys) keeps fromFindingArray strictly boolean throughout.

Files Reviewed (2 files)
  • scripts/hivekeep-local-review.ts - 0 issues
  • src/server/services/local-review.ts - 0 issues

Previous review (commit 4668d95)

Status: No Issues Found | Recommendation: Merge

The incremental commit 4668d958 (fix: treat review timeouts as failures and validate review mode) correctly addresses the two prior cursor[bot] findings on the changed lines:

  • src/server/services/local-review.ts:362-366 — new resolveReviewMode validates input.mode ?? config.codeReview.defaultMode and throws on any value other than advisory or blocking, closing the invalid-mode path at evaluateGate (previously flagged at line 378).
  • src/server/services/local-review.ts:400failed = exec.timedOut || (exec.exitCode !== 0 && findings.length === 0) now treats timeouts as failures, with the conditional error message at src/server/services/local-review.ts:402-406 distinguishing "${provider} review timed out" from the exit-code path.
  • scripts/hivekeep-local-review.ts:15-20 — new readMode() rejects invalid --mode / HIVEKEEP_LOCAL_REVIEW_MODE values with console.error + process.exit(1), complementing the server-side validation.

No new CRITICAL, WARNING, or SUGGESTION issues were introduced on changed lines. exec.timedOut is properly typed (boolean) in ExecResult (src/server/services/local-review.ts:87-91) and set explicitly at every return site, so the new truthiness checks and nested ternaries are type-safe.

Files Reviewed (2 files)
  • src/server/services/local-review.ts - 0 issues
  • scripts/hivekeep-local-review.ts - 0 issues

Previous review (commit 7501d57)

Status: No Issues Found | Recommendation: Merge

The two previous issues on the changed lines are now correctly fixed:

  • Multi-line JSON findings dropped (was line 225) — the new parseJsonLines fallback at lines 211-222 scans for the first { or [ and parses the remainder when whole-buffer and JSONL parses both fail, recovering pretty-printed JSON that follows log prefixes such as [INFO].
  • Unknown provider runs Kilo (was line 391) — the new resolveReviewProviders helper at lines 354-360 explicitly validates the provider against LOCAL_REVIEW_PROVIDERS and throws on unknown values, replacing the prior inline expression that defaulted unknown strings to the Kilo path.

No new CRITICAL, WARNING, or SUGGESTION issues were introduced on changed lines.

Files Reviewed (1 file)
  • src/server/services/local-review.ts - 0 issues

Previous review (commit 070c5fa)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
src/server/services/local-review.ts 179 checkKiloAuth authenticated regex yields false positives — kilo auth list exiting 0 with text like "No credentials configured" matches \bcredentials?\b and returns authenticated: true, so the auth-skip gate in runOneProvider still rarely trips for Kilo.
src/server/services/local-review.ts 378 runOneProvider catch now returns blocked: false regardless of input.mode, changing blocking reviews from fail-closed to fail-open on unhandled errors (network failure, auth crash, etc.).
src/server/services/local-review.test.ts 84-124 The two artifact tests still call runLocalCodeReview without setting HIVEKEEP_CODE_REVIEW_DIR, so ensureArtifactDir resolves to ${HIVEKEEP_DATA_DIR ?? './data'}/code-reviews (production default) and bun test writes real artifacts there. The fake-bin temp dirs do NOT include the artifact dir.

SUGGESTION

File Line Issue
src/server/services/local-review.ts 303 parseReviewFindings still pushes a parent that matches the candidate predicate (type/severity/file/etc.) and then recurses into obj[key] for findings/issues/etc., producing both the parent and each child as findings — e.g. {type:'finding', severity:'major', findings:[{severity:'minor',…}]} yields two entries. The hardening pass added more descended keys (text, output, summary, message, content) but did not gate the recursion.
Files Reviewed (11 fil

[Snapshot truncated.]

Additional previous summary content was truncated to keep this comment within platform limits.


Reviewed by minimax-m3 · Input: 64.4K · Output: 9.2K · Cached: 1.3M

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
docs/local-review-agents.md (1)

67-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid baking a one-off Kilo CLI version into the spec.

Kilo 7.3.44 was verified locally will drift as soon as the locally tested CLI changes, but nothing in the runtime contract shown here keeps the doc in sync. Prefer version-agnostic wording here, or move exact compatibility claims into a dated support matrix.

Suggested wording
-Kilo 7.3.44 was verified locally. The CLI exposes `kilo run --format json --auto --dir <repo>` for non-interactive runs.
+The Kilo CLI was verified locally during implementation. The CLI exposes `kilo run --format json --auto --dir <repo>` for non-interactive runs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/local-review-agents.md` around lines 67 - 69, The docs text currently
hard-codes a specific Kilo CLI version in the local-review-agents guidance,
which will quickly become stale. Update the wording in this section to be
version-agnostic, or move the compatibility claim into a separate dated support
matrix, while keeping the references to the Kilo CLI commands and the structured
review prompt fallback intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/hivekeep-local-review.ts`:
- Around line 4-6: The argv/env parsing in readArg and the provider/mode
dispatch should validate values before casting, since a flag token like --mode
or an invalid HIVEKEEP_LOCAL_REVIEW_PROVIDER can currently be treated as a real
ReviewProvider/ReviewMode. Update the parsing around readArg, the provider/mode
selection, and the dispatch logic to reject any value that is missing, starts
with --, or is not one of the allowed ReviewProvider / 'all' / ReviewMode
options, and fail fast with a clear error instead of defaulting into the Kilo
path.

In `@src/server/services/local-review.test.ts`:
- Around line 45-47: The local-review test is still invoking the real pipeline
through runLocalCodeReview, so it depends on the host’s CodeRabbit CLI state and
can become flaky. Update the test to stub the provider availability/runner used
by runLocalCodeReview so it always takes the skipped-review path, then assert
the artifact persistence behavior against that forced branch. Use the existing
local-review test case and the runLocalCodeReview flow to keep the test
deterministic.

In `@src/server/services/local-review.ts`:
- Around line 261-264: The persisted artifact is being written before the final
artifact path is assigned and before the results are updated, so the saved JSON
can diverge from the returned summary. In local-review.ts, adjust the flow
around persistArtifact and the run/result update logic so artifactPath is
finalized first, then the run object is updated with that path and the result
entries, and only after that persistArtifact is called to write the final state.
- Around line 185-190: The JSON parsing in parseJsonLines only handles
line-delimited records, so pretty-printed JSON objects with findings[] are
missed and can bypass review checks. Update parseJsonLines to first attempt
parsing the entire raw output as JSON (including the findings[] shape used by
the review response handling near the JSON object parsing logic), then fall back
to the existing line-by-line JSONL parsing if that fails. Keep the current
behavior for mixed log output and preserve the existing JSON parse filtering in
local-review.ts.
- Around line 297-301: In local-review.ts, the result classification in the run
completion logic currently only checks exec.exitCode and findings, so timed-out
provider executions can still be marked succeeded. Update the status computation
around the failed/statusValue/error/blockled return flow to also treat
exec.timedOut as a failure condition, and make sure the error message reflects
the timeout alongside the existing provider/exit-code fallback. Keep the change
localized to the result object assembly that uses summarize, evaluateGate, and
the exec result.
- Around line 110-127: The execReviewCli flow in local-review.ts still allows
Bun.spawn or cwd setup failures to escape and it buffers the entire
stdout/stderr before clamping. Wrap the spawn/init path in the existing guarded
handling so synchronous failures are caught, and update the
proc.stdout/proc.stderr handling to stream into byte-capped buffers instead of
using full Response(...).text() materialization; keep the timeout and exitCode
logic in execReviewCli consistent with the new streaming approach.

---

Nitpick comments:
In `@docs/local-review-agents.md`:
- Around line 67-69: The docs text currently hard-codes a specific Kilo CLI
version in the local-review-agents guidance, which will quickly become stale.
Update the wording in this section to be version-agnostic, or move the
compatibility claim into a separate dated support matrix, while keeping the
references to the Kilo CLI commands and the structured review prompt fallback
intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 039bbbaf-1e4a-4049-b87f-5857e081f83a

📥 Commits

Reviewing files that changed from the base of the PR and between 235f44b and e688d84.

📒 Files selected for processing (11)
  • docs/local-review-agents.md
  • scripts/hivekeep-local-review.ts
  • src/client/locales/en.json
  • src/client/locales/fr.json
  • src/server/config.ts
  • src/server/services/local-review.test.ts
  • src/server/services/local-review.ts
  • src/server/services/toolboxes.ts
  • src/server/tools/code-review-tools.ts
  • src/server/tools/register.ts
  • src/test-helpers.ts

Comment thread scripts/hivekeep-local-review.ts Outdated
Comment thread src/server/services/local-review.test.ts Outdated
Comment thread src/server/services/local-review.ts Outdated
Comment thread src/server/services/local-review.ts
Comment thread src/server/services/local-review.ts Outdated
Comment thread src/server/services/local-review.ts Outdated
@cursor

cursor Bot commented Jun 28, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
New subprocess execution of external CLIs and filesystem artifacts on admin-triggered and agent tool paths; blocking mode can gate push/PR workflows, but scope is limited to configured repos and admin UI.

Overview
Introduces CodeRabbit and Kilo Code as first-class reviewer agents with a shared local-review pipeline, not one-off shell wrappers.

Backend: New local-review service runs provider CLIs (CodeRabbit cr review --agent; Kilo /local-review via kilo run, with prompt fallback), normalizes findings from JSON/Markdown output, applies advisory vs blocking gates (major/critical block only in blocking mode), and persists runs as capped-output JSON artifacts. reviewer-agents layers stable agent metadata, seeded checklists in reviewer-knowledge.json, and admin HTTP API under /api/reviewer-agents. Agent tools (list_local_reviewers, check_code_review_auth, run_local_code_review) are registered and added to the code toolbox, with repo paths constrained to the task workspace and CLI env forwarded without secrets in prompts.

Client: Admin Reviewer Agents page (/reviewer-agents) with activity-bar entry—cards, auth status, run controls, run history, finding remediation states, and checklist editing (spawn-fix handoff remains stubbed).

Ops: config.codeReview (artifact dir, default mode, timeouts, output caps), scripts/hivekeep-local-review.ts for hooks/CI with mode-based exit codes, and documentation in docs/local-review-agents.md.

Reviewed by Cursor Bugbot for commit cbfaaec. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread src/server/services/local-review.ts
Comment thread src/server/services/local-review.ts
Comment thread src/server/services/local-review.ts Outdated
Comment thread src/server/services/local-review.ts Outdated
…vider names

parseJsonLines now falls back to scanning embedded JSON objects when
line-by-line parsing yields no events, handling pretty-printed payloads
after log lines like [INFO]. runLocalCodeReview rejects unknown provider
values instead of silently running Kilo.
Comment thread src/server/services/local-review.ts Outdated
Comment thread src/server/services/local-review.ts
Comment thread src/server/tools/code-review-tools.ts
- Mark provider runs as failed when execReviewCli times out, even if partial
  findings were parsed from truncated output
- Reject invalid mode values in runLocalCodeReview and the hook script so
  typos cannot silently disable blocking gate behavior
Comment thread scripts/hivekeep-local-review.ts
Comment thread src/server/services/local-review.ts
Add readProvider() to hivekeep-local-review.ts so invalid --provider
or HIVEKEEP_LOCAL_REVIEW_PROVIDER values exit 1 with a clear message,
matching readMode() and documented hook exit semantics.

Tighten parseReviewFindings so operational JSON objects with severity
but no finding type/location are not treated as review findings, preventing
false gate blocks in blocking mode.
Comment thread src/server/services/local-review.ts Outdated
Comment thread src/server/services/local-review.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Autofix Details

a a.id - b.id

StrReplace
path
/workspace/src/server/services/local-review.ts
old_string
function clampOutput(s: string): string {
const max = config.codeReview.maxOutputBytes
if (s.length <= max) return s
return […truncated ${s.length - max} chars from the head…]\n${s.slice(-max)}
}
new_string
function clampOutput(s: string a.id -b.id -a.id - b.id

StrReplace
path
/workspace/src/server/services/local-review.ts
old_string
async function runKilo(input: ReviewInput & { repoPath: string }): Promise {
const slash = await execReviewCli('kilo', kiloSlashCommandArgs(input), input.repoPath, input.repoPath, input.timeoutMs)
const slashRaw = [slash.stdout, slash.stderr].filter(Boolean).join('\n a.id - b.id
if (slash.exitCode === 0 || parseReviewFindings('kilo', slashRaw).length > 0) return { ...slash, localReviewMode: 'slash-command' }

const fallback = await execReviewCli('kilo', kiloPromptFallbackArgs(input), input.repoPath, input.timeoutMs)
return {
...fallback,
stdout: fallback.stdout,
stderr: [Kilo slash-command local review failed; used prompt fallback., slashRaw, fallback.stderr].filter(Boolean).join('\n'),
localReviewMode: 'prompt-fallback',
}
}
new_string
async function runKilo(input: ReviewInput & { repoPath: string }): Promise {
const budgetMs = input.timeoutMs ?? config.codeReview.defaultTimeoutMs
const deadline = Date.now() + budgetMs
const remainingMs = () => Math.max(0, a.id - b.id)

const slash = await execReviewCli('kilo', kiloSlashCommandArgs(input), input.repoPath, remainingMs())
const slashRaw = [slash.stdout, slash.stderr].filter(Boolean).join('\n')
a.id - b.id
if (slash.exitCode === 0 || parseReviewFindings('kilo', slashRaw).length > 0) return { ...slash, localReviewMode: 'slash-command' }

const left = remainingMs()
if (left <= 0) return { ...slash, localReviewMode: 'slash-command' }

const fallback = await execReviewCli('kilo', kiloPromptFallbackArgs(input), input.repoPath, left)
return {
...fallback,
stdout: fallback.stdout,
stderr: [Kilo slash-command local review failed; used prompt fallback., slashRaw, fallback.stderr].filter(Boolean).join('\n'),
timedOut: fallback.timedOut || slash.timedOut,
localReviewMode: 'prompt-fallback',
}
}

You can send follow-ups to the cloud agent here.

Comment thread src/server/services/local-review.ts Outdated
Comment thread src/server/services/local-review.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server/services/local-review.ts`:
- Around line 461-465: The slash-command path in runOneProvider/local-review
should not fall through to prompt fallback when execReviewCli('kilo',
kiloSlashCommandArgs(...)) times out without parseable findings. Update the
slash result handling so a timeout from the initial slash-command attempt is
returned or propagated immediately instead of continuing to
kiloPromptFallbackArgs, preserving the timed-out status in the slash-command
branch and preventing the provider from being marked successful.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fc045ce8-0693-41f0-83f3-b29e07958bec

📥 Commits

Reviewing files that changed from the base of the PR and between 7501d57 and 5f44109.

📒 Files selected for processing (4)
  • docs/local-review-agents.md
  • scripts/hivekeep-local-review.ts
  • src/server/services/local-review.test.ts
  • src/server/services/local-review.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/hivekeep-local-review.ts
  • docs/local-review-agents.md

Comment thread src/server/services/local-review.ts Outdated
Comment thread src/server/services/local-review.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Kilo slash ignores base ref
    • Kilo slash-command args now append base or baseCommit to /local-review (e.g. /local-review origin/main) so the CLI reviews the requested diff scope, matching CodeRabbit behavior.

You can send follow-ups to the cloud agent here.

Comment thread src/server/services/local-review.ts
cursoragent and others added 2 commits June 29, 2026 02:02
Append base or baseCommit as slash-command arguments so Kilo reviews
the same diff scope as CodeRabbit and the recorded review metadata.
Comment thread src/server/tools/code-review-tools.ts
Comment thread src/server/services/local-review.ts
Comment thread src/server/services/local-review.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Hook ignores configured default mode
    • Changed the hook script fallback from hardcoded 'advisory' to config.codeReview.defaultMode so HIVEKEEP_CODE_REVIEW_MODE blocking mode applies consistently to pre-push hooks.

You can send follow-ups to the cloud agent here.

Comment thread scripts/hivekeep-local-review.ts
Use config.codeReview.defaultMode (HIVEKEEP_CODE_REVIEW_MODE) as the
hook fallback instead of hardcoding advisory, so pre-push exit codes
match in-app run_local_code_review blocking semantics.
Merge per-task env (e.g. HIVEKEEP_GH_TOKEN) into CodeRabbit/Kilo subprocess
spawns via resolveToolEnv, matching run_shell behavior for worktree reviews.

Base Kilo auth heuristics on kilo auth list output only so unrelated
config check "not configured" messages no longer block reviews.
@cursor

cursor Bot commented Jun 29, 2026

Copy link
Copy Markdown

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Worktree token omitted from CLI
    • Local review tools now pass resolveToolEnv (including HIVEKEEP_GH_TOKEN) through ReviewInput into all execReviewCli subprocess spawns, matching run_shell worktree behavior.
  • ✅ Fixed: Kilo auth rejects unrelated output
    • Kilo auth heuristics now evaluate only kilo auth list output, so unrelated config check "not configured" messages no longer cause false authentication failures.
Preview (5746e97d17)
diff --git a/docs/local-review-agents.md b/docs/local-review-agents.md
new file mode 100644
--- /dev/null
+++ b/docs/local-review-agents.md
@@ -1,0 +1,125 @@
+# Local review agents: CodeRabbit and Kilo Code
+
+Hivekeep exposes two in-house local code reviewers as first-class reviewer providers rather than arbitrary shell commands:
+
+- **CodeRabbit** (`coderabbit`) through the CodeRabbit CLI (`cr` or `coderabbit`).
+- **Kilo Code** (`kilo`) through the Kilo CLI.
+
+The goal is a hook-style workflow that can run before push or PR creation, persist review artifacts, and give Agents/users a structured gating decision without making every commit depend on network services by default.
+
+## Workflow
+
+1. An Agent or hook calls `run_local_code_review` (or `bun scripts/hivekeep-local-review.ts`).
+2. Hivekeep resolves the repo/worktree path and records review metadata: provider, base/head, mode, timestamps, findings, and artifact path.
+3. Each reviewer runs as a dedicated provider adapter:
+   - CodeRabbit: `cr review --agent --dir <repo> --light` plus `--base` / `--base-commit` when supplied.
+   - Kilo Code: Kilo's documented local-review slash commands through `kilo run --format json --auto --dir <repo> /local-review` (or `/local-review-uncommitted` for working-tree-only review).
+4. Kilo falls back to a structured prompt (`kilo run --format json --auto --dir <repo> <review prompt>`) only if the slash-command run fails without parseable findings.
+5. JSON-line or JSON-object output is parsed into `ReviewFinding[]`.
+6. The gate blocks automatic push/PR only in `blocking` mode when any `critical` or `major` findings exist. Advisory mode always reports but does not block.
+7. A durable JSON artifact is written under `config.codeReview.artifactDir` (default `data/code-reviews`).
+
+## Tools
+
+- `list_local_reviewers` — lists CodeRabbit and Kilo Code status.
+- `check_code_review_auth` — checks CLI install/auth/doctor state for one or both reviewers.
+- `run_local_code_review` — runs reviewers and returns structured results.
+
+The tools are available to main Agents and sub-Agents and are included in the built-in `code` toolbox.
+
+## Hook runner
+
+Advisory pre-push example:
+
+```bash
+bun scripts/hivekeep-local-review.ts --provider all --mode advisory --base origin/main
+```
+
+Blocking pre-PR example:
+
+```bash
+bun scripts/hivekeep-local-review.ts --provider all --mode blocking --base origin/main
+```
+
+Exit codes:
+
+- `0` — review passed or advisory findings were reported.
+- `1` — reviewer failed in blocking mode.
+- `2` — blocking mode found major/critical issues.
+
+Hivekeep does **not** install this as a mandatory git hook automatically. Teams can opt in by calling the script from `.git/hooks/pre-push`, a PR creation wrapper, or CI.
+
+## Authentication and data flow
+
+Never put secrets in prompts or repo files.
+
+### CodeRabbit
+
+Install and authenticate the CodeRabbit CLI using CodeRabbit's documented setup. Hivekeep checks:
+
+- `cr --version` or `coderabbit --version`
+- `cr auth status --agent`
+- `cr doctor`
+
+Reviews are run with `cr review --agent`; review data is sent wherever the CodeRabbit CLI sends it under its authenticated account.
+
+### Kilo Code
+
+The Kilo CLI was verified locally during implementation. Its help exposes the scriptable runner contract:
+
+```bash
+kilo run --format json --auto --dir <repo> <message-or-slash-command>
+```
+
+The Kilo CLI docs at <https://kilo.ai/docs/code-with-ai/platforms/cli> document:
+
+- `kilo run [message..]` with `--format json` for raw JSON events.
+- `--auto` autonomous mode, including exit codes (`0` success, `124` timeout, `1` error).
+- `--dir` to choose the working directory.
+- built-in slash commands `/local-review` and `/local-review-uncommitted` under "Local Code Reviews".
+
+Hivekeep therefore treats this as Kilo's primary local-review contract and runs:
+
+```bash
+kilo run --format json --auto --dir <repo> /local-review
+```
+
+For explicitly working-tree-only review (`head: "working tree"` with no base/base commit), Hivekeep uses:
+
+```bash
+kilo run --format json --auto --dir <repo> /local-review-uncommitted
+```
+
+The provider status/result exposes `localReviewMode: "slash-command"` for this path. If the slash command exits non-zero and produces no parseable findings, Hivekeep retries once with the older structured prompt fallback and marks `localReviewMode: "prompt-fallback"` in the result so callers can distinguish native Kilo review from fallback review.
+
+Observed local behavior on this branch:
+
+- `/local-review` returned JSON events and spawned Kilo's built-in "Run local review" task. It produced findings against the branch diff without modifying files.
+- `/local-review-uncommitted` returned JSON events and reported no findings on a clean working tree.
+- Top-level commands such as `kilo review`, `kilo local-review`, `kilo reviews`, and `kilo code-review` are not separate commands in 7.3.44; they print top-level help. The stable entrypoint is the slash command through `kilo run`.
+- `kilo run --command local-review` did not complete in a short safety test, so Hivekeep does not use it.
+
+Review data is sent to the model/provider configured in Kilo. Use `kilo auth`, `kilo models`, `kilo profile`, and `kilo debug` locally to verify account/provider state.
+
+## Artifact schema
+
+Artifacts contain:
+
+- run id, mode, status, blocked flag
+- repo path, base/baseCommit/head
+- per-provider status and raw capped output
+- normalized findings: severity, confidence, title, message, file/line, rule id
+- summary suitable for chat/task reporting
+
+This JSON persistence is deliberately small and filesystem-backed for the MVP. The service boundary in `src/server/services/local-review.ts` is ready to swap to a DB-backed persistence layer later.
+
+## Remediation loop
+
+The review result is structured so Hiro/Kaito or ticket-bound sub-Agents can consume findings, fix high-severity issues, and rerun review before push/PR. In blocking mode, major/critical findings should prevent automatic push or PR creation unless a human explicitly overrides the gate.
+
+## Deferred enhancements
+
+- Rich React renderer for review artifacts/findings.
+- DB migration and review-run history UI.
+- Richer extraction for Kilo's nested task output if Kilo publishes a tighter machine-readable finding schema than JSON events containing review text.
+- Automatic PR wrapper integration after the local runner has settled.

diff --git a/scripts/hivekeep-local-review.ts b/scripts/hivekeep-local-review.ts
new file mode 100644
--- /dev/null
+++ b/scripts/hivekeep-local-review.ts
@@ -1,0 +1,41 @@
+#!/usr/bin/env bun
+import { config } from '@/server/config'
+import { runLocalCodeReview, type ReviewProvider, type ReviewMode } from '@/server/services/local-review'
+
+function readArg(name: string): string | undefined {
+  const i = process.argv.indexOf(name)
+  if (i < 0) return undefined
+  const value = process.argv[i + 1]
+  return value && !value.startsWith('-') ? value : undefined
+}
+
+function has(name: string): boolean {
+  return process.argv.includes(name)
+}
+
+function readMode(): ReviewMode {
+  const raw = readArg('--mode') ?? process.env.HIVEKEEP_LOCAL_REVIEW_MODE ?? config.codeReview.defaultMode
+  if (raw === 'advisory' || raw === 'blocking') return raw
+  console.error(`Invalid review mode: ${raw}. Valid values: advisory, blocking`)
+  process.exit(1)
+}
+
+function readProvider(): ReviewProvider | 'all' {
+  const raw = readArg('--provider') ?? process.env.HIVEKEEP_LOCAL_REVIEW_PROVIDER ?? 'all'
+  if (raw === 'coderabbit' || raw === 'kilo' || raw === 'all') return raw
+  console.error(`Invalid review provider: ${raw}. Valid values: coderabbit, kilo, all`)
+  process.exit(1)
+}
+
+const provider = readProvider()
+const mode = readMode()
+const repoPath = readArg('--repo') ?? process.cwd()
+const base = readArg('--base') ?? process.env.HIVEKEEP_LOCAL_REVIEW_BASE
+const baseCommit = readArg('--base-commit') ?? process.env.HIVEKEEP_LOCAL_REVIEW_BASE_COMMIT
+const head = readArg('--head') ?? process.env.HIVEKEEP_LOCAL_REVIEW_HEAD
+const light = !has('--full')
+
+const result = await runLocalCodeReview({ repoPath, provider, mode, base, baseCommit, head, light })
+console.log(JSON.stringify(result, null, 2))
+if (result.blocked) process.exit(2)
+if (result.status === 'failed' && mode === 'blocking') process.exit(1)

diff --git a/src/client/locales/en.json b/src/client/locales/en.json
--- a/src/client/locales/en.json
+++ b/src/client/locales/en.json
@@ -2862,7 +2862,10 @@
       "update_project_knowledge": "Update Project Knowledge",
       "delete_project_knowledge": "Delete Project Knowledge",
       "pin_project_knowledge": "Pin Project Knowledge",
-      "get_mini_app_backend_status": "Mini-App Backend Status"
+      "get_mini_app_backend_status": "Mini-App Backend Status",
+      "list_local_reviewers": "List Local Reviewers",
+      "check_code_review_auth": "Check Code Review Auth",
+      "run_local_code_review": "Run Local Code Review"
     },
     "renderers": {
       "rawJson": "Raw JSON",

diff --git a/src/client/locales/fr.json b/src/client/locales/fr.json
--- a/src/client/locales/fr.json
+++ b/src/client/locales/fr.json
@@ -2862,7 +2862,10 @@
       "update_project_knowledge": "Modifier une connaissance projet",
       "delete_project_knowledge": "Supprimer une connaissance projet",
       "pin_project_knowledge": "Épingler une connaissance projet",
-      "get_mini_app_backend_status": "Statut du backend de Mini-App"
+      "get_mini_app_backend_status": "Statut du backend de Mini-App",
+      "list_local_reviewers": "Lister les reviewers locaux",
+      "check_code_review_auth": "Vérifier l’authentification de revue de code",
+      "run_local_code_review": "Lancer une revue de code locale"
     },
     "renderers": {
       "rawJson": "JSON brut",

diff --git a/src/server/config.ts b/src/server/config.ts
--- a/src/server/config.ts
+++ b/src/server/config.ts
@@ -541,6 +541,16 @@
     worktreeSweepIntervalMin: Number(process.env.HIVEKEEP_WORKTREE_SWEEP_INTERVAL_MIN ?? 5),
   },
 
+  codeReview: {
+    /** Durable local-review JSON artifacts. Defaults outside Agent workspaces. */
+    artifactDir: process.env.HIVEKEEP_CODE_REVIEW_DIR ?? `${dataDir}/code-reviews`,
+    /** Advisory by default: report findings without making network CLIs a hard local gate. */
+    defaultMode: (process.env.HIVEKEEP_CODE_REVIEW_MODE === 'blocking' ? 'blocking' : 'advisory') as 'advisory' | 'blocking',
+    defaultTimeoutMs: Number(process.env.HIVEKEEP_CODE_REVIEW_TIMEOUT_MS ?? 300_000),
+    maxTimeoutMs: Number(process.env.HIVEKEEP_CODE_REVIEW_MAX_TIMEOUT_MS ?? 900_000),
+    maxOutputBytes: Number(process.env.HIVEKEEP_CODE_REVIEW_MAX_OUTPUT_BYTES ?? 256 * 1024),
+  },
+
   upload: {
     dir: process.env.UPLOAD_DIR ?? `${dataDir}/uploads`,
     maxFileSizeMb: Number(process.env.UPLOAD_MAX_FILE_SIZE ?? 50),

diff --git a/src/server/services/local-review.test.ts b/src/server/services/local-review.test.ts
new file mode 100644
--- /dev/null
+++ b/src/server/services/local-review.test.ts
@@ -1,0 +1,213 @@
+import { afterEach, describe, expect, it } from 'bun:test'
+import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
+import { join } from 'path'
+import { tmpdir } from 'os'
+import { config } from '@/server/config'
+import { _LOCAL_REVIEW_INTERNALS_FOR_TEST, runLocalCodeReview } from './local-review'
+
+const { parseReviewFindings, evaluateGate, parseJsonLines, kiloSlashCommandArgs, kiloPromptFallbackArgs } = _LOCAL_REVIEW_INTERNALS_FOR_TEST
+const originalPath = process.env.PATH
+const originalArtifactDir = config.codeReview.artifactDir
+const mutableCodeReviewConfig = config.codeReview as { artifactDir: string }
+
+afterEach(() => {
+  process.env.PATH = originalPath
+  mutableCodeReviewConfig.artifactDir = originalArtifactDir
+})
+
+function makeFakeBin(name: string, body: string): string {
+  const root = mkdtempSync(join(tmpdir(), 'hivekeep-local-review-test-'))
+  const bin = join(root, name)
+  writeFileSync(bin, `#!/usr/bin/env bash\n${body}\n`)
+  chmodSync(bin, 0o755)
+  process.env.PATH = `${root}:${originalPath ?? ''}`
+  return root
+}
+
+describe('local-review parsing', () => {
+  it('parses JSON-line CodeRabbit findings', () => {
+    const raw = [
+      JSON.stringify({ type: 'finding', severity: 'critical', title: 'SQL injection', message: 'Use parameters', file: 'src/db.ts', line: 12, confidence: 'high' }),
+      JSON.stringify({ event: 'complete', findings: [{ severity: 'minor', title: 'Naming', path: 'src/a.ts' }] }),
+    ].join('\n')
+    const findings = parseReviewFindings('coderabbit', raw)
+    expect(findings).toHaveLength(2)
+    expect(findings[0]).toMatchObject({ provider: 'coderabbit', severity: 'critical', file: 'src/db.ts', line: 12, confidence: 'high' })
+    expect(findings[1]).toMatchObject({ severity: 'minor', file: 'src/a.ts' })
+  })
+
+  it('parses pretty-printed whole JSON objects before JSONL fallback', () => {
+    const raw = JSON.stringify({ findings: [{ severity: 'major', title: 'Pretty', file: 'src/pretty.ts', line: 7 }] }, null, 2)
+    const events = parseJsonLines(raw)
+    expect(events).toHaveLength(1)
+    expect(parseReviewFindings('kilo', raw)[0]).toMatchObject({ severity: 'major', title: 'Pretty', file: 'src/pretty.ts' })
+  })
+
+  it('ignores non-json log lines while keeping valid JSON events', () => {
+    const events = parseJsonLines('hello\n{"type":"finding","title":"x"}\nnot json')
+    expect(events).toHaveLength(1)
+  })
+
+  it('parses Kilo slash-command findings from nested results', () => {
+    const findings = parseReviewFindings('kilo', JSON.stringify({ findings: [{ severity: 'high', title: 'Race', description: 'Shared state', location: { path: 'src/race.ts', line: '5' } }] }))
+    expect(findings).toHaveLength(1)
+    expect(findings[0]).toMatchObject({ provider: 'kilo', severity: 'major', file: 'src/race.ts', line: 5 })
+  })
+
+  it('does not duplicate nested finding containers or parse candidate string fields twice', () => {
+    const raw = JSON.stringify({
+      type: 'finding',
+      severity: 'major',
+      title: 'Parent container',
+      message: '| severity | title | file |\n|---|---|---|\n| high | Nested table | src/table.ts |',
+      findings: [{ severity: 'minor', title: 'Child finding', file: 'src/child.ts' }],
+    })
+    const findings = parseReviewFindings('kilo', raw)
+    expect(findings).toHaveLength(1)
+    expect(findings[0]).toMatchObject({ severity: 'minor', title: 'Child finding', file: 'src/child.ts' })
+  })
+
+  it('parses CodeRabbit agent findings with fileName and codegenInstructions fields', () => {
+    const findings = parseReviewFindings('coderabbit', JSON.stringify({ type: 'finding', severity: 'major', fileName: 'src/review.ts', codegenInstructions: 'Fix this path' }))
+    expect(findings).toHaveLength(1)
+    expect(findings[0]).toMatchObject({ provider: 'coderabbit', severity: 'major', file: 'src/review.ts', title: 'Fix this path' })
+  })
+
+  it('parses Kilo JSON event text containing a Markdown finding table', () => {
+    const raw = JSON.stringify({
+      type: 'text',
+      text: '| severity | title | message | file | line | confidence |\n|---|---|---|---|---:|---|\n| high | Missing guard | Add validation | src/a.ts | 42 | high |',
+    })
+    const findings = parseReviewFindings('kilo', raw)
+    expect(findings).toHaveLength(1)
+    expect(findings[0]).toMatchObject({ provider: 'kilo', severity: 'major', title: 'Missing guard', file: 'src/a.ts', line: 42, confidence: 'high' })
+  })
+})
+
+describe('Kilo local-review adapter', () => {
+  it('builds the documented slash-command invocation before prompt fallback', () => {
+    expect(kiloSlashCommandArgs({ repoPath: '/repo', base: 'origin/main' })).toEqual(['run', '--format', 'json', '--auto', '--dir', '/repo', '/local-review origin/main'])
+    expect(kiloSlashCommandArgs({ repoPath: '/repo', baseCommit: 'abc123' })).toEqual(['run', '--format', 'json', '--auto', '--dir', '/repo', '/local-review abc123'])
+    expect(kiloSlashCommandArgs({ repoPath: '/repo', head: 'working tree' })).toEqual(['run', '--format', 'json', '--auto', '--dir', '/repo', '/local-review-uncommitted'])
+    expect(kiloPromptFallbackArgs({ repoPath: '/repo', base: 'origin/main' }).at(-1)).toContain('dedicated local code reviewer')
+  })
+
+  it('records slash-command adapter mode in Kilo results', async () => {
+    const root = makeFakeBin('kilo', `
+if [[ "$1" == "--version" ]]; then echo "7.3.44"; exit 0; fi
+if [[ "$1" == "auth" ]]; then echo 'Kilo Gateway credential active'; exit 0; fi
+if [[ "$1" == "config" ]]; then echo 'ok'; exit 0; fi
+if [[ "$1" == "run" && "$7" == /local-review* ]]; then echo '{"findings":[{"severity":"minor","title":"Kilo nit","file":"src/kilo.ts"}]}'; exit 0; fi
+echo "unexpected args: $*" >&2
+exit 1
+`)
+    try {
+      mkdirSync(join(root, 'repo'), { recursive: true })
+      const result = await runLocalCodeReview({ repoPath: join(root, 'repo'), provider: 'kilo', base: 'origin/main', mode: 'advisory', timeoutMs: 1000 })
+      expect(result.results[0]).toMatchObject({ provider: 'kilo', status: 'succeeded', localReviewMode: 'slash-command' })
+      expect(result.findings[0]).toMatchObject({ provider: 'kilo', severity: 'minor', file: 'src/kilo.ts' })
+    } finally {
+      rmSync(root, { recursive: true, force: true })
+    }
+  })
+
+  it('falls back to prompt mode only when Kilo slash command fails', async () => {
+    const root = makeFakeBin('kilo', `
+if [[ "$1" == "--version" ]]; then echo "7.3.44"; exit 0; fi
+if [[ "$1" == "auth" ]]; then echo 'Kilo Gateway credential active'; exit 0; fi
+if [[ "$1" == "config" ]]; then echo 'ok'; exit 0; fi
+if [[ "$1" == "run" && "$7" == /local-review* ]]; then echo 'slash failed' >&2; exit 1; fi
+if [[ "$1" == "run" ]]; then echo '{"findings":[{"severity":"major","title":"Fallback finding","file":"src/fallback.ts"}]}'; exit 0; fi
+exit 1
+`)
+    try {
+      mkdirSync(join(root, 'repo'), { recursive: true })
+      const result = await runLocalCodeReview({ repoPath: join(root, 'repo'), provider: 'kilo', base: 'origin/main', mode: 'advisory', timeoutMs: 1000 })
+      expect(result.results[0]).toMatchObject({ provider: 'kilo', status: 'succeeded', localReviewMode: 'prompt-fallback' })
+      expect(result.results[0]?.rawOutput).toContain('slash-command local review failed')
+      expect(result.findings[0]).toMatchObject({ provider: 'kilo', severity: 'major', file: 'src/fallback.ts' })
+    } finally {
+      rmSync(root, { recursive: true, force: true })
+    }
+  })
+
+  it('does not fall back to prompt mode after a timed-out slash command', async () => {
+    const root = makeFakeBin('kilo', `
+if [[ "$1" == "--version" ]]; then echo "7.3.44"; exit 0; fi
+if [[ "$1" == "auth" ]]; then echo 'Kilo Gateway credential active'; exit 0; fi
+if [[ "$1" == "config" ]]; then echo 'ok'; exit 0; fi
+if [[ "$1" == "run" && "$7" == /local-review* ]]; then sleep 1; echo '{"findings":[{"severity":"major","title":"late","file":"src/late.ts"}]}'; exit 0; fi
+if [[ "$1" == "run" ]]; then echo '{"findings":[{"severity":"critical","title":"should not run","file":"src/fallback.ts"}]}'; exit 0; fi
+exit 1
+`)
+    try {
+      mkdirSync(join(root, 'repo'), { recursive: true })
+      const result = await runLocalCodeReview({ repoPath: join(root, 'repo'), provider: 'kilo', base: 'origin/main', mode: 'advisory', timeoutMs: 50 })
+      expect(result.results[0]).toMatchObject({ provider: 'kilo', status: 'failed', localReviewMode: 'slash-command' })
+      expect(result.results[0]?.error).toContain('timed out')
+      expect(result.results[0]?.rawOutput).not.toContain('should not run')
+      expect(result.findings).toHaveLength(0)
+    } finally {
+      rmSync(root, { recursive: true, force: true })
+    }
+  })
+})
+
+describe('local-review gating', () => {
+  it('blocks major/critical findings only in blocking mode', () => {
+    const findings = parseReviewFindings('coderabbit', JSON.stringify({ findings: [{ severity: 'major', title: 'Bug' }] }))
+    expect(evaluateGate(findings, 'blocking')).toBe(true)
+    expect(evaluateGate(findings, 'advisory')).toBe(false)
+  })
+
+  it('allows advisory/no high severity findings', () => {
+    const findings = parseReviewFindings('coderabbit', JSON.stringify({ findings: [{ severity: 'minor', title: 'Nit' }] }))
+    expect(evaluateGate(findings, 'blocking')).toBe(false)
+  })
+})
+
+describe('local-review artifacts', () => {
+  it('persists finalized artifact paths and provider metadata', async () => {
+    const root = makeFakeBin('cr', `
+if [[ "$1" == "--version" ]]; then echo "0.0.0-test"; exit 0; fi
+if [[ "$1" == "auth" ]]; then echo '{"authenticated":true}'; exit 0; fi
+if [[ "$1" == "doctor" ]]; then echo 'doctor ok'; exit 0; fi
+if [[ "$1" == "review" ]]; then echo '{"type":"finding","severity":"minor","title":"Nit","file":"src/a.ts"}'; exit 0; fi
+exit 1
+`)
+    try {
+      mkdirSync(join(root, 'repo'), { recursive: true })
+      mutableCodeReviewConfig.artifactDir = join(root, 'artifacts')
+      const result = await runLocalCodeReview({ repoPath: join(root, 'repo'), provider: 'coderabbit', mode: 'advisory', timeoutMs: 1000 })
+      expect(result.artifactPath.endsWith(`${result.id}.json`)).toBe(true)
+      const saved = JSON.parse(readFileSync(result.artifactPath, 'utf8'))
+      expect(saved.artifactPath).toBe(result.artifactPath)
+      expect(saved.results[0].artifactPath).toBe(result.artifactPath)
+      expect(saved.results[0]).toMatchObject({ provider: 'coderabbit', repoPath: join(root, 'repo'), status: 'succeeded' })
+      expect(saved.findings[0]).toMatchObject({ provider: 'coderabbit', severity: 'minor', file: 'src/a.ts' })
+    } finally {
+      rmSync(root, { recursive: true, force: true })
+    }
+  })
+
+  it('reports all-skipped advisory runs as skipped and fails closed on blocking readiness errors', async () => {
+    const root = mkdtempSync(join(tmpdir(), 'hivekeep-local-review-empty-path-'))
+    process.env.PATH = root
+    mkdirSync(join(root, 'repo'), { recursive: true })
+    try {
+      mutableCodeReviewConfig.artifactDir = join(root, 'artifacts')
+      const advisory = await runLocalCodeReview({ repoPath: join(root, 'repo'), provider: 'coderabbit', mode: 'advisory', timeoutMs: 1000 })
+      expect(advisory.blocked).toBe(false)
+      expect(advisory.status).toBe('skipped')
+      expect(advisory.results[0]).toMatchObject({ status: 'skipped', blocked: false })
+
+      const blocking = await runLocalCodeReview({ repoPath: join(root, 'repo'), provider: 'coderabbit', mode: 'blocking', timeoutMs: 1000 })
+      expect(blocking.blocked).toBe(false)
+      expect(blocking.status).toBe('failed')
+      expect(blocking.results[0]).toMatchObject({ status: 'failed', blocked: false })
+      expect(blocking.results[0]?.error).toContain('CodeRabbit CLI not found')
+    } finally {
+      rmSync(root, { recursive: true, force: true })
+    }
+  })
+})

diff --git a/src/server/services/local-review.ts b/src/server/services/local-review.ts
new file mode 100644
--- /dev/null
+++ b/src/server/services/local-review.ts
@@ -1,0 +1,508 @@
+import { mkdirSync, writeFileSync } from 'fs'
+import { join, resolve } from 'path'
+import { randomUUID } from 'crypto'
+import { config } from '@/server/config'
+
+export type ReviewProvider = 'coderabbit' | 'kilo'
+export type ReviewRunStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'skipped'
+export type ReviewSeverity = 'info' | 'minor' | 'major' | 'critical'
+export type ReviewConfidence = 'low' | 'medium' | 'high'
+export type ReviewMode = 'advisory' | 'blocking'
+export type LocalReviewAdapterMode = 'native' | 'slash-command' | 'prompt-fallback'
+
+export interface ReviewFinding {
+  id: string
+  provider: ReviewProvider
+  severity: ReviewSeverity
+  confidence: ReviewConfidence
+  title: string
+  message: string
+  file?: string
+  line?: number
+  endLine?: number
+  ruleId?: string
+  raw?: unknown
+}
+
+export interface ReviewInput {
+  repoPath: string
+  provider?: ReviewProvider | 'all'
+  base?: string
+  baseCommit?: string
+  head?: string
+  mode?: ReviewMode
+  light?: boolean
+  taskId?: string
+  agentId?: string
+  timeoutMs?: number
+  env?: Record<string, string | undefined>
+}
+
+export interface ReviewProviderStatus {
+  provider: ReviewProvider
+  displayName: string
+  installed: boolean
+  authenticated: boolean | null
+  version?: string
+  authStatus?: string
+  doctor?: string
+  localReviewMode?: LocalReviewAdapterMode
+  error?: string
+}
+
+export interface ReviewResult {
+  id: string
+  provider: ReviewProvider
+  status: ReviewRunStatus
+  startedAt: string
+  completedAt?: string
+  repoPath: string
+  base?: string
+  baseCommit?: string
+  head?: string
+  mode: ReviewMode
+  light: boolean
+  findings: ReviewFinding[]
+  summary: string
+  rawOutput?: string
+  error?: string
+  artifactPath?: string
+  localReviewMode?: LocalReviewAdapterMode
+  blocked: boolean
+}
+
+export interface ReviewRunSummary {
+  id: string
+  status: ReviewRunStatus
+  mode: ReviewMode
+  blocked: boolean
+  results: ReviewResult[]
+  findings: ReviewFinding[]
+  artifactPath: string
+  summary: string
+}
+
+export const LOCAL_REVIEW_PROVIDERS: Array<{ id: ReviewProvider; displayName: string; description: string }> = [
+  { id: 'coderabbit', displayName: 'CodeRabbit', description: 'CodeRabbit CLI-backed local reviewer (`cr review --agent --light`).' },
+  { id: 'kilo', displayName: 'Kilo Code', description: "Kilo CLI-backed local reviewer using Kilo's `/local-review` slash command via `kilo run --format json --auto`." },
+]
+
+interface ExecResult {
+  exitCode: number
+  stdout: string
+  stderr: string
+  timedOut: boolean
+  localReviewMode?: LocalReviewAdapterMode
+}
+
+function asString(v: unknown): string | undefined {
+  return typeof v === 'string' && v.trim().length > 0 ? v : undefined
+}
+
+function clampOutput(s: string): string {
+  const max = config.codeReview.maxOutputBytes
+  if (s.length <= max) return s
+  const edge = Math.max(1, Math.floor(max / 2))
+  return `${s.slice(0, edge)}\n[…truncated ${s.length - max} chars from the middle…]\n${s.slice(-edge)}`
+}
+
+function redactSensitiveOutput(s: string): string {
+  return s
+    .replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, '$1[redacted]')
+    .replace(/\b(token|api[_-]?key|secret|password|authorization)(\s*[:=]\s*)([^\s'"`,;]+)/gi, '$1$2[redacted]')
+}
+
+function parseOptionalLine(value: unknown): number | undefined {
+  if (typeof value === 'number' && Number.isFinite(value)) return value
+  if (typeof value === 'string' && /^\d+$/.test(value.trim())) return Number(value.trim())
+  return undefined
+}
+
+export async function execReviewCli(
+  command: string,
+  args: string[],
+  cwd: string,
+  timeoutMs = config.codeReview.defaultTimeoutMs,
+  env?: Record<string, string | undefined>,
+): Promise<ExecResult> {
+  let proc: Bun.Subprocess<'ignore', 'pipe', 'pipe'>
+  try {
+    proc = Bun.spawn([command, ...args], {
+      cwd,
+      env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0', ...env },
+      stdout: 'pipe',
+      stderr: 'pipe',
+    })
+  } catch (err) {
+    return { exitCode: 127, stdout: '', stderr: err instanceof Error ? err.message : String(err), timedOut: false }
+  }
+
+  let timedOut = false
+  const timer = setTimeout(() => {
+    timedOut = true
+    proc.kill()
+  }, Math.min(timeoutMs, config.codeReview.maxTimeoutMs))
+  try {
+    const [stdout, stderr, exitCode] = await Promise.all([
+      new Response(proc.stdout).text(),
+      new Response(proc.stderr).text(),
+      proc.exited,
+    ])
+    return { exitCode, stdout: clampOutput(stdout), stderr: clampOutput(stderr), timedOut }
+  } catch (err) {
+    return { exitCode: 127, stdout: '', stderr: err instanceof Error ? err.message : String(err), timedOut }
+  } finally {
+    clearTimeout(timer)
+  }
+}
+
+async function firstWorking(names: string[], args: string[], cwd: string, env?: Record<string, string | undefined>): Promise<{ name: string; result: ExecResult } | null> {
+  for (const name of names) {
+    const result = await execReviewCli(name, args, cwd, 10_000, env)
+    if (result.exitCode !== 127 && !/command not found/i.test(result.stderr)) return { name, result }
+  }
+  return null
+}
+
+export async function listLocalReviewers(repoPath = process.cwd(), env?: Record<string, string | undefined>): Promise<ReviewProviderStatus[]> {
+  return [await checkCodeRabbitAuth(repoPath, env), await checkKiloAuth(repoPath, env)]
+}
+
+export async function checkCodeRabbitAuth(repoPath = process.cwd(), env?: Record<string, string | undefined>): Promise<ReviewProviderStatus> {
+  const found = await firstWorking(['cr', 'coderabbit'], ['--version'], repoPath, env)
+  if (!found) return { provider: 'coderabbit', displayName: 'CodeRabbit', installed: false, authenticated: null, error: 'CodeRabbit CLI not found (`cr` or `coderabbit`).' }
+  const auth = await execReviewCli(found.name, ['auth', 'status', '--agent'], repoPath, 15_000, env)
+  const doctor = await execReviewCli(found.name, ['doctor'], repoPath, 30_000, env)
+  const authText = redactSensitiveOutput([auth.stdout, auth.stderr].filter(Boolean).join('\n').trim())
+  const doctorText = redactSensitiveOutput([doctor.stdout, doctor.stderr].filter(Boolean).join('\n').trim())
+  return {
+    provider: 'coderabbit',
+    displayName: 'CodeRabbit',
+    installed: true,
+    authenticated: auth.exitCode === 0,
+    version: found.result.stdout.trim() || found.result.stderr.trim(),
+    authStatus: authText || undefined,
+    doctor: doctorText || undefined,
+    localReviewMode: 'native',
+    error: auth.exitCode === 0 ? undefined : authText || 'CodeRabbit auth status failed.',
+  }
+}
+
+export async function checkKiloAuth(repoPath = process.cwd(), env?: Record<string, string | undefined>): Promise<ReviewProviderStatus> {
+  const found = await firstWorking(['kilo'], ['--version'], repoPath, env)
+  if (!found) return { provider: 'kilo', displayName: 'Kilo Code', installed: false, authenticated: null, error: 'Kilo CLI not found (`kilo`).' }
+  const auth = await execReviewCli('kilo', ['auth', 'list'], repoPath, 15_000, env)
+  const configCheck = await execReviewCli('kilo', ['config', 'check'], repoPath, 15_000, env)
+  const authText = redactSensitiveOutput([auth.stdout, auth.stderr].filter(Boolean).join('\n').trim())
+  const configText = redactSensitiveOutput([configCheck.stdout, configCheck.stderr].filter(Boolean).join('\n').trim())
+  const text = [authText, configText].filter(Boolean).join('\n')
+  const negativeAuth = /\b(no|not|missing|absent|invalid|expired|unauthorized|unauthenticated)\b.{0,40}\b(credential|credentials|auth|login|provider|account|token|key)s?\b|\bnot logged in\b/i.test(authText)
+  const positiveAuth = /\b(Kilo Gateway|OpenAI|oauth)\b|\bcredential(s)?\b.{0,40}\b(active|configured|found|available|connected|present)\b/i.test(authText)
+  const authenticated = auth.exitCode === 0 && positiveAuth && !negativeAuth
+  return {
+    provider: 'kilo',
+    displayName: 'Kilo Code',
+    installed: true,
+    authenticated,
+    version: found.result.stdout.trim() || found.result.stderr.trim(),
+    authStatus: text || undefined,
+    localReviewMode: 'slash-command',
+    error: authenticated ? undefined : text || 'Kilo auth list did not report configured credentials.',
+  }
+}
+
+export function parseJsonLines(raw: string): unknown[] {
+  const trimmedRaw = raw.trim()
+  if (trimmedRaw) {
+    try {
+      const parsed = JSON.parse(trimmedRaw)
+      return Array.isArray(parsed) ? parsed : [parsed]
+    } catch {
+      // Fall through to JSONL/log parsing.
+    }
+  }
+
+  const events: unknown[] = []
+  for (const line of raw.split(/\r?\n/)) {
+    const trimmed = line.trim()
+    if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) continue
+    try { events.push(JSON.parse(trimmed)) } catch { /* ignore non-json log lines */ }
+  }
+  if (events.length > 0) return events
+
+  if (trimmedRaw) {
+    for (let i = 0; i < trimmedRaw.length; i++) {
+      const ch = trimmedRaw[i]
+      if (ch !== '{' && ch !== '[') continue
+      try {
+        const parsed = JSON.parse(trimmedRaw.slice(i))
+        return Array.isArray(parsed) ? parsed : [parsed]
+      } catch {
+        // Try the next `{` or `[` (e.g. skip log prefixes like `[INFO]`).
+      }
+    }
+  }
+
+  return events
+}
+
+function severityFrom(value: unknown): ReviewSeverity {
+  const s = String(value ?? '').toLowerCase()
+  if (['critical', 'blocker', 'security'].includes(s)) return 'critical'
+  if (['major', 'high', 'error'].includes(s)) return 'major'
+  if (['minor', 'medium', 'warning', 'warn'].includes(s)) return 'minor'
+  return 'info'
+}
+
+function confidenceFrom(value: unknown): ReviewConfidence {
+  const s = String(value ?? '').toLowerCase()
+  if (s === 'high') return 'high'
+  if (s === 'low') return 'low'
+  return 'medium'
+}
+
+function findingFromObject(provider: ReviewProvider, obj: Record<string, unknown>, index: number): ReviewFinding | null {
+  const title = asString(obj.title) ?? asString(obj.message) ?? asString(obj.body) ?? asString(obj.description) ?? asString(obj.codegenInstructions)
+  if (!title) return null
+  const loc = (obj.location && typeof obj.location === 'object') ? obj.location as Record<string, unknown> : {}
+  return {
+    id: asString(obj.id) ?? `${provider}-${index + 1}`,
+    provider,
+    severity: severityFrom(obj.severity ?? obj.level ?? obj.priority),
+    confidence: confidenceFrom(obj.confidence),
+    title: title.slice(0, 240),
+    message: (asString(obj.message) ?? asString(obj.body) ?? asString(obj.description) ?? title).slice(0, 4000),
+    file: asString(obj.file) ?? asString(obj.fileName) ?? asString(obj.path) ?? asString(loc.path) ?? asString(loc.file),
+    line: parseOptionalLine(obj.line) ?? parseOptionalLine(loc.line),
+    endLine: parseOptionalLine(obj.endLine) ?? parseOptionalLine(loc.endLine),
+    ruleId: asString(obj.ruleId) ?? asString(obj.rule_id) ?? asString(obj.code),
+    raw: obj,
+  }
+}
+
+function parseEmbeddedJson(provider: ReviewProvider, text: string): ReviewFinding[] {
+  const trimmed = text.trim()
+  if (!trimmed) return []
+  const firstJson = Math.min(...['{', '['].map((ch) => {
+    const i = trimmed.indexOf(ch)
+    return i < 0 ? Number.POSITIVE_INFINITY : i
+  }))
+  if (!Number.isFinite(firstJson)) return []
+  const jsonLike = trimmed.slice(firstJson)
+  try {
+    return parseReviewFindings(provider, jsonLike)
+  } catch {
+    return []
+  }
+}
+
+function parseMarkdownFindingTable(provider: ReviewProvider, text: string): ReviewFinding[] {
+  const lines = text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith('|') && line.endsWith('|'))
+  if (lines.length < 3) return []
+  const header = lines[0]!.split('|').map((cell) => cell.trim().toLowerCase()).filter(Boolean)
+  const separator = lines[1]!
+  if (!header.includes('severity') || (!header.includes('title') && !header.includes('issue')) || !/^[|\s:-]+$/.test(separator)) return []
+  const idx = (name: string) => header.indexOf(name)
+  return lines.slice(2).map((line, i) => {
+    const cells = line.split('|').map((cell) => cell.trim()).filter((_, cellIndex, arr) => cellIndex > 0 && cellIndex < arr.length - 1)
+    const get = (name: string) => {
+      const index = idx(name)
+      return index >= 0 ? cells[index] : undefined
+    }
+    const fileLine = get('file:line') ?? get('file')
+    const parsedLocation = fileLine?.match(/^(.*):(\d+)$/)
+    return findingFromObject(provider, {
+      id: `${provider}-table-${i + 1}`,
+      severity: get('severity'),
+      title: get('title') ?? get('issue'),
+      message: get('message') ?? get('issue'),
+      file: parsedLocation?.[1] ?? fileLine,
+      line: Number(get('line') ?? parsedLocation?.[2]) || undefined,
+      confidence: get('confidence'),
+    }, i)
+  }).filter((f): f is ReviewFinding => Boolean(f))
+}
+
+const FINDING_ARRAY_KEYS = new Set(['findings', 'issues', 'comments', 'diagnostics'])
+
+function isFindingCandidate(obj: Record<string, unknown>): boolean {
+  const type = String(obj.type ?? obj.event ?? '').toLowerCase()
+  if (type.includes('finding') || type.includes('issue')) return true
+  return Boolean(obj.location || obj.file || obj.path || obj.fileName)
+}
+
+function isFindingArrayItem(obj: Record<string, unknown>): boolean {
+  return Boolean(obj.severity || obj.title || obj.message || obj.body || obj.description || obj.codegenInstructions)
+}
+
+export function parseReviewFindings(provider: ReviewProvider, raw: string): ReviewFinding[] {
+  const events = parseJsonLines(raw)
+  const candidates: Record<string, unknown>[] = []
+  const stringFindings: ReviewFinding[] = []
+  const seenStrings = new Set<string>()
+  const visit = (v: unknown, fromFindingArray = false) => {
+    if (Array.isArray(v)) return v.forEach((item) => visit(item, fromFindingArray))
+    if (typeof v === 'string') {
... diff truncated: showing 800 of 1111 lines

You can send follow-ups to the cloud agent here.

Comment thread src/server/services/local-review.ts
Comment thread src/server/routes/reviewer-agents.ts
…min guard

- Reject traversal sequences in review run IDs and verify resolved artifact
  paths stay within the configured artifact directory
- Persist finding state updates via validated artifactPathFor(runId) instead of
  trusting artifactPath from loaded JSON
- Require admin role on all /api/reviewer-agents routes

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Gate summary ignores failed runs
    • Moved runStatus before summary construction and append 'Gate: FAILED.' when runStatus is failed instead of always defaulting to 'Gate: passed/advisory.' when not blocked.

You can send follow-ups to the cloud agent here.

Comment thread src/server/services/local-review.ts Outdated
Comment thread src/client/pages/reviewer-agents/ReviewerAgentsPage.tsx
When a review run ends with status failed but has no major/critical
findings, append 'Gate: FAILED.' instead of 'Gate: passed/advisory.'

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issues.

Reviewed by Cursor Bugbot for commit a45e384. Configure here.

Comment thread src/server/services/local-review.ts
Comment thread src/client/pages/reviewer-agents/ReviewerAgentsPage.tsx Outdated
Comment thread src/client/pages/reviewer-agents/ReviewerAgentsPage.tsx Outdated
Comment thread src/client/components/layout/ActivityBar.tsx
Comment thread src/server/services/local-review.ts
Comment thread src/server/services/local-review.ts
@cursor

cursor Bot commented Jun 29, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_1dd0cc0b-5158-48d1-b80a-1fc11ab8b15b)

@kdegeek kdegeek merged commit 3f83950 into main Jun 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants