195 lines
15 KiB
Markdown
195 lines
15 KiB
Markdown
---
|
|
status: draft
|
|
created: 2026-07-24
|
|
owner: jcfs
|
|
---
|
|
|
|
# Native Code Review
|
|
|
|
## Why
|
|
|
|
Review feedback on agent-written code only reaches a Kandev user after they push and open a pull request, because every reviewer Kandev can reach today (CodeRabbit, Greptile, cubic) lives in GitHub CI. That leaves the working diff — the thing the user is actually looking at in the Changes/Review panel — unreviewed, gives nothing at all to GitLab, Azure DevOps, and local-only work, and never lands in Kandev's own review surface where anchored comments, reviewed hashes, and stale detection already live.
|
|
|
|
## What
|
|
|
|
- A user SHALL be able to ask Kandev to review a task's current changes from the Review surface, without a pull request, a remote, or a Git host.
|
|
- A review pass produces **findings**: each finding is anchored to a repository, file path, and line range, and carries a severity, a category, a one-line title, and a Markdown body.
|
|
- Findings render as inline annotations in the existing Changes/Review diff, at their anchored line, using the same annotation surface as anchored review comments.
|
|
- Findings are **advisory**. For each finding the human can **Resolve** it, **Dismiss** it, or **Send to agent** — which turns the finding into ordinary agent context and sends it to the active session. Kandev SHALL NOT apply, stage, or commit a fix on its own.
|
|
- A review pass SHALL be triggerable two ways: on demand from the Review/Changes surface, and as a workflow-step entry action so a review sits between an implement step and a human review step.
|
|
- The reviewing runtime is configurable independently of the agent that wrote the code. The
|
|
on-demand path uses the effective profile selected by the built-in `code-review` utility agent;
|
|
the workflow-step path can additionally name an agent profile directly. Both paths use the
|
|
profile's complete launch and permission configuration as defined by
|
|
[Profile-backed Utility Agents](../agents/utility-agent-profiles.md).
|
|
- For a multi-repository task, findings carry their repository and are grouped per repository exactly like the rest of the Changes panel.
|
|
- When the diff moves under a finding, the finding SHALL become **stale** rather than being dropped or rendered against unrelated code. Staleness reuses the per-file diff-hash mechanism that already drives review-mark staleness.
|
|
- An agent with task MCP SHALL be able to publish findings directly, so a full agent session (workflow step, or a user prompt) can produce the same findings as the built-in pass.
|
|
- Every review pass is visible as a **run** with a status, a finding count, and a failure reason when it fails.
|
|
- The review surface SHALL have full capability parity on phones, using native mobile presentation for the findings list and per-finding actions.
|
|
|
|
## Data model
|
|
|
|
Two new tables in the task SQLite repository (`internal/task/repository/sqlite/`).
|
|
|
|
```
|
|
task_review_runs
|
|
id string PK
|
|
task_id string FK -> tasks.id (cascade delete), indexed
|
|
session_id string nullable; session whose workspace supplied the diff
|
|
trigger enum manual | workflow_step | agent
|
|
workflow_step_id string nullable; set when trigger = workflow_step
|
|
agent_profile_id string effective profile used for execution; "" when trigger = agent
|
|
agent_id string resolved inference agent CLI snapshot; "" when trigger = agent
|
|
model string resolved model snapshot; "" when trigger = agent
|
|
status enum pending | running | completed | failed | cancelled
|
|
error_message string "" unless status = failed
|
|
summary string optional agent-authored one-paragraph summary
|
|
finding_count int findings persisted by this run
|
|
file_count int changed files submitted to the reviewer
|
|
repository_count int repositories covered
|
|
prompt_tokens int
|
|
response_tokens int
|
|
duration_ms int
|
|
created_at timestamp
|
|
completed_at timestamp nullable
|
|
```
|
|
|
|
```
|
|
task_review_findings
|
|
id string PK
|
|
run_id string FK -> task_review_runs.id (cascade delete), indexed
|
|
task_id string FK -> tasks.id (cascade delete), indexed
|
|
repository_id string "" for a single-repository task
|
|
repository_name string sanitized repo dir name; "" for a single-repository task
|
|
file_path string repository-relative path
|
|
start_line int > 0
|
|
end_line int >= start_line
|
|
side enum additions | deletions (default additions)
|
|
severity enum blocker | major | minor | nit
|
|
category string short kebab-case slug, <= 40 chars
|
|
title string <= 120 chars, no newlines
|
|
body string Markdown
|
|
suggestion string optional suggested replacement text; display-only
|
|
anchor_text string the anchored diff lines at publish time, <= 2000 chars
|
|
file_diff_hash string djb2 hash of the file's normalized diff at publish time
|
|
status enum open | resolved | dismissed
|
|
resolved_at timestamp nullable
|
|
created_at timestamp
|
|
updated_at timestamp
|
|
```
|
|
|
|
`(task_id, status)` and `(task_id, repository_name, file_path)` are indexed. A task keeps findings from more than one run; publishing a new run does not delete earlier findings, but `open` findings from a previous run whose `(repository_name, file_path, start_line, end_line, title)` tuple repeats are superseded — the older row is deleted so the same issue is not listed twice.
|
|
|
|
`file_diff_hash` uses the same djb2 hash as `apps/web/lib/utils/hash.ts` and `session_file_reviews.diff_hash`, over the same normalized diff text, so the frontend can compare a stored hash against a freshly computed one without a second algorithm.
|
|
|
|
Deleting a task deletes its runs and findings. Deleting a repository attachment does not delete findings; they simply stop matching a file and are reported as stale.
|
|
|
|
## API surface
|
|
|
|
### WebSocket actions (client → server)
|
|
|
|
| Action | Payload | Response |
|
|
|---|---|---|
|
|
| `task.review.run` | `{task_id, session_id, repository_id?, agent_profile_id?}` | `{run: TaskReviewRun}` |
|
|
| `task.review.cancel` | `{run_id}` | `{run: TaskReviewRun}` |
|
|
| `task.review.get` | `{task_id}` | `{runs: TaskReviewRun[], findings: TaskReviewFinding[]}` |
|
|
| `task.review.finding.update` | `{finding_id, status}` | `{finding: TaskReviewFinding}` |
|
|
| `task.review.clear` | `{task_id}` | `{success: true}` |
|
|
|
|
`task.review.run` rejects with `review_agent_unavailable` when no effective agent profile can be resolved, and with `review_no_changes` when the task has no changed files. A second `task.review.run` for a task that already has a `pending` or `running` run returns that run unchanged instead of starting a second pass.
|
|
|
|
### WebSocket events (server → client)
|
|
|
|
- `task.review.run_updated` — payload is the run; fired on every status change.
|
|
- `task.review.findings_published` — `{task_id, run_id, findings: TaskReviewFinding[]}`.
|
|
- `task.review.finding_updated` — `{task_id, finding: TaskReviewFinding}`.
|
|
- `task.review.cleared` — `{task_id}`.
|
|
|
|
### Task MCP tool
|
|
|
|
`publish_review_findings_kandev`
|
|
|
|
```
|
|
task_id string required
|
|
summary string optional one-paragraph summary of the review
|
|
findings array required, >= 1 entry; each entry:
|
|
repo string optional multi-repo subpath; omit for single-repo tasks
|
|
file string required, repository-relative path
|
|
line int required, > 0
|
|
line_end int optional, >= line
|
|
severity string required, one of blocker | major | minor | nit
|
|
category string required, short kebab-case slug
|
|
title string required
|
|
body string required, Markdown
|
|
suggestion string optional
|
|
```
|
|
|
|
Returns the number of findings stored and the run id. The call creates a `task_review_runs` row with `trigger = agent` and `status = completed`. Rejecting a malformed entry rejects the whole call so an agent never persists a partially-anchored review.
|
|
|
|
### Workflow step action
|
|
|
|
A new `on_enter` action type `run_code_review`, with an optional `agent_profile_id`. Entering a step with this action starts a review pass with `trigger = workflow_step`. The action is available in the step editor next to **Auto-start agent** and is exported/imported with the workflow, referencing the agent profile by portable agent name, model, and mode like every other step profile reference.
|
|
|
|
## State machine
|
|
|
|
A review run:
|
|
|
|
| From | To | Trigger | Actor |
|
|
|---|---|---|---|
|
|
| — | `pending` | `task.review.run`, `run_code_review` step entry, or `publish_review_findings_kandev` | user, workflow engine, agent |
|
|
| `pending` | `running` | the run acquires the task's diff and dispatches the reviewer prompt | review service |
|
|
| `pending`/`running` | `cancelled` | `task.review.cancel`, or the backend shuts down mid-run | user, system |
|
|
| `running` | `completed` | reviewer returned a parseable result; findings persisted | review service |
|
|
| `running` | `failed` | diff unavailable, no capable agent, reviewer error, or unparseable result | review service |
|
|
|
|
A finding: `open` → `resolved` (user resolves) or `dismissed` (user dismisses); both are reversible back to `open`. Staleness is not a stored state — it is derived per render by comparing `file_diff_hash` against the current diff for that file, so a finding becomes stale and un-stale as the diff changes.
|
|
|
|
## Failure modes
|
|
|
|
| Condition | Behavior |
|
|
|---|---|
|
|
| No effective utility profile, or the resolved profile is missing, disabled, non-inference-capable, or CLI-passthrough-only | The run fails immediately with `review_agent_unavailable`. The Review surface shows an inline message naming **Settings → Utility Agents** and does not retry. No findings are written. |
|
|
| Resolved profile has no usable model | Same as above; the error names the profile and missing model. |
|
|
| Task workspace not materialized, or agentctl unreachable | The run fails with `review_workspace_unavailable`. Existing findings are untouched. |
|
|
| Task has no changed files | `task.review.run` returns `review_no_changes` without creating a run. |
|
|
| Reviewer returns text that contains no parseable findings block | The run fails with `review_unparseable_response`, and the raw response is retained on the run's `error_message` (truncated) for debugging. No findings are written. |
|
|
| A single finding in an otherwise valid response is malformed (missing file, non-positive line, unknown severity) | That finding is skipped and counted; the run still completes. The run's summary reports how many entries were rejected. Malformed entries submitted through `publish_review_findings_kandev` reject the whole call instead, because an agent can retry. |
|
|
| A finding anchors to a file that is not in the current changed-file set | The finding is persisted and listed in the findings overview under its repository, marked **not in current changes**. It is not rendered inside any file's diff. |
|
|
| Diff exceeds the reviewer's context | The diff is submitted in per-file batches; a file whose own diff cannot fit is skipped and named in the run summary. The run still completes with findings from the files that were reviewed. |
|
|
| Backend restarts while a run is `pending` or `running` | On boot, those runs are marked `cancelled` with `error_message = "interrupted by restart"`. They are never silently resumed. |
|
|
|
|
## Persistence guarantees
|
|
|
|
Runs and findings survive a Kandev restart; they are ordinary task-scoped SQLite rows and are removed only when the task is deleted or the user clears the task's review. In-flight review passes do not survive a restart — see Failure modes. Finding `status` changes are persisted immediately, so a resolved finding stays resolved across reloads and across browsers, unlike pending inline comments, which remain browser-local `sessionStorage` state.
|
|
|
|
Derived staleness is not persisted. A finding's `file_diff_hash` and `anchor_text` are, so staleness is recomputed identically on every client.
|
|
|
|
## Scenarios
|
|
|
|
- **GIVEN** a task with uncommitted changes and a configured `code-review` utility agent, **WHEN** the user selects **Review changes** in the Review toolbar, **THEN** a run appears with status `running`, and on completion each returned finding renders as an annotation at its anchored line in that file's diff.
|
|
- **GIVEN** a completed review with three findings, **WHEN** the user resolves one, **THEN** that annotation collapses to a resolved state, the findings count drops to two open, and the state survives a page reload.
|
|
- **GIVEN** an open finding on `apps/web/foo.ts:42`, **WHEN** the user selects **Send to agent**, **THEN** the finding's file, line range, and body are sent to the active session as agent context, and the finding remains `open`.
|
|
- **GIVEN** a multi-repository task with findings in `frontend` and `backend`, **WHEN** the Review panel renders, **THEN** each finding renders only inside its own repository's file, and the findings overview groups them under per-repository headings.
|
|
- **GIVEN** an open finding whose file diff has changed since the run, **WHEN** the Review panel renders, **THEN** the finding is shown as stale in that file's section and is not rendered against the file's current line 42.
|
|
- **GIVEN** a workflow step configured with the `run_code_review` entry action and an agent profile whose model differs from the implementing step's, **WHEN** a task moves into that step, **THEN** a run with `trigger = workflow_step` starts using that profile's agent and model, and the resulting findings are visible in the task's Review panel.
|
|
- **GIVEN** a workspace with no effective utility profile configured, **WHEN** the user selects
|
|
**Review changes**, **THEN** no run is created and the surface shows an actionable message
|
|
pointing at Settings → Utility Agents.
|
|
- **GIVEN** a task whose only agent profile is CLI-passthrough, **WHEN** a `run_code_review` step is entered, **THEN** the run fails with `review_agent_unavailable` and the task still enters the step.
|
|
- **GIVEN** an agent session with task MCP, **WHEN** it calls `publish_review_findings_kandev` with two valid findings, **THEN** a `completed` run with `trigger = agent` is stored and both findings appear in the Review panel without a page reload.
|
|
- **GIVEN** an agent calls `publish_review_findings_kandev` with one finding missing `file`, **WHEN** the call is handled, **THEN** it returns an error, and no run or finding is stored.
|
|
- **GIVEN** a review run in `running` state, **WHEN** the user selects **Cancel review**, **THEN** the run moves to `cancelled` and no findings are written.
|
|
- **GIVEN** a task with review findings, **WHEN** the task is deleted, **THEN** its runs and findings are removed.
|
|
- **GIVEN** a phone-sized viewport with a completed review, **WHEN** the user opens Changes, **THEN** the findings list is reachable as a bottom sheet and every desktop per-finding action is available from it.
|
|
|
|
## Out of scope
|
|
|
|
- Auto-applying, staging, or committing a fix from a finding. `suggestion` is display-only in this iteration.
|
|
- Replacing or duplicating GitHub CI reviewers. This runs earlier and is complementary; nothing here reads or writes GitHub review threads.
|
|
- Reconciling findings with a pull request's review comments, in either direction.
|
|
- Cross-task or whole-repository review. Scope is one task's changed files.
|
|
- Reviewing files that are not part of the task's changed set (no "review the whole module for context" mode).
|
|
- A dedicated review-findings history UI beyond the per-task run list.
|
|
- Scheduled or automatic-on-every-turn review. Triggers are explicit: user action, workflow-step entry, or an agent MCP call.
|