feat(config): expose dual-AI combine strategy as a per-repo setting - #2677
Conversation
ai-review.ts's dual-AI CombineStrategy/onMerge/reviewers config was only settable via the self-host operator's AI_REVIEW_PLAN boot config, with no per-repo override in .gittensory.yml. Adds gate.aiReview.combine/onMerge/ reviewers to the focus manifest so a repo can refine the operator's plan, following the same config-as-code-only pattern already used for gate.aiReview.closeConfidence (no DB column, no dashboard write route). The onMerge field carries real strictness semantics (either = any one reviewer's blocker holds/blocks; both = requires every reviewer to agree), so a naive per-repo override could let a repo silently loosen an operator-imposed floor. resolveEffectiveAiReviewOnMerge in ai-review.ts clamps a repo's "both" back to the operator's "either" floor, incrementing a metric so the clamp is surfaced rather than silently ignored; a repo may still freely tighten, and when the operator set no floor any value passes through unclamped. CombineStrategy/OnMerge move from services/ai-review.ts into types.ts as their canonical definition (re-exported from ai-review.ts for compat): focus-manifest.ts and types.ts are imported by the UI workspace, and a type-only reference into ai-review.ts pulled its ambient Cloudflare Workers types (Env, D1Database, ...) into the UI's typecheck graph and broke it. Closes #2567
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | 8e2fc7e | Commit Preview URL Branch Preview URL |
Jul 03 2026, 05:50 AM |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2677 +/- ##
=======================================
Coverage 96.13% 96.13%
=======================================
Files 239 239
Lines 26739 26779 +40
Branches 9699 9721 +22
=======================================
+ Hits 25705 25745 +40
Misses 424 424
Partials 610 610
🚀 New features to boost your workflow:
|
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-03 05:40:47 UTC
⏸️ Suggested Action - Manual Review
Review summary Nits — 7 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
# Conflicts: # apps/gittensory-ui/public/openapi.json # src/queue/processors.ts # test/unit/focus-manifest.test.ts
Summary
ai-review.ts's dual-AICombineStrategy/onMerge/reviewersconfig (src/services/ai-review.ts:90-92,1099-1101) was only settable via the self-host operator'sAI_REVIEW_PLANboot config (env.AI_REVIEW_PLAN,src/env.d.ts:77) — there was noreview.combine/review.onMerge/review.reviewersfield in the manifest schema, so a repo could not refine the operator's default.gate.aiReview.combine/gate.aiReview.onMerge/gate.aiReview.reviewerstoFocusManifestGateConfiginsrc/signals/focus-manifest.ts, parsed and round-tripped the same way as the existing sibling fields in that block (aiReviewMode,aiReviewByok,aiReviewCloseConfidence, ...).gate.aiReview.closeConfidence(src/types.ts,src/signals/focus-manifest.ts:1266region): config-as-code only — no DB column, no Drizzle field, no migration, no dashboard settings-write route insrc/api/routes.ts. VerifiedaiReviewCloseConfidencehas none of those either before following suit.runAiReviewForAdvisoryinsrc/queue/processors.tsnow forwardssettings.aiReviewCombine/aiReviewOnMerge/aiReviewReviewers(already resolved byresolveEffectiveSettings) intorunGittensoryAiReview's input, in place of relying solely on the unconditionalenv.AI_REVIEW_PLANread.Precedence design decision (the core of this issue)
onMergecarries real strictness semantics, unlikecombine:either— ANY single reviewer's blocker holds/blocks the PR (the stricter rule).both— requires every reviewer to agree before a blocker counts (the more permissive rule).So a naive per-repo override could let a repo silently loosen an operator-imposed floor (e.g. operator sets
eitherinstance-wide to guarantee any flagged blocker is caught; a repo setsbothand now needs both reviewers to agree, which is strictly weaker). I resolved this by addingresolveEffectiveAiReviewOnMergeinsrc/services/ai-review.ts, called fromrunGittensoryAiReviewwhere both the per-repoinput.onMergeand the operator'splan?.onMergeare visible:either+ repo overrideboth→ clamped back toeither(an attempted loosening).either+ repo overrideeither→either(a no-op tightening — never treated as a clamp).both(or unset) → the repo override wins unclamped — there is no stricter floor to violate.A fired clamp increments
gittensory_ai_review_onmerge_clamped_total{mode}(mirrors the existinggittensory_ai_review_inconclusive_totalobservability pattern in the same file) so the attempted loosening is surfaced, not silently dropped.combineitself (single/consensus/synthesis) is not floor-clamped — the three strategies aren't ordered by strictness, so there's no single "loosening" direction to clamp against.reviewers(which providers run) also isn't floor-clamped — the floor is about what triggers a hold/block, not who evaluates it.Unrelated fix required to keep this change UI-safe
CombineStrategy/OnMergewere originally defined insrc/services/ai-review.ts, which pulls in ambient Cloudflare Workers types (Env,D1Database, ...) that theapps/gittensory-uiworkspace'stsconfig.jsonlibdoesn't declare.src/signals/focus-manifest.tsandsrc/types.tsare both imported by the UI (maintainer-settings.tsx,registration-workspace.ts), so a type-onlyimport("../services/ai-review").CombineStrategyreference from either file dragged that whole module graph into the UI's typecheck and brokenpm run ui:typecheck(Cannot find name 'Env'/'D1Database'acrosssrc/db/*.ts,src/review/*.ts, etc.). Moved the canonicalCombineStrategy/OnMergedefinitions intosrc/types.ts(a zero-import leaf file, alongside the existingGateRuleMode/GatePolicyPack) and re-exported them fromsrc/services/ai-review.tsfor backward compatibility with existing importers (src/env.d.ts, test files).Test plan
npm run typecheck— clean.npm run ui:typecheck— clean (confirmed this specifically catches theEnv/D1Databaseleak described above; reproduced it, fixed it, reproduced the fix).npx vitest run test/unit/focus-manifest.test.ts test/unit/ai-review.test.ts test/unit/ai-review-advisory.test.ts— 299 passed. New coverage:onMergefloor unchanged (zero behavior change).either → eitheragainst aneitherfloor is a no-op, not a clamp.either → bothagainst aneitherfloor is clamped back toeither, end-to-end throughrunGittensoryAiReview, and asserted via thegittensory_ai_review_onmerge_clamped_totalmetric (logged/surfaced, not silently ignored).bothagainst aboth(or unset) floor is honored unclamped.resolveEffectiveAiReviewOnMergecovering every combination of{null, undefined, either, both}×{null, undefined, either, both}.gate.aiReview.combine/onMerge/reviewersparsing: valid values, invalid values (warn + drop), round-trip throughgateConfigToJson→parseFocusManifest, resolution into effective settings, and the manifest-presentflag.gate.aiReview.reviewerslist handling: valid entries, non-mapping/blank-model entries dropped individually, the 4-entry cap, and the "absent means null not []" contract.runAiReviewForAdvisorywiring test:settings.aiReviewCombine/aiReviewReviewersreach the actual reviewer call (only the configured reviewer runs).consensusstrategy.npm run test:coverage(unsharded) — 6748 tests passed, 0 failed; every line I added infocus-manifest.tsandai-review.tsis covered (verified the reported "Uncovered Line #s" for both files fall entirely in pre-existing code untouched by this diff, viagit diff --unified=0).npm run test:ci— full local gate green (actionlint, migrations check, cf-typegen check, typecheck, coverage suite, workers test, mcp build/pack, UI openapi/lint/typecheck/test/build).npm audit --audit-level=moderate— 0 vulnerabilities.npm run ui:openapi:settings-parity—RepositorySettingsSchemamatchesRepositorySettings(65 fields); regenerated and committedapps/gittensory-ui/public/openapi.jsonvianpm run ui:openapi.Closes #2567