161 lines
16 KiB
Markdown
161 lines
16 KiB
Markdown
---
|
|
spec: docs/specs/agents/requirements/native-code-review.md
|
|
created: 2026-07-24
|
|
status: building
|
|
---
|
|
|
|
# Implementation Plan: Native Code Review
|
|
|
|
## Overview
|
|
|
|
Findings are a new task-scoped persisted entity that reuses the existing anchored-annotation rendering surface in the Changes/Review diff. Persistence and the publish contract land first, then the run orchestrator that turns a task's working diff into findings through the utility-agent inference substrate, then the two triggers (WS action, workflow `on_enter` action) and the MCP publish tool, then the frontend store/annotations/toolbar, then mobile parity and E2E. The walkthrough feature (`task_walkthroughs` → `WalkthroughService` → bus event → WS → `walkthroughs` store slice → `walkthrough-step` diff annotation) is the reference pattern end to end; findings differ by being many-per-task, repository-scoped, and status-mutable.
|
|
|
|
---
|
|
|
|
## Backend
|
|
|
|
### Schema & models (`internal/task/`)
|
|
|
|
- `internal/task/models/models.go` — add `TaskReviewRun`, `TaskReviewFinding`, the `ReviewRunStatus`/`ReviewRunTrigger`/`ReviewSeverity`/`ReviewFindingStatus` string enums, `ErrTaskReviewRunNotFound`, `ErrTaskReviewFindingNotFound`.
|
|
- `internal/task/repository/sqlite/base_schema.go` — `initTaskReviewSchema()` creating `task_review_runs` and `task_review_findings` exactly as specified in the spec's Data model, both with `CREATE TABLE IF NOT EXISTS`.
|
|
- `internal/task/repository/sqlite/base_migrations.go` — idempotent `CREATE INDEX IF NOT EXISTS` for `task_review_findings(task_id, status)`, `task_review_findings(task_id, repository_name, file_path)`, `task_review_findings(run_id)`, `task_review_runs(task_id)`. Indexes live in migrations, never in the schema-init block (see `apps/backend/AGENTS.md`).
|
|
- `internal/task/repository/sqlite/review.go` — `CreateTaskReviewRun`, `UpdateTaskReviewRun`, `GetTaskReviewRun`, `ListTaskReviewRuns(taskID, limit)`, `CancelInFlightTaskReviewRuns()` (restart sweep), `CreateTaskReviewFindings(ctx, []*TaskReviewFinding)` (single transaction), `ListTaskReviewFindings(taskID)`, `GetTaskReviewFinding`, `UpdateTaskReviewFindingStatus`, `DeleteTaskReviewFindingsByTask`, `DeleteSupersededTaskReviewFindings(taskID, runID, keys)`.
|
|
- `internal/utility/hash/djb2.go` (new small package) — `DJB2(string) string`, byte-for-byte equal to `apps/web/lib/utils/hash.ts` (`hash = 5381`, `((hash<<5)+hash+ch) | 0` as int32, output `uint32` lowercase hex).
|
|
|
|
### Review service (`internal/task/service/review_service.go`)
|
|
|
|
- `ReviewService` with the same shape as `WalkthroughService`: minimal local `reviewRepo` interface, `bus.EventBus`, logger.
|
|
- `PublishFindings(ctx, PublishFindingsRequest) (*TaskReviewRun, []*TaskReviewFinding, error)` — normalizes + validates every finding (file required, `start_line > 0`, `end_line >= start_line`, known severity, title/body non-empty, title ≤ 120 chars and newline-stripped, category slug ≤ 40 chars, `anchor_text` truncated at 2000 chars), rejects the whole batch on any invalid entry, deletes superseded rows, inserts, publishes `events.TaskReviewFindingsPublished`.
|
|
- `UpdateFindingStatus`, `ClearTaskReview`, `GetTaskReview(taskID)`, `CreateRun`, `MarkRunRunning`, `CompleteRun`, `FailRun`, `CancelRun` — each publishing `events.TaskReviewRunUpdated` / `TaskReviewFindingUpdated` / `TaskReviewCleared`.
|
|
- `events.go` (`internal/events/types.go`) — `TaskReviewRunUpdated`, `TaskReviewFindingsPublished`, `TaskReviewFindingUpdated`, `TaskReviewCleared`.
|
|
- `internal/gateway/websocket/task_notifications.go` + `pkg/websocket/actions.go` — forward those bus events to the client actions listed in the spec, following the `task.walkthrough.*` precedent.
|
|
|
|
### Run orchestration (`internal/review/`, new package)
|
|
|
|
- `resolver.go` — `AgentResolver` resolving `(agentID, model)` for a run: explicit `agent_profile_id` → agent profile's agent/model/mode; otherwise the enabled `code-review` builtin utility agent; otherwise the user's default utility agent/model. Returns `ErrReviewAgentUnavailable` when nothing resolves or the resolved agent profile is CLI-passthrough.
|
|
- `diff.go` — `CollectChanges(ctx, sessionID, repositoryID)` builds `[]ChangedFile{RepositoryID, RepositoryName, Path, Status, Diff, DiffHash}` by merging agentctl git-status (uncommitted) and cumulative-diff (committed) results, uncommitted winning dedup — mirroring `buildAllFiles` in `apps/web/components/review/review-dialog.tsx` so both halves agree on the file set. `DiffHash` uses `hash.DJB2` over the same normalized diff text the frontend hashes.
|
|
- `batch.go` — `PlanBatches(files, budget)` groups files into prompt batches under a byte budget (`reviewPromptBudgetBytes = 120_000`), never splitting a single file; a file whose own diff exceeds the budget is returned in `Skipped`.
|
|
- `runner.go` — `Runner.Run(ctx, RunRequest)`: create run → mark running → collect changes → plan batches → for each batch resolve the `code-review` prompt template with `template.Context{GitDiff, ChangedFiles, TaskTitle, TaskDescription, BranchName, BaseBranch}` and execute through the inference substrate (session-bound `ExecuteInferencePrompt` when the task has a live session, else the sessionless host-utility executor) → parse → accumulate → publish once → complete run. A run is tracked in an in-memory `map[taskID]runID` guard so a second request returns the in-flight run.
|
|
- `parse.go` — `ParseFindings(response string) (summary string, findings []Finding, rejected int, err error)`. Accepts a fenced ```json block or a bare JSON object with a `findings` array; tolerates prose before/after. Returns `ErrUnparseableResponse` when no array is recoverable.
|
|
- `config/utilityagents/code-review.md` + `internal/utility/store/builtins.go` — new `builtin-code-review` entry named `code-review`. The prompt instructs a strict JSON contract and carries the sentinel line `KANDEV_CODE_REVIEW_REQUEST` used by the mock agent.
|
|
|
|
### Triggers
|
|
|
|
- `internal/mcp/server/server.go` + `internal/mcp/server/handlers.go` + `internal/mcp/handlers/handlers.go` + `pkg/websocket/actions.go` — register `publish_review_findings_kandev` with the schema in the spec, routed through `ws.ActionMCPPublishReviewFindings` to `ReviewService.PublishFindings` with `trigger = agent`. Mirrors `show_walkthrough_kandev`.
|
|
- `internal/workflow/models/models.go` — `OnEnterRunCodeReview OnEnterActionType = "run_code_review"`, `GenericActionRunCodeReview`, and a `RunCodeReview *RunCodeReviewAction` payload carrying `AgentProfileID`.
|
|
- `internal/workflow/engine/types.go` — `ActionRunCodeReview ActionKind = "run_code_review"` plus the `RunCodeReview` field on the action struct; `internal/workflow/engine/adapters.go` maps the model action to the engine action.
|
|
- `internal/orchestrator/workflow_callbacks.go` — `runCodeReviewCallback` registered when `svc.reviewRunner != nil`, calling `Runner.Run` with `trigger = workflow_step`. A failed run does not fail step entry: the callback logs and returns nil after the run row records the failure.
|
|
- `internal/workflow/models/export.go` + `internal/workflow/service/` — export/import and validation for the new action, referencing the agent profile portably like existing step profiles.
|
|
- `internal/task/controller` / WS handlers — `task.review.run`, `task.review.cancel`, `task.review.get`, `task.review.finding.update`, `task.review.clear` with the DTOs from the spec, plus the boot-payload/`e2e_reset.go` additions (`DeleteTaskReviewByWorkspace` cascade) required for worker-scoped E2E resets.
|
|
|
|
---
|
|
|
|
## Frontend
|
|
|
|
### Types, API client, store
|
|
|
|
- `apps/web/lib/types/http.ts` — `TaskReviewRun`, `TaskReviewFinding`, `ReviewSeverity`, `ReviewFindingStatus`.
|
|
- `apps/web/lib/api/domains/review-api.ts` — `runTaskReview`, `cancelTaskReview`, `getTaskReview`, `updateReviewFindingStatus`, `clearTaskReview` over the WS client (same shape as `walkthrough-api.ts`).
|
|
- `apps/web/lib/state/slices/review/` — `review-slice.ts` with `runsByTaskId`, `findingsByTaskId`, `activeRunByTaskId`; actions `setTaskReview`, `upsertReviewRun`, `addReviewFindings`, `updateReviewFinding`, `clearTaskReview`. Registered in `lib/state/store.ts` and `lib/state/default-state.ts`.
|
|
- `apps/web/lib/ws/handlers/review.ts` + `lib/ws/router.ts` — handlers for the four server events.
|
|
- `apps/web/lib/review/findings.ts` — pure helpers: `findingFileKey(finding)` (reuses the `reviewFileKey` NUL composite), `isFindingStale(finding, currentDiffHash)`, `groupFindingsByFile(findings)`, `groupFindingsByRepository(findings)`, `openFindingCount`, `severityRank`, `reanchorFinding(finding, diff)` best-effort `anchor_text` relocation.
|
|
|
|
### Diff annotations
|
|
|
|
- `apps/web/components/diff/review-finding-card.tsx` — severity chip, category, title, Markdown body, optional suggestion block, and the Resolve / Dismiss / Send-to-agent actions; a muted variant for `resolved`/`dismissed`.
|
|
- `apps/web/components/diff/use-diff-annotation-renderer.tsx` — add `"review-finding"` to `AnnotationMetadata.type` and render `ReviewFindingCard`.
|
|
- `apps/web/components/diff/use-diff-viewer-state.ts` — `useReviewFindingAnnotations(filePath, repo, diffHash)` alongside `useWalkthroughSelection`; non-stale open/resolved findings anchor at `end_line`, stale findings are excluded here and surfaced by the file header instead.
|
|
- `apps/web/components/review/review-file-findings-banner.tsx` — per-file stale-findings group rendered above the diff so a stale finding is visible without being mis-anchored.
|
|
|
|
### Review surface
|
|
|
|
- `apps/web/components/review/review-run-button.tsx` — **Review changes** control in `review-top-bar.tsx` with idle / running / failed states, a Cancel action while running, and the `review_agent_unavailable` message linking to Settings → Utility Agents.
|
|
- `apps/web/components/review/review-findings-overview.tsx` — findings list grouped by repository then file, severity-sorted, with counts; mirrors `review-comments-overview.tsx`.
|
|
- `apps/web/components/review/review-diff-list.tsx`, `review-file-tree.tsx` — per-file open-finding badge next to the existing comment badge.
|
|
- `apps/web/components/task/changes-panel-header.tsx` / `changes-top-bar.tsx` — the same **Review changes** control on the Changes panel.
|
|
- `apps/web/hooks/domains/review/use-task-review.ts` — WS subscription + backfill via `getTaskReview` on mount; `use-send-finding-to-agent.ts` converting a finding into agent context through the existing pending-comment/context-item path.
|
|
- `apps/web/components/settings/workflows/` — step editor checkbox + agent-profile picker for `run_code_review`, with the self-documenting copy the frontend conventions require.
|
|
|
|
### Mobile
|
|
|
|
- `apps/web/components/task/mobile/mobile-changes-panel.tsx` — findings entry point opening a `Drawer` bottom sheet listing findings with the full desktop action set; run control in the mobile changes toolbar.
|
|
|
|
---
|
|
|
|
## Tests
|
|
|
|
| What | File | How |
|
|
|---|---|---|
|
|
| djb2 parity with the TS implementation | `internal/utility/hash/djb2_test.go` | table-driven against fixtures also asserted in `apps/web/lib/utils/hash.test.ts` |
|
|
| Run/finding CRUD, cascade delete, supersede | `internal/task/repository/sqlite/review_test.go` | real in-memory SQLite, fresh-DB + replay |
|
|
| Finding validation & whole-batch rejection | `internal/task/service/review_service_test.go` | table-driven over malformed entries |
|
|
| Event publication on publish/update/clear | `internal/task/service/review_service_test.go` | fake `bus.EventBus`, assert event types + payloads |
|
|
| Restart sweep cancels in-flight runs | `internal/task/repository/sqlite/review_test.go` | seed `running` rows, call `CancelInFlightTaskReviewRuns` |
|
|
| Agent/model resolution incl. passthrough rejection and no-agent case | `internal/review/resolver_test.go` | table-driven with fake profile + utility stores |
|
|
| Batch planning and oversized-file skip | `internal/review/batch_test.go` | table-driven |
|
|
| Response parsing: fenced JSON, bare JSON, prose-wrapped, malformed entry counted, unparseable | `internal/review/parse_test.go` | table-driven |
|
|
| Full run happy path + `review_no_changes` + `review_agent_unavailable` + in-flight dedup | `internal/review/runner_test.go` | fake executor + fake diff source |
|
|
| MCP publish tool accepts valid, rejects malformed | `internal/mcp/server/server_test.go` | existing MCP test harness |
|
|
| `run_code_review` action evaluation and failure isolation | `internal/workflow/engine/engine_test.go`, `internal/orchestrator/workflow_callbacks_test.go` | engine trigger with a fake runner |
|
|
| Findings helpers: file key, staleness, grouping, severity sort, re-anchor | `apps/web/lib/review/findings.test.ts` | vitest table-driven |
|
|
| Review slice reducers | `apps/web/lib/state/slices/review/review-slice.test.ts` | vitest |
|
|
| WS handlers update the slice | `apps/web/lib/ws/handlers/review.test.ts` | vitest |
|
|
| Finding annotation placement, stale exclusion | `apps/web/components/diff/use-diff-viewer-state.test.ts` | vitest |
|
|
| Run button states incl. unavailable message | `apps/web/components/review/review-run-button.test.tsx` | RTL |
|
|
| Findings overview grouping per repository | `apps/web/components/review/review-findings-overview.test.tsx` | RTL |
|
|
| Send-to-agent builds correct context | `apps/web/hooks/domains/review/use-send-finding-to-agent.test.ts` | vitest |
|
|
|
|
---
|
|
|
|
## E2E Tests
|
|
|
|
`apps/backend/cmd/mock-agent/handler.go` gains `isCodeReviewRequest(prompt)` (matching the `KANDEV_CODE_REVIEW_REQUEST` sentinel) returning a deterministic fenced-JSON findings payload, so both paths are hermetic. Rebuild with `make -C apps/backend build-mock-agent` before running.
|
|
|
|
| Scenario | File | Verify |
|
|
|---|---|---|
|
|
| GIVEN changed files, WHEN **Review changes** is used, THEN findings render at their anchored lines | `apps/web/e2e/review/code-review-on-demand.spec.ts` | run chip reaches completed; finding card visible in the diff at the expected line; overview count |
|
|
| GIVEN a finding, WHEN resolved, THEN it shows resolved and survives reload | same file | card state + count after `page.reload()` |
|
|
| GIVEN a finding, WHEN **Send to agent**, THEN the chat receives the finding context | same file | context chip / message content |
|
|
| GIVEN no capable agent, WHEN **Review changes**, THEN the unavailable message appears and no run is created | same file | inline message with the Settings link |
|
|
| GIVEN a workflow step with `run_code_review`, WHEN a task enters it, THEN findings appear in Review | `apps/web/e2e/review/code-review-workflow-step.spec.ts` | move task into step; findings visible; run trigger reported as workflow step |
|
|
| GIVEN a phone viewport with findings, WHEN Changes is opened, THEN the findings sheet exposes every action | `apps/web/e2e/mobile/code-review-findings.spec.ts` | bottom sheet visible; resolve works from it |
|
|
|
|
---
|
|
|
|
## Implementation Waves
|
|
|
|
```
|
|
Wave 1 (parallel):
|
|
- [x] [task-01-schema-and-repository](task-01-schema-and-repository.md)
|
|
- [x] [task-02-djb2-and-diff-collection](task-02-djb2-and-diff-collection.md)
|
|
|
|
Wave 2:
|
|
- [x] [task-03-review-service-and-events](task-03-review-service-and-events.md)
|
|
|
|
Wave 3 (parallel):
|
|
- [x] [task-04-run-orchestration](task-04-run-orchestration.md)
|
|
- [x] [task-05-mcp-publish-tool](task-05-mcp-publish-tool.md)
|
|
|
|
Wave 4 (parallel):
|
|
- [x] [task-06-ws-actions-and-dtos](task-06-ws-actions-and-dtos.md)
|
|
- [x] [task-07-workflow-step-action](task-07-workflow-step-action.md)
|
|
|
|
Wave 5:
|
|
- [x] [task-08-frontend-state-and-types](task-08-frontend-state-and-types.md)
|
|
|
|
Wave 6:
|
|
- [x] [task-09-diff-finding-annotations](task-09-diff-finding-annotations.md)
|
|
|
|
Wave 7:
|
|
- [ ] [task-10-review-surface-controls](task-10-review-surface-controls.md)
|
|
|
|
Wave 8:
|
|
- [ ] [task-11-mobile-parity](task-11-mobile-parity.md)
|
|
|
|
Wave 9:
|
|
- [ ] [task-12-e2e-and-mock-agent](task-12-e2e-and-mock-agent.md)
|
|
|
|
Wave 10:
|
|
- [ ] [task-13-public-docs](task-13-public-docs.md)
|
|
```
|