Skip to content

Commit 9720bf0

Browse files
committed
feat(review): make orb check publication configurable
Adds settings.reviewCheckMode / gate.checkMode ("required" | "visible" | "disabled") so a repo can keep GitHub branch protection pinned to the review check, publish it as advisory-only UI, or stop publishing it entirely, without weakening real CI/codecov gating or the autonomous merge/close decision engine. Legacy gate.enabled/gateCheckMode continue to map to required/disabled for back-compat; an explicit reviewCheckMode always wins over the legacy boolean, at both the .gittensory.yml layer and the settings write routes. The autonomous decision engine no longer depends on the check-run's existence: gate evaluation now runs whenever the check is published OR an automation agent is configured, decoupling "should we publish a check-run" from "should we compute a gate verdict." Also fixes a related CI-deferral staleness bug: a required status context that never reports was being held under the same 30-minute stale-CI cap as genuinely pending CI, leaving PRs looking stuck for up to half an hour. Missing-required-context now defers under its own short 2-minute cap so review proceeds promptly once real CI is clear, without adding polling or extra GitHub API calls.
1 parent 5d998ff commit 9720bf0

39 files changed

Lines changed: 938 additions & 81 deletions

.gittensory.yml.example

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,32 @@ gate:
9595
# Bool. Default: false (gate off).
9696
enabled: true
9797

98+
# Review-CHECK publish mode (#2852) — controls ONLY whether/how the "Gittensory Orb Review Agent"
99+
# check-run is created/updated on GitHub. Never affects gate evaluation, comments, labels, audit
100+
# records, or autonomous merge/close — all of those run identically in every mode (the autonomous
101+
# decision engine already excludes the bot's own check-run from the live CI it merges/closes against,
102+
# specifically to avoid a self-deadlock). Takes precedence over the legacy `enabled` boolean above
103+
# when both are set; when unset, `enabled: true` maps to `required` and `enabled: false` maps to
104+
# `disabled` for backward compatibility.
105+
# required — legacy/current behavior: publish/update the check exactly as before. Use this if you
106+
# intentionally keep it as a required branch-protection status check.
107+
# visible — publish/update the SAME check-run, but for UI visibility only. Never add this check as
108+
# a required branch-protection status check — behaves identically to `required` on the
109+
# publish side (same API calls); the distinction is purely how you configure GitHub.
110+
# disabled — never create/update the check-run at all. RECOMMENDED for high-volume autonomous
111+
# self-hosting: avoids GitHub showing "Expected — Waiting for status to be reported"
112+
# under review-queue pressure, and reduces GitHub API calls (no check-run create/update).
113+
# Before switching to `disabled`, remove "Gittensory Orb Review Agent" from this repo's
114+
# branch-protection / ruleset required-status-checks list — Gittensory cannot do this on
115+
# your behalf (it is a GitHub branch-protection setting), and leaving it required with
116+
# nothing to satisfy it means GitHub will show a pending status forever. Keep your real
117+
# CI/codecov/security checks required — this only ever affects Gittensory's own check.
118+
# required | visible | disabled. Default: disabled for a never-configured repo (matches the existing
119+
# opt-in default); an already-configured repo keeps its current effective behavior. RECOMMENDED for
120+
# high-volume autonomous self-hosting: `visible` or `disabled`, not `required` — Gittensory's own
121+
# merge/close decisions never depend on this check either way.
122+
checkMode: visible
123+
98124
# Policy pack.
99125
# gittensor — confirmed-contributor-gated, registry-aware.
100126
# oss-anti-slop — runs the deterministic rules against ANY author on ANY repo.

apps/gittensory-ui/public/openapi.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3630,6 +3630,14 @@
36303630
"items": {
36313631
"type": "string"
36323632
}
3633+
},
3634+
"reviewCheckMode": {
3635+
"type": "string",
3636+
"enum": [
3637+
"required",
3638+
"visible",
3639+
"disabled"
3640+
]
36333641
}
36343642
},
36353643
"required": [
@@ -3639,6 +3647,7 @@
36393647
"publicAudienceMode",
36403648
"checkRunMode",
36413649
"gateCheckMode",
3650+
"reviewCheckMode",
36423651
"quietByDefault",
36433652
"behavior",
36443653
"warnings"
@@ -9064,6 +9073,14 @@
90649073
"items": {
90659074
"type": "string"
90669075
}
9076+
},
9077+
"reviewCheckMode": {
9078+
"type": "string",
9079+
"enum": [
9080+
"required",
9081+
"visible",
9082+
"disabled"
9083+
]
90679084
}
90689085
},
90699086
"required": [
@@ -9074,6 +9091,7 @@
90749091
"checkRunMode",
90759092
"checkRunDetailLevel",
90769093
"gateCheckMode",
9094+
"reviewCheckMode",
90779095
"gatePack",
90789096
"linkedIssueGateMode",
90799097
"duplicatePrGateMode",
@@ -9164,6 +9182,14 @@
91649182
},
91659183
"autoLabelEnabled": {
91669184
"type": "boolean"
9185+
},
9186+
"reviewCheckMode": {
9187+
"type": "string",
9188+
"enum": [
9189+
"required",
9190+
"visible",
9191+
"disabled"
9192+
]
91679193
}
91689194
},
91699195
"required": [
@@ -9172,6 +9198,7 @@
91729198
"publicAudienceMode",
91739199
"checkRunMode",
91749200
"gateCheckMode",
9201+
"reviewCheckMode",
91759202
"autoLabelEnabled"
91769203
]
91779204
}
@@ -9734,6 +9761,14 @@
97349761
},
97359762
"typeLabelsEnabled": {
97369763
"type": "boolean"
9764+
},
9765+
"reviewCheckMode": {
9766+
"type": "string",
9767+
"enum": [
9768+
"required",
9769+
"visible",
9770+
"disabled"
9771+
]
97379772
}
97389773
},
97399774
"required": [
@@ -9744,6 +9779,7 @@
97449779
"checkRunMode",
97459780
"checkRunDetailLevel",
97469781
"gateCheckMode",
9782+
"reviewCheckMode",
97479783
"gatePack",
97489784
"linkedIssueGateMode",
97499785
"duplicatePrGateMode",

config/examples/global.gittensory.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
# Baseline gate policy shared by every repo unless a per-repo file overrides it.
1717
gate:
1818
enabled: true
19+
# Review-check publish mode (#2852): required | visible | disabled. RECOMMENDED fleet-wide default
20+
# for high-volume autonomous self-hosting is `visible` or `disabled`, NOT `required` — Gittensory's
21+
# own merge/close decisions never depend on this check either way. If you set this to `disabled`
22+
# instance-wide, remove "Gittensory Orb Review Agent" from every repo's branch-protection required-
23+
# status-checks list first (Gittensory cannot do this for you — it's a GitHub branch-protection
24+
# setting), or GitHub will show a pending status forever on repos that still require it.
25+
checkMode: visible
1926
duplicates: block
2027
linkedIssue: advisory
2128

config/examples/repo-override.gittensory.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@
1010
# Replace `owner`/`repo` in the destination path with the real values before using this file.
1111
# ============================================================================
1212

13-
# Overrides ONLY `gate.enabled` and `gate.expectedCiContexts` for this repo — every other `gate.*`
14-
# key (e.g. `duplicates`, `linkedIssue`) is inherited from the global default untouched.
13+
# Overrides ONLY `gate.enabled`, `gate.checkMode`, and `gate.expectedCiContexts` for this repo —
14+
# every other `gate.*` key (e.g. `duplicates`, `linkedIssue`) is inherited from the global default
15+
# untouched.
1516
gate:
1617
enabled: true
18+
# This repo goes fully dark (#2852): no "Gittensory Orb Review Agent" check-run at all, overriding
19+
# the global default above. Reviews/comments/labels/audit and autonomous merge/close all keep
20+
# working — only the check-run publish is affected. Remove the check from THIS repo's branch-
21+
# protection required-status-checks before relying on this, or GitHub will show it pending forever.
22+
checkMode: disabled
1723
# This repo's own CI job/check names to trust as required when its branch protection is
1824
# unreadable (a common self-host case — see ../../.gittensory.yml.example). Each repo's CI
1925
# naming differs, so this is a per-repo override rather than a global default.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Configurable review-check publish surface (#2852): `gateCheckMode` was a binary off/enabled switch for the
2+
-- "Gittensory Orb Review Agent" check-run, with no way to publish it non-required (visibility only) or to go
3+
-- fully dark without leaving a GitHub branch-protection required-status-check permanently unsatisfied. Defaults
4+
-- to 'disabled' (matches gate_check_mode's own 'off' default for never-configured repos -- opt-in, unchanged).
5+
-- Existing rows backfill from their CURRENT gate_check_mode so already-configured repos keep today's behavior
6+
-- exactly: 'enabled' -> 'required' (still publishes, still fine to require in branch protection); 'off' (or any
7+
-- other/legacy value) keeps the column default 'disabled' (still never publishes). gate_check_mode itself is
8+
-- left untouched -- read by settings-preview.ts/the dashboard for back-compat display; the runtime publish
9+
-- decision reads review_check_mode.
10+
ALTER TABLE repository_settings ADD COLUMN review_check_mode TEXT NOT NULL DEFAULT 'disabled';
11+
UPDATE repository_settings SET review_check_mode = 'required' WHERE gate_check_mode = 'enabled';

src/api/routes.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,13 @@ const repositorySettingsSchema = z.object({
609609
checkRunMode: z.enum(["off", "enabled"]).default("off"),
610610
checkRunDetailLevel: z.enum(["minimal", "standard", "deep"]).default("standard"),
611611
gateCheckMode: z.enum(["off", "enabled"]).default("off"),
612+
// #2852: deliberately NO `.default()` here (unlike every sibling field above) -- this is a non-partial,
613+
// full-replace schema (see upsertRepositorySettings call below, which passes every parsed field straight
614+
// through with no read-merge of the current row), so an eager default would mask a legacy caller that only
615+
// sends gateCheckMode: the value would already be "defined" (the default) by the time it reaches
616+
// upsertRepositorySettings, and its own `settings.reviewCheckMode ?? (gateCheckMode==="enabled" ? ...)`
617+
// fallback only fires on `undefined`. Leaving this genuinely optional lets that fallback do its job.
618+
reviewCheckMode: z.enum(["required", "visible", "disabled"]).optional(),
612619
gatePack: z.enum(["gittensor", "oss-anti-slop"]).default("gittensor"),
613620
linkedIssueGateMode: z.enum(["off", "advisory", "block"]).default("advisory"),
614621
duplicatePrGateMode: z.enum(["off", "advisory", "block"]).default("block"),
@@ -658,6 +665,7 @@ const maintainerSettingsSchema = z
658665
checkRunMode: z.enum(["off", "enabled"]),
659666
checkRunDetailLevel: z.enum(["minimal", "standard", "deep"]),
660667
gateCheckMode: z.enum(["off", "enabled"]),
668+
reviewCheckMode: z.enum(["required", "visible", "disabled"]),
661669
gatePack: z.enum(["gittensor", "oss-anti-slop"]),
662670
linkedIssueGateMode: z.enum(["off", "advisory", "block"]),
663671
duplicatePrGateMode: z.enum(["off", "advisory", "block"]),
@@ -2181,6 +2189,14 @@ export function createApp() {
21812189
const current = await getRepositorySettings(c.env, fullName);
21822190
const changes = Object.fromEntries(Object.entries(parsed.data).filter(([, value]) => value !== undefined)) as Partial<RepositorySettings>;
21832191
if (changes.qualityGateMode !== undefined) changes.qualityGateMode = downgradeQualityGateMode(changes.qualityGateMode);
2192+
// #2852: a legacy client (the maintainer dashboard's "Review agent check" toggle) only ever sends
2193+
// gateCheckMode, never the newer reviewCheckMode -- which must keep its historical effect on the ACTUAL
2194+
// publish authority. Derive it here, before merging onto `current` (which always has a DEFINED
2195+
// reviewCheckMode already), because upsertRepositorySettings's own legacy-fallback only fires on
2196+
// `undefined` and would never see this change as unset once merged with the current row.
2197+
if (changes.gateCheckMode !== undefined && changes.reviewCheckMode === undefined) {
2198+
changes.reviewCheckMode = changes.gateCheckMode === "enabled" ? "required" : "disabled";
2199+
}
21842200
const updated = await upsertRepositorySettings(c.env, { ...current, ...changes, repoFullName: fullName });
21852201
await recordAuditEvent(c.env, {
21862202
eventType: "repo.settings_updated",
@@ -2298,6 +2314,7 @@ export function createApp() {
22982314
return c.json({
22992315
repoFullName: fullName,
23002316
gateCheckMode: updated.gateCheckMode,
2317+
reviewCheckMode: updated.reviewCheckMode,
23012318
checkRunMode: updated.checkRunMode,
23022319
linkedIssueGateMode: updated.linkedIssueGateMode,
23032320
duplicatePrGateMode: updated.duplicatePrGateMode,
@@ -3505,6 +3522,11 @@ export function createApp() {
35053522
checkRunMode: parsed.data.checkRunMode,
35063523
checkRunDetailLevel: parsed.data.checkRunDetailLevel,
35073524
gateCheckMode: parsed.data.gateCheckMode,
3525+
// #2852: this route is a full-replace, non-partial schema (every sibling field has a `.default()`),
3526+
// so a caller that only ever sends gateCheckMode must still get its historical effect on the actual
3527+
// publish authority -- derive it explicitly here rather than relying on a passthrough `undefined`
3528+
// (which `exactOptionalPropertyTypes` disallows assigning to RepositorySettings anyway).
3529+
reviewCheckMode: parsed.data.reviewCheckMode ?? (parsed.data.gateCheckMode === "enabled" ? "required" : "disabled"),
35083530
gatePack: parsed.data.gatePack,
35093531
linkedIssueGateMode: parsed.data.linkedIssueGateMode,
35103532
duplicatePrGateMode: parsed.data.duplicatePrGateMode,

src/db/repositories.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
478478
checkRunMode: "off",
479479
checkRunDetailLevel: "minimal",
480480
gateCheckMode: "off",
481+
reviewCheckMode: "disabled",
481482
gatePack: "gittensor",
482483
linkedIssueGateMode: "advisory",
483484
duplicatePrGateMode: "block",
@@ -546,6 +547,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
546547
checkRunMode: parseCheckRunMode(row.checkRunMode),
547548
checkRunDetailLevel: parseCheckRunDetailLevel(row.checkRunDetailLevel),
548549
gateCheckMode: parseGateCheckMode(row.gateCheckMode),
550+
reviewCheckMode: parseReviewCheckMode(row.reviewCheckMode),
549551
gatePack: parseGatePack(row.gatePack),
550552
linkedIssueGateMode: parseGateRuleMode(row.linkedIssueGateMode),
551553
duplicatePrGateMode: parseGateRuleMode(row.duplicatePrGateMode),
@@ -650,6 +652,14 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
650652
checkRunMode: settings.checkRunMode ?? "off",
651653
checkRunDetailLevel: settings.checkRunDetailLevel ?? "minimal",
652654
gateCheckMode: settings.gateCheckMode ?? "off",
655+
// Legacy-write compatibility (#2852): a caller that sets ONLY gateCheckMode (never touching the newer,
656+
// more expressive reviewCheckMode) must keep its historical effect -- "enabled" still means the check
657+
// publishes. This is safe under this function's existing "no field is preserved from the DB, an absent
658+
// field always gets a fresh default" contract (see the route-handler comment above): a true partial-update
659+
// caller already read-merges the full current settings (including its persisted reviewCheckMode) before
660+
// calling this, so `settings.reviewCheckMode` is never actually undefined for that path -- this fallback
661+
// only fires for callers that never cared about reviewCheckMode at all.
662+
reviewCheckMode: settings.reviewCheckMode ?? (settings.gateCheckMode === "enabled" ? "required" : "disabled"),
653663
gatePack: parseGatePack(settings.gatePack),
654664
linkedIssueGateMode: settings.linkedIssueGateMode ?? "advisory",
655665
duplicatePrGateMode: settings.duplicatePrGateMode ?? "block",
@@ -720,6 +730,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
720730
checkRunMode: resolved.checkRunMode,
721731
checkRunDetailLevel: resolved.checkRunDetailLevel,
722732
gateCheckMode: resolved.gateCheckMode,
733+
reviewCheckMode: resolved.reviewCheckMode,
723734
gatePack: resolved.gatePack,
724735
linkedIssueGateMode: resolved.linkedIssueGateMode,
725736
duplicatePrGateMode: resolved.duplicatePrGateMode,
@@ -789,6 +800,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
789800
checkRunMode: resolved.checkRunMode,
790801
checkRunDetailLevel: resolved.checkRunDetailLevel,
791802
gateCheckMode: resolved.gateCheckMode,
803+
reviewCheckMode: resolved.reviewCheckMode,
792804
gatePack: resolved.gatePack,
793805
linkedIssueGateMode: resolved.linkedIssueGateMode,
794806
duplicatePrGateMode: resolved.duplicatePrGateMode,
@@ -6118,6 +6130,10 @@ function parseGateCheckMode(value: string): RepositorySettings["gateCheckMode"]
61186130
return value === "enabled" ? "enabled" : "off";
61196131
}
61206132

6133+
function parseReviewCheckMode(value: string): RepositorySettings["reviewCheckMode"] {
6134+
return value === "required" || value === "visible" ? value : "disabled";
6135+
}
6136+
61216137
function parseGatePack(value: string | null | undefined): RepositorySettings["gatePack"] {
61226138
return value === "oss-anti-slop" ? "oss-anti-slop" : "gittensor";
61236139
}

src/db/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const repositorySettings = sqliteTable("repository_settings", {
5151
checkRunMode: text("check_run_mode").notNull().default("off"),
5252
checkRunDetailLevel: text("check_run_detail_level").notNull().default("minimal"),
5353
gateCheckMode: text("gate_check_mode").notNull().default("off"),
54+
reviewCheckMode: text("review_check_mode").notNull().default("disabled"),
5455
gatePack: text("gate_pack").notNull().default("gittensor"),
5556
// Missing a linked issue is advisory-only by default -- issues aren't always available, so it only
5657
// blocks when a repo explicitly opts in (linkedIssueGateMode: "block" or the requireLinkedIssue toggle;

0 commit comments

Comments
 (0)