Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gittensory.minimal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# - "src/**"

gate:
enabled: false
checkMode: disabled

settings:
autonomy:
Expand Down
10 changes: 7 additions & 3 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,13 @@ publicNotes:
# block — the finding can become a hard `Gittensory Gate` blocker
# (always confirmed-contributor-gated).
gate:
# Gate master switch. Turns the whole deterministic gate on; the per-dimension
# modes below only refine an already-enabled gate.
# Bool. Default: false (gate off).
# Legacy check-run publish switch (#5355) — despite the name, this does NOT turn the deterministic
# gate itself on or off. Gate evaluation, comments, labels, audit records, spend, and autonomous
# merge/close all run identically whether this is true, false, or unset; the per-dimension modes
# below are what actually configure gate evaluation. `enabled` is only a boolean shorthand for
# `checkMode` below: true maps to `required`, false maps to `disabled`, and it cannot express
# `checkMode`'s third state (`visible`). Prefer setting `checkMode` explicitly instead of this field.
# Bool. Default: unset (falls through to `checkMode`, then to the dashboard `reviewCheckMode` setting).
enabled: true

# Review-CHECK publish mode (#2852) — controls ONLY whether/how the "Gittensory Orb Review Agent"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ GITHUB_METADATA_CACHE_TTL_SECONDS=600`}
{
title: "Gate activation (DB or private config)",
description:
"Whether the Gittensory check-run and deterministic gate rules run for a repo. Stored in the instance database (control panel, PUT /v1/repos/:owner/:repo/settings, or POST …/activation) and/or in gate.checkMode / gate.enabled in .gittensory.yml. The one-click activation endpoint applies advisory-first defaults: gate on, linked-issue/duplicate/quality rules in advisory mode, AI review still off.",
"The one-click POST …/activation endpoint bundles two independent axes into one advisory-first default: the review-check publish mode (reviewCheckMode: required, checkRunMode: enabled) and the actual per-dimension gate rules (linkedIssueGateMode, duplicatePrGateMode, qualityGateMode: all advisory; AI review still off). .gittensory.yml's gate.checkMode / gate.enabled only ever set the first axis (the check-run publish mode) — the dimension rules themselves are configured separately via gate.linkedIssue, gate.duplicates, gate.readiness.mode, etc. (see Tuning your reviews). Gate rule evaluation itself is never gated by checkMode/enabled/checkRunMode; those only control whether/how the check-run publishes on GitHub.",
},
{
title: "Gittensor registration (is_registered)",
Expand Down Expand Up @@ -468,7 +468,7 @@ GITTENSORY_REVIEW_REPUTATION=false`}
<CodeBlock
filename="owner__repo/.gittensory.yml"
code={`gate:
enabled: true
checkMode: visible
aiReview:
mode: advisory
allAuthors: true
Expand Down
15 changes: 11 additions & 4 deletions apps/gittensory-ui/src/routes/docs.tuning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,15 @@ function Tuning() {
</li>
</ul>
<p>
The master switch is <code>gate.enabled</code> (<code>off</code> / <code>enabled</code>).
The per-dimension modes refine an already-enabled gate. The main dimensions:
There is no single gate master switch — each dimension below is independently controlled by
its own mode field (most default to <code>off</code> or <code>advisory</code>; see each
dimension's default below). <code>gate.enabled</code> is a legacy, unrelated field: it is
only a boolean shorthand for <code>gate.checkMode</code> (<code>required</code> /{" "}
<code>visible</code> / <code>disabled</code>), which controls solely whether the{" "}
<code>Gittensory Orb Review Agent</code> check-run publishes on GitHub. Neither field turns
gate evaluation, comments, labels, audit, or autonomous merge/close on or off — set the
dimension modes below directly, and set <code>gate.checkMode</code> explicitly instead of
the ambiguous <code>gate.enabled</code>. The main dimensions:
</p>
<ul>
<li>
Expand Down Expand Up @@ -531,9 +538,9 @@ testExpectations:
- "tests/**"
linkedIssuePolicy: preferred

# Gate policy (refines an enabled gate)
# Gate policy — checkMode is set explicitly (not the legacy, ambiguous "enabled" alias)
gate:
enabled: true
checkMode: visible
pack: gittensor
duplicates: block
linkedIssue: advisory
Expand Down
10 changes: 7 additions & 3 deletions config/examples/gittensory.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,13 @@ publicNotes:
# block — the finding can become a hard `Gittensory Gate` blocker
# (always confirmed-contributor-gated).
gate:
# Gate master switch. Turns the whole deterministic gate on; the per-dimension
# modes below only refine an already-enabled gate.
# Bool. Default: false (gate off).
# Legacy check-run publish switch (#5355) — despite the name, this does NOT turn the deterministic
# gate itself on or off. Gate evaluation, comments, labels, audit records, spend, and autonomous
# merge/close all run identically whether this is true, false, or unset; the per-dimension modes
# below are what actually configure gate evaluation. `enabled` is only a boolean shorthand for
# `checkMode` below: true maps to `required`, false maps to `disabled`, and it cannot express
# `checkMode`'s third state (`visible`). Prefer setting `checkMode` explicitly instead of this field.
# Bool. Default: unset (falls through to `checkMode`, then to the dashboard `reviewCheckMode` setting).
enabled: true

# Review-CHECK publish mode (#2852) — controls ONLY whether/how the "Gittensory Orb Review Agent"
Expand Down
2 changes: 1 addition & 1 deletion config/examples/gittensory.minimal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
# - "src/**"

gate:
enabled: false
checkMode: disabled

settings:
autonomy:
Expand Down
24 changes: 23 additions & 1 deletion packages/gittensory-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,18 @@ export type FocusManifestIssueDiscoveryPolicy = "encouraged" | "neutral" | "disc
*/
export type FocusManifestGateConfig = {
present: boolean;
/** `gate.enabled` (legacy): a boolean shorthand for `checkMode` below -- `true` maps to `"required"`,
* `false` maps to `"disabled"` (see resolveEffectiveSettings/applyGateConfigOverrides). Despite the name,
* this controls ONLY whether the "Gittensory Orb Review Agent" check-run publishes -- it has NO effect on
* gate evaluation, disposition, comments, labels, audit, spend, or autonomous merge/close, all of which run
* identically regardless of this field. It also cannot express `checkMode`'s third state (`"visible"`), and
* is silently ignored once `checkMode` is set (see the parse-time warning a few lines below this type).
* Caused two real incidents under this exact ambiguity (2026-07: an operator flipping `enabled` to restore
* unrelated spend/gate behavior twice got a surprise check-run side effect instead) -- prefer `checkMode`
* explicitly for any new config. */
enabled: boolean | null;
/** `gate.checkMode` (#2852): explicit required|visible|disabled review-check publish mode. Takes
* precedence over the legacy `enabled` boolean below when both are set (see resolveEffectiveSettings).
* precedence over the legacy `enabled` boolean above when both are set (see resolveEffectiveSettings).
* null (unset) ⇒ fall back to `enabled`, then to `settings.reviewCheckMode` (DB/dashboard), then default. */
checkMode: ReviewCheckMode | null;
pack: GatePolicyPack | null;
Expand Down Expand Up @@ -1365,6 +1374,19 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
if (gate.firstTimeContributorGrace === true) {
warnings.push(`Manifest field "gate.firstTimeContributorGrace" is currently reserved/inert — it does not soften a blocker outcome for first-time contributors.`);
}
// gate.enabled only controls whether the "Gittensory Orb Review Agent" check-run publishes (the legacy
// enabled -> reviewCheckMode alias in applyGateConfigOverrides) -- it does NOT gate spend, merge, comment,
// label, or close behavior, and it cannot express checkMode's "visible" state. checkMode always wins when
// both are set, so once an operator sets checkMode, a lingering enabled becomes pure inert noise they have
// no reason to notice or remove -- only warn while checkMode is still absent (a paired enabled+checkMode
// config, the documented convention, is not worth flagging). Caused two real incidents under this exact
// ambiguity (an operator flipping enabled to restore unrelated spend/gate behavior got a surprise check-run
// side effect instead, twice).
if (gate.enabled !== null && gate.checkMode === null) {
warnings.push(
`Manifest field "gate.enabled" only controls whether the Gittensory Orb Review Agent check-run publishes — it does not gate spend, merge, comment, label, or close behavior, and it cannot express reviewCheckMode's "visible" state. Set "gate.checkMode" (required/visible/disabled) explicitly instead of "gate.enabled".`,
);
}
gate.present =
gate.enabled !== null ||
gate.checkMode !== null ||
Expand Down
3 changes: 3 additions & 0 deletions src/github/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,9 @@ async function createOrUpdateNamedCheckRun(
return null;
}
};
// Upgrade-migration safety net, not dead code (see GITTENSORY_LEGACY_GATE_CHECK_NAME's doc comment):
// completes any pre-rename "Gittensory Gate" check-run still pending on this SHA once the current
// check finishes, so a self-hoster upgrading mid-flight never gets stuck with a permanently-pending status.
const finalizeLegacyPendingCheckRuns = async (): Promise<void> => {
const legacyNames = check.supersedeLegacyNames ?? [];
if (legacyNames.length === 0 || check.checkRunId) return;
Expand Down
6 changes: 6 additions & 0 deletions src/review/check-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import type { ReviewCheckMode } from "../types";

export const GITTENSORY_CONTEXT_CHECK_NAME = "Gittensory Context";
export const GITTENSORY_GATE_CHECK_NAME = "Gittensory Orb Review Agent";
/** Pre-rename check-run name ("Gittensory Gate"). NOT dead code: any self-hosted repo that still has an
* old-named check-run stuck pending from before the rename (e.g. a self-hoster who upgrades mid-flight,
* or a check-run left open across a deploy) would otherwise show a permanently-pending, never-completed
* status on GitHub. `finalizeLegacyPendingCheckRuns` in src/github/app.ts uses this name to find and
* complete (neutral, "superseded") any such stale legacy-named run once the new-named one finishes. Keep
* this until self-hosters can no longer be upgrading across the rename boundary. */
export const GITTENSORY_LEGACY_GATE_CHECK_NAME = "Gittensory Gate";

/** Single point of truth for whether `reviewCheckMode` publishes the Gittensory Orb Review Agent check-run
Expand Down
9 changes: 7 additions & 2 deletions test/unit/config-templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,14 @@ describe("config/examples review templates (#1682)", () => {
const manifest = parseFocusManifestContent(readConfigExample("gittensory.minimal.yml"), "repo_file");
expect(manifest.warnings).toEqual([]);
expect(manifest.present).toBe(true);
expect(manifest.gate.enabled).toBe(false);
// #5355: the minimal starter uses checkMode: disabled (not the ambiguous legacy enabled: false) so it
// stays warnings-clean and demonstrates the current best practice.
expect(manifest.gate.enabled).toBeNull();
expect(manifest.gate.checkMode).toBe("disabled");
expect(isAgentConfigured(manifest.settings.autonomy)).toBe(false);
const round = parseFocusManifest({ gate: gateConfigToJson(manifest.gate), settings: { autonomy: manifest.settings.autonomy } });
expect(round.warnings).toEqual([]);
expect(round.gate.enabled).toBe(false);
expect(round.gate.checkMode).toBe("disabled");
expect(isAgentConfigured(round.settings.autonomy)).toBe(false);
});

Expand Down Expand Up @@ -254,6 +257,7 @@ describe("config/examples review templates (#1682)", () => {
const yml = [
"gate:",
" enabled: true",
" checkMode: required",
" linkedIssue: block",
"review:",
" inline_comments: true",
Expand All @@ -267,6 +271,7 @@ describe("config/examples review templates (#1682)", () => {
expect(imported.warnings).toEqual([]);
expect(imported.present).toBe(true);
expect(imported.gate.enabled).toBe(true);
expect(imported.gate.checkMode).toBe("required");
expect(imported.gate.linkedIssue).toBe("block");
expect(imported.review.inlineComments).toBe(true);
expect(imported.review.excludePaths).toEqual(["dist/**"]);
Expand Down
14 changes: 14 additions & 0 deletions test/unit/focus-manifest-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,4 +528,18 @@ describe("focus-manifest loader — container-private config (self-host)", () =>
"Container-private shared base manifest (`review.shared_config`) is malformed or oversized; ignoring it and continuing (#2046).",
);
});

it("surfaces the gate.enabled/checkMode ambiguity warning from a private-config `gate:` block (#5355)", async () => {
// The real 2026-07 incident: an operator's private VPS config set gate.enabled without gate.checkMode.
// loadPublicRepoFocusManifest (the PR-comment path) never consults the local reader at all -- so this
// warning can only ever reach a human through loadRepoFocusManifest, i.e. the maintainer-gated
// /v1/repos/:owner/:repo/focus-manifest API route. Lock in that the private config's own `gate.enabled`
// reaches the loader's returned manifest.warnings, not just the public repo_file path.
const env = createTestEnv();
setLocalManifestReader(async () => "gate:\n enabled: true\n");
const manifest = await loadRepoFocusManifest(env, "owner/private");
expect(manifest.gate.enabled).toBe(true);
expect(manifest.gate.checkMode).toBeNull();
expect(manifest.warnings.some((w) => /gate\.enabled.*only controls whether the Gittensory Orb Review Agent check-run publishes/.test(w))).toBe(true);
});
});
30 changes: 28 additions & 2 deletions test/unit/focus-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,14 @@ describe("parseFocusManifestContent", () => {
const manifest = parseFocusManifestContent(content, "repo_file");
expect(manifest.warnings).toEqual([]);
expect(manifest.present).toBe(true);
expect(manifest.gate.enabled).toBe(false);
// #5355: the minimal starter uses checkMode: disabled (not the ambiguous legacy enabled: false) so it
// stays warnings-clean and demonstrates the current best practice.
expect(manifest.gate.enabled).toBeNull();
expect(manifest.gate.checkMode).toBe("disabled");
expect(isAgentConfigured(manifest.settings.autonomy)).toBe(false);
const round = parseFocusManifest({ gate: gateConfigToJson(manifest.gate), settings: { autonomy: manifest.settings.autonomy } });
expect(round.warnings).toEqual([]);
expect(round.gate.enabled).toBe(false);
expect(round.gate.checkMode).toBe("disabled");
expect(isAgentConfigured(round.settings.autonomy)).toBe(false);
});
});
Expand Down Expand Up @@ -1170,6 +1173,29 @@ describe("parseFocusManifest gate config", () => {
expect(explicitFalse.warnings.some((w) => /firstTimeContributorGrace/i.test(w))).toBe(false);
});

// #5355: gate.enabled only controls whether the check-run publishes -- it does not gate spend, merge,
// comment, label, or close behavior. Caused two real incidents under this exact ambiguity.
it("warns that gate.enabled is ambiguous when set without an explicit gate.checkMode, regardless of value (#5355)", () => {
const enabledTrue = parseFocusManifest({ gate: { enabled: true } });
expect(enabledTrue.warnings.some((w) => /gate\.enabled.*only controls whether the Gittensory Orb Review Agent check-run publishes/.test(w))).toBe(true);
// Unlike firstTimeContributorGrace, both true AND false are equally ambiguous here -- both map through
// the same silent enabled -> reviewCheckMode alias, so an explicit false is just as worth flagging.
const enabledFalse = parseFocusManifest({ gate: { enabled: false } });
expect(enabledFalse.warnings.some((w) => /gate\.enabled.*only controls whether the Gittensory Orb Review Agent check-run publishes/.test(w))).toBe(true);
});

it("does not warn about gate.enabled when checkMode is also set explicitly (the documented, encouraged pairing)", () => {
const paired = parseFocusManifest({ gate: { enabled: true, checkMode: "disabled" } });
expect(paired.warnings.some((w) => /gate\.enabled.*only controls whether/.test(w))).toBe(false);
});

it("does not warn about gate.enabled when it is unset, whether or not checkMode is set", () => {
const neitherSet = parseFocusManifest({ gate: { linkedIssue: "block" } });
expect(neitherSet.warnings.some((w) => /gate\.enabled.*only controls whether/.test(w))).toBe(false);
const onlyCheckModeSet = parseFocusManifest({ gate: { checkMode: "required" } });
expect(onlyCheckModeSet.warnings.some((w) => /gate\.enabled.*only controls whether/.test(w))).toBe(false);
});

it("parses gate.selfAuthoredLinkedIssue + settings.selfAuthoredLinkedIssueGateMode, round-trips + resolves them (the gate alias wins)", () => {
const m = parseFocusManifest({ gate: { selfAuthoredLinkedIssue: "block" }, settings: { selfAuthoredLinkedIssueGateMode: "advisory" } });
expect(m.gate.present).toBe(true);
Expand Down
3 changes: 2 additions & 1 deletion test/unit/selfhost-config-lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ maintainerNotes: [private maintainer note]
publicNotes: [keep reviews focused]
gate:
enabled: false
checkMode: disabled
settings:
commentMode: all_prs
review:
Expand Down Expand Up @@ -223,7 +224,7 @@ unknownSecretKey: super-secret-value
});

it("keeps known JSON manifests quiet and non-object JSON invalid", () => {
expect(lintManifestText(JSON.stringify({ gate: { enabled: true } }))).toMatchObject({
expect(lintManifestText(JSON.stringify({ gate: { enabled: true, checkMode: "required" } }))).toMatchObject({
ok: true,
warnings: [],
recognizedFields: ["gate"],
Expand Down
Loading