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
11 changes: 11 additions & 0 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,17 @@ gate:
# DB-backed (dashboard-settable too); this overrides the stored value.
selfAuthoredLinkedIssue: advisory

# Linked-issue satisfaction gate (#1961/#3906) — an AI assessment of whether
# this PR's diff actually satisfies its PRIMARY linked issue's intent/
# acceptance criteria (distinct from linkedIssue above, which only checks a
# link EXISTS). off = the assessment never runs; advisory = it runs and
# renders as a collapsible "Linked issue satisfaction" section in the review
# comment, but never blocks; block = a confidence-floor-passing "unaddressed"
# verdict ALSO becomes a hard blocker (linked_issue_scope_mismatch).
# off | advisory | block. Default: off. DB-backed (dashboard-settable too);
# this overrides the stored value.
linkedIssueSatisfaction: off

# Gate-check dry-run. When true, the posted check conclusion remains the real
# non-enforcing verdict while comments/check text may also show the would-be
# stricter verdict for AI-review blocker mode. It does not disable downstream
Expand Down
18 changes: 18 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9298,6 +9298,14 @@
"audit",
"enforce"
]
},
"linkedIssueSatisfactionGateMode": {
"type": "string",
"enum": [
"off",
"advisory",
"block"
]
}
},
"required": [
Expand All @@ -9318,6 +9326,7 @@
"mergeReadinessGateMode",
"manifestPolicyGateMode",
"selfAuthoredLinkedIssueGateMode",
"linkedIssueSatisfactionGateMode",
"firstTimeContributorGrace",
"slopAiAdvisory",
"aiReviewMode",
Expand Down Expand Up @@ -10026,6 +10035,14 @@
},
"publicQualityMetrics": {
"type": "boolean"
},
"linkedIssueSatisfactionGateMode": {
"type": "string",
"enum": [
"off",
"advisory",
"block"
]
}
},
"required": [
Expand All @@ -10046,6 +10063,7 @@
"mergeReadinessGateMode",
"manifestPolicyGateMode",
"selfAuthoredLinkedIssueGateMode",
"linkedIssueSatisfactionGateMode",
"firstTimeContributorGrace",
"autoLabelEnabled",
"typeLabelsEnabled",
Expand Down
8 changes: 8 additions & 0 deletions apps/gittensory-ui/src/routes/docs.github-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ GET /v1/installations/:id/repair`}
<code>selfAuthoredLinkedIssueGateMode</code> — flags or blocks a PR whose author also
opened the linked issue. Default <code>advisory</code>.
</li>
<li>
<code>linkedIssueSatisfactionGateMode</code> — an AI assessment of whether the PR's diff
actually satisfies its primary linked issue's intent, distinct from{" "}
<code>linkedIssueGateMode</code> (which only checks a link exists). Default{" "}
<code>off</code>; <code>advisory</code> renders in the review comment without blocking,{" "}
<code>block</code> additionally lets a confidence-floor-passing "unaddressed" verdict
become a blocker.
</li>
<li>
<code>moderationGateMode</code> — whether the moderation-rules engine (contributor cap,
blacklist, review-nag feeding a shared cross-repo violation tally) runs on this repo.
Expand Down
9 changes: 9 additions & 0 deletions apps/gittensory-ui/src/routes/docs.how-reviews-work.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ function HowReviewsWork() {
<code>selfAuthoredLinkedIssueGateMode</code>, default <code>advisory</code>) — flags or
blocks a PR whose author also opened the linked issue.
</li>
<li>
<strong>Linked-issue satisfaction gate</strong> (
<code>linkedIssueSatisfactionGateMode</code>, default <code>off</code>) — an AI assessment
of whether the PR's diff actually satisfies its primary linked issue's intent (distinct
from the linked-issue gate above, which only checks that a link exists).{" "}
<code>advisory</code> renders the assessment in the review comment without ever blocking;{" "}
<code>block</code> additionally lets a confidence-floor-passing "unaddressed" verdict
become a hard blocker.
</li>
<li>
<strong>Moderation-rules engine</strong> (<code>moderationGateMode</code>, default{" "}
<code>inherit</code>) — whether the contributor-cap / blacklist / review-nag mechanisms
Expand Down
8 changes: 8 additions & 0 deletions apps/gittensory-ui/src/routes/docs.tuning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,14 @@ function Tuning() {
<code>gate.selfAuthoredLinkedIssue</code> — whether a PR may link an issue opened by the
same author. Default <code>advisory</code>.
</li>
<li>
<code>gate.linkedIssueSatisfaction</code> — an AI assessment of whether the PR's diff
actually satisfies its primary linked issue's intent, distinct from{" "}
<code>gate.linkedIssue</code> (which only checks a link exists). Default <code>off</code>.{" "}
<code>advisory</code> renders the assessment in the review comment without blocking;{" "}
<code>block</code> additionally lets a confidence-floor-passing "unaddressed" verdict
become a blocker.
</li>
<li>
<code>settings.moderationGateMode</code> — whether the moderation-rules engine
(contributor cap, blacklist, review-nag feeding a shared cross-repo violation tally) runs
Expand Down
11 changes: 11 additions & 0 deletions config/examples/gittensory.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,17 @@ gate:
# DB-backed (dashboard-settable too); this overrides the stored value.
selfAuthoredLinkedIssue: advisory

# Linked-issue satisfaction gate (#1961/#3906) — an AI assessment of whether
# this PR's diff actually satisfies its PRIMARY linked issue's intent/
# acceptance criteria (distinct from linkedIssue above, which only checks a
# link EXISTS). off = the assessment never runs; advisory = it runs and
# renders as a collapsible "Linked issue satisfaction" section in the review
# comment, but never blocks; block = a confidence-floor-passing "unaddressed"
# verdict ALSO becomes a hard blocker (linked_issue_scope_mismatch).
# off | advisory | block. Default: off. DB-backed (dashboard-settable too);
# this overrides the stored value.
linkedIssueSatisfaction: off

# Gate-check dry-run. When true, the posted check conclusion remains the real
# non-enforcing verdict while comments/check text may also show the would-be
# stricter verdict for AI-review blocker mode. It does not disable downstream
Expand Down
8 changes: 8 additions & 0 deletions migrations/0123_linked_issue_satisfaction_gate_mode.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Linked-issue satisfaction gate (#1961/#3906). Off by default -- byte-identical to today for every repo
-- that doesn't opt in. When "advisory", an AI assessment of whether the PR's diff satisfies its primary
-- linked issue's intent renders in the review comment but never blocks. When "block", a confidence-floor-
-- passing "unaddressed" verdict additionally becomes a hard blocker (linked_issue_scope_mismatch), closing
-- the gap where the deterministic linked-issue check only verified existence/openness, never scope match
-- (JSONbored/metagraphed PR #3910's repro: a cited issue asked for an SSE stream, the PR delivered an
-- unrelated REST endpoint, and the structured "Linked issue" signal still read "Linked" with no blocker).
ALTER TABLE repository_settings ADD COLUMN linked_issue_satisfaction_gate_mode TEXT NOT NULL DEFAULT 'off';
20 changes: 20 additions & 0 deletions migrations/0124_linked_issue_satisfaction_cache.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- Linked-issue satisfaction assessment cache (#1961/#3906): mirrors ai_slop_cache (migration 0119) -- the
-- assessment makes a real, bounded-retry LLM call with no caching, so a repeated scheduled sweep pass would
-- re-spend it on every tick even at an unchanged head SHA. The PRIMARY KEY additionally includes
-- linked_issue_number (unlike ai_slop_cache) because a PR's cited PRIMARY linked issue can change between
-- passes (an edited body re-links a different issue) -- reusing a stored verdict for a DIFFERENT issue would
-- silently answer the wrong question.
CREATE TABLE IF NOT EXISTS linked_issue_satisfaction_cache (
repo_full_name TEXT NOT NULL,
pull_number INTEGER NOT NULL,
head_sha TEXT NOT NULL,
linked_issue_number INTEGER NOT NULL,
-- Fingerprints the one input that can change independently of the head SHA + issue number: which provider
-- produced the opinion (free/default reviewer vs. a maintainer's BYOK key/model).
input_fingerprint TEXT NOT NULL,
status TEXT NOT NULL,
result_json TEXT,
estimated_neurons INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (repo_full_name, pull_number, head_sha, linked_issue_number)
);
13 changes: 13 additions & 0 deletions packages/gittensory-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ export type FocusManifestGateConfig = {
mergeReadiness: GateRuleMode | null;
manifestPolicy: GateRuleMode | null;
selfAuthoredLinkedIssue: GateRuleMode | null;
/** `gate.linkedIssueSatisfaction` (#1961/#3906): off|advisory|block, off by default. When not off, an AI
* assessment of whether the PR's diff satisfies its primary linked issue's intent runs and renders as a
* collapsible section in the review comment; `block` additionally lets a confidence-floor-passing
* "unaddressed" verdict become a hard blocker. DB-backed (dashboard-settable too); this overrides the
* stored value -- mirrors `aiReviewMode` above, not the config-as-code-only `unlinkedIssueGuardrail`
* pattern. Distinct from the pre-existing, config-as-code-only `review.linkedIssueSatisfaction` (#2173,
* below) -- that field is parsed but not yet consumed by any decision path; this `gate:` field is the one
* the merge/close decision actually reads. */
linkedIssueSatisfaction: GateRuleMode | null;
dryRun: boolean | null;
firstTimeContributorGrace: boolean | null;
/** `gate.premergeContentRecheck` (#2550): for a PR touching `migrations/**`, re-verify against a live,
Expand Down Expand Up @@ -790,6 +799,7 @@ const EMPTY_GATE_CONFIG: FocusManifestGateConfig = {
mergeReadiness: null,
manifestPolicy: null,
selfAuthoredLinkedIssue: null,
linkedIssueSatisfaction: null,
dryRun: null,
firstTimeContributorGrace: null,
premergeContentRecheck: null,
Expand Down Expand Up @@ -1119,6 +1129,7 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
mergeReadiness: normalizeOptionalGateMode(record.mergeReadiness, "gate.mergeReadiness", warnings),
manifestPolicy: normalizeOptionalGateMode(record.manifestPolicy, "gate.manifestPolicy", warnings),
selfAuthoredLinkedIssue: normalizeOptionalGateMode(record.selfAuthoredLinkedIssue, "gate.selfAuthoredLinkedIssue", warnings),
linkedIssueSatisfaction: normalizeOptionalGateMode(record.linkedIssueSatisfaction, "gate.linkedIssueSatisfaction", warnings),
dryRun: normalizeOptionalBoolean(record.dryRun, "gate.dryRun", warnings),
firstTimeContributorGrace: normalizeOptionalBoolean(record.firstTimeContributorGrace, "gate.firstTimeContributorGrace", warnings),
premergeContentRecheck: normalizeOptionalBoolean(record.premergeContentRecheck, "gate.premergeContentRecheck", warnings),
Expand Down Expand Up @@ -1161,6 +1172,7 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
gate.mergeReadiness !== null ||
gate.manifestPolicy !== null ||
gate.selfAuthoredLinkedIssue !== null ||
gate.linkedIssueSatisfaction !== null ||
gate.dryRun !== null ||
gate.firstTimeContributorGrace !== null ||
gate.premergeContentRecheck !== null ||
Expand Down Expand Up @@ -1230,6 +1242,7 @@ export function gateConfigToJson(gate: FocusManifestGateConfig): JsonValue {
if (gate.mergeReadiness !== null) out.mergeReadiness = gate.mergeReadiness;
if (gate.manifestPolicy !== null) out.manifestPolicy = gate.manifestPolicy;
if (gate.selfAuthoredLinkedIssue !== null) out.selfAuthoredLinkedIssue = gate.selfAuthoredLinkedIssue;
if (gate.linkedIssueSatisfaction !== null) out.linkedIssueSatisfaction = gate.linkedIssueSatisfaction;
if (gate.dryRun !== null) out.dryRun = gate.dryRun;
if (gate.firstTimeContributorGrace !== null) out.firstTimeContributorGrace = gate.firstTimeContributorGrace;
if (gate.premergeContentRecheck !== null) out.premergeContentRecheck = gate.premergeContentRecheck;
Expand Down
1 change: 1 addition & 0 deletions scripts/check-docs-drift.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const GATE_MODE_MANIFEST = [
{ field: "mergeReadinessGateMode", aliases: ["mergeReadinessGateMode", "gate.mergeReadiness"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx"] },
{ field: "manifestPolicyGateMode", aliases: ["manifestPolicyGateMode", "gate.manifestPolicy"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx"] },
{ field: "selfAuthoredLinkedIssueGateMode", aliases: ["selfAuthoredLinkedIssueGateMode", "gate.selfAuthoredLinkedIssue"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx", "docs.github-app.tsx"] },
{ field: "linkedIssueSatisfactionGateMode", aliases: ["linkedIssueSatisfactionGateMode", "gate.linkedIssueSatisfaction"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx", "docs.github-app.tsx"] },
{ field: "moderationGateMode", aliases: ["moderationGateMode", "settings.moderationGateMode"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx", "docs.github-app.tsx"] },
];

Expand Down
1 change: 1 addition & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,7 @@ const maintainerSettingsSchema = z
mergeReadinessGateMode: z.enum(["off", "advisory", "block"]),
manifestPolicyGateMode: z.enum(["off", "advisory", "block"]),
selfAuthoredLinkedIssueGateMode: z.enum(["off", "advisory", "block"]),
linkedIssueSatisfactionGateMode: z.enum(["off", "advisory", "block"]),
firstTimeContributorGrace: z.boolean(),
slopGateMode: z.enum(["off", "advisory", "block"]),
slopGateMinScore: z.number().int().min(0).max(100).nullable(),
Expand Down
Loading
Loading