From ddb92b50d58b7ca8092b12083a2687d967b96625 Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Fri, 10 Jul 2026 02:24:07 -0700
Subject: [PATCH 1/3] feat(review): one-shot AI review cadence, configurable
globally + per repo
AI-generated review content (main review, slop advisory, linked-issue
satisfaction) now freezes after its first pass by default -- no further
automatic push/CI-completion/sweep trigger spends a fresh AI call, only
an explicit maintainer retrigger does. Configurable via the new
GITTENSORY_REVIEW_CONTINUOUS fleet-wide env default and the per-repo
review.auto_review.cadence .gittensory.yml override (either direction),
so self-hosters who want the traditional re-review-on-push behavior can
opt back in. The deterministic gate is unaffected and always re-evaluates.
Fixes #4636
---
.../src/routes/docs.privacy-security.tsx | 1 +
apps/gittensory-ui/src/routes/docs.tuning.tsx | 12 +
.../gittensory-engine/src/focus-manifest.ts | 20 ++
src/db/repositories.ts | 32 +++
src/env.d.ts | 9 +
src/queue/processors.ts | 137 +++++++--
src/signals/focus-manifest.ts | 2 +
test/unit/ai-slop-cache.test.ts | 29 +-
test/unit/auto-review-config-matrix.test.ts | 9 +
test/unit/focus-manifest.test.ts | 11 +-
test/unit/gate-check-policy.test.ts | 20 +-
.../linked-issue-satisfaction-cache.test.ts | 25 +-
test/unit/queue.test.ts | 264 +++++++++++++++++-
test/unit/signals-coverage.test.ts | 2 +-
worker-configuration.d.ts | 4 +-
wrangler.jsonc | 7 +
16 files changed, 551 insertions(+), 33 deletions(-)
diff --git a/apps/gittensory-ui/src/routes/docs.privacy-security.tsx b/apps/gittensory-ui/src/routes/docs.privacy-security.tsx
index 780fa86183..f653ed75ff 100644
--- a/apps/gittensory-ui/src/routes/docs.privacy-security.tsx
+++ b/apps/gittensory-ui/src/routes/docs.privacy-security.tsx
@@ -98,6 +98,7 @@ GITTENSORY_REVIEW_SCREENSHOTS="true" # before/after visual capture f
GITTENSORY_REVIEW_E2E_TESTS="true" # AI-generated E2E test coverage (needs features.e2eTests too)
# Global (cron / endpoint) flags, not scoped by GITTENSORY_REVIEW_REPOS.
+GITTENSORY_REVIEW_CONTINUOUS="true" # fleet-wide default: re-review on every push (else one-shot)
GITTENSORY_REVIEW_OPS="true" # read-only anomaly scan + outcome stats endpoint
GITTENSORY_REVIEW_SELFTUNE="true" # self-tightening tuning loop, never loosens
GITTENSORY_REVIEW_PARITY_AUDIT="true" # shadow-record gate-decision parity readiness
diff --git a/apps/gittensory-ui/src/routes/docs.tuning.tsx b/apps/gittensory-ui/src/routes/docs.tuning.tsx
index f24df09925..7452eec78f 100644
--- a/apps/gittensory-ui/src/routes/docs.tuning.tsx
+++ b/apps/gittensory-ui/src/routes/docs.tuning.tsx
@@ -138,6 +138,18 @@ function Tuning() {
needs its own features.e2eTests: true override in{" "}
.gittensory.yml before the feature is active for it. Per-PR.
+
+ GITTENSORY_REVIEW_CONTINUOUS — fleet-wide default AI review re-trigger
+ cadence. Off by default (one-shot): AI-generated content (main review, slop advisory,
+ linked-issue satisfaction) is produced once per PR and never regenerated automatically
+ afterward — only an explicit maintainer retrigger (the PR-panel checkbox, or{" "}
+ @gittensory review as a maintainer) spends a fresh call. Truthy switches the
+ fleet default to continuous — every push/CI-completion/sweep re-runs AI content
+ generation. A repo's own review.auto_review.cadence in{" "}
+ .gittensory.yml always overrides this default, in either direction. Never
+ affects the deterministic gate (CI status, mergeability, static-rule blockers), which
+ always re-evaluates regardless.
+
GITTENSORY_REVIEW_RAG — retrieval-augmented context: queries the codebase
vector index for related code and docs (callers, related modules, existing conventions)
diff --git a/packages/gittensory-engine/src/focus-manifest.ts b/packages/gittensory-engine/src/focus-manifest.ts
index 0651c06e87..ab3f264e61 100644
--- a/packages/gittensory-engine/src/focus-manifest.ts
+++ b/packages/gittensory-engine/src/focus-manifest.ts
@@ -644,10 +644,25 @@ export type LabelingRule = {
descriptionContains: string | null;
};
+/** `review.auto_review.cadence` (#one-shot-review-cadence). `one_shot` = the AI-generated content (main review,
+ * slop advisory, linked-issue satisfaction) is produced once per PR and never automatically regenerated
+ * afterward — not on a new push, not on CI-check completion, not on a scheduled sweep tick; only an explicit
+ * maintainer retrigger (the PR-panel checkbox or `@gittensory review` as a maintainer) spends a fresh call.
+ * `continuous` = the traditional behavior — every trigger re-runs AI content generation, subject to each
+ * feature's own head-SHA cache. Orthogonal to `aiReviewMode`'s enforcement-strictness axis (off/advisory/
+ * block) — the deterministic gate (CI status, mergeability, static-rule blockers) is NEVER affected by this
+ * and always re-evaluates on every pass regardless of cadence. */
+export const AI_REVIEW_CADENCES = ["one_shot", "continuous"] as const;
+export type AiReviewCadence = (typeof AI_REVIEW_CADENCES)[number];
+
/** Per-repo AI review eligibility knobs under `review.auto_review`. Unset fields are byte-identical defaults. */
export type AutoReviewConfig = {
/** `review.auto_review.skip_drafts`: when true, draft PRs skip AI review. null (default) ⇒ drafts reviewed as today. (#2038) */
skipDrafts: boolean | null;
+ /** `review.auto_review.cadence`: per-repo override of the AI review re-trigger cadence. null (default) ⇒
+ * inherit the operator's fleet-wide GITTENSORY_REVIEW_CONTINUOUS default (itself "one_shot" when unset).
+ * (#one-shot-review-cadence) */
+ cadence: AiReviewCadence | null;
/** `review.auto_review.ignore_authors`: author-login globs whose PRs skip AI review. Empty ⇒ every author. (#2039) */
ignoreAuthors: string[];
/** `review.auto_review.ignore_title_keywords`: case-insensitive title substrings that skip AI review. Empty ⇒ no skip. (#2040) */
@@ -677,6 +692,7 @@ export const EMPTY_MAX_FINDINGS_CONFIG: MaxFindingsConfig = { blockers: null, ni
export const EMPTY_AUTO_REVIEW_CONFIG: AutoReviewConfig = {
skipDrafts: null,
+ cadence: null,
ignoreAuthors: [],
ignoreTitleKeywords: [],
skipLabels: [],
@@ -2299,6 +2315,7 @@ function overlayMaxFindingsConfig(base: MaxFindingsConfig, override: MaxFindings
function overlayAutoReviewConfig(base: AutoReviewConfig, override: AutoReviewConfig): AutoReviewConfig {
return {
skipDrafts: pickOverlayNullable(override.skipDrafts, base.skipDrafts),
+ cadence: pickOverlayNullable(override.cadence, base.cadence),
ignoreAuthors: pickOverlayStringList(override.ignoreAuthors, base.ignoreAuthors),
ignoreTitleKeywords: pickOverlayStringList(override.ignoreTitleKeywords, base.ignoreTitleKeywords),
skipLabels: pickOverlayStringList(override.skipLabels, base.skipLabels),
@@ -2493,6 +2510,7 @@ function parseReviewLabelingRules(value: JsonValue | undefined, warnings: string
function autoReviewPresent(config: AutoReviewConfig): boolean {
return (
config.skipDrafts !== null ||
+ config.cadence !== null ||
config.ignoreAuthors.length > 0 ||
config.ignoreTitleKeywords.length > 0 ||
config.skipLabels.length > 0 ||
@@ -2514,6 +2532,7 @@ function parseAutoReviewConfig(value: JsonValue | undefined, warnings: string[])
const record = value as Record;
return {
skipDrafts: normalizeOptionalBoolean(record.skip_drafts, "review.auto_review.skip_drafts", warnings),
+ cadence: normalizeOptionalEnum(record.cadence, "review.auto_review.cadence", AI_REVIEW_CADENCES, warnings),
ignoreAuthors: parseManifestGlobList(record.ignore_authors, "review.auto_review.ignore_authors", warnings),
ignoreTitleKeywords: parseAutoReviewTitleKeywords(record.ignore_title_keywords, warnings),
skipLabels: parseAutoReviewSkipLabels(record.skip_labels, warnings),
@@ -2937,6 +2956,7 @@ export function reviewConfigToJson(review: FocusManifestReviewConfig): JsonValue
if (autoReviewPresent(review.autoReview)) {
const autoReview: Record = {};
if (review.autoReview.skipDrafts !== null) autoReview.skip_drafts = review.autoReview.skipDrafts;
+ if (review.autoReview.cadence !== null) autoReview.cadence = review.autoReview.cadence;
if (review.autoReview.ignoreAuthors.length > 0) autoReview.ignore_authors = [...review.autoReview.ignoreAuthors];
if (review.autoReview.ignoreTitleKeywords.length > 0) autoReview.ignore_title_keywords = [...review.autoReview.ignoreTitleKeywords];
if (review.autoReview.skipLabels.length > 0) autoReview.skip_labels = [...review.autoReview.skipLabels];
diff --git a/src/db/repositories.ts b/src/db/repositories.ts
index 2d3491c7c1..842b94203b 100644
--- a/src/db/repositories.ts
+++ b/src/db/repositories.ts
@@ -4759,6 +4759,19 @@ export async function putCachedAiSlopAdvisory(
.run();
}
+/** #one-shot-review-cadence: does at least one slop-advisory row exist for this PR, regardless of head SHA?
+ * Consulted ONLY when the resolved AI review cadence is "one_shot" — an existing row means this PR already
+ * had its one-shot slop pass, so a later automatic trigger (push/CI-completion/sweep) must not spend another
+ * LLM call. Existence-only (no reuse-for-display): mirrors the pre-existing commitThresholdReached silent-skip
+ * precedent for this same feature, which also does not resurface a prior finding once paused. */
+export async function hasPublishedAiSlopAdvisory(env: Env, repoFullName: string, pullNumber: number): Promise {
+ const row = await env.DB
+ .prepare("SELECT 1 AS present FROM ai_slop_cache WHERE repo_full_name = ? AND pull_number = ? LIMIT 1")
+ .bind(repoFullName, pullNumber)
+ .first<{ present: number }>();
+ return Boolean(row);
+}
+
/** #linked-issue-satisfaction-cache: the stored linked-issue satisfaction result for (repo, pull, head SHA,
* linked issue number), or null on a miss. Mirrors getCachedAiSlopAdvisory -- every stored row is
* unconditionally durable (no cacheable/allowNonCacheable/maxAgeMs dimension). A nullish head SHA is always a
@@ -4814,6 +4827,25 @@ export async function putCachedLinkedIssueSatisfaction(
.run();
}
+/** #one-shot-review-cadence: does a linked-issue satisfaction row exist for this PR + linked issue number,
+ * regardless of head SHA? Consulted ONLY when the resolved AI review cadence is "one_shot", mirroring
+ * hasPublishedAiSlopAdvisory. Scoped ADDITIONALLY to linkedIssueNumber (not just the PR) — a PR's primary
+ * linked issue can change between passes (see linked_issue_satisfaction_cache's own doc comment), and a
+ * newly-linked issue has never been assessed, so it must still get its own first pass under one-shot mode
+ * rather than being silently blocked by an unrelated issue's prior assessment. */
+export async function hasPublishedLinkedIssueSatisfaction(
+ env: Env,
+ repoFullName: string,
+ pullNumber: number,
+ linkedIssueNumber: number,
+): Promise {
+ const row = await env.DB
+ .prepare("SELECT 1 AS present FROM linked_issue_satisfaction_cache WHERE repo_full_name = ? AND pull_number = ? AND linked_issue_number = ? LIMIT 1")
+ .bind(repoFullName, pullNumber, linkedIssueNumber)
+ .first<{ present: number }>();
+ return Boolean(row);
+}
+
/** #4499 (grounding-file-content-cache): the stored file content for (repo, path, head SHA), or null on a
* miss. Unlike linked_issue_satisfaction_cache, every stored row is durable with NO input-fingerprint
* dimension -- file content at an immutable head SHA has exactly one correct value, so a hit is always safe
diff --git a/src/env.d.ts b/src/env.d.ts
index fb965729e5..b3344af813 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -259,6 +259,15 @@ declare global {
* E2E test coverage feature. Default OFF — unset/false the feature is never active for any repo regardless
* of a per-repo `features.e2eTests` override. */
GITTENSORY_REVIEW_E2E_TESTS?: string;
+ /** #one-shot-review-cadence: the operator's FLEET-WIDE default for AI review re-trigger cadence, consulted
+ * only when a repo's `.gittensory.yml review.auto_review.cadence` is unset (a per-repo value always wins
+ * regardless of this flag — see resolveAiReviewCadence). Default OFF (unset/false) ⇒ "one_shot": the
+ * AI-generated content (main review, slop advisory, linked-issue satisfaction) freezes after its first
+ * pass for every repo, and only an explicit maintainer retrigger spends a fresh call. Truthy ⇒
+ * "continuous": the traditional behavior where every push/CI-completion/sweep trigger re-runs AI content
+ * generation, for operators who prefer that over one-shot. Never affects the deterministic gate (CI
+ * status, mergeability, static-rule blockers), which always re-evaluates regardless of this flag. */
+ GITTENSORY_REVIEW_CONTINUOUS?: string;
/** Convergence (reputation): when truthy, the INTERNAL-only ported submitter-reputation signal extends the
* AI-spend gate — a new / burst / low-reputation submitter is downgraded to a deterministic-only review
* (the AI neurons are skipped), and the per-(project, submitter) outcome is recorded after the gate
diff --git a/src/queue/processors.ts b/src/queue/processors.ts
index e4e6cff6ac..32ad3aba8c 100644
--- a/src/queue/processors.ts
+++ b/src/queue/processors.ts
@@ -52,8 +52,10 @@ import {
markAiReviewPublished,
getCachedAiSlopAdvisory,
putCachedAiSlopAdvisory,
+ hasPublishedAiSlopAdvisory,
getCachedLinkedIssueSatisfaction,
putCachedLinkedIssueSatisfaction,
+ hasPublishedLinkedIssueSatisfaction,
markPullRequestsRegated,
markPullRequestsBacklogConvergenceRegated,
markPullRequestReviewsInvalidated,
@@ -430,6 +432,7 @@ import {
resolveReviewPromptOverrides,
resolveReviewMemoryManifestToggle,
resolveReviewVisualConfig,
+ type AiReviewCadence,
type FocusManifestFinding,
type FocusManifest,
type ReviewPathInstruction,
@@ -6819,6 +6822,19 @@ export function shouldRunSlopAiAdvisory(
return settings.slopAiAdvisory && settings.slopGateMode !== "off";
}
+/** #one-shot-review-cadence: resolve the effective AI review re-trigger cadence. The per-repo
+ * `review.auto_review.cadence` manifest field (`configuredCadence`, already resolved by
+ * resolveReviewAutoReviewConfig) always wins when set; otherwise falls back to the operator's fleet-wide
+ * GITTENSORY_REVIEW_CONTINUOUS default. Both unset ⇒ "one_shot" — see AutoReviewConfig["cadence"]'s own doc
+ * comment for the full semantics. */
+export function resolveAiReviewCadence(
+ env: { GITTENSORY_REVIEW_CONTINUOUS?: string | undefined },
+ configuredCadence: AiReviewCadence | null,
+): AiReviewCadence {
+ if (configuredCadence !== null) return configuredCadence;
+ return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_CONTINUOUS ?? "") ? "continuous" : "one_shot";
+}
+
function shouldProcessPullRequestPublicSurface(
eventName: string,
action: string | undefined,
@@ -8877,6 +8893,13 @@ async function maybePublishPrPublicSurface(
return undefined;
const reviewManifest = await loadRepoFocusManifest(env, repoFullName).catch(() => null);
const autoReviewConfig = resolveReviewAutoReviewConfig(reviewManifest);
+ // #one-shot-review-cadence: resolved once, up front, so all three AI dispatch sites below (slop,
+ // linked-issue satisfaction, main review) see the same answer. An explicit maintainer retrigger
+ // (forceAiReview, set by the PR-panel checkbox or a maintainer's `@gittensory review`) always bypasses
+ // one-shot mode regardless of cadence -- that is the whole point of "one-shot until you ask again."
+ const oneShotCadenceActive =
+ resolveAiReviewCadence(env, autoReviewConfig.cadence) === "one_shot" &&
+ webhook.forceAiReview !== true;
const reviewEligibility = decideReviewEligibility({
authorLogin: author,
ignoreAuthors: autoReviewConfig.ignoreAuthors,
@@ -9560,42 +9583,81 @@ async function maybePublishPrPublicSurface(
// AI-assisted slop advisory (#533, opt-in). Reuses the already-fetched files; appends at most one
// advisory-only finding. Deliberately does NOT update slopRisk — only the deterministic core blocks.
if (shouldRunSlopAiAdvisory(settings)) {
- // #ai-slop-repeat-spend: same commit-threshold cap ai_review already applies (auto_pause_after_reviewed_commits)
- // — a PR the sweep keeps re-visiting stops getting a fresh slop advisory once it's been reviewed enough
- // times, instead of re-attempting (and re-touching the shared neuron/provider budget) on every single pass.
- const slopReviewedCommitCount = await countPublishedAiReviewHeads(env, repoFullName, pr.number).catch(() => 0);
- await runAiSlopForAdvisory(env, {
+ // #one-shot-review-cadence: a repeat automatic trigger (push/CI-completion/sweep) under one-shot mode
+ // must not spend another slop LLM call once this PR has already had ITS one-shot slop pass -- silent
+ // skip (mirrors commitThresholdReached just below: no reuse-for-display, matching the pre-existing
+ // precedent for this same feature). An explicit maintainer retrigger already unset oneShotCadenceActive
+ // above, so it always reaches the fresh call regardless of prior passes.
+ const slopOneShotSkip =
+ oneShotCadenceActive &&
+ (await hasPublishedAiSlopAdvisory(env, repoFullName, pr.number).catch(() => false));
+ if (slopOneShotSkip) {
+ await recordAuditEvent(env, {
+ eventType: "github_app.ai_slop_one_shot_skip",
+ actor: author,
+ targetKey: `${repoFullName}#${pr.number}`,
+ outcome: "completed",
+ detail: "one-shot review cadence: this PR already had its slop advisory pass; not spending a fresh call",
+ metadata: { repoFullName, headSha: advisory.headSha ?? null },
+ }).catch(() => undefined);
+ } else {
+ // #ai-slop-repeat-spend: same commit-threshold cap ai_review already applies (auto_pause_after_reviewed_commits)
+ // — a PR the sweep keeps re-visiting stops getting a fresh slop advisory once it's been reviewed enough
+ // times, instead of re-attempting (and re-touching the shared neuron/provider budget) on every single pass.
+ const slopReviewedCommitCount = await countPublishedAiReviewHeads(env, repoFullName, pr.number).catch(() => 0);
+ await runAiSlopForAdvisory(env, {
+ mode,
+ settings,
+ advisory,
+ repoFullName,
+ pr,
+ author,
+ files: slopFiles,
+ deterministicBand: slop.band,
+ confirmedContributor,
+ commitThresholdReached: isAutoReviewCommitThresholdReached(autoReviewConfig, slopReviewedCommitCount),
+ });
+ }
+ }
+ }
+ // Linked-issue satisfaction assessment (#1961/#3906, opt-in via linkedIssueSatisfactionGateMode). Assesses
+ // only the PR's primary linked issue -- see runLinkedIssueSatisfactionForAdvisory's own doc comment for
+ // the multi-linked-issue rationale. `off` (default) short-circuits before any fetch or model call, so this
+ // is byte-identical to before this feature existed for every repo that hasn't opted in. (Declared/hoisted
+ // to function scope above, alongside gateEvaluation, since it is consumed later outside this try block.)
+ if (settings.linkedIssueSatisfactionGateMode !== "off" && pr.linkedIssues.length > 0) {
+ // #one-shot-review-cadence: mirrors the slop advisory's skip above, scoped to the PR's PRIMARY linked
+ // issue (matching runLinkedIssueSatisfactionForAdvisory's own "assesses only the first linked issue"
+ // contract) -- a newly-linked issue never assessed before still gets its own first pass even when the
+ // PR itself already had a satisfaction pass for a DIFFERENT (now-superseded) linked issue.
+ const primaryLinkedIssueNumber = pr.linkedIssues[0];
+ const linkedIssueOneShotSkip =
+ oneShotCadenceActive &&
+ primaryLinkedIssueNumber !== undefined &&
+ (await hasPublishedLinkedIssueSatisfaction(env, repoFullName, pr.number, primaryLinkedIssueNumber).catch(() => false));
+ if (linkedIssueOneShotSkip) {
+ await recordAuditEvent(env, {
+ eventType: "github_app.linked_issue_satisfaction_one_shot_skip",
+ actor: author,
+ targetKey: `${repoFullName}#${pr.number}`,
+ outcome: "completed",
+ detail: "one-shot review cadence: this PR's linked issue already had its satisfaction pass; not spending a fresh call",
+ metadata: { repoFullName, headSha: advisory.headSha ?? null },
+ }).catch(() => undefined);
+ } else {
+ linkedIssueSatisfaction = await runLinkedIssueSatisfactionForAdvisory(env, {
mode,
settings,
advisory,
repoFullName,
pr,
author,
- files: slopFiles,
- deterministicBand: slop.band,
+ files: await getReviewFiles(),
confirmedContributor,
- commitThresholdReached: isAutoReviewCommitThresholdReached(autoReviewConfig, slopReviewedCommitCount),
+ installationId,
});
}
}
- // Linked-issue satisfaction assessment (#1961/#3906, opt-in via linkedIssueSatisfactionGateMode). Assesses
- // only the PR's primary linked issue -- see runLinkedIssueSatisfactionForAdvisory's own doc comment for
- // the multi-linked-issue rationale. `off` (default) short-circuits before any fetch or model call, so this
- // is byte-identical to before this feature existed for every repo that hasn't opted in. (Declared/hoisted
- // to function scope above, alongside gateEvaluation, since it is consumed later outside this try block.)
- if (settings.linkedIssueSatisfactionGateMode !== "off" && pr.linkedIssues.length > 0) {
- linkedIssueSatisfaction = await runLinkedIssueSatisfactionForAdvisory(env, {
- mode,
- settings,
- advisory,
- repoFullName,
- pr,
- author,
- files: await getReviewFiles(),
- confirmedContributor,
- installationId,
- });
- }
// Focus-manifest policy (#555, opt-in via manifestPolicyGateMode). Reload the CACHED manifest (the
// settings resolver discards the raw manifest, but loadRepoFocusManifest is cached so this is cheap),
// recompute the guidance over the PR's changed files, and push ONLY the three enforceable policy
@@ -9843,10 +9905,20 @@ async function maybePublishPrPublicSurface(
isReputationEnabled(env) && isConvergenceRepoAllowed(env, repoFullName)
? await shouldSkipAiForReputation(env, { project: repoFullName, submitter: author })
: undefined;
+ // #one-shot-review-cadence: only even attempts the lookup when the review would otherwise be eligible to
+ // run fresh this pass (mirrors how the frozen/paused branches below are similarly mutually exclusive) --
+ // a PR that's blacklisted/frozen/already-skipped for another reason never shows AI content at all today,
+ // and one-shot mode must not change that. A non-null result here means this PR already had its one-shot
+ // main-review pass, so the fresh call below must be skipped and this reused instead.
+ const oneShotPriorReview =
+ oneShotCadenceActive && !authorBlacklisted && !isFrozenForManualReview && !autoReviewSkipReason
+ ? await getLatestPublishedAiReview(env, repoFullName, pr.number, settings.aiReviewMode).catch(() => null)
+ : null;
const aiReviewWillRun =
!authorBlacklisted &&
!isFrozenForManualReview &&
!autoReviewSkipReason &&
+ !oneShotPriorReview &&
(await shouldStartAiReviewForAdvisory(env, {
settings,
advisory,
@@ -9899,6 +9971,19 @@ async function maybePublishPrPublicSurface(
metadata: { deliveryId: webhook.deliveryId, repoFullName, headSha: advisory.headSha ?? null },
}).catch(() => undefined);
}
+ } else if (oneShotPriorReview && hasPublicReviewAssessment(oneShotPriorReview.notes)) {
+ advisory.findings.push(...oneShotPriorReview.findings);
+ aiReview = oneShotPriorReview;
+ aiReviewWasReused = true;
+ incr("gittensory_ai_review_one_shot_reuse_total");
+ await recordAuditEvent(env, {
+ eventType: "github_app.ai_review_one_shot_reuse",
+ actor: author,
+ targetKey: `${repoFullName}#${pr.number}`,
+ outcome: "completed",
+ detail: "one-shot review cadence: reused the last published AI review instead of spending a fresh call",
+ metadata: { deliveryId: webhook.deliveryId, repoFullName, headSha: advisory.headSha ?? null },
+ }).catch(() => undefined);
}
// Review-evasion protection (#review-evasion-protection): durably record that a review pass is starting
// for this EXACT head BEFORE any cost-bearing AI-review work begins (including the reviewing placeholder
diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts
index 8f131310fd..ad91b57f7c 100644
--- a/src/signals/focus-manifest.ts
+++ b/src/signals/focus-manifest.ts
@@ -4,6 +4,7 @@
* `src/` modules (`classifyChangedFile`, `mergeContributorBlacklists`, etc.).
*/
export {
+ AI_REVIEW_CADENCES,
COMMENT_VERBOSITY_LEVELS,
CONVERGED_FEATURE_KEYS,
E2E_TEST_DELIVERY_MODES,
@@ -33,6 +34,7 @@ export {
reviewRecapConfigToJson,
maintainerRecapConfigToJson,
settingsOverrideToJson,
+ type AiReviewCadence,
type AutoReviewConfig,
type CommentVerbosity,
type ConvergedFeatureKey,
diff --git a/test/unit/ai-slop-cache.test.ts b/test/unit/ai-slop-cache.test.ts
index fdb029bd2f..114b7913d9 100644
--- a/test/unit/ai-slop-cache.test.ts
+++ b/test/unit/ai-slop-cache.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest";
-import { getCachedAiSlopAdvisory, putCachedAiSlopAdvisory } from "../../src/db/repositories";
+import { getCachedAiSlopAdvisory, hasPublishedAiSlopAdvisory, putCachedAiSlopAdvisory } from "../../src/db/repositories";
import { aiSlopCacheInputFingerprint } from "../../src/review/ai-slop-cache-input";
import { createTestEnv } from "../helpers/d1";
@@ -86,6 +86,33 @@ describe("AI slop advisory cache (#ai-slop-cache)", () => {
});
});
+describe("hasPublishedAiSlopAdvisory (#one-shot-review-cadence)", () => {
+ it("is false when no row exists for the PR", async () => {
+ const env = createTestEnv();
+ expect(await hasPublishedAiSlopAdvisory(env, "o/r", 20)).toBe(false);
+ });
+
+ it("is true once ANY row exists, regardless of head SHA -- existence-only, not scoped to the current head", async () => {
+ const env = createTestEnv();
+ const fingerprint = await fp();
+ await putCachedAiSlopAdvisory(env, "o/r", 21, "sha1", fingerprint, { status: "ok", band: "low", finding: null, estimatedNeurons: 4 });
+ expect(await hasPublishedAiSlopAdvisory(env, "o/r", 21)).toBe(true);
+ // A DIFFERENT, later head SHA for the SAME PR still reads as "already had its pass" -- one-shot mode's
+ // whole point is that a new push does not reset this. The lookup carries no headSha parameter at all,
+ // so a query for the SAME PR after a would-be new commit lands is unaffected by which SHA is now current.
+ await putCachedAiSlopAdvisory(env, "o/r", 21, "sha2", fingerprint, { status: "ok", band: "low", finding: null, estimatedNeurons: 4 });
+ expect(await hasPublishedAiSlopAdvisory(env, "o/r", 21)).toBe(true);
+ });
+
+ it("scopes strictly to (repo, pull) -- a different PR or repo is unaffected", async () => {
+ const env = createTestEnv();
+ const fingerprint = await fp();
+ await putCachedAiSlopAdvisory(env, "o/r", 22, "sha1", fingerprint, { status: "ok", band: "low", finding: null, estimatedNeurons: 4 });
+ expect(await hasPublishedAiSlopAdvisory(env, "o/r", 23)).toBe(false); // different PR
+ expect(await hasPublishedAiSlopAdvisory(env, "o/r2", 22)).toBe(false); // different repo
+ });
+});
+
describe("aiSlopCacheInputFingerprint", () => {
const promptInput = {
title: "Tidy",
diff --git a/test/unit/auto-review-config-matrix.test.ts b/test/unit/auto-review-config-matrix.test.ts
index 9798a87b35..e7d373d4d7 100644
--- a/test/unit/auto-review-config-matrix.test.ts
+++ b/test/unit/auto-review-config-matrix.test.ts
@@ -20,6 +20,8 @@ describe("review.auto_review parse ↔ reviewConfigToJson round-trip (#2071)", (
const roundTripCases: Array<{ name: string; autoReview: Record }> = [
{ name: "skip_drafts: true", autoReview: { skip_drafts: true } },
{ name: "skip_drafts: false", autoReview: { skip_drafts: false } },
+ { name: "cadence: one_shot", autoReview: { cadence: "one_shot" } },
+ { name: "cadence: continuous", autoReview: { cadence: "continuous" } },
{ name: "ignore_authors", autoReview: { ignore_authors: ["*[bot]", "dependabot[bot]"] } },
{ name: "ignore_title_keywords", autoReview: { ignore_title_keywords: ["WIP", "draft"] } },
{ name: "skip_labels", autoReview: { skip_labels: ["do-not-review", "wip"] } },
@@ -33,6 +35,7 @@ describe("review.auto_review parse ↔ reviewConfigToJson round-trip (#2071)", (
name: "all knobs together",
autoReview: {
skip_drafts: true,
+ cadence: "one_shot",
ignore_authors: ["*[bot]"],
ignore_title_keywords: ["WIP"],
skip_labels: ["do-not-review"],
@@ -80,6 +83,12 @@ describe("review.auto_review malformed config (#2071)", () => {
expect(bad.review.autoReview.autoPauseAfterReviewedCommits).toBeNull();
expect(bad.warnings.some((w) => /auto_pause_after_reviewed_commits.*non-negative integer/.test(w))).toBe(true);
});
+
+ it("warns on an invalid cadence value and drops the knob (#one-shot-review-cadence)", () => {
+ const bad = parseFocusManifest({ review: { auto_review: { cadence: "sometimes" } } });
+ expect(bad.review.autoReview.cadence).toBeNull();
+ expect(bad.warnings.some((w) => /auto_review\.cadence.*must be one of/.test(w))).toBe(true);
+ });
});
describe("evaluateAutoReviewSkipReason predicate precedence (#2071)", () => {
diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts
index 8639685147..eb5d0bba5b 100644
--- a/test/unit/focus-manifest.test.ts
+++ b/test/unit/focus-manifest.test.ts
@@ -3686,14 +3686,22 @@ describe("overlayReviewConfig / review.shared_config (#2046)", () => {
});
it("merges nested auto_review and partial field maps key-by-key", () => {
- const base = parseReviewConfigMapping({ auto_review: { skip_drafts: true, ignore_authors: ["bot"] }, fields: { relatedWork: false } }, []);
+ const base = parseReviewConfigMapping({ auto_review: { skip_drafts: true, cadence: "one_shot", ignore_authors: ["bot"] }, fields: { relatedWork: false } }, []);
const override = parseReviewConfigMapping({ auto_review: { ignore_authors: ["dependabot"] }, fields: { openPrQueue: true } }, []);
const merged = overlayReviewConfig(base, override);
expect(merged.autoReview.skipDrafts).toBe(true);
+ // #one-shot-review-cadence: override left cadence unset, so the base's value fills the gap (pickOverlayNullable).
+ expect(merged.autoReview.cadence).toBe("one_shot");
expect(merged.autoReview.ignoreAuthors).toEqual(["dependabot"]);
expect(merged.fields).toEqual({ relatedWork: false, openPrQueue: true });
});
+ it("lets an explicit override cadence win over the base's (#one-shot-review-cadence)", () => {
+ const base = parseReviewConfigMapping({ auto_review: { cadence: "one_shot" } }, []);
+ const override = parseReviewConfigMapping({ auto_review: { cadence: "continuous" } }, []);
+ expect(overlayReviewConfig(base, override).autoReview.cadence).toBe("continuous");
+ });
+
it("preserves sharedConfigSource from the override when set", () => {
const base = parseReviewConfigMapping({ tone: "house" }, []);
const override = { ...parseReviewConfigMapping({ profile: "assertive" }, []), sharedConfigSource: "_shared/.gittensory.yml" };
@@ -3722,6 +3730,7 @@ describe("review.auto_review (#1954 / #2038–#2041)", () => {
});
expect(m.review.autoReview).toEqual({
skipDrafts: true,
+ cadence: null,
ignoreAuthors: ["*[bot]", "dependabot[bot]"],
ignoreTitleKeywords: ["WIP", "draft"],
skipLabels: ["do-not-review", "wip"],
diff --git a/test/unit/gate-check-policy.test.ts b/test/unit/gate-check-policy.test.ts
index c2011a12ce..9815779dc3 100644
--- a/test/unit/gate-check-policy.test.ts
+++ b/test/unit/gate-check-policy.test.ts
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { generateKeyPairSync } from "node:crypto";
import { clearInstallationTokenCacheForTest } from "../../src/github/app";
-import { buildAuthorizedPrActionAdvisory, gateCheckPolicy, resolveLinkedIssueAuthorLogins, shouldCollectLinkedIssueEvidence, shouldCollectSlopEvidence, shouldRefreshFilesForPreMergeChecks, shouldRunSlopAiAdvisory } from "../../src/queue/processors";
+import { buildAuthorizedPrActionAdvisory, gateCheckPolicy, resolveAiReviewCadence, resolveLinkedIssueAuthorLogins, shouldCollectLinkedIssueEvidence, shouldCollectSlopEvidence, shouldRefreshFilesForPreMergeChecks, shouldRunSlopAiAdvisory } from "../../src/queue/processors";
import { createTestEnv } from "../helpers/d1";
import { upsertIssueFromGitHub, upsertRepositoryFromGitHub } from "../../src/db/repositories";
import { evaluateGateCheck } from "../../src/rules/advisory";
@@ -512,6 +512,24 @@ describe("merge-readiness evidence collection (#551)", () => {
expect(shouldRunSlopAiAdvisory(settings({ slopGateMode: "off", mergeReadinessGateMode: "advisory", slopAiAdvisory: true }))).toBe(false);
expect(shouldRunSlopAiAdvisory(settings({ slopGateMode: "advisory", slopAiAdvisory: false }))).toBe(false);
});
+
+ describe("resolveAiReviewCadence (#one-shot-review-cadence)", () => {
+ it("lets an explicit configured per-repo cadence win over the fleet env default, in both directions", () => {
+ expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "true" }, "one_shot")).toBe("one_shot");
+ expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "false" }, "continuous")).toBe("continuous");
+ });
+
+ it('falls back to "continuous" when unconfigured and the fleet env flag is truthy', () => {
+ expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "true" }, null)).toBe("continuous");
+ expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "1" }, null)).toBe("continuous");
+ });
+
+ it('defaults to "one_shot" when unconfigured and the fleet env flag is unset or falsy', () => {
+ expect(resolveAiReviewCadence({}, null)).toBe("one_shot");
+ expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "false" }, null)).toBe("one_shot");
+ expect(resolveAiReviewCadence({ GITTENSORY_REVIEW_CONTINUOUS: "nonsense" }, null)).toBe("one_shot");
+ });
+ });
});
describe("merge-readiness composite gate (#551)", () => {
diff --git a/test/unit/linked-issue-satisfaction-cache.test.ts b/test/unit/linked-issue-satisfaction-cache.test.ts
index 94d0b5d351..1bd89cf155 100644
--- a/test/unit/linked-issue-satisfaction-cache.test.ts
+++ b/test/unit/linked-issue-satisfaction-cache.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest";
-import { getCachedLinkedIssueSatisfaction, putCachedLinkedIssueSatisfaction } from "../../src/db/repositories";
+import { getCachedLinkedIssueSatisfaction, hasPublishedLinkedIssueSatisfaction, putCachedLinkedIssueSatisfaction } from "../../src/db/repositories";
import { linkedIssueSatisfactionCacheInputFingerprint } from "../../src/review/linked-issue-satisfaction-cache-input";
import { createTestEnv } from "../helpers/d1";
@@ -91,6 +91,29 @@ describe("linked-issue satisfaction cache (#1961/#3906)", () => {
});
});
+describe("hasPublishedLinkedIssueSatisfaction (#one-shot-review-cadence)", () => {
+ it("is false when no row exists for the PR + linked issue number", async () => {
+ const env = createTestEnv();
+ expect(await hasPublishedLinkedIssueSatisfaction(env, "o/r", 20, 1)).toBe(false);
+ });
+
+ it("is true once ANY row exists for that issue number, regardless of head SHA", async () => {
+ const env = createTestEnv();
+ const fingerprint = await fp();
+ await putCachedLinkedIssueSatisfaction(env, "o/r", 21, "sha1", 5, fingerprint, { status: "ok", result: { status: "addressed", rationale: "r", confidence: 0.9 }, estimatedNeurons: 4 });
+ expect(await hasPublishedLinkedIssueSatisfaction(env, "o/r", 21, 5)).toBe(true);
+ });
+
+ it("scopes to (repo, pull, linkedIssueNumber) -- a newly-linked (never-assessed) issue still reads false even though the PR already has a pass for a DIFFERENT issue", async () => {
+ const env = createTestEnv();
+ const fingerprint = await fp();
+ await putCachedLinkedIssueSatisfaction(env, "o/r", 22, "sha1", 5, fingerprint, { status: "ok", result: { status: "addressed", rationale: "r", confidence: 0.9 }, estimatedNeurons: 4 });
+ expect(await hasPublishedLinkedIssueSatisfaction(env, "o/r", 22, 6)).toBe(false); // different (newly-linked) issue on the SAME PR
+ expect(await hasPublishedLinkedIssueSatisfaction(env, "o/r", 23, 5)).toBe(false); // different PR
+ expect(await hasPublishedLinkedIssueSatisfaction(env, "o/r2", 22, 5)).toBe(false); // different repo
+ });
+});
+
describe("linkedIssueSatisfactionCacheInputFingerprint", () => {
it("is stable for the same input", async () => {
const a = await linkedIssueSatisfactionCacheInputFingerprint({ byok: false, provider: null, model: null });
diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts
index cb8e3edf17..bb5bd5eceb 100644
--- a/test/unit/queue.test.ts
+++ b/test/unit/queue.test.ts
@@ -53,6 +53,8 @@ import {
upsertRepositoryFromGitHub,
putCachedAiReview,
markAiReviewPublished,
+ putCachedAiSlopAdvisory,
+ putCachedLinkedIssueSatisfaction,
recordReviewSuppression,
listReviewSuppressions,
setGlobalAgentFrozen,
@@ -3907,6 +3909,10 @@ describe("queue processors", () => {
AI_DAILY_NEURON_BUDGET: "100000",
});
await seedRegateChurnRepo(env);
+ // #one-shot-review-cadence: this test is about the non-cacheable-outcome cooldown specifically, not
+ // cadence -- opt into continuous so the SAME unchanged-head assertions below exercise that mechanism in
+ // isolation, unaffected by the one_shot default now suppressing repeat automatic passes for a different reason.
+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } });
await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 60, title: "Quiet PR", state: "open", user: { login: "contributor" }, head: { sha: "a60" }, labels: [], body: "Closes #1" });
await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 60, status: "complete", reviewsSyncedAt: new Date().toISOString() });
vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
@@ -5276,6 +5282,9 @@ describe("queue processors", () => {
AI_DAILY_NEURON_BUDGET: "100000",
});
await seedRegateChurnRepo(env);
+ // #one-shot-review-cadence: isolate this test to the cooldown-vs-real-state-change mechanism it's actually
+ // about (see the PR 60 test above for the identical rationale).
+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } });
await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 63, title: "Pushed PR", state: "open", user: { login: "contributor" }, head: { sha: "a63" }, labels: [], body: "Closes #1" });
await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 63, status: "complete", reviewsSyncedAt: new Date().toISOString() });
let liveHeadSha = "a63";
@@ -5447,6 +5456,8 @@ describe("queue processors", () => {
AI_DAILY_NEURON_BUDGET: "100000",
});
await seedRegateChurnRepo(env, { publicSurface: "comment_and_label" });
+ // #one-shot-review-cadence: isolate this test to the unchanged-head/label-repair mechanism it's about.
+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } });
await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 70, title: "Fix the retry loop", state: "open", user: { login: "contributor" }, head: { sha: "a70" }, labels: [], body: "Closes #1" });
await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 70, status: "complete", reviewsSyncedAt: new Date().toISOString() });
@@ -5587,6 +5598,8 @@ describe("queue processors", () => {
AI_DAILY_NEURON_BUDGET: "100000",
});
await seedRegateChurnRepo(env, { publicSurface: "comment_only" });
+ // #one-shot-review-cadence: isolate this test to the real-code-change-triggers-fresh-review mechanism.
+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } });
await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 72, title: "Evolving PR", state: "open", user: { login: "contributor" }, head: { sha: "a72" }, labels: [], body: "Closes #1" });
await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 72, status: "complete", reviewsSyncedAt: new Date().toISOString() });
@@ -5878,6 +5891,245 @@ describe("queue processors", () => {
expect(bypassAudit?.outcome).toBe("completed");
});
+ describe("one-shot AI review cadence (#one-shot-review-cadence)", () => {
+ it("default (one_shot, no manual-review label): a genuinely NEW push does not spend a fresh main-review AI call -- reuses the prior published review", async () => {
+ let aiCalls = 0;
+ const env = createTestEnv({
+ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(),
+ AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Fresh (should not happen under one-shot).", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai,
+ AI_SUMMARIES_ENABLED: "true",
+ AI_PUBLIC_COMMENTS_ENABLED: "true",
+ AI_DAILY_NEURON_BUDGET: "100000",
+ });
+ // No cadence override anywhere (no yml, no GITTENSORY_REVIEW_CONTINUOUS) -- one_shot is the codebase default.
+ await seedRegateChurnRepo(env, { publicSurface: "comment_only" });
+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 91, title: "One-shot PR", state: "open", user: { login: "contributor" }, head: { sha: "a91-v1" }, labels: [], body: "Closes #1" });
+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 91, status: "complete", reviewsSyncedAt: new Date().toISOString() });
+ await putCachedAiReview(env, "JSONbored/gittensory", 91, "a91-v1", "block", { notes: "Original one-shot review.", reviewerCount: 1 });
+ await markAiReviewPublished(env, "JSONbored/gittensory", 91, "a91-v1");
+
+ let publicCommentBody = "";
+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = input.toString();
+ const method = init?.method ?? "GET";
+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
+ // A genuinely NEW push -- a new head SHA, no manual-review label anywhere.
+ if (url.includes("/pulls/91/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]);
+ if (url.endsWith("/pulls/91")) return Response.json({ number: 91, title: "One-shot PR", state: "open", user: { login: "contributor" }, head: { sha: "a91-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" });
+ if (url.includes("/commits/a91-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
+ if (url.includes("/commits/a91-v2/status")) return Response.json({ state: "success", statuses: [] });
+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
+ if (url.includes("/issues/91/comments") && method === "GET") return Response.json([]);
+ if (url.includes("/issues/91/comments") && method === "POST") {
+ publicCommentBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? "");
+ return Response.json({ id: 1 }, { status: 201 });
+ }
+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
+ return Response.json({});
+ });
+
+ await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-push", repoFullName: "JSONbored/gittensory", prNumber: 91, installationId: 123 });
+
+ expect(aiCalls).toBe(0); // the new head never bought a fresh AI review
+ expect(publicCommentBody).toContain("Original one-shot review.");
+ const reuseAudit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? and target_key = ?")
+ .bind("github_app.ai_review_one_shot_reuse", "JSONbored/gittensory#91")
+ .first<{ outcome: string; detail: string }>();
+ expect(reuseAudit?.outcome).toBe("completed");
+ expect(reuseAudit?.detail).toContain("one-shot review cadence");
+ });
+
+ it("default (one_shot): a PR with no prior review yet still gets its first pass -- one-shot never blocks the FIRST review", async () => {
+ let aiCalls = 0;
+ const env = createTestEnv({
+ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(),
+ AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "First pass.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai,
+ AI_SUMMARIES_ENABLED: "true",
+ AI_PUBLIC_COMMENTS_ENABLED: "true",
+ AI_DAILY_NEURON_BUDGET: "100000",
+ });
+ await seedRegateChurnRepo(env, { publicSurface: "comment_only" });
+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 92, title: "Brand-new PR", state: "open", user: { login: "contributor" }, head: { sha: "a92" }, labels: [], body: "Closes #1" });
+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 92, status: "complete", reviewsSyncedAt: new Date().toISOString() });
+ // Deliberately NO prior putCachedAiReview/markAiReviewPublished -- this PR has never been reviewed.
+
+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = input.toString();
+ const method = init?.method ?? "GET";
+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
+ if (url.includes("/pulls/92/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]);
+ if (url.endsWith("/pulls/92")) return Response.json({ number: 92, title: "Brand-new PR", state: "open", user: { login: "contributor" }, head: { sha: "a92" }, labels: [], body: "Closes #1", mergeable_state: "clean" });
+ if (url.includes("/commits/a92/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
+ if (url.includes("/commits/a92/status")) return Response.json({ state: "success", statuses: [] });
+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
+ if (url.includes("/issues/92/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 });
+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
+ return Response.json({});
+ });
+
+ await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-first-pass", repoFullName: "JSONbored/gittensory", prNumber: 92, installationId: 123 });
+
+ expect(aiCalls).toBeGreaterThan(0); // the very first pass is never suppressed by one-shot mode
+ const reuseAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?")
+ .bind("github_app.ai_review_one_shot_reuse", "JSONbored/gittensory#92")
+ .first<{ n: number }>();
+ expect(reuseAudit?.n).toBe(0); // no reuse fired -- there was nothing to reuse
+ });
+
+ it("default (one_shot): a repeat trigger does not spend a fresh SLOP advisory call once this PR already had one, regardless of head SHA", async () => {
+ let aiCalls = 0;
+ const env = createTestEnv({
+ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(),
+ AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ risk: "low", rationale: "fine" }) }; } } as unknown as Ai,
+ AI_SUMMARIES_ENABLED: "true",
+ AI_PUBLIC_COMMENTS_ENABLED: "true",
+ AI_DAILY_NEURON_BUDGET: "100000",
+ });
+ await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", slopGateMode: "advisory", slopAiAdvisory: true });
+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 93, title: "Slop one-shot PR", state: "open", user: { login: "contributor" }, head: { sha: "a93-v1" }, labels: [], body: "Closes #1" });
+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 93, status: "complete", reviewsSyncedAt: new Date().toISOString() });
+ // A slop advisory row already exists for an EARLIER head -- this PR already had its one-shot slop pass.
+ await putCachedAiSlopAdvisory(env, "JSONbored/gittensory", 93, "a93-v1", "seed-fp", { status: "ok", band: "low", finding: null, estimatedNeurons: 4 });
+
+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = input.toString();
+ const method = init?.method ?? "GET";
+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
+ if (url.includes("/pulls/93/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]);
+ if (url.endsWith("/pulls/93")) return Response.json({ number: 93, title: "Slop one-shot PR", state: "open", user: { login: "contributor" }, head: { sha: "a93-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" });
+ if (url.includes("/commits/a93-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
+ if (url.includes("/commits/a93-v2/status")) return Response.json({ state: "success", statuses: [] });
+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
+ if (url.includes("/issues/93/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 });
+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
+ return Response.json({});
+ });
+
+ await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-slop-push", repoFullName: "JSONbored/gittensory", prNumber: 93, installationId: 123 });
+
+ expect(aiCalls).toBe(0); // no fresh slop LLM call on the new head
+ const skipAudit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? and target_key = ?")
+ .bind("github_app.ai_slop_one_shot_skip", "JSONbored/gittensory#93")
+ .first<{ outcome: string; detail: string }>();
+ expect(skipAudit?.outcome).toBe("completed");
+ expect(skipAudit?.detail).toContain("one-shot review cadence");
+ });
+
+ it("default (one_shot): a NEWLY-linked issue still gets its own linked-issue-satisfaction pass even though the PR already has one for a DIFFERENT issue", async () => {
+ let aiCalls = 0;
+ const env = createTestEnv({
+ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(),
+ AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ status: "addressed", rationale: "looks done" }) }; } } as unknown as Ai,
+ AI_SUMMARIES_ENABLED: "true",
+ AI_PUBLIC_COMMENTS_ENABLED: "true",
+ AI_DAILY_NEURON_BUDGET: "100000",
+ });
+ await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", linkedIssueSatisfactionGateMode: "advisory" });
+ // The PR now cites issue #2 as its primary linked issue -- a prior pass exists only for issue #1, a
+ // DIFFERENT (now-superseded) issue, so issue #2 must still be treated as never-assessed.
+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 94, title: "Re-linked PR", state: "open", user: { login: "contributor" }, head: { sha: "a94" }, labels: [], body: "Closes #2" });
+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 94, status: "complete", reviewsSyncedAt: new Date().toISOString() });
+ await putCachedLinkedIssueSatisfaction(env, "JSONbored/gittensory", 94, "a94-old", 1, "seed-fp", { status: "ok", result: { status: "addressed", rationale: "old issue was done", confidence: 0.9 }, estimatedNeurons: 4 });
+
+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = input.toString();
+ const method = init?.method ?? "GET";
+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
+ if (url.includes("/pulls/94/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]);
+ if (url.endsWith("/pulls/94")) return Response.json({ number: 94, title: "Re-linked PR", state: "open", user: { login: "contributor" }, head: { sha: "a94" }, labels: [], body: "Closes #2", mergeable_state: "clean" });
+ if (url.includes("/commits/a94/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
+ if (url.includes("/commits/a94/status")) return Response.json({ state: "success", statuses: [] });
+ if (url.includes("/issues/2")) return Response.json({ number: 2, title: "Second issue", state: "open", labels: [], user: { login: "reporter" }, body: "Do the second thing." });
+ if (url.includes("/issues/94/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 });
+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
+ return Response.json({});
+ });
+
+ await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-relinked", repoFullName: "JSONbored/gittensory", prNumber: 94, installationId: 123 });
+
+ expect(aiCalls).toBeGreaterThan(0); // issue #2 was never assessed before -- gets its own first pass
+ const skipAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?")
+ .bind("github_app.linked_issue_satisfaction_one_shot_skip", "JSONbored/gittensory#94")
+ .first<{ n: number }>();
+ expect(skipAudit?.n).toBe(0); // never skipped -- issue #2 genuinely had no prior pass
+ });
+
+ it("per-repo override (continuous via .gittensory.yml): a new push DOES spend a fresh main-review AI call, unlike the one_shot default", async () => {
+ let aiCalls = 0;
+ const env = createTestEnv({
+ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(),
+ AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Continuous re-review.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai,
+ AI_SUMMARIES_ENABLED: "true",
+ AI_PUBLIC_COMMENTS_ENABLED: "true",
+ AI_DAILY_NEURON_BUDGET: "100000",
+ });
+ await seedRegateChurnRepo(env, { publicSurface: "comment_only" });
+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } });
+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 95, title: "Continuous-mode PR", state: "open", user: { login: "contributor" }, head: { sha: "a95-v1" }, labels: [], body: "Closes #1" });
+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 95, status: "complete", reviewsSyncedAt: new Date().toISOString() });
+ await putCachedAiReview(env, "JSONbored/gittensory", 95, "a95-v1", "block", { notes: "Original review.", reviewerCount: 1 });
+ await markAiReviewPublished(env, "JSONbored/gittensory", 95, "a95-v1");
+
+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = input.toString();
+ const method = init?.method ?? "GET";
+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
+ if (url.includes("/pulls/95/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]);
+ if (url.endsWith("/pulls/95")) return Response.json({ number: 95, title: "Continuous-mode PR", state: "open", user: { login: "contributor" }, head: { sha: "a95-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" });
+ if (url.includes("/commits/a95-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
+ if (url.includes("/commits/a95-v2/status")) return Response.json({ state: "success", statuses: [] });
+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
+ if (url.includes("/issues/95/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 });
+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
+ return Response.json({});
+ });
+
+ await processJob(env, { type: "agent-regate-pr", deliveryId: "continuous-push", repoFullName: "JSONbored/gittensory", prNumber: 95, installationId: 123 });
+
+ expect(aiCalls).toBeGreaterThan(0); // continuous mode: the new head DOES buy a fresh AI review
+ const reuseAudit = await env.DB.prepare("select count(*) as n from audit_events where event_type = ? and target_key = ?")
+ .bind("github_app.ai_review_one_shot_reuse", "JSONbored/gittensory#95")
+ .first<{ n: number }>();
+ expect(reuseAudit?.n).toBe(0); // one-shot reuse never engaged -- this repo opted out
+ });
+
+ it("fleet-wide env default (GITTENSORY_REVIEW_CONTINUOUS): applies when the repo has no yml override, but a repo override still wins over it", async () => {
+ let aiCalls = 0;
+ const env = createTestEnv({
+ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(),
+ GITTENSORY_REVIEW_CONTINUOUS: "true",
+ AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Fleet-wide continuous.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai,
+ AI_SUMMARIES_ENABLED: "true",
+ AI_PUBLIC_COMMENTS_ENABLED: "true",
+ AI_DAILY_NEURON_BUDGET: "100000",
+ });
+ await seedRegateChurnRepo(env, { publicSurface: "comment_only" });
+ // No per-repo cadence override -- inherits the fleet-wide GITTENSORY_REVIEW_CONTINUOUS default.
+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 96, title: "Fleet-default PR", state: "open", user: { login: "contributor" }, head: { sha: "a96-v1" }, labels: [], body: "Closes #1" });
+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 96, status: "complete", reviewsSyncedAt: new Date().toISOString() });
+ await putCachedAiReview(env, "JSONbored/gittensory", 96, "a96-v1", "block", { notes: "Original review.", reviewerCount: 1 });
+ await markAiReviewPublished(env, "JSONbored/gittensory", 96, "a96-v1");
+
+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = input.toString();
+ const method = init?.method ?? "GET";
+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
+ if (url.includes("/pulls/96/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]);
+ if (url.endsWith("/pulls/96")) return Response.json({ number: 96, title: "Fleet-default PR", state: "open", user: { login: "contributor" }, head: { sha: "a96-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" });
+ if (url.includes("/commits/a96-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
+ if (url.includes("/commits/a96-v2/status")) return Response.json({ state: "success", statuses: [] });
+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
+ if (url.includes("/issues/96/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 });
+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
+ return Response.json({});
+ });
+
+ await processJob(env, { type: "agent-regate-pr", deliveryId: "fleet-continuous-push", repoFullName: "JSONbored/gittensory", prNumber: 96, installationId: 123 });
+
+ expect(aiCalls).toBeGreaterThan(0); // the fleet-wide env default applied since the repo set no override
+ });
+ });
+
it("#freeze-owner-exemption (incident, confirmed live on PR #3476): the repo owner's OWN held PR is never frozen -- a new push gets a fresh AI review", async () => {
// The owner pushing a genuine fix to their OWN held PR must not keep replaying the ORIGINAL, now-stale
// verdict pass after pass -- confirmed live via github_app.ai_review_frozen_reuse firing on every one of
@@ -5894,6 +6146,10 @@ describe("queue processors", () => {
AI_DAILY_NEURON_BUDGET: "100000",
});
await seedRegateChurnRepo(env, { publicSurface: "comment_only" });
+ // #one-shot-review-cadence: isolate this test to the owner-exemption-from-the-LABEL-freeze mechanism --
+ // without this, a prior published review would ALSO be reused by the (correctly firing) one_shot default,
+ // for an unrelated reason, masking whether the label-freeze exemption itself actually works.
+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } });
await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 79, title: "Owner's held PR", state: "open", user: { login: "JSONbored" }, head: { sha: "a79-v1" }, labels: [{ name: "manual-review" }], body: "Closes #1" });
await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 79, status: "complete", reviewsSyncedAt: new Date().toISOString() });
await putCachedAiReview(env, "JSONbored/gittensory", 79, "a79-v1", "block", { notes: "Original stale review.", reviewerCount: 1 });
@@ -5933,6 +6189,8 @@ describe("queue processors", () => {
AI_DAILY_NEURON_BUDGET: "100000",
});
await seedRegateChurnRepo(env, { publicSurface: "comment_only" });
+ // #one-shot-review-cadence: isolate this test to the admin-exemption-from-the-LABEL-freeze mechanism.
+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } });
await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 80, title: "Admin's held PR", state: "open", user: { login: "fleet-admin" }, head: { sha: "a80-v1" }, labels: [{ name: "manual-review" }], body: "Closes #1" });
await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 80, status: "complete", reviewsSyncedAt: new Date().toISOString() });
await putCachedAiReview(env, "JSONbored/gittensory", 80, "a80-v1", "block", { notes: "Original stale review.", reviewerCount: 1 });
@@ -5970,6 +6228,8 @@ describe("queue processors", () => {
AI_DAILY_NEURON_BUDGET: "100000",
});
await seedRegateChurnRepo(env, { publicSurface: "comment_only" });
+ // #one-shot-review-cadence: isolate this test to the automation-bot-exemption-from-the-LABEL-freeze mechanism.
+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { review: { auto_review: { cadence: "continuous" } } });
await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 81, title: "Bot's held PR", state: "open", user: { login: "dependabot[bot]" }, head: { sha: "a81-v1" }, labels: [{ name: "manual-review" }], body: "Closes #1" });
await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 81, status: "complete", reviewsSyncedAt: new Date().toISOString() });
await putCachedAiReview(env, "JSONbored/gittensory", 81, "a81-v1", "block", { notes: "Original stale review.", reviewerCount: 1 });
@@ -6008,7 +6268,9 @@ describe("queue processors", () => {
});
await seedRegateChurnRepo(env, { publicSurface: "comment_only" });
// manualReviewLabel is config-as-code only (.gittensory.yml), not a DB-backed repository setting.
- await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { manualReviewLabel: null } });
+ // #one-shot-review-cadence: also opts into continuous here so this test stays isolated to the
+ // manualReviewLabel-disabled mechanism it's actually about.
+ await upsertRepoFocusManifest(env, "JSONbored/gittensory", { settings: { manualReviewLabel: null }, review: { auto_review: { cadence: "continuous" } } });
// The PR carries the literal "manual-review" text as a label, but with the mechanism disabled repo-wide
// there is no configured label to match against — the freeze must never engage on text alone.
await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 76, title: "Held PR, mechanism disabled", state: "open", user: { login: "contributor" }, head: { sha: "a76" }, labels: [{ name: "manual-review" }], body: "Closes #1" });
diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts
index 0a04d33f39..10c7717602 100644
--- a/test/unit/signals-coverage.test.ts
+++ b/test/unit/signals-coverage.test.ts
@@ -1138,7 +1138,7 @@ describe("signal coverage edge cases", () => {
collisions: buildCollisionReport(directRepo.fullName, [], [currentPr]),
preflight: buildPreflightResult({ repoFullName: directRepo.fullName, title: "Fix isolated issue", body: "Fixes #99", linkedIssues: [99] }, directRepo, [], [currentPr]),
settings: gateSettings,
- review: { present: true, footerText: "Reviewed by the Acme maintainer bot.", note: "Run npm test before pushing.", fields: { relatedWork: false }, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, e2eTestDelivery: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { skipDrafts: null, ignoreAuthors: [], ignoreTitleKeywords: [], skipLabels: [], skipDocsOnly: null, maxAddedLines: 0, maxFiles: 0, baseBranches: [], autoPauseAfterReviewedCommits: null }, labelingRules: [], aiModel: { claudeModel: null, claudeEffort: null, codexModel: null, codexEffort: null, ollamaModel: null, openaiModel: null, openaiCompatibleModel: null, anthropicModel: null }, visual: { productionUrl: null, preview: { urlTemplate: null }, routes: { paths: [], maxRoutes: null }, themes: [], gif: false, enabled: null, themeStorageKey: null, actionsFallback: false }, linkedIssueSatisfaction: null, sharedConfigSource: null },
+ review: { present: true, footerText: "Reviewed by the Acme maintainer bot.", note: "Run npm test before pushing.", fields: { relatedWork: false }, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, e2eTestDelivery: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { skipDrafts: null, cadence: null, ignoreAuthors: [], ignoreTitleKeywords: [], skipLabels: [], skipDocsOnly: null, maxAddedLines: 0, maxFiles: 0, baseBranches: [], autoPauseAfterReviewedCommits: null }, labelingRules: [], aiModel: { claudeModel: null, claudeEffort: null, codexModel: null, codexEffort: null, ollamaModel: null, openaiModel: null, openaiCompatibleModel: null, anthropicModel: null }, visual: { productionUrl: null, preview: { urlTemplate: null }, routes: { paths: [], maxRoutes: null }, themes: [], gif: false, enabled: null, themeStorageKey: null, actionsFallback: false }, linkedIssueSatisfaction: null, sharedConfigSource: null },
aiReview: { notes: "The change is focused.\n\n**Nits (2)**\n- Add a test for the edge case.\n- Keep the validator helper scoped." },
});
expect(customizedComment).toContain("Reviewed by the Acme maintainer bot."); // custom footer lead
diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts
index 4765b27099..538b5ebbc9 100644
--- a/worker-configuration.d.ts
+++ b/worker-configuration.d.ts
@@ -1,5 +1,5 @@
/* eslint-disable */
-// Generated by Wrangler by running `wrangler types` (hash: 20fa12547178d4e155857d65d878ebff)
+// Generated by Wrangler by running `wrangler types` (hash: 7eeadad15ce002765f06a319222e9f94)
// Runtime types generated with workerd@1.20260701.1 2026-05-28 nodejs_compat
interface __BaseEnv_Env {
DB: D1Database;
@@ -23,6 +23,7 @@ interface __BaseEnv_Env {
GITTENSORY_REVIEW_SCREENSHOTS: "false";
GITTENSORY_REVIEW_GROUNDING: "false";
GITTENSORY_REVIEW_E2E_TESTS: "false";
+ GITTENSORY_REVIEW_CONTINUOUS: "false";
GITTENSORY_REVIEW_REPUTATION: "false";
GITTENSORY_REVIEW_OPS: "false";
GITTENSORY_SWEEP_WATCHDOG: "false";
@@ -76,6 +77,7 @@ declare namespace NodeJS {
| "GITTENSORY_PUBLIC_STATS"
| "GITTENSORY_PUBLIC_STATS_REPOS"
| "GITTENSORY_REVIEW_CONTENT_LANE"
+ | "GITTENSORY_REVIEW_CONTINUOUS"
| "GITTENSORY_REVIEW_CULTURE_PROFILE"
| "GITTENSORY_REVIEW_DRAFT"
| "GITTENSORY_REVIEW_E2E_TESTS"
diff --git a/wrangler.jsonc b/wrangler.jsonc
index a53a529316..84ce6dff20 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -63,6 +63,13 @@
// E2E test coverage feature. Default OFF — flag-OFF the feature is never active for any repo regardless of
// a per-repo features.e2eTests override.
"GITTENSORY_REVIEW_E2E_TESTS": "false",
+ // #one-shot-review-cadence: fleet-wide default AI review re-trigger cadence, used only when a repo's
+ // .gittensory.yml review.auto_review.cadence is unset (a per-repo value always wins). Default OFF (false)
+ // = "one_shot": AI-generated content (main review, slop, linked-issue satisfaction) freezes after its
+ // first pass; only an explicit maintainer retrigger spends a fresh call. Truthy = "continuous": every
+ // push/CI-completion/sweep re-runs AI content generation, for operators who prefer that. The deterministic
+ // gate always re-evaluates regardless of this flag.
+ "GITTENSORY_REVIEW_CONTINUOUS": "false",
// Convergence (reputation): factor the INTERNAL-only ported submitter-reputation signal into the AI-spend
// gate — a new / burst / low-reputation submitter is downgraded to a deterministic-only review (AI neurons
// skipped), and the per-(project, submitter) outcome is recorded after the gate decides. The reputation is
From a3203e6c52280a43ecb893235d62668e896884bd Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Fri, 10 Jul 2026 02:48:52 -0700
Subject: [PATCH 2/3] test(review): close linked-issue-skip coverage gap + mark
unreachable headSha branches
Add the missing positive-case test for the linked-issue-satisfaction
one-shot skip actually firing (previously only the negative/never-skip
case was covered), and mark the advisory.headSha ?? null fallback on
the three new one-shot-cadence audit events as v8-ignored, mirroring
the identical, already-established treatment of the same fallback
shape elsewhere in maybePublishPrPublicSurface.
---
src/queue/processors.ts | 9 +++++++++
test/unit/queue.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 49 insertions(+)
diff --git a/src/queue/processors.ts b/src/queue/processors.ts
index 32ad3aba8c..88a46baeea 100644
--- a/src/queue/processors.ts
+++ b/src/queue/processors.ts
@@ -9598,6 +9598,8 @@ async function maybePublishPrPublicSurface(
targetKey: `${repoFullName}#${pr.number}`,
outcome: "completed",
detail: "one-shot review cadence: this PR already had its slop advisory pass; not spending a fresh call",
+ /* v8 ignore next -- reached only when a PRIOR slop pass already published, and an open PR does not
+ * lose its head SHA once set; the `?? null` is a type-level fallback for an unreachable branch. */
metadata: { repoFullName, headSha: advisory.headSha ?? null },
}).catch(() => undefined);
} else {
@@ -9642,6 +9644,9 @@ async function maybePublishPrPublicSurface(
targetKey: `${repoFullName}#${pr.number}`,
outcome: "completed",
detail: "one-shot review cadence: this PR's linked issue already had its satisfaction pass; not spending a fresh call",
+ /* v8 ignore next -- reached only when a PRIOR satisfaction pass already published for this issue
+ * number, and an open PR does not lose its head SHA once set; the `?? null` is a type-level
+ * fallback for an unreachable branch. */
metadata: { repoFullName, headSha: advisory.headSha ?? null },
}).catch(() => undefined);
} else {
@@ -9982,6 +9987,10 @@ async function maybePublishPrPublicSurface(
targetKey: `${repoFullName}#${pr.number}`,
outcome: "completed",
detail: "one-shot review cadence: reused the last published AI review instead of spending a fresh call",
+ /* v8 ignore next -- a truthy `oneShotPriorReview` means markAiReviewPublished previously stamped a row
+ * for a non-null head SHA, and an open PR does not lose its head SHA once set; the `?? null` is a
+ * type-level fallback for a practically-unreachable branch, mirroring the identical fallback on the
+ * frozen-reuse and paused-reuse audit events just above. */
metadata: { deliveryId: webhook.deliveryId, repoFullName, headSha: advisory.headSha ?? null },
}).catch(() => undefined);
}
diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts
index bb5bd5eceb..5554af35b7 100644
--- a/test/unit/queue.test.ts
+++ b/test/unit/queue.test.ts
@@ -6054,6 +6054,46 @@ describe("queue processors", () => {
expect(skipAudit?.n).toBe(0); // never skipped -- issue #2 genuinely had no prior pass
});
+ it("default (one_shot): a repeat trigger does not spend a fresh linked-issue-satisfaction call once the SAME primary issue already has one", async () => {
+ let aiCalls = 0;
+ const env = createTestEnv({
+ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(),
+ AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ status: "addressed", rationale: "looks done" }) }; } } as unknown as Ai,
+ AI_SUMMARIES_ENABLED: "true",
+ AI_PUBLIC_COMMENTS_ENABLED: "true",
+ AI_DAILY_NEURON_BUDGET: "100000",
+ });
+ await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", linkedIssueSatisfactionGateMode: "advisory" });
+ // The PR's primary linked issue is #1 -- SAME issue the prior pass already assessed (at an earlier head).
+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 97, title: "Same-issue PR", state: "open", user: { login: "contributor" }, head: { sha: "a97-v1" }, labels: [], body: "Closes #1" });
+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 97, status: "complete", reviewsSyncedAt: new Date().toISOString() });
+ await putCachedLinkedIssueSatisfaction(env, "JSONbored/gittensory", 97, "a97-v1", 1, "seed-fp", { status: "ok", result: { status: "addressed", rationale: "already done", confidence: 0.9 }, estimatedNeurons: 4 });
+
+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = input.toString();
+ const method = init?.method ?? "GET";
+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
+ // A genuinely NEW push -- a new head SHA, but the SAME primary linked issue (#1).
+ if (url.includes("/pulls/97/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 2, deletions: 0, changes: 2, patch: "@@\n+export const ok = true;\n+export const also = 1;" }]);
+ if (url.endsWith("/pulls/97")) return Response.json({ number: 97, title: "Same-issue PR", state: "open", user: { login: "contributor" }, head: { sha: "a97-v2" }, labels: [], body: "Closes #1", mergeable_state: "clean" });
+ if (url.includes("/commits/a97-v2/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
+ if (url.includes("/commits/a97-v2/status")) return Response.json({ state: "success", statuses: [] });
+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
+ if (url.includes("/issues/97/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 });
+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
+ return Response.json({});
+ });
+
+ await processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-same-issue-push", repoFullName: "JSONbored/gittensory", prNumber: 97, installationId: 123 });
+
+ expect(aiCalls).toBe(0); // no fresh linked-issue-satisfaction call on the new head -- same issue already assessed
+ const skipAudit = await env.DB.prepare("select outcome, detail from audit_events where event_type = ? and target_key = ?")
+ .bind("github_app.linked_issue_satisfaction_one_shot_skip", "JSONbored/gittensory#97")
+ .first<{ outcome: string; detail: string }>();
+ expect(skipAudit?.outcome).toBe("completed");
+ expect(skipAudit?.detail).toContain("one-shot review cadence");
+ });
+
it("per-repo override (continuous via .gittensory.yml): a new push DOES spend a fresh main-review AI call, unlike the one_shot default", async () => {
let aiCalls = 0;
const env = createTestEnv({
From a6b84b9d700ad4f6681817617e6f454a526ffc89 Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Fri, 10 Jul 2026 03:15:42 -0700
Subject: [PATCH 3/3] test(review): cover the one-shot-cadence fail-safe catch
arms codecov flagged
codecov/patch flagged 6 lines at 81.82% patch coverage: the .catch()
fallback bodies for the three new existence-check lookups and their
paired audit-event writes never fired in the initial test suite (the
happy path never rejects). Adds one combined read-failure +
audit-write-failure regression per feature (slop, linked-issue
satisfaction, main-review reuse), mirroring the exact two-part pattern
already used for the pre-existing frozen-reuse fail-safe test.
---
test/unit/queue.test.ts | 136 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 136 insertions(+)
diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts
index 5554af35b7..cbf3a32fd8 100644
--- a/test/unit/queue.test.ts
+++ b/test/unit/queue.test.ts
@@ -6168,6 +6168,142 @@ describe("queue processors", () => {
expect(aiCalls).toBeGreaterThan(0); // the fleet-wide env default applied since the repo set no override
});
+
+ it("swallows a hasPublishedAiSlopAdvisory read failure and a slop one-shot-skip audit write failure without throwing", async () => {
+ let aiCalls = 0;
+ const env = createTestEnv({
+ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(),
+ AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ risk: "low", rationale: "fine" }) }; } } as unknown as Ai,
+ AI_SUMMARIES_ENABLED: "true",
+ AI_PUBLIC_COMMENTS_ENABLED: "true",
+ AI_DAILY_NEURON_BUDGET: "100000",
+ });
+ await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", slopGateMode: "advisory", slopAiAdvisory: true });
+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 98, title: "Slop flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a98" }, labels: [], body: "Closes #1" });
+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 98, status: "complete", reviewsSyncedAt: new Date().toISOString() });
+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = input.toString();
+ const method = init?.method ?? "GET";
+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
+ if (url.includes("/pulls/98/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]);
+ if (url.endsWith("/pulls/98")) return Response.json({ number: 98, title: "Slop flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a98" }, labels: [], body: "Closes #1", mergeable_state: "clean" });
+ if (url.includes("/commits/a98/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
+ if (url.includes("/commits/a98/status")) return Response.json({ state: "success", statuses: [] });
+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
+ if (url.includes("/issues/98/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 });
+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
+ return Response.json({});
+ });
+
+ const readSpy = vi.spyOn(repositoriesModule, "hasPublishedAiSlopAdvisory").mockRejectedValueOnce(new Error("D1 read error"));
+ await expect(
+ processJob(env, { type: "agent-regate-pr", deliveryId: "slop-existence-read-fails", repoFullName: "JSONbored/gittensory", prNumber: 98, installationId: 123 }),
+ ).resolves.toBeUndefined();
+ readSpy.mockRestore();
+ expect(aiCalls).toBeGreaterThan(0); // fail-safe treats the read failure as "not skipped" -- a fresh slop call still runs
+
+ await putCachedAiSlopAdvisory(env, "JSONbored/gittensory", 98, "a98", "seed-fp", { status: "ok", band: "low", finding: null, estimatedNeurons: 4 });
+ const originalRecordAuditEvent = repositoriesModule.recordAuditEvent;
+ const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => {
+ if (event.eventType === "github_app.ai_slop_one_shot_skip") throw new Error("audit DB down");
+ await originalRecordAuditEvent(auditEnv, event);
+ });
+ await expect(
+ processJob(env, { type: "agent-regate-pr", deliveryId: "slop-skip-audit-fails", repoFullName: "JSONbored/gittensory", prNumber: 98, installationId: 123 }),
+ ).resolves.toBeUndefined();
+ auditSpy.mockRestore();
+ });
+
+ it("swallows a hasPublishedLinkedIssueSatisfaction read failure and a linked-issue one-shot-skip audit write failure without throwing", async () => {
+ let aiCalls = 0;
+ const env = createTestEnv({
+ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(),
+ AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ status: "addressed", rationale: "looks done" }) }; } } as unknown as Ai,
+ AI_SUMMARIES_ENABLED: "true",
+ AI_PUBLIC_COMMENTS_ENABLED: "true",
+ AI_DAILY_NEURON_BUDGET: "100000",
+ });
+ await seedRegateChurnRepo(env, { publicSurface: "comment_only", aiReviewMode: "off", linkedIssueSatisfactionGateMode: "advisory" });
+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 99, title: "Linked-issue flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a99" }, labels: [], body: "Closes #1" });
+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 99, status: "complete", reviewsSyncedAt: new Date().toISOString() });
+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = input.toString();
+ const method = init?.method ?? "GET";
+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
+ if (url.includes("/pulls/99/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]);
+ if (url.endsWith("/pulls/99")) return Response.json({ number: 99, title: "Linked-issue flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a99" }, labels: [], body: "Closes #1", mergeable_state: "clean" });
+ if (url.includes("/commits/a99/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
+ if (url.includes("/commits/a99/status")) return Response.json({ state: "success", statuses: [] });
+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
+ if (url.includes("/issues/99/comments")) return method === "GET" ? Response.json([]) : Response.json({ id: 1 }, { status: 201 });
+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
+ return Response.json({});
+ });
+
+ const readSpy = vi.spyOn(repositoriesModule, "hasPublishedLinkedIssueSatisfaction").mockRejectedValueOnce(new Error("D1 read error"));
+ await expect(
+ processJob(env, { type: "agent-regate-pr", deliveryId: "linked-issue-existence-read-fails", repoFullName: "JSONbored/gittensory", prNumber: 99, installationId: 123 }),
+ ).resolves.toBeUndefined();
+ readSpy.mockRestore();
+ expect(aiCalls).toBeGreaterThan(0); // fail-safe treats the read failure as "not skipped" -- a fresh assessment still runs
+
+ await putCachedLinkedIssueSatisfaction(env, "JSONbored/gittensory", 99, "a99", 1, "seed-fp", { status: "ok", result: { status: "addressed", rationale: "already done", confidence: 0.9 }, estimatedNeurons: 4 });
+ const originalRecordAuditEvent = repositoriesModule.recordAuditEvent;
+ const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => {
+ if (event.eventType === "github_app.linked_issue_satisfaction_one_shot_skip") throw new Error("audit DB down");
+ await originalRecordAuditEvent(auditEnv, event);
+ });
+ await expect(
+ processJob(env, { type: "agent-regate-pr", deliveryId: "linked-issue-skip-audit-fails", repoFullName: "JSONbored/gittensory", prNumber: 99, installationId: 123 }),
+ ).resolves.toBeUndefined();
+ auditSpy.mockRestore();
+ });
+
+ it("swallows a one-shot-reuse getLatestPublishedAiReview read failure and a one-shot-reuse audit write failure without throwing", async () => {
+ let aiCalls = 0;
+ const env = createTestEnv({
+ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(),
+ AI: { run: async () => { aiCalls += 1; return { response: JSON.stringify({ assessment: "Fresh.", blockers: [], nits: [], suggestions: [] }) }; } } as unknown as Ai,
+ AI_SUMMARIES_ENABLED: "true",
+ AI_PUBLIC_COMMENTS_ENABLED: "true",
+ AI_DAILY_NEURON_BUDGET: "100000",
+ });
+ await seedRegateChurnRepo(env, { publicSurface: "comment_only" });
+ await upsertPullRequestFromGitHub(env, "JSONbored/gittensory", { number: 100, title: "One-shot flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a100" }, labels: [], body: "Closes #1" });
+ await upsertPullRequestDetailSyncState(env, { repoFullName: "JSONbored/gittensory", pullNumber: 100, status: "complete", reviewsSyncedAt: new Date().toISOString() });
+ await putCachedAiReview(env, "JSONbored/gittensory", 100, "a100-old", "block", { notes: "Old.", reviewerCount: 1 });
+ await markAiReviewPublished(env, "JSONbored/gittensory", 100, "a100-old");
+ vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = input.toString();
+ const method = init?.method ?? "GET";
+ if (url.includes("/access_tokens")) return Response.json({ token: "fake-installation-token" });
+ if (url.includes("/pulls/100/files")) return Response.json([{ filename: "src/a.ts", status: "modified", additions: 1, deletions: 0, changes: 1, patch: "@@\n+export const ok = true;" }]);
+ if (url.endsWith("/pulls/100")) return Response.json({ number: 100, title: "One-shot flaky-read PR", state: "open", user: { login: "contributor" }, head: { sha: "a100" }, labels: [], body: "Closes #1", mergeable_state: "clean" });
+ if (url.includes("/commits/a100/check-runs")) return Response.json({ total_count: 0, check_runs: [] });
+ if (url.includes("/commits/a100/status")) return Response.json({ state: "success", statuses: [] });
+ if (url.includes("/issues/1")) return Response.json({ number: 1, title: "Issue", state: "open", labels: [], user: { login: "reporter" } });
+ if (url.includes("/issues/100/comments")) return method === "POST" || method === "PATCH" ? Response.json({ id: 1 }, { status: 201 }) : Response.json([]);
+ if (url.includes("/branches/")) return Response.json({ protected: false, protection: { required_status_checks: { contexts: [] } } });
+ return Response.json({});
+ });
+
+ const readSpy = vi.spyOn(repositoriesModule, "getLatestPublishedAiReview").mockRejectedValueOnce(new Error("D1 read error"));
+ await expect(
+ processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-reuse-read-fails", repoFullName: "JSONbored/gittensory", prNumber: 100, installationId: 123 }),
+ ).resolves.toBeUndefined();
+ readSpy.mockRestore();
+ expect(aiCalls).toBeGreaterThan(0); // fail-safe treats the read failure as "nothing to reuse" -- a fresh review still runs
+
+ const originalRecordAuditEvent = repositoriesModule.recordAuditEvent;
+ const auditSpy = vi.spyOn(repositoriesModule, "recordAuditEvent").mockImplementation(async (auditEnv, event) => {
+ if (event.eventType === "github_app.ai_review_one_shot_reuse") throw new Error("audit DB down");
+ await originalRecordAuditEvent(auditEnv, event);
+ });
+ await expect(
+ processJob(env, { type: "agent-regate-pr", deliveryId: "one-shot-reuse-audit-fails", repoFullName: "JSONbored/gittensory", prNumber: 100, installationId: 123 }),
+ ).resolves.toBeUndefined();
+ auditSpy.mockRestore();
+ });
});
it("#freeze-owner-exemption (incident, confirmed live on PR #3476): the repo owner's OWN held PR is never frozen -- a new push gets a fresh AI review", async () => {