Skip to content

v0.3.0: server-side runner profiles and multi-review fusion - #20

Merged
sergeyfast merged 35 commits into
masterfrom
feat/runner-profiles
Jul 28, 2026
Merged

v0.3.0: server-side runner profiles and multi-review fusion#20
sergeyfast merged 35 commits into
masterfrom
feat/runner-profiles

Conversation

@sergeyfast

Copy link
Copy Markdown
Member

Summary

  • Add runner profiles — server-side runner configuration. New runner_profile entity (docs/patches/2026-06-13-runner-profiles.sql, pkg/db) holding runner, model, effort, API provider/base URL, token and params (allowDangerousPermissions); projects reference a profile. VT admin CRUD (pkg/vt/runner_profile*.go) with hardened validation, plus admin UI (RunnerProfilesPage/RunnerProfileFormPage) with model autocomplete (ComboInput) and masked token input (SecretInput, maskKey in vt/format — now covers the openai provider).
  • Add internal reviewctl JSON-RPC API (pkg/reviewctl, mounted on the CI-only /v1/reviewctl/ path, never on the public RPC server): returns the resolved run Config (incl. the real profile token — env vars still win on the client), TrackerConfig and the assembled prompt. reviewctl is now driven by the server-side profile instead of CLI flags; typed client generated via rpcgen (pkg/reviewer/ctl/reviewctlclient/client.generated.go), debug flags hidden.
  • Add multi-review (fusion panel): schema (docs/patches/2026-06-18-multi-review.sql), per-project panel editor in the admin (config and breakdown moved into tabs, FKListEditor). reviewctl fans the panel out into per-member git worktrees (pkg/reviewer/ctl/panel.go) — worktrees created sequentially, members reviewed in parallel (capped at 4), per-member failures tolerated; a judge stage fuses member outputs into one review (pkg/reviewer/fusion_prompt.go). Server filters/links member reviews to the fusion review; UI shows the panel breakdown (PanelBreakdownCard.vue) and per-issue provenance badges (ProvenanceBadge.vue); tab named Multi-review.
  • Add tracker-scoped http_fetch to the direct runner (pkg/reviewer/direct/tools_http.go): GET-only, scoped to the project task-tracker base URL, auth header inferred from token shape, token delivered out-of-band ({{TOKEN}}$REVIEW_TRACKER_TOKEN) so it never appears in the prompt.
  • Models: add Claude Opus 5 / Sonnet 5 and GPT-5.6.
  • CI template decoupled from Claude-only credentials (pkg/vt/gitlab-review.yml.tmpl): the runner profile supplies the token; REVIEW_API_KEY or provider-specific vars are only needed when the profile has none, and reviewctl fails fast when no credential is available from either source.
  • Hardening (94136f9 and friends): opencode worktree escape fixed via --dir, empty member output → review marked failed + debug bundle uploaded, direct-runner fixes; dropped the single-default profile index.
  • Housekeeping: simplified local-run and Docker helpers, consistent custom select chevron across the UI, runner profiles + multi-review documented in both READMEs, TS client regeneration fixed (leaked comments).

Test plan

  • make fmt lint test — green
  • Admin: create a runner profile, assign it to a project — token masked in the UI and in VT responses
  • reviewctl review with no runner/model flags — pulls its config from /v1/reviewctl/, runs with the profile's runner/model/effort
  • Multi-review: configure a panel (2–3 members + judge) → members run in parallel worktrees, fused review uploaded with breakdown and per-issue provenance badges
  • Kill one panel member — fusion still produced from the remaining members; all members failing → review failed + debug bundle
  • Direct runner: http_fetch reads the MR's tracker issue; the tracker token is absent from the prompt and transcript
  • CI: template runs with a profile-supplied token and no ANTHROPIC_API_KEY set; fails fast when neither profile nor env provides a credential

- Add "runnerProfiles" table (runner/model/effort/apiProvider/apiBaseURL/
  token/params/isDefault) with a single-default partial unique index; seed
  one default claude/opus/xhigh profile
- Add nullable "projects"."runnerProfileId" FK (null falls back to default)
  and a "reviews"."runnerProfile" jsonb snapshot column
- Add RunnerProfileParams and ReviewRunnerProfile JSON types; the review
  snapshot deliberately excludes the token (secrets never persisted)
- Regenerate db models/repos/test helpers via mfd; restore the hand-tuned
  IssueStatusFilter in review.go that regeneration reverted
- Keep PGD, reviewsrv.sql and init.sql in sync with the pgmigrator patch
- Add RunnerProfileService (Count/Get/GetByID/Add/Update/Delete/Validate)
  in the project namespace, registered as the "runnerProfile" RPC;
  regenerate vt_zenrpc
- Mask the token in the admin API: never return it raw (masked display +
  hasToken flag), set-or-keep semantics on update
- Enforce a single default profile (unset the previous default on
  add/update) and validate runner/apiProvider/effort enum values
- Wire runnerProfileId onto the Project DTO/search/summary with an FK
  existence check and a runnerProfile relation
- Refresh project/review .vt.xml metadata via mfd-vt-xml
- Add Runner Profiles list + form pages (enum selects for runner/effort/
  apiProvider, conditional direct/opencode fields) with nav + routes
- Mask the token in the form (write-only set-or-keep, shows tokenMasked)
- Wire a Runner Profile FKSelect onto the project form (— None — = default)
  and mask the projectKey with copy-to-clipboard on the project list/form
- Use a VT-local RunnerProfileParams type so the TS client generates a
  clean interface; regenerate the vt/factory TS clients (restore @ts-nocheck)
- Add /v1/reviewctl/rpc/ (JSON-RPC 2.0): ReviewConfig(projectKey) resolves
  the project's runner profile or the global default and returns it (with
  the real token, CI-internal path); Prompt(projectKey) assembles the prompt
- Add ProjectManager.RunnerProfile resolving pinned profile or default
- Duplicate the review upload endpoints under /v1/reviewctl/upload/
- Snapshot the resolved profile onto the review: ReviewDraft carries a
  runnerProfile block persisted to reviews.runnerProfile (token excluded)
- Fetch ReviewConfig over /v1/reviewctl/rpc/ before building the runner;
  the profile fills runner/model/effort/apiProvider/apiBaseURL/params and
  explicit flags still override it (CI needs only image + creds + key + URL)
- Resolve credentials env-first, then the profile token: pass it to the
  direct provider and export it to ANTHROPIC_API_KEY/OPENAI_API_KEY for the
  claude/codex CLIs when unset
- Fetch the prompt via the same RPC (Prompt) instead of GET /v1/prompt
- Upload via /v1/reviewctl/upload/ and snapshot the resolved profile onto
  the review (token excluded)
- Update client tests for the JSON-RPC endpoints
- Remove the standalone GET /v1/prompt/ endpoint and its handler; the
  prompt is now served by /v1/reviewctl/rpc/ (Prompt)
- Keep /v1/upload/ as deprecated aliases for older CI images
- Update README: architecture, endpoint table, runner profiles, thin CI
  (credentials depend on the profile's runner), and internal URL gating
Drops a goconst finding introduced with the rpc client.
Replace the hand-rolled JSON-RPC handler (pkg/rest/reviewctl.go) with a real
zenrpc service in pkg/reviewctl, mounted as a separate internal server at
/v1/reviewctl/rpc/ — kept off the public SMD/api.ts since ReviewConfig returns
the real runner-profile token. reviewctl now calls the rpcgen-generated Go client
in pkg/reviewer/ctl/reviewctlclient instead of marshalling JSON-RPC by hand. Adds
the -go_client flag and the `make go-client` target mirroring -ts_client.
Enforce a single default runner profile purely in code: soft-delete keeps
isDefault set, so a partial-unique index would count a deleted profile as a live
default and block setting any new one. Removes UNQ_runnerProfiles_isDefault from
the patch/schema/dump; unsetCurrentDefault is now the only guarantee. Validate
runner/provider against the canonical sources (runner.Runner* constants and the
new direct.IsValidProvider, which exports the provider ids) so they can't drift.
Adds a test that a new default can be set after the old default was deleted.
Dedupe the projectKey maskKey helper (duplicated in the project list and form)
into a shared frontend/src/vt/format.ts. Add 'openai' to the runner-profile
provider options, matching the new direct.ProviderOpenAI.
Local golangci-lint (v2.12.2) flags goconst that CI (v2.5.0) does not. Clean
fixes where a constant is the right call: reuse the existing Severity* constants
in calcIssueStats, add TrafficLight{Red,Yellow,Green}, and extract the API-key
env var names in reviewctl. The rest are silenced with explained //nolint:goconst
— validator tag names that only coincidentally equal FieldError* values, pg
column names, provider/model ids and API field names where a shared const adds
no clarity.
The Model field gets runner-aware suggestions (and provider-aware for the direct
runner) via a new ComboInput — a free-text combobox with a theme-styled dropdown,
since the native <datalist> popup renders unthemed (light) in dark mode. Also makes
the API Base URL placeholder provider-specific and drops the effort levels a
runner/provider can't use (no max for codex; an 'ignored' hint for deepseek/
openai-compat/opencode). Suggestion lists track ResolveDefaults and the codex.go /
provider_factory.go price tables (2026-06).
- Add per-project panel and judge: projects.runnerProfileIds jsonb
  and judgeRunnerProfileId fk to runnerProfiles
- Add review roles and member linking: reviews.reviewRole plus
  parentReviewId self-fk (single/member/fusion)
- Add per-issue provenance: issues.sources jsonb
- Add ReviewRole enum and IssueSources/ProjectRunnerProfileIDs jsonb
  types with Value/Scan JSON round-trip and a contract unit test
- Restore hand-tuned IssueStatusFilter that mfd regen reverted
- Regenerate mfd model/repo/db-test; sync PGD and SQL dump; add
  patch 2026-06-18-multi-review.sql
- Add --multi flag (runner:model panel members) that runs each member
  in its own detached git worktree, sequentially and fail-fast
- Upload members as reviewRole=member via a runner factory injected
  into the Controller; --multi bypasses server config and uses ambient
  credentials, leaving the single-review path unchanged
- Add ReviewRole to the upload draft and map empty to single in ToModel
  so single uploads keep the canonical role
- Test the --multi parser, git-worktree add/remove, and reviewRole
  persistence through CreateReview
- Add FusionPrompt: the judge reads the staged panel members, keeps
  consensus, verifies singletons against the code, normalizes severity
  and tags each kept issue with its source models
- Add --judge runner:model; reviewPanel runs the judge over the member
  worktrees and uploads one fused review (reviewRole=fusion) that links
  the members as children
- Add MemberReviewIDs and per-issue Sources to the upload draft and map
  sources to db.Issue.Sources
- Link members to their fusion via ReviewManager.LinkMembers, which sets
  parentReviewId
- Degrade to a single review when the judge fails after a retry or only
  one member ran
- Test sourceLabels dedup, fusion sources persistence and member linking
- Run panel members concurrently, bounded by panelConcurrency, adding
  the worktrees sequentially first since git worktree add is not safe to
  run concurrently against one repo
- Tolerate per-member failures (log and skip) and require only one
  success instead of failing the whole panel on the first error
- Add --timeout / $REVIEW_TIMEOUT (default 30m) bounding each member and
  the judge run, plus an EnvDuration helper
- Test failure tolerance and panel ordering under the race detector
- Exclude reviewRole=member from default review lists and
  aggregations (ListReviews/CountReviews via ReviewSearch,
  ProjectsStats, FillLastVersions) so the fusion is the primary
  review; members stay reachable by direct id and opt-in via
  IncludeMembers/ParentReviewID
- Rewrite LinkMembers as one project- and role-scoped UPDATE with a
  RowsAffected guard (no foreign, non-member, or already-parented
  ids) and run it inside the fusion-create transaction via
  CreateReviewWithMembers so a bad member id rolls the fusion back
- Validate reviewRole, memberReviewIds (require fusion), and per-issue
  sources in ReviewDraft.Validate
- Return the fusion panel breakdown (member children + summed panel
  cost) and per-issue sources from ReviewService.GetByID via a new
  ListMembers and rpc Review/PanelMember/Issue fields
- Regenerate rpc zenrpc and the public TS client for the new fields
- Add ProjectManager.ReviewProfiles resolving a project's primary
  runner, panel members (runnerProfileIds, order preserved, disabled
  ids skipped) and judge (judgeRunnerProfileId)
- Reshape the reviewctl ReviewConfig RPC to return {primary, panel,
  judge} with real tokens and add a FusionPrompt method serving the
  built-in judge prompt; regenerate zenrpc and the go client
- Wire reviewctl to build cfg.Multi (the [primary] + panel run set)
  and cfg.Judge from the server config when a judge is set; the judge
  now fetches FusionPrompt over the RPC; --multi/--judge stay a local
  override
- Pass the runner-profile token per-process via cmd.Env instead of a
  global os.Setenv, so concurrent panel members with different tokens
  no longer race (ambient env still wins)
- Carry each member's resolved profile in MemberSpec so it runs and
  snapshots with its own credentials and settings
- Add tests for ReviewProfiles, the ReviewConfig/FusionPrompt RPC,
  credEnv injection and the [primary] + panel assembly
Phase 5 regenerated the public TS client from rpc/model.go but the new
fields carried multi-line Go doc comments, which the rpcgen TS codegen
emits as a trailing // comment and leaks the extra lines as bare code —
a syntax error. The same regen also dropped the file's // @ts-nocheck
header. Both went unnoticed because only Go tests ran.

- Collapse the Review/Issue multi-review field comments to single lines
  so the generated TS stays valid
- Restore // @ts-nocheck in factory.generated.ts (the generator omits it)
- Regenerate rpc_zenrpc.go and factory.generated.ts
- Expose runnerProfileIds + judgeRunnerProfileId on the VT Project
  model, converter and ToDB, plus the judge summary relation
- Validate the judge FK and each panel member FK in code, like the
  existing runnerProfileId
- Add an ordered FKListEditor component (add/remove/reorder, duplicates
  allowed) and wire the project form: relabel the runner profile
  "Primary / fallback", add the panel members list and a nullable Judge
  select, with a hint when members are set but no judge is chosen
- Regenerate vt_zenrpc and the VT TS client for the new fields
- Add a VT validation test for the new panel FKs
- Fold the repeated per-member config setup (clone, runner/model, clear
  panel fields, profile overlay) into one Controller.memberConfig,
  replacing applyMemberProfile and its two duplicated call sites
- Extract newReviewFileSummaries, shared by the review-list row and the
  fusion panel breakdown instead of two identical loops
- Refresh the stale FusionPrompt comment: it is served via rpc and the
  CI client fetches it, so the constant is the single source of truth
- Add a PanelBreakdownCard on the fusion review page listing each
  member child (traffic light, model, issue count, cost, link to the
  member review) with panel/judge/total cost
- Add a ProvenanceBadge and render an issue's sources in the issue
  detail — the model labels that flagged it (or "judge" for a verified
  net-new) plus an agreement count when more than one model concurred
- Re-export the PanelMember type from the public api client
- Public review: show the fusion panel breakdown in a dedicated "Panel"
  tab (after Issues) instead of an always-expanded card, so it opens on
  demand and the header stays compact; the tab appears only for fusion
  reviews
- VT project form: group Runner Profile / Panel / Judge into a new
  "Runner" tab between General and Instructions, decluttering General
- Clearer for an end user that the review was synthesized from several
  models; the inner "Panel breakdown" heading and the Panel/Judge cost
  split keep "Panel" where it precisely means the member set
- Replace the OS-native select arrow with a single themed chevron via a
  shared .app-select class (appearance:none + an SVG background that
  follows --color-fg-subtle in light/dark)
- Apply it to every select: PSelect (public), VSelect, FillExampleSelect
  and the FKListEditor add dropdown (VT)
- Extract applyRunResult to set model/runner/timing metadata and the
  resolved profile snapshot, replacing the identical block that was
  duplicated across the single review, panel members and the judge
- Remove the unused write-only memberOutput.spec field
- Collapse the repeated server-profile flag overrides in
  applyReviewConfig into a serverDefault helper
- Hide the local --multi/--judge debug flags so they keep working but
  do not become a stable CLI contract; multi-review is profile-driven
- Reframe reviewctl flags as local overrides; runner config comes
  from the project runner profile fetched at run time
- Add the missing runner/effort/api/timeout/continue flags and fix
  the --author env and --model default in the reviewctl flag table
- Mark the LLM API key optional in CI now the profile may carry a token
- Document the panel + judge fusion multi-review flow
- Note ReviewConfig returns the panel and add FusionPrompt
- Collapse the runner-specific tabs in the local-run dialog into one
  script; the runner and model now come from the project profile
- Rebuild the CI Dockerfile helper to layer the latest reviewctl
  release on the vmkteam/claude-ci base instead of inlining settings
- Make PROJECT_KEY and REVIEWSRV_URL the only required CI vars; LLM
  credentials now come from the server-side runner profile or, as a
  fallback, REVIEW_API_KEY / provider-specific keys
- Document the dockerimage as the claude-ci + reviewctl combined image
  and default it to a registry placeholder
- Annotate the MR session cache as a Claude-runner prompt-cache reuse
  that is a safe no-op for the opencode/codex/direct runners
- Add gpt-5.6 family (sol/terra/luna + bare alias) to the codex
  price table for token-based cost estimation
- Fix claude-fable pricing in the direct runner: split from the
  claude-opus case, $10/$50 per MTok with matching cache rates
- Add claude-opus-5, claude-sonnet-5 and gpt-5.6 tiers to runner
  profile model suggestions in the VT admin form
- Extend pricing tests to cover opus-5, sonnet-5 and gpt-5.6
- Add http_fetch tool locked to the task tracker origin and path:
  GET-only, same-scope redirects, 100KB clip, JSON/text bodies only,
  responses wrapped as untrusted data; the Authorization header is
  derived from the token shape (email:secret → Basic for Jira Cloud,
  anything else → Bearer, so YouTrack perm tokens stay intact)
- Keep the tracker token out of assembled prompts: {{TOKEN}} renders
  as $REVIEW_TRACKER_TOKEN, reviewctl exports it to CLI runner
  processes per-process (ambient env wins) and the REVIEW_TRACKER_TOKEN
  CI variable overrides the server-stored token
- Serve tracker URL/token out-of-band via the ReviewConfig RPC;
  ReviewProfiles now returns a domain ReviewSetup (profiles + tracker)
  in a single project load; drop the unused RunnerProfile resolver
- Preserve backward compatibility with an optional tokenEnv flag on
  the Prompt RPC: legacy reviewctl clients keep receiving the
  old-style prompt with the real token substituted in
- Surface direct-runner tool errors in the runner log at warn level
- Document the runner-agnostic fetchPrompt example and tracker access
  in VT.md, Model.md, README and the CI template; regenerate zenrpc
  bindings and the reviewctl client
- Raise Anthropic output cap to 64k and handle truncated rounds:
  notify the model, reject empty add_issues batches, abort after 3
  truncated rounds in a row (after running surviving tool calls)
- Preserve the prompt cache: keep the compaction head byte-identical,
  defer compaction on 1M-context providers (direct.DefaultCompactAt),
  clip grep output and matched lines (rune-safe), emit compact events
- Hide reviewer artifacts from diffs, walks and preload via a canonical
  list in pkg/reviewer; root-level only so the judge's staged
  members/<label>/ files stay visible; clean review.html before runs
- Detect the diff base from origin/HEAD for local runs without CI vars
- Fix tracker API calls returning SPA HTML: trim trailing slash from
  tracker URL on write and in {{URL}} substitution, normalize http_fetch
  paths with path.Clean before the scope check (also blocks ".."
  escapes); refuse direct reads of .claude/ local agent state
- Panel: treat an untouched-skeleton member review as failure, upload a
  debug bundle before the worktree is cleaned, retry an empty judge
  review; pin opencode to its worktree with --dir (it escaped to the
  main repo via the shared git dir)
- Persist run metadata into review.json, clear placeholder fields and
  fall back to git metadata for local runs
- vt admin: write-only secrets with set-or-keep (nil or blank keeps the
  stored value), SecretInput wraps VInput and owns the "Saved" hint,
  move @ts-nocheck from Makefile perl into the TS client generator
- Install the arch-aware static-pie ast-index release in the VT CI
  Setup Dockerfile so the direct runner's ast_* tools work in CI
- Pin the version via ARG and verify the binary at build time
- Move go.mod, workflows and the Docker builder image to Go 1.26
- Pin golangci-lint-action and LINT_VERSION to v2.12.2: CI ran
  v2.5.0, which flags prealloc issues the current linter dropped,
  so PR lint failed while local runs were clean
- go get -u direct deps: anthropic-sdk-go v1.61.0, zenrpc v2.3.3,
  goldmark v1.8.5, echo v4.15.4, prometheus client v1.24.1,
  rpcgen v2.5.5, sentry-go v0.44.0 and transitive updates
- Keep sentry-go at v0.44.0: v0.45+ moves the echo integration to
  echo/v5, which breaks appkit's echo/v4 middleware
- Refresh vendor
@sergeyfast
sergeyfast merged commit e5c7546 into master Jul 28, 2026
3 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.

1 participant