16 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/agents/requirements/native-code-review.md | 2026-07-24 | 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— addTaskReviewRun,TaskReviewFinding, theReviewRunStatus/ReviewRunTrigger/ReviewSeverity/ReviewFindingStatusstring enums,ErrTaskReviewRunNotFound,ErrTaskReviewFindingNotFound.internal/task/repository/sqlite/base_schema.go—initTaskReviewSchema()creatingtask_review_runsandtask_review_findingsexactly as specified in the spec's Data model, both withCREATE TABLE IF NOT EXISTS.internal/task/repository/sqlite/base_migrations.go— idempotentCREATE INDEX IF NOT EXISTSfortask_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 (seeapps/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 toapps/web/lib/utils/hash.ts(hash = 5381,((hash<<5)+hash+ch) | 0as int32, outputuint32lowercase hex).
Review service (internal/task/service/review_service.go)
ReviewServicewith the same shape asWalkthroughService: minimal localreviewRepointerface,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_texttruncated at 2000 chars), rejects the whole batch on any invalid entry, deletes superseded rows, inserts, publishesevents.TaskReviewFindingsPublished.UpdateFindingStatus,ClearTaskReview,GetTaskReview(taskID),CreateRun,MarkRunRunning,CompleteRun,FailRun,CancelRun— each publishingevents.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 thetask.walkthrough.*precedent.
Run orchestration (internal/review/, new package)
resolver.go—AgentResolverresolving(agentID, model)for a run: explicitagent_profile_id→ agent profile's agent/model/mode; otherwise the enabledcode-reviewbuiltin utility agent; otherwise the user's default utility agent/model. ReturnsErrReviewAgentUnavailablewhen 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 — mirroringbuildAllFilesinapps/web/components/review/review-dialog.tsxso both halves agree on the file set.DiffHashuseshash.DJB2over 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 inSkipped.runner.go—Runner.Run(ctx, RunRequest): create run → mark running → collect changes → plan batches → for each batch resolve thecode-reviewprompt template withtemplate.Context{GitDiff, ChangedFiles, TaskTitle, TaskDescription, BranchName, BaseBranch}and execute through the inference substrate (session-boundExecuteInferencePromptwhen 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-memorymap[taskID]runIDguard 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 afindingsarray; tolerates prose before/after. ReturnsErrUnparseableResponsewhen no array is recoverable.config/utilityagents/code-review.md+internal/utility/store/builtins.go— newbuiltin-code-reviewentry namedcode-review. The prompt instructs a strict JSON contract and carries the sentinel lineKANDEV_CODE_REVIEW_REQUESTused by the mock agent.
Triggers
internal/mcp/server/server.go+internal/mcp/server/handlers.go+internal/mcp/handlers/handlers.go+pkg/websocket/actions.go— registerpublish_review_findings_kandevwith the schema in the spec, routed throughws.ActionMCPPublishReviewFindingstoReviewService.PublishFindingswithtrigger = agent. Mirrorsshow_walkthrough_kandev.internal/workflow/models/models.go—OnEnterRunCodeReview OnEnterActionType = "run_code_review",GenericActionRunCodeReview, and aRunCodeReview *RunCodeReviewActionpayload carryingAgentProfileID.internal/workflow/engine/types.go—ActionRunCodeReview ActionKind = "run_code_review"plus theRunCodeReviewfield on the action struct;internal/workflow/engine/adapters.gomaps the model action to the engine action.internal/orchestrator/workflow_callbacks.go—runCodeReviewCallbackregistered whensvc.reviewRunner != nil, callingRunner.Runwithtrigger = 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.clearwith the DTOs from the spec, plus the boot-payload/e2e_reset.goadditions (DeleteTaskReviewByWorkspacecascade) 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,clearTaskReviewover the WS client (same shape aswalkthrough-api.ts).apps/web/lib/state/slices/review/—review-slice.tswithrunsByTaskId,findingsByTaskId,activeRunByTaskId; actionssetTaskReview,upsertReviewRun,addReviewFindings,updateReviewFinding,clearTaskReview. Registered inlib/state/store.tsandlib/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 thereviewFileKeyNUL composite),isFindingStale(finding, currentDiffHash),groupFindingsByFile(findings),groupFindingsByRepository(findings),openFindingCount,severityRank,reanchorFinding(finding, diff)best-effortanchor_textrelocation.
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 forresolved/dismissed.apps/web/components/diff/use-diff-annotation-renderer.tsx— add"review-finding"toAnnotationMetadata.typeand renderReviewFindingCard.apps/web/components/diff/use-diff-viewer-state.ts—useReviewFindingAnnotations(filePath, repo, diffHash)alongsideuseWalkthroughSelection; non-stale open/resolved findings anchor atend_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 inreview-top-bar.tsxwith idle / running / failed states, a Cancel action while running, and thereview_agent_unavailablemessage linking to Settings → Utility Agents.apps/web/components/review/review-findings-overview.tsx— findings list grouped by repository then file, severity-sorted, with counts; mirrorsreview-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 viagetTaskReviewon mount;use-send-finding-to-agent.tsconverting a finding into agent context through the existing pending-comment/context-item path.apps/web/components/settings/workflows/— step editor checkbox + agent-profile picker forrun_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 aDrawerbottom 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)