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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ GITTENSORY_REVIEW_REPUTATION=false
# legacy multi-panel comment. OFF keeps the legacy comment byte-identical.
GITTENSORY_REVIEW_UNIFIED_COMMENT=false

# Quiet inline review comments (CodeRabbit-style): on top of the decision summary, leave
# NON-BLOCKING inline comments on changed lines. Also requires the repo in GITTENSORY_REVIEW_REPOS
# AND review.inline_comments: true in its .gittensory.yml. OFF the model is never asked for them.
GITTENSORY_REVIEW_INLINE_COMMENTS=false

# --- Global capabilities (NOT scoped by GITTENSORY_REVIEW_REPOS) -------------

# Observability (read-only): cron anomaly scan over the gate-block ledger emits
Expand Down
9 changes: 8 additions & 1 deletion docs/review-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ per-PR feature activates only when **(its own flag is ON) AND (the repo is allow

| Flag | What it does | Default | How to tune | Sample |
| --- | --- | --- | --- | --- |
| `GITTENSORY_REVIEW_REPOS` | **Per-repo cutover allowlist.** Comma-separated `owner/repo` names that may run the per-PR review features (`SAFETY`, `GROUNDING`, `RAG`, `REPUTATION`, `UNIFIED_COMMENT`). A per-PR feature runs on a repo only if its global flag is ON **and** the repo is listed here. Empty/unset = **no repos** → every per-PR feature stays dormant for everyone regardless of the global flags. Cron/endpoint flags (`OPS`, `SELFTUNE`, `PARITY_AUDIT`, `CONTENT_LANE`, `DRAFT`) are **not** scoped by this. | `""` (no repos) | Add repos one at a time as you roll forward; remove to roll back. Case-insensitive, trimmed; stray commas are ignored. | `"JSONbored/gittensory,JSONbored/awesome-claude"` |
| `GITTENSORY_REVIEW_REPOS` | **Per-repo cutover allowlist.** Comma-separated `owner/repo` names that may run the per-PR review features (`SAFETY`, `GROUNDING`, `RAG`, `REPUTATION`, `UNIFIED_COMMENT`, `INLINE_COMMENTS`). A per-PR feature runs on a repo only if its global flag is ON **and** the repo is listed here. Empty/unset = **no repos** → every per-PR feature stays dormant for everyone regardless of the global flags. Cron/endpoint flags (`OPS`, `SELFTUNE`, `PARITY_AUDIT`, `CONTENT_LANE`, `DRAFT`) are **not** scoped by this. | `""` (no repos) | Add repos one at a time as you roll forward; remove to roll back. Case-insensitive, trimmed; stray commas are ignored. | `"JSONbored/gittensory,JSONbored/awesome-claude"` |
| `GITTENSORY_REVIEW_SAFETY` | **Safety scan** in the review path: (1) defangs untrusted PR title/body/diff (prompt-injection neutralization) before the AI reviewer sees it, and (2) scans the PR diff for leaked secrets, surfacing a `secret_leak` blocker. Per-PR — also requires the repo to be in `GITTENSORY_REVIEW_REPOS`. | `false` | Flip to `true`, then add the repo to `GITTENSORY_REVIEW_REPOS`. No per-repo tuning beyond that. | `"true"` |
| `GITTENSORY_REVIEW_GROUNDING` | **Grounds** the AI-reviewer prompt with the PR's *finished* CI status + the *full post-change content* of the changed files, so a non-frontier model verifies its claims against reality instead of predicting CI or flagging symbols defined just outside the hunk. Per-PR — also gated by `GITTENSORY_REVIEW_REPOS`. | `false` | Flip to `true` + allowlist the repo. Both grounding inputs (CI + full files) are gathered together; there is no partial mode. | `"true"` |
| `GITTENSORY_REVIEW_RAG` | **Retrieval-augmented context.** At review time, queries the codebase vector index for code/docs semantically related to the changed files (callers, related modules, existing conventions) and appends a "Relevant existing code / docs" section to the reviewer prompt — additive only, like grounding. Per-PR — also gated by `GITTENSORY_REVIEW_REPOS`. **Inert until a vector index exists** for the repo (a cold/missing index degrades to no context). | `false` | Flip to `true` + allowlist the repo **and** bind/populate the `VECTORIZE` index. Without an index it is a safe no-op. | `"true"` |
| `GITTENSORY_REVIEW_REPUTATION` | **Submitter-reputation spend control (internal-only).** Extends the AI-spend gate: a new / burst / low-reputation submitter is downgraded to a deterministic-only review (the paid AI neurons are skipped); good-reputation submitters proceed normally. The per-(project, submitter) outcome is recorded after the gate decides. **Never surfaced publicly** — no comment, label, or check shows reputation. Per-PR — also gated by `GITTENSORY_REVIEW_REPOS`. | `false` | Flip to `true` + allowlist the repo. Thresholds are generic anti-abuse defaults (they reveal no review direction) and are not per-repo tunable. | `"true"` |
| `GITTENSORY_REVIEW_UNIFIED_COMMENT` | Renders the public PR comment as **one in-place unified comment** (the converged comment shape) instead of the legacy multi-panel comment. Per-PR — also gated by `GITTENSORY_REVIEW_REPOS`. | `false` | Flip to `true` + allowlist the repo. Flag-OFF keeps the legacy comment byte-identical. | `"true"` |
| `GITTENSORY_REVIEW_INLINE_COMMENTS` | **Quiet inline review comments** (CodeRabbit-style). On top of the decision summary, the AI reviewer leaves **non-blocking** inline comments on specific changed lines (`event: COMMENT`, never a change-request) — so a contributor sees exactly what to fix on a resubmission without the gate ever changing. Each comment's line is validated against the PR diff (out-of-diff findings are dropped, never a 422). Per-PR — also requires the repo in `GITTENSORY_REVIEW_REPOS` **and** `review.inline_comments: true` in its `.gittensory.yml`. | `false` | Flip to `true`, allowlist the repo, and set `review.inline_comments: true`. Flag-OFF the model is never asked for inline findings (byte-identical). | `"true"` |
| `GITTENSORY_REVIEW_OPS` | **Observability (read-only).** Drives two operator surfaces off your own review-outcome data: (1) on the cron tick, an anomaly scan over the gate-block ledger + recommendation/slop calibration emits a structured `ops_anomaly` log when something drifts (gate false-positive spike, slop score inverting, recommendation negative-rate spike); and (2) a bearer-gated `GET /v1/internal/ops/stats` outcome aggregate. **Read-only** — does not mutate config. Global (not scoped by `GITTENSORY_REVIEW_REPOS`). | `false` | Flip to `true` to enable the anomaly cron + the stats endpoint. Endpoint is bearer-gated (see secrets). | `"true"` |
| `GITTENSORY_REVIEW_SELFTUNE` | **Self-improvement / auto-tune loop.** On the cron tick, computes tuning recommendations from your own review-outcome data, **shadow-soaks** any strictly-tightening recommendation, and auto-promotes it to live **only** after the soak window passes the gate; every action is audited. It can **only ever tighten** the gate — a loosening recommendation is never applied. Global. *Note:* reading a promoted override back into the live gate is a deferred follow-up; today it records recommendations + shadow-soak + audit. | `false` | Flip to `true` to enable the self-tuning cron. Direction is enforced (tightening-only) — safe to leave on. | `"true"` |
| `GITTENSORY_REVIEW_PARITY_AUDIT` | **Parity readiness (shadow, record-only).** Shadow-records each finalized native gate decision into the audit-source table and serves a pre-cutover parity readiness report at `GET /v1/internal/parity`. Recording changes **no** review behavior. Global. | `false` | Flip to `true` during a validation window to collect parity data; turn off when done. | `"true"` |
Expand Down Expand Up @@ -218,6 +219,12 @@ settings:
- https://github.com/owner/repo/pull/1
addedAt: "2026-06-26"
- bad-farmer # bare login shorthand is also accepted

# Review write-up + inline-review overrides (manifest-only; no dashboard equivalent)
review:
profile: balanced # chill | balanced | assertive — how nitpicky the AI write-up is
inline_comments: true # leave quiet, non-blocking inline comments on changed lines
# (also needs GITTENSORY_REVIEW_INLINE_COMMENTS=true + the repo allowlisted)
```

---
Expand Down
10 changes: 8 additions & 2 deletions docs/self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,14 @@ Self-host runs the identical engine, so the behavior is configured exactly as on
acts on its decisions, gated by the same guardrails (protected-path manual-review globs, owner-PR
no-auto-close, mergeability + green-CI before approve).

Per-PR capabilities (safety scan, CI/full-file grounding, RAG, unified comment, content lane, self-tune,
parity audit) are the `GITTENSORY_REVIEW_*` flags — every flag defaults **off** and is fully inert until
- **Quiet inline comments (CodeRabbit-style).** Set `GITTENSORY_REVIEW_INLINE_COMMENTS=true` + the repo in
`GITTENSORY_REVIEW_REPOS` + `review.inline_comments: true` in the repo's config, and on top of the decision
summary the reviewer leaves **non-blocking** inline comments on changed lines (`event: COMMENT`) — useful even
in advisory mode, telling a contributor exactly what to fix for their next submission. Out-of-diff findings are
dropped (never a 422), and a failure never affects the gate.

Per-PR capabilities (safety scan, CI/full-file grounding, RAG, unified comment, inline comments, content lane,
self-tune, parity audit) are the `GITTENSORY_REVIEW_*` flags — every flag defaults **off** and is fully inert until
turned on. Per-repo settings (autonomy, required approvals, protected paths) live in `.gittensory.yml` /
repository settings. The authoritative reference for all of these is
[`docs/review-configuration.md`](./review-configuration.md).
Expand Down
5 changes: 5 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ declare global {
* (ONE in-place comment in the converged shape) instead of the legacy `buildPublicPrIntelligenceComment`
* panel. Default OFF — unset/false keeps the legacy panel byte-identical. */
GITTENSORY_REVIEW_UNIFIED_COMMENT?: string;
/** Inline comments (#inline-comments): when truthy (AND the repo is in GITTENSORY_REVIEW_REPOS AND the repo's
* `.gittensory.yml` sets `review.inline_comments: true`), the AI reviewer ALSO leaves quiet, NON-BLOCKING
* inline comments on specific changed lines, layered on top of the decision summary. Default OFF —
* unset/false keeps the review path byte-identical (the model is never asked for inline findings). */
GITTENSORY_REVIEW_INLINE_COMMENTS?: string;
/** Convergence (safety): when truthy, the ported safety scan runs in the review path — (1) untrusted PR
* title/body/diff is defanged (prompt-injection neutralized) before it reaches the AI reviewer, and (2)
* the PR diff is scanned for leaked secrets, surfacing a `secret_leak` blocker. Default OFF —
Expand Down
29 changes: 29 additions & 0 deletions src/github/pr-actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createInstallationToken } from "./app";
import { makeInstallationOctokit } from "./client";
import type { AgentActionMode } from "../settings/agent-execution";
import type { AutoMergeMethod } from "../types";

const ISSUE_EVENTS_PAGE_SIZE = 100;
Expand Down Expand Up @@ -40,6 +41,34 @@ export async function createPullRequestReview(
return { id: (response.data as { id: number }).id };
}

/** Post a quiet, NON-BLOCKING review (`event: "COMMENT"`) carrying line-anchored inline comments — the
* CodeRabbit-style inline code notes (#inline-comments). `commitId` anchors them to the reviewed head SHA so
* GitHub places each on the right diff line. Mirrors {@link createPullRequestReview}; the action `mode` is
* threaded so a dry-run instance suppresses the write. Throws on a non-2xx — the caller
* (`postInlineReviewComments`) owns the fail-safe try/catch + audit. */
export async function createPullRequestReviewComments(
env: Env,
installationId: number,
repoFullName: string,
pullNumber: number,
commitId: string,
comments: Array<{ path: string; line: number; side: "RIGHT" | "LEFT"; body: string }>,
mode: AgentActionMode,
): Promise<{ id: number }> {
const { owner, repo } = splitRepo(repoFullName);
const token = await createInstallationToken(env, installationId);
const octokit = makeInstallationOctokit(env, token, mode);
const response = await octokit.request("POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews", {
owner,
repo,
pull_number: pullNumber,
commit_id: commitId,
event: "COMMENT",
comments,
});
return { id: (response.data as { id: number }).id };
}

/** Merge a pull request with the configured method. Pass `sha` to make the merge fail (409) if the head moved
* since we evaluated it — a guard against merging a PR that changed under us. */
export async function mergePullRequest(
Expand Down
34 changes: 29 additions & 5 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ import { buildFocusManifestGuidance, excludeReviewPaths, resolveReviewPathInstru
import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import { resolveRepositorySettings } from "../settings/repository-settings";
import type { LocalBranchAnalysisInput } from "../signals/local-branch";
import { runGittensoryAiReview } from "../services/ai-review";
import { runGittensoryAiReview, type InlineFinding } from "../services/ai-review";
import { maybePostInlineComments, shouldRequestInlineFindings } from "../review/inline-comments";
import { evaluatePreMergeChecks } from "../review/pre-merge-checks";
import { secretLeakFinding } from "../review/safety";
import { buildIssuePlanComment, classifyPlanCommandRequest, generateIssuePlan, isPlanCommand, isPlannerEnabled } from "../review/planner";
Expand Down Expand Up @@ -2129,8 +2130,12 @@ export async function runAiReviewForAdvisory(
// manifest. Globs whose files are dropped from the AI review (diff + grounding + RAG) — generated/lockfiles
// the maintainer doesn't want reviewed. Empty ⇒ every file is reviewed (byte-identical). The gate is unaffected.
reviewExcludePaths?: string[] | undefined;
// `.gittensory.yml` review.inline_comments (#inline-comments), resolved by the caller from the cached manifest
// (the per-repo toggle). ANDed here with the operator flag + cutover allowlist to decide whether to ASK the
// model for line-anchored inline findings. Absent/false ⇒ the reviewer prompt is byte-identical (no findings).
reviewInlineComments?: boolean | undefined;
},
): Promise<{ notes: string; reviewerCount: number } | undefined> {
): Promise<{ notes: string; reviewerCount: number; inlineFindings: InlineFinding[] } | undefined> {
const packAllowsAnyAuthorBlockingReview = args.settings.gatePack === "oss-anti-slop" && args.settings.aiReviewMode === "block";
if (args.settings.aiReviewMode === "off" || (!args.confirmedContributor && !packAllowsAnyAuthorBlockingReview) || !args.advisory.headSha) return undefined;
// Per-repo cutover gate (GITTENSORY_REVIEW_REPOS): the converged review features (reputation AI-skip,
Expand Down Expand Up @@ -2198,6 +2203,9 @@ export async function runAiReviewForAdvisory(
grounding,
ragContext,
profile: args.reviewProfile ?? null,
// Inline comments (#inline-comments): ask the model for line-anchored findings only when the operator flag,
// the cutover allowlist, AND the per-repo manifest toggle all pass. Otherwise the prompt is byte-identical.
inlineFindings: shouldRequestInlineFindings(env, args.repoFullName, args.reviewInlineComments),
pathGuidance: resolveReviewPathInstructions(
args.reviewPathInstructions ?? [],
files.map((file) => file.path),
Expand Down Expand Up @@ -2235,7 +2243,7 @@ export async function runAiReviewForAdvisory(
action: "The gate is held for a human reviewer rather than passed automatically; it re-evaluates on the next update.",
});
}
return result.advisoryNotes ? { notes: result.advisoryNotes, reviewerCount: result.reviewerCount } : undefined;
return result.advisoryNotes ? { notes: result.advisoryNotes, reviewerCount: result.reviewerCount, inlineFindings: result.inlineFindings } : undefined;
} catch (error) {
console.error(JSON.stringify({ level: "warn", event: "ai_review_failed", repository: args.repoFullName, pullNumber: args.pr.number, error: errorMessage(error) }));
return undefined;
Expand Down Expand Up @@ -2536,7 +2544,9 @@ async function maybePublishPrPublicSurface(
let queueHealth!: ReturnType<typeof buildQueueHealth>;
let preflight!: ReturnType<typeof buildPreflightResult>;
let gateEvaluation: ReturnType<typeof evaluateGateCheck> | undefined;
let aiReview: { notes: string; reviewerCount: number } | undefined;
// inlineFindings is present ONLY on a FRESH review (cache miss) with inline comments enabled; the AI cache
// round-trips just notes + reviewerCount, so a cache hit carries no findings and never re-posts (#inline-comments).
let aiReview: { notes: string; reviewerCount: number; inlineFindings?: InlineFinding[] } | undefined;
let gateFinalized = false;
// The PR's changed files are needed by the slop/manifest gates, the AI review + grounding + RAG, the secret
// scan, the check-run, and the unified comment. Resolve them AT MOST ONCE per review and share across the
Expand Down Expand Up @@ -2710,7 +2720,7 @@ async function maybePublishPrPublicSurface(
// resolution, so a cheap cache hit — no extra fetch) and thread them into the AI review. Profile shapes
// nitpickiness; path-instructions add per-path guidance; exclude-paths drop files from review. Absent ⇒
// byte-identical prompt. Fail-safe to defaults on any read error (resolveReviewPromptOverrides).
const { profile: reviewProfile, pathInstructions: reviewPathInstructions, excludePaths: reviewExcludePaths } = resolveReviewPromptOverrides(await loadRepoFocusManifest(env, repoFullName).catch(() => null));
const { profile: reviewProfile, inlineComments: reviewInlineComments, pathInstructions: reviewPathInstructions, excludePaths: reviewExcludePaths } = resolveReviewPromptOverrides(await loadRepoFocusManifest(env, repoFullName).catch(() => null));
aiReview = await runAiReviewForAdvisory(env, {
settings,
advisory,
Expand All @@ -2722,6 +2732,7 @@ async function maybePublishPrPublicSurface(
reviewProfile,
reviewPathInstructions,
reviewExcludePaths,
reviewInlineComments,
});
if (aiReview) await putCachedAiReview(env, repoFullName, pr.number, advisory.headSha, settings.aiReviewMode, aiReview).catch(() => undefined);
}
Expand Down Expand Up @@ -3066,6 +3077,19 @@ async function maybePublishPrPublicSurface(
failedOutputs.push({ output: "comment", error: message });
await recordPublicSurfaceOutputFailure(env, "comment", author, repoFullName, pr.number, webhook.deliveryId, message);
}
// Quiet inline review comments (#inline-comments): layer the AI's line-anchored findings on top of the
// summary just posted, as a NON-BLOCKING COMMENT review. A no-op (no extra work) unless this is a fresh
// review that actually produced findings — a cache hit carries none, so the ~2-min re-gate sweep never
// reposts. Fully fail-safe: drops out-of-diff lines (no 422), threads `mode`, and never affects the gate.
await maybePostInlineComments(env, {
aiReview,
installationId,
repoFullName,
pullNumber: pr.number,
commitId: advisory.headSha,
getFiles: getReviewFiles,
mode,
});
}
if (decision.willLabel) {
try {
Expand Down
Loading
Loading