Skip to content

Commit c606d65

Browse files
committed
feat(gate): guardrailEscalation.onCleanReview — release the guardrail hold when the escalated review is clean
The second half of #9808, as an explicit MODE rather than a policy flip. #9821 shipped the escalation knobs, so a guarded PR now gets a high-effort review -- but the disposition still held it unconditionally: guardrailHit sat in heldForManualReview regardless of what the escalated review found. The 74 held PRs / 14 days kept landing on the maintainer, just with better reviews attached. gate.guardrailEscalation.onCleanReview: hold (default) today's behavior exactly -- human-in-the-loop proceed a clean escalated review releases the guardrail hold and the normal approve/merge path continues -- full-autonomy mode Because it is a manifest field it layers global -> per-repo like everything else, so repos can be flipped one at a time and the mode is reversible without a deploy. Fail-closed on every axis: - `proceed` is inert unless at least one escalation knob is actually SET: the release is justified by extra scrutiny, so absent scrutiny nothing vouches. - the release requires reviewGood -- gate success (which folds in the AI verdict's blockers) AND green CI -- and the disposition re-checks that independently of the caller's flag. - only the guardrail term is released: migration collisions, unlinked-issue holds, advisory-check holds, and unstable merge states all still hold. - the manual-hold reason no longer claims "guarded path -> manual review" for a cleared PR.
1 parent e3247ed commit c606d65

16 files changed

Lines changed: 156 additions & 5 deletions

File tree

.loopover.yml.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,14 @@ gate:
289289
selfConsistencyRuns: 3
290290
# provider: anthropic
291291
# model: claude-opus-5
292+
# What a CLEAN escalated review buys (#9808). `hold` (default): the PR still waits for a human even when
293+
# the escalated review found nothing — human-in-the-loop mode. `proceed`: a clean escalated review
294+
# (gate success + green CI) RELEASES the guardrail hold and the normal approve/merge path continues —
295+
# full-autonomy mode, where the guarded path is protected by the escalated review instead of a queue.
296+
# Fail-closed: `proceed` is inert unless at least one escalation knob above is actually set, so the hold
297+
# can never be released without the extra scrutiny that justifies releasing it. Layered like every other
298+
# manifest field (global -> per-repo), so repos can be flipped to full-auto one at a time.
299+
# onCleanReview: proceed
292300

293301
advisoryCheckRuns:
294302
- name: Contributor trust

apps/loopover-ui/public/openapi.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10191,6 +10191,15 @@
1019110191
"guardrailEscalationSelfConsistencyRuns": {
1019210192
"type": "number",
1019310193
"nullable": true
10194+
},
10195+
"guardrailEscalationOnCleanReview": {
10196+
"type": "string",
10197+
"nullable": true,
10198+
"enum": [
10199+
"hold",
10200+
"proceed",
10201+
null
10202+
]
1019410203
}
1019510204
},
1019610205
"required": [

config/examples/loopover.full.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,14 @@ gate:
303303
selfConsistencyRuns: 3
304304
# provider: anthropic
305305
# model: claude-opus-5
306+
# What a CLEAN escalated review buys (#9808). `hold` (default): the PR still waits for a human even when
307+
# the escalated review found nothing — human-in-the-loop mode. `proceed`: a clean escalated review
308+
# (gate success + green CI) RELEASES the guardrail hold and the normal approve/merge path continues —
309+
# full-autonomy mode, where the guarded path is protected by the escalated review instead of a queue.
310+
# Fail-closed: `proceed` is inert unless at least one escalation knob above is actually set, so the hold
311+
# can never be released without the extra scrutiny that justifies releasing it. Layered like every other
312+
# manifest field (global -> per-repo), so repos can be flipped to full-auto one at a time.
313+
# onCleanReview: proceed
306314

307315
advisoryCheckRuns:
308316
- name: Contributor trust

packages/loopover-contract/src/api-schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ export const RepositorySettingsSchema = z
459459
guardrailEscalationModel: z.string().nullable().optional(),
460460
guardrailEscalationEffort: z.enum(["low", "medium", "high", "xhigh", "max"]).nullable().optional(),
461461
guardrailEscalationSelfConsistencyRuns: z.number().nullable().optional(),
462+
guardrailEscalationOnCleanReview: z.enum(["hold", "proceed"]).nullable().optional(),
462463
copycatGateMode: z.enum(["off", "warn", "label", "block"]).optional(),
463464
copycatGateMinScore: z.number().nullable().optional(),
464465
gateDryRun: z.boolean().optional(),

packages/loopover-engine/src/focus-manifest.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ export type FocusManifestGateConfig = {
130130
guardrailEscalationModel: string | null;
131131
guardrailEscalationEffort: "low" | "medium" | "high" | "xhigh" | "max" | null;
132132
guardrailEscalationSelfConsistencyRuns: number | null;
133+
/** `gate.guardrailEscalation.onCleanReview` (#9808 second half): what a CLEAN escalated review buys.
134+
* `hold` (default) keeps today's behavior -- the PR still waits for a human even when the escalated
135+
* review found nothing. `proceed` releases the guardrail hold when the gate passed and CI is green, so a
136+
* guarded path is protected by the ESCALATED REVIEW rather than by a human queue -- the full-autonomy
137+
* mode. Fail-closed: `proceed` does nothing unless at least one escalation knob is actually set, so the
138+
* hold can never be released without the extra scrutiny that justifies releasing it. */
139+
guardrailEscalationOnCleanReview: "hold" | "proceed" | null;
133140
aiReviewAllAuthors: boolean | null;
134141
/** `gate.aiReview.closeConfidence` (#7): minimum calibrated AI-reviewer confidence (0-1) for an AI defect to BLOCK
135142
* under `aiReview.mode: block`. null (unset) ⇒ the gate's 0.93 default. Clamped to [0,1] at parse time. */
@@ -653,6 +660,7 @@ export type FocusManifestSettings = Partial<
653660
| "guardrailEscalationModel"
654661
| "guardrailEscalationEffort"
655662
| "guardrailEscalationSelfConsistencyRuns"
663+
| "guardrailEscalationOnCleanReview"
656664
| "aiReviewAllAuthors"
657665
| "aiReviewConfirmedContributorsOnly"
658666
| "closeOwnerAuthors"
@@ -1370,6 +1378,7 @@ const EMPTY_GATE_CONFIG: FocusManifestGateConfig = {
13701378
guardrailEscalationModel: null,
13711379
guardrailEscalationEffort: null,
13721380
guardrailEscalationSelfConsistencyRuns: null,
1381+
guardrailEscalationOnCleanReview: null,
13731382
aiReviewAllAuthors: null,
13741383
aiReviewCloseConfidence: null,
13751384
aiReviewSalvageabilityMinScore: null,
@@ -1920,6 +1929,7 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
19201929
guardrailEscalationModel: normalizeOptionalString(escalationRecord?.model, "gate.guardrailEscalation.model", warnings),
19211930
guardrailEscalationEffort: normalizeOptionalEnum(escalationRecord?.effort, "gate.guardrailEscalation.effort", ["low", "medium", "high", "xhigh", "max"] as const, warnings),
19221931
guardrailEscalationSelfConsistencyRuns: normalizeOptionalNonNegativeInt(escalationRecord?.selfConsistencyRuns, "gate.guardrailEscalation.selfConsistencyRuns", warnings),
1932+
guardrailEscalationOnCleanReview: normalizeOptionalEnum(escalationRecord?.onCleanReview, "gate.guardrailEscalation.onCleanReview", ["hold", "proceed"] as const, warnings),
19231933
aiReviewAllAuthors: normalizeOptionalBoolean(aiReviewRecord?.allAuthors, "gate.aiReview.allAuthors", warnings),
19241934
aiReviewCloseConfidence: normalizeOptionalConfidence(aiReviewRecord?.closeConfidence, "gate.aiReview.closeConfidence", warnings),
19251935
aiReviewSalvageabilityMinScore: normalizeOptionalScore(aiReviewRecord?.salvageabilityMinScore, "gate.aiReview.salvageabilityMinScore", warnings),
@@ -2017,6 +2027,7 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
20172027
gate.guardrailEscalationModel !== null ||
20182028
gate.guardrailEscalationEffort !== null ||
20192029
gate.guardrailEscalationSelfConsistencyRuns !== null ||
2030+
gate.guardrailEscalationOnCleanReview !== null ||
20202031
gate.ignoredCheckRuns !== null ||
20212032
gate.aiJudgmentBlockersMode !== null ||
20222033
gate.copycatMode !== null ||
@@ -2105,13 +2116,15 @@ export function gateConfigToJson(gate: FocusManifestGateConfig): JsonValue {
21052116
gate.guardrailEscalationProvider !== null ||
21062117
gate.guardrailEscalationModel !== null ||
21072118
gate.guardrailEscalationEffort !== null ||
2108-
gate.guardrailEscalationSelfConsistencyRuns !== null
2119+
gate.guardrailEscalationSelfConsistencyRuns !== null ||
2120+
gate.guardrailEscalationOnCleanReview !== null
21092121
) {
21102122
const escalation: Record<string, JsonValue> = {};
21112123
if (gate.guardrailEscalationProvider !== null) escalation.provider = gate.guardrailEscalationProvider;
21122124
if (gate.guardrailEscalationModel !== null) escalation.model = gate.guardrailEscalationModel;
21132125
if (gate.guardrailEscalationEffort !== null) escalation.effort = gate.guardrailEscalationEffort;
21142126
if (gate.guardrailEscalationSelfConsistencyRuns !== null) escalation.selfConsistencyRuns = gate.guardrailEscalationSelfConsistencyRuns;
2127+
if (gate.guardrailEscalationOnCleanReview !== null) escalation.onCleanReview = gate.guardrailEscalationOnCleanReview;
21152128
out.guardrailEscalation = escalation;
21162129
}
21172130
if (gate.mergeReadiness !== null) out.mergeReadiness = gate.mergeReadiness;

packages/loopover-engine/src/types/manifest-deps-types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,10 @@ export type RepositorySettings = {
309309
guardrailEscalationModel?: string | null | undefined;
310310
guardrailEscalationEffort?: "low" | "medium" | "high" | "xhigh" | "max" | null | undefined;
311311
guardrailEscalationSelfConsistencyRuns?: number | null | undefined;
312+
/** `gate.guardrailEscalation.onCleanReview` (#9808 second half): `proceed` releases the guardrail hold
313+
* when the escalated review came back clean (gate success + CI green); `hold` (default) keeps a human in
314+
* the loop even then. Fail-closed: `proceed` is inert unless an escalation knob is actually set. */
315+
guardrailEscalationOnCleanReview?: "hold" | "proceed" | null | undefined;
312316
/** Review EVERY PR's author, not only confirmed Gittensor contributors. Only meaningful when
313317
* {@link aiReviewConfirmedContributorsOnly} is also `true` (that field opts INTO confirmed-only
314318
* scoping in the first place — see its own doc comment for the full invariant: AI review runs for

packages/loopover-engine/test/focus-manifest-review-knobs.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,18 @@ test("a non-mapping guardrailEscalation is ignored wholesale, and absence leaves
8888
const json = gateConfigToJson(absent.gate) as Record<string, unknown>;
8989
assert.equal(json.guardrailEscalation, undefined);
9090
});
91+
92+
test("onCleanReview parses, round-trips, makes the gate present alone, and rejects junk (#9808)", () => {
93+
const parsed = parseFocusManifest({ gate: { guardrailEscalation: { onCleanReview: "proceed" } } });
94+
assert.equal(parsed.gate.present, true);
95+
assert.equal(parsed.gate.guardrailEscalationOnCleanReview, "proceed");
96+
assert.deepEqual(parseFocusManifest({ gate: gateConfigToJson(parsed.gate) }).gate, parsed.gate);
97+
98+
const hold = parseFocusManifest({ gate: { guardrailEscalation: { onCleanReview: "hold", effort: "high" } } });
99+
assert.equal(hold.gate.guardrailEscalationOnCleanReview, "hold");
100+
assert.deepEqual(parseFocusManifest({ gate: gateConfigToJson(hold.gate) }).gate, hold.gate);
101+
102+
const junk = parseFocusManifest({ gate: { guardrailEscalation: { onCleanReview: "yolo" } } });
103+
assert.equal(junk.gate.guardrailEscalationOnCleanReview, null);
104+
assert.ok(junk.warnings.some((w) => /guardrailEscalation\.onCleanReview/.test(w)));
105+
});

scripts/check-docs-drift.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export const SETTINGS_ALIAS_MANIFEST: AliasManifestRow[] = [
182182
{ field: "guardrailEscalationModel", aliases: ["guardrailEscalation:"] },
183183
{ field: "guardrailEscalationEffort", aliases: ["guardrailEscalation:"] },
184184
{ field: "guardrailEscalationSelfConsistencyRuns", aliases: ["guardrailEscalation:"] },
185+
{ field: "guardrailEscalationOnCleanReview", aliases: ["guardrailEscalation:"] },
185186
{ field: "aiReviewAllAuthors", aliases: ["allAuthors"] },
186187
{ field: "aiReviewCloseConfidence", aliases: ["closeConfidence"] },
187188
{ field: "aiReviewSalvageabilityMinScore", aliases: ["salvageabilityMinScore"] },

src/openapi/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,7 @@ export const RepositorySettingsSchema = z
754754
guardrailEscalationModel: z.string().nullable().optional(),
755755
guardrailEscalationEffort: z.enum(["low", "medium", "high", "xhigh", "max"]).nullable().optional(),
756756
guardrailEscalationSelfConsistencyRuns: z.number().nullable().optional(),
757+
guardrailEscalationOnCleanReview: z.enum(["hold", "proceed"]).nullable().optional(),
757758
copycatGateMode: z.enum(["off", "warn", "label", "block"]).optional(),
758759
copycatGateMinScore: z.number().nullable().optional(),
759760
gateDryRun: z.boolean().optional(),

src/queue/processors.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2865,6 +2865,13 @@ function buildAgentMaintenancePlanInput(args: {
28652865
slopGateMinScore: settings.slopGateMinScore,
28662866
changedPaths,
28672867
hardGuardrailGlobs,
2868+
// #9808 second half: the escalation settings ride with the globs they modify, so the planner can release
2869+
// the guardrail hold when a clean escalated review has vouched for the guarded path.
2870+
guardrailEscalationOnCleanReview: settings.guardrailEscalationOnCleanReview ?? null,
2871+
guardrailEscalationEffort: settings.guardrailEscalationEffort ?? null,
2872+
guardrailEscalationSelfConsistencyRuns: settings.guardrailEscalationSelfConsistencyRuns ?? null,
2873+
guardrailEscalationModel: settings.guardrailEscalationModel ?? null,
2874+
guardrailEscalationProvider: settings.guardrailEscalationProvider ?? null,
28682875
manualReviewLabel: settings.manualReviewLabel,
28692876
readyToMergeLabel: settings.readyToMergeLabel,
28702877
changesRequestedLabel: settings.changesRequestedLabel,

0 commit comments

Comments
 (0)